1pub mod actions;
29
30use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
31use std::sync::Arc;
32
33use anyhow::{anyhow, Result};
34use serde_json::json;
35use socket2::{Domain, Protocol as SockProtocol, SockAddr, Socket, Type};
36use tokio::io::unix::AsyncFd;
37use tokio::net::UdpSocket;
38use tokio::sync::mpsc;
39use tracing::{debug, error, warn};
40
41use crate::llm::action_helper::call_llm;
42use crate::llm::actions::protocol_trait::ActionResult;
43use crate::llm::ollama_client::OllamaClient;
44use crate::logging::emit::Log;
45use crate::protocol::{Event, StartupParams};
46use crate::server::connection::ConnectionId;
47use crate::state::app_state::AppState;
48use crate::utils::truncate_for_log;
49use actions::{RawIpProtocol, RAWIP_PACKET_RECEIVED_EVENT};
50
51pub const IPV4_MIN_HEADER_LEN: usize = 20;
57pub const IPV6_HEADER_LEN: usize = 40;
59
60pub const EVENT_PAYLOAD_LIMIT: usize = 2048;
66
67#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum IpDecodeError {
73 TooShort {
75 got: usize,
76 need: usize,
77 version: u8,
78 },
79 UnsupportedVersion(u8),
81 IhlTooSmall(u8),
83 HeaderTruncated { ihl_bytes: usize, got: usize },
85}
86
87impl std::fmt::Display for IpDecodeError {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 IpDecodeError::TooShort {
91 got,
92 need,
93 version,
94 } => write!(
95 f,
96 "truncated IPv{version} packet: {got} bytes captured, {need} needed for the header"
97 ),
98 IpDecodeError::UnsupportedVersion(v) => {
99 write!(f, "not an IP packet: version nibble is {v}, expected 4 or 6")
100 }
101 IpDecodeError::IhlTooSmall(ihl) => write!(
102 f,
103 "malformed IPv4 header: IHL is {ihl} words, the minimum is 5 (RFC 791 §3.1)"
104 ),
105 IpDecodeError::HeaderTruncated { ihl_bytes, got } => write!(
106 f,
107 "malformed IPv4 header: IHL claims {ihl_bytes} header bytes but only {got} were captured"
108 ),
109 }
110 }
111}
112
113impl std::error::Error for IpDecodeError {}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct Ipv4Header {
118 pub ihl: u8,
119 pub header_length: usize,
120 pub dscp: u8,
121 pub ecn: u8,
122 pub total_length: u16,
123 pub identification: u16,
124 pub reserved_flag: bool,
126 pub dont_fragment: bool,
127 pub more_fragments: bool,
128 pub fragment_offset: u16,
130 pub ttl: u8,
131 pub protocol: u8,
132 pub header_checksum: u16,
133 pub source: Ipv4Addr,
134 pub destination: Ipv4Addr,
135 pub options_present: bool,
136 pub options_length: usize,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct Ipv6Header {
148 pub traffic_class: u8,
149 pub flow_label: u32,
150 pub payload_length: u16,
152 pub next_header: u8,
153 pub hop_limit: u8,
154 pub source: Ipv6Addr,
155 pub destination: Ipv6Addr,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum IpHeader {
161 V4(Ipv4Header),
162 V6(Ipv6Header),
163}
164
165impl IpHeader {
166 pub fn version(&self) -> u8 {
168 match self {
169 IpHeader::V4(_) => 4,
170 IpHeader::V6(_) => 6,
171 }
172 }
173
174 pub fn upper_protocol(&self) -> u8 {
176 match self {
177 IpHeader::V4(h) => h.protocol,
178 IpHeader::V6(h) => h.next_header,
179 }
180 }
181
182 pub fn source(&self) -> IpAddr {
183 match self {
184 IpHeader::V4(h) => IpAddr::V4(h.source),
185 IpHeader::V6(h) => IpAddr::V6(h.source),
186 }
187 }
188
189 pub fn destination(&self) -> IpAddr {
190 match self {
191 IpHeader::V4(h) => IpAddr::V4(h.destination),
192 IpHeader::V6(h) => IpAddr::V6(h.destination),
193 }
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct DecodedPacket {
200 pub header: IpHeader,
201 pub payload: Vec<u8>,
202 pub payload_incomplete: bool,
206}
207
208pub fn decode_ip_packet(bytes: &[u8]) -> std::result::Result<DecodedPacket, IpDecodeError> {
217 let version = match bytes.first() {
218 Some(b) => b >> 4,
219 None => {
220 return Err(IpDecodeError::TooShort {
221 got: 0,
222 need: IPV4_MIN_HEADER_LEN,
223 version: 0,
224 })
225 }
226 };
227
228 match version {
229 4 => decode_ipv4(bytes),
230 6 => decode_ipv6(bytes),
231 other => Err(IpDecodeError::UnsupportedVersion(other)),
232 }
233}
234
235fn decode_ipv4(bytes: &[u8]) -> std::result::Result<DecodedPacket, IpDecodeError> {
236 if bytes.len() < IPV4_MIN_HEADER_LEN {
237 return Err(IpDecodeError::TooShort {
238 got: bytes.len(),
239 need: IPV4_MIN_HEADER_LEN,
240 version: 4,
241 });
242 }
243
244 let ihl = bytes[0] & 0x0F;
245 if ihl < 5 {
246 return Err(IpDecodeError::IhlTooSmall(ihl));
247 }
248 let header_length = ihl as usize * 4;
249 if bytes.len() < header_length {
250 return Err(IpDecodeError::HeaderTruncated {
251 ihl_bytes: header_length,
252 got: bytes.len(),
253 });
254 }
255
256 let total_length = u16::from_be_bytes([bytes[2], bytes[3]]);
257 let flags_and_offset = u16::from_be_bytes([bytes[6], bytes[7]]);
258
259 let header = Ipv4Header {
260 ihl,
261 header_length,
262 dscp: bytes[1] >> 2,
263 ecn: bytes[1] & 0x03,
264 total_length,
265 identification: u16::from_be_bytes([bytes[4], bytes[5]]),
266 reserved_flag: flags_and_offset & 0x8000 != 0,
267 dont_fragment: flags_and_offset & 0x4000 != 0,
268 more_fragments: flags_and_offset & 0x2000 != 0,
269 fragment_offset: flags_and_offset & 0x1FFF,
270 ttl: bytes[8],
271 protocol: bytes[9],
272 header_checksum: u16::from_be_bytes([bytes[10], bytes[11]]),
273 source: Ipv4Addr::new(bytes[12], bytes[13], bytes[14], bytes[15]),
274 destination: Ipv4Addr::new(bytes[16], bytes[17], bytes[18], bytes[19]),
275 options_present: ihl > 5,
276 options_length: header_length - IPV4_MIN_HEADER_LEN,
277 };
278
279 let declared_end = total_length as usize;
284 let available_end = bytes.len();
285 let end = if declared_end >= header_length && declared_end <= available_end {
286 declared_end
287 } else {
288 available_end
289 };
290 let payload_incomplete = declared_end > available_end;
291
292 Ok(DecodedPacket {
293 header: IpHeader::V4(header),
294 payload: bytes[header_length..end].to_vec(),
295 payload_incomplete,
296 })
297}
298
299fn decode_ipv6(bytes: &[u8]) -> std::result::Result<DecodedPacket, IpDecodeError> {
300 if bytes.len() < IPV6_HEADER_LEN {
301 return Err(IpDecodeError::TooShort {
302 got: bytes.len(),
303 need: IPV6_HEADER_LEN,
304 version: 6,
305 });
306 }
307
308 let mut src = [0u8; 16];
309 src.copy_from_slice(&bytes[8..24]);
310 let mut dst = [0u8; 16];
311 dst.copy_from_slice(&bytes[24..40]);
312
313 let payload_length = u16::from_be_bytes([bytes[4], bytes[5]]);
314
315 let header = Ipv6Header {
316 traffic_class: ((bytes[0] & 0x0F) << 4) | (bytes[1] >> 4),
317 flow_label: (((bytes[1] & 0x0F) as u32) << 16)
318 | ((bytes[2] as u32) << 8)
319 | (bytes[3] as u32),
320 payload_length,
321 next_header: bytes[6],
322 hop_limit: bytes[7],
323 source: Ipv6Addr::from(src),
324 destination: Ipv6Addr::from(dst),
325 };
326
327 let declared_end = IPV6_HEADER_LEN + payload_length as usize;
328 let available_end = bytes.len();
329 let end = if declared_end <= available_end {
330 declared_end
331 } else {
332 available_end
333 };
334
335 Ok(DecodedPacket {
336 header: IpHeader::V6(header),
337 payload: bytes[IPV6_HEADER_LEN..end].to_vec(),
338 payload_incomplete: declared_end > available_end,
339 })
340}
341
342pub fn ip_protocol_name(number: u8) -> Option<&'static str> {
352 Some(match number {
353 0 => "HOPOPT",
354 1 => "ICMP",
355 2 => "IGMP",
356 3 => "GGP",
357 4 => "IPv4-in-IPv4",
358 5 => "ST",
359 6 => "TCP",
360 8 => "EGP",
361 9 => "IGP",
362 17 => "UDP",
363 27 => "RDP",
364 33 => "DCCP",
365 41 => "IPv6-in-IPv4",
366 43 => "IPv6-Route",
367 44 => "IPv6-Frag",
368 46 => "RSVP",
369 47 => "GRE",
370 50 => "ESP",
371 51 => "AH",
372 58 => "IPv6-ICMP",
373 59 => "IPv6-NoNxt",
374 60 => "IPv6-Opts",
375 88 => "EIGRP",
376 89 => "OSPFIGP",
377 94 => "IPIP",
378 97 => "ETHERIP",
379 98 => "ENCAP",
380 103 => "PIM",
381 108 => "IPComp",
382 112 => "VRRP",
383 115 => "L2TP",
384 124 => "ISIS-over-IPv4",
385 132 => "SCTP",
386 133 => "FC",
387 135 => "Mobility-Header",
388 136 => "UDPLite",
389 137 => "MPLS-in-IP",
390 139 => "HIP",
391 140 => "Shim6",
392 141 => "WESP",
393 142 => "ROHC",
394 143 => "Ethernet",
395 253 | 254 => "Experimental",
396 255 => "Reserved",
397 _ => return None,
398 })
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub enum PayloadEncoding {
410 Utf8,
411 Hex,
412}
413
414impl PayloadEncoding {
415 pub fn as_str(self) -> &'static str {
416 match self {
417 PayloadEncoding::Utf8 => "utf8",
418 PayloadEncoding::Hex => "hex",
419 }
420 }
421
422 pub fn parse(s: &str) -> Result<Self> {
425 match s {
426 "utf8" | "utf-8" => Ok(PayloadEncoding::Utf8),
427 "hex" => Ok(PayloadEncoding::Hex),
428 other => Err(anyhow!(
429 "unknown payload encoding '{other}': use \"utf8\" (default) or \"hex\""
430 )),
431 }
432 }
433}
434
435pub fn decode_payload(text: &str, encoding: PayloadEncoding) -> Result<Vec<u8>> {
441 match encoding {
442 PayloadEncoding::Utf8 => Ok(text.as_bytes().to_vec()),
443 PayloadEncoding::Hex => {
444 let cleaned: String = text.chars().filter(|c| !c.is_whitespace()).collect();
445 hex::decode(&cleaned).map_err(|e| {
446 anyhow!(
447 "payload was declared encoding=\"hex\" but is not valid hex: {e}. \
448 Send the bytes as an even-length string of hex digits, or set \
449 encoding to \"utf8\" to send the text itself."
450 )
451 })
452 }
453 }
454}
455
456pub fn encode_payload_for_event(payload: &[u8]) -> (PayloadEncoding, String) {
462 match std::str::from_utf8(payload) {
463 Ok(text)
464 if text
465 .chars()
466 .all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t') =>
467 {
468 (PayloadEncoding::Utf8, text.to_string())
469 }
470 _ => (PayloadEncoding::Hex, hex::encode(payload)),
471 }
472}
473
474pub fn packet_event_data(
479 packet: &DecodedPacket,
480 listening_protocol_number: u8,
481 connection_id: ConnectionId,
482) -> serde_json::Value {
483 let full_len = packet.payload.len();
484 let shown = &packet.payload[..full_len.min(EVENT_PAYLOAD_LIMIT)];
485 let (encoding, payload) = encode_payload_for_event(shown);
486
487 let mut data = json!({
488 "connection_id": connection_id.to_string(),
489 "listening_protocol_number": listening_protocol_number,
490 "ip_version": packet.header.version(),
491 "source": packet.header.source().to_string(),
492 "destination": packet.header.destination().to_string(),
493 "payload_length": full_len,
494 "payload": payload,
495 "payload_encoding": encoding.as_str(),
496 "payload_truncated": full_len > EVENT_PAYLOAD_LIMIT,
497 "payload_incomplete": packet.payload_incomplete,
498 });
499
500 let obj = data.as_object_mut().expect("json! built an object");
501
502 match &packet.header {
503 IpHeader::V4(h) => {
504 obj.insert("ihl".into(), json!(h.ihl));
505 obj.insert("header_length".into(), json!(h.header_length));
506 obj.insert("dscp".into(), json!(h.dscp));
507 obj.insert("ecn".into(), json!(h.ecn));
508 obj.insert("total_length".into(), json!(h.total_length));
509 obj.insert("identification".into(), json!(h.identification));
510 obj.insert(
511 "flags".into(),
512 json!({
513 "reserved": h.reserved_flag,
514 "dont_fragment": h.dont_fragment,
515 "more_fragments": h.more_fragments,
516 }),
517 );
518 obj.insert("fragment_offset".into(), json!(h.fragment_offset));
519 obj.insert(
520 "fragmented".into(),
521 json!(h.more_fragments || h.fragment_offset != 0),
522 );
523 obj.insert("ttl".into(), json!(h.ttl));
524 obj.insert("protocol".into(), json!(h.protocol));
525 obj.insert("protocol_name".into(), json!(ip_protocol_name(h.protocol)));
526 obj.insert("header_checksum".into(), json!(h.header_checksum));
527 obj.insert("options_present".into(), json!(h.options_present));
528 obj.insert("options_length".into(), json!(h.options_length));
529 }
530 IpHeader::V6(h) => {
531 obj.insert("traffic_class".into(), json!(h.traffic_class));
532 obj.insert("flow_label".into(), json!(h.flow_label));
533 obj.insert("ipv6_payload_length".into(), json!(h.payload_length));
536 obj.insert("next_header".into(), json!(h.next_header));
537 obj.insert(
538 "next_header_name".into(),
539 json!(ip_protocol_name(h.next_header)),
540 );
541 obj.insert("protocol".into(), json!(h.next_header));
542 obj.insert(
543 "protocol_name".into(),
544 json!(ip_protocol_name(h.next_header)),
545 );
546 obj.insert("hop_limit".into(), json!(h.hop_limit));
547 }
548 }
549
550 data
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub enum IpVersion {
560 V4,
561 V6,
562}
563
564#[derive(Debug, Clone, Copy, PartialEq, Eq)]
566pub enum RawIpTransport {
567 Raw,
569 Udp,
572}
573
574#[derive(Debug, Clone, PartialEq, Eq)]
576pub struct RawIpConfig {
577 pub protocol_number: u8,
578 pub ip_version: IpVersion,
579 pub transport: RawIpTransport,
580}
581
582impl RawIpConfig {
583 pub fn from_startup_params(params: Option<&StartupParams>) -> Result<Self> {
588 let params = params.ok_or_else(|| {
589 anyhow!(
590 "rawip requires a 'protocol_number' startup parameter naming the IP protocol \
591 to listen for (for example 47 for GRE, 50 for ESP, 132 for SCTP). It is \
592 generic by design and has no default number to fall back on."
593 )
594 })?;
595
596 let raw = params.get_i64("protocol_number")?;
597 if !(0..=255).contains(&raw) {
598 return Err(anyhow!(
599 "protocol_number must be an IP protocol number in 0..=255, got {raw}"
600 ));
601 }
602 let protocol_number = raw as u8;
603
604 if protocol_number == 6 || protocol_number == 17 {
605 let (name, feature) = if protocol_number == 6 {
606 ("TCP", "tcp")
607 } else {
608 ("UDP", "udp")
609 };
610 return Err(anyhow!(
611 "protocol_number {protocol_number} is {name}, which netget implements properly \
612 as its own protocol — start the '{feature}' server instead. A SOCK_RAW listener \
613 on {protocol_number} would compete with the kernel's own {name} stack for the \
614 same packets, and it cannot complete a {name} session in any case."
615 ));
616 }
617
618 let ip_version = match params.get_optional_string("ip_version")? {
619 None => IpVersion::V4,
620 Some(v) => match v.to_ascii_lowercase().as_str() {
621 "ipv4" | "v4" | "4" => IpVersion::V4,
622 "ipv6" | "v6" | "6" => IpVersion::V6,
623 other => {
624 return Err(anyhow!(
625 "ip_version must be \"ipv4\" or \"ipv6\", got \"{other}\""
626 ))
627 }
628 },
629 };
630
631 let transport = match params.get_optional_string("transport")? {
632 None => RawIpTransport::Raw,
633 Some(v) => match v.to_ascii_lowercase().as_str() {
634 "raw" => RawIpTransport::Raw,
635 "udp" => RawIpTransport::Udp,
636 other => {
637 return Err(anyhow!(
638 "transport must be \"raw\" (default) or \"udp\" (the unprivileged test \
639 transport), got \"{other}\""
640 ))
641 }
642 },
643 };
644
645 Ok(Self {
646 protocol_number,
647 ip_version,
648 transport,
649 })
650 }
651}
652
653enum Emitter {
659 Raw {
662 socket: std::sync::Mutex<Socket>,
663 version: IpVersion,
664 },
665 Udp { socket: Arc<UdpSocket> },
667}
668
669struct RawIpState {
670 config: RawIpConfig,
671 emitter: Emitter,
672 local_addr: SocketAddr,
673}
674
675impl RawIpState {
676 async fn emit(
680 &self,
681 destination: Option<IpAddr>,
682 ttl: Option<u8>,
683 payload: &[u8],
684 peer: Option<SocketAddr>,
685 ) -> Result<(usize, String)> {
686 match &self.emitter {
687 Emitter::Raw { socket, version } => {
688 let destination = destination.ok_or_else(|| {
689 anyhow!(
690 "send_rawip_packet needs a 'destination' IP address: a raw socket has \
691 no connection to reply on"
692 )
693 })?;
694 let n = send_raw(socket, *version, destination, ttl, payload)?;
695 Ok((n, destination.to_string()))
696 }
697 Emitter::Udp { socket } => {
698 let peer = peer.ok_or_else(|| {
699 anyhow!("the UDP test transport can only answer the peer that sent a datagram")
700 })?;
701 let n = socket.send_to(payload, peer).await?;
702 Ok((n, peer.to_string()))
703 }
704 }
705 }
706}
707
708fn send_raw(
710 socket: &std::sync::Mutex<Socket>,
711 version: IpVersion,
712 destination: IpAddr,
713 ttl: Option<u8>,
714 payload: &[u8],
715) -> Result<usize> {
716 let guard = socket
717 .lock()
718 .map_err(|_| anyhow!("rawip send socket mutex was poisoned by a previous panic"))?;
719 if let Some(ttl) = ttl {
720 match version {
721 IpVersion::V4 => guard.set_ttl(ttl as u32)?,
722 IpVersion::V6 => guard.set_unicast_hops_v6(ttl as u32)?,
723 }
724 }
725 let addr = SockAddr::from(SocketAddr::new(destination, 0));
726 Ok(guard.send_to(payload, &addr)?)
727}
728
729pub struct RawIpServer;
731
732impl RawIpServer {
733 pub async fn spawn_with_llm_actions(
741 listen_addr: SocketAddr,
742 llm_client: OllamaClient,
743 app_state: Arc<AppState>,
744 status_tx: mpsc::UnboundedSender<String>,
745 server_id: crate::state::ServerId,
746 startup_params: Option<StartupParams>,
747 ) -> Result<SocketAddr> {
748 let config = RawIpConfig::from_startup_params(startup_params.as_ref())?;
749
750 let protocol_label = match ip_protocol_name(config.protocol_number) {
751 Some(name) => format!("{} ({})", config.protocol_number, name),
752 None => format!("{} (no IANA name)", config.protocol_number),
753 };
754
755 match config.transport {
756 RawIpTransport::Raw => {
757 Self::spawn_raw(
758 listen_addr,
759 config,
760 protocol_label,
761 llm_client,
762 app_state,
763 status_tx,
764 server_id,
765 )
766 .await
767 }
768 RawIpTransport::Udp => {
769 Self::spawn_udp(
770 listen_addr,
771 config,
772 protocol_label,
773 llm_client,
774 app_state,
775 status_tx,
776 server_id,
777 )
778 .await
779 }
780 }
781 }
782
783 #[allow(clippy::too_many_arguments)]
784 async fn spawn_raw(
785 listen_addr: SocketAddr,
786 config: RawIpConfig,
787 protocol_label: String,
788 llm_client: OllamaClient,
789 app_state: Arc<AppState>,
790 status_tx: mpsc::UnboundedSender<String>,
791 server_id: crate::state::ServerId,
792 ) -> Result<SocketAddr> {
793 let domain = match config.ip_version {
794 IpVersion::V4 => Domain::IPV4,
795 IpVersion::V6 => Domain::IPV6,
796 };
797 let sock_protocol = SockProtocol::from(config.protocol_number as i32);
798
799 let recv_socket = Socket::new(domain, Type::RAW, Some(sock_protocol)).map_err(|e| {
800 anyhow!(
801 "failed to create the raw IP protocol-{} receive socket (needs root, or \
802 CAP_NET_RAW on Linux): {e}",
803 config.protocol_number
804 )
805 })?;
806 recv_socket.set_nonblocking(true).map_err(|e| {
807 anyhow!("failed to put the rawip receive socket in non-blocking mode: {e}")
808 })?;
809
810 let send_socket = Socket::new(domain, Type::RAW, Some(sock_protocol))
811 .map_err(|e| anyhow!("failed to create the rawip send socket: {e}"))?;
812
813 let version = config.ip_version;
814 let state = Arc::new(RawIpState {
815 config,
816 emitter: Emitter::Raw {
817 socket: std::sync::Mutex::new(send_socket),
818 version,
819 },
820 local_addr: listen_addr,
821 });
822
823 Log::new(Some(&status_tx)).info(format!(
824 "Raw IP server listening for IP protocol {protocol_label} (requires root)"
825 ));
826
827 let async_socket = AsyncFd::new(recv_socket)
828 .map_err(|e| anyhow!("failed to register the rawip socket with the reactor: {e}"))?;
829
830 let task_registrar = app_state.clone();
831 let handle = tokio::spawn(async move {
832 let mut buffer = vec![std::mem::MaybeUninit::<u8>::uninit(); 65535];
833 loop {
834 let mut guard = match async_socket.readable().await {
835 Ok(g) => g,
836 Err(e) => {
837 error!("rawip socket error: {e}");
838 break;
839 }
840 };
841
842 match guard.try_io(|inner| inner.get_ref().recv(&mut buffer)) {
843 Ok(Ok(0)) => continue,
844 Ok(Ok(n)) => {
845 let bytes = unsafe {
847 std::slice::from_raw_parts(buffer.as_ptr() as *const u8, n).to_vec()
848 };
849 Self::spawn_packet_task(
850 bytes,
851 None,
852 llm_client.clone(),
853 app_state.clone(),
854 status_tx.clone(),
855 state.clone(),
856 server_id,
857 )
858 .await;
859 }
860 Ok(Err(e)) => error!("rawip recv error: {e}"),
861 Err(_would_block) => continue,
862 }
863 }
864 warn!("rawip receive loop terminated");
865 });
866
867 task_registrar.register_server_task(server_id, handle).await;
868
869 Ok(listen_addr)
870 }
871
872 #[allow(clippy::too_many_arguments)]
873 async fn spawn_udp(
874 listen_addr: SocketAddr,
875 config: RawIpConfig,
876 protocol_label: String,
877 llm_client: OllamaClient,
878 app_state: Arc<AppState>,
879 status_tx: mpsc::UnboundedSender<String>,
880 server_id: crate::state::ServerId,
881 ) -> Result<SocketAddr> {
882 let socket =
883 Arc::new(UdpSocket::bind(listen_addr).await.map_err(|e| {
884 anyhow!("rawip UDP test transport could not bind {listen_addr}: {e}")
885 })?);
886 let local_addr = socket.local_addr()?;
887
888 let state = Arc::new(RawIpState {
889 config,
890 emitter: Emitter::Udp {
891 socket: socket.clone(),
892 },
893 local_addr,
894 });
895
896 Log::new(Some(&status_tx)).info(format!(
897 "Raw IP server on the UDP test transport at {local_addr}, decoding whole IP packets \
898 for protocol {protocol_label} (no raw socket, no privilege — not a production \
899 transport)"
900 ));
901
902 let task_registrar = app_state.clone();
903 let handle = tokio::spawn(async move {
904 let mut buffer = vec![0u8; 65535];
905 loop {
906 match socket.recv_from(&mut buffer).await {
907 Ok((n, peer)) => {
908 Self::spawn_packet_task(
909 buffer[..n].to_vec(),
910 Some(peer),
911 llm_client.clone(),
912 app_state.clone(),
913 status_tx.clone(),
914 state.clone(),
915 server_id,
916 )
917 .await;
918 }
919 Err(e) => {
920 Log::new(Some(&status_tx))
921 .error(format!("rawip UDP test transport receive error: {e}"));
922 break;
923 }
924 }
925 }
926 });
927
928 task_registrar.register_server_task(server_id, handle).await;
929
930 Ok(local_addr)
931 }
932
933 #[allow(clippy::too_many_arguments)]
934 async fn spawn_packet_task(
935 bytes: Vec<u8>,
936 peer: Option<SocketAddr>,
937 llm_client: OllamaClient,
938 app_state: Arc<AppState>,
939 status_tx: mpsc::UnboundedSender<String>,
940 state: Arc<RawIpState>,
941 server_id: crate::state::ServerId,
942 ) {
943 let task_owner = app_state.clone();
945 task_owner
946 .spawn_server_task(server_id, async move {
947 if let Err(e) = Self::handle_packet(
948 bytes, peer, llm_client, app_state, status_tx, state, server_id,
949 )
950 .await
951 {
952 error!("rawip packet handling failed: {e}");
953 }
954 })
955 .await;
956 }
957
958 #[allow(clippy::too_many_arguments)]
959 async fn handle_packet(
960 bytes: Vec<u8>,
961 peer: Option<SocketAddr>,
962 llm_client: OllamaClient,
963 app_state: Arc<AppState>,
964 status_tx: mpsc::UnboundedSender<String>,
965 state: Arc<RawIpState>,
966 server_id: crate::state::ServerId,
967 ) -> Result<()> {
968 let packet = match decode_ip_packet(&bytes) {
969 Ok(p) => p,
970 Err(e) => {
971 Log::new(Some(&status_tx)).debug(format!(
975 "rawip dropped {} bytes: {e} (first bytes: {})",
976 bytes.len(),
977 truncate_for_log(&hex::encode(&bytes), 64)
978 ));
979 return Ok(());
980 }
981 };
982
983 let source = packet.header.source();
984 let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
985
986 Self::record_connection(
987 &app_state,
988 server_id,
989 connection_id,
990 SocketAddr::new(source, 0),
991 state.local_addr,
992 bytes.len(),
993 )
994 .await;
995 let _ = status_tx.send("__UPDATE_UI__".to_string());
996
997 let (shown_encoding, shown_payload) = encode_payload_for_event(&packet.payload);
998 Log::new(Some(&status_tx)).debug(format!(
999 "rawip IPv{} {} -> {} proto={} payload={} bytes",
1000 packet.header.version(),
1001 source,
1002 packet.header.destination(),
1003 packet.header.upper_protocol(),
1004 packet.payload.len()
1005 ));
1006 Log::new(Some(&status_tx)).trace(format!(
1007 "rawip payload ({}): {}",
1008 shown_encoding.as_str(),
1009 truncate_for_log(&shown_payload, 512)
1010 ));
1011
1012 let event = Event::new(
1013 &RAWIP_PACKET_RECEIVED_EVENT,
1014 packet_event_data(&packet, state.config.protocol_number, connection_id),
1015 );
1016
1017 let protocol = RawIpProtocol::new();
1018 let result = call_llm(
1019 &llm_client,
1020 &app_state,
1021 server_id,
1022 Some(connection_id),
1023 &event,
1024 &protocol,
1025 )
1026 .await;
1027
1028 match result {
1029 Ok(execution) => {
1030 for message in &execution.messages {
1031 Log::new(Some(&status_tx)).info(message.to_string());
1032 }
1033
1034 let answered_nothing = execution.raw_actions.is_empty();
1035 let explicit_silence = !answered_nothing
1036 && execution.raw_actions.iter().all(|a| {
1037 a.get("type").and_then(|v| v.as_str()) == Some(actions::NO_RESPONSE)
1038 });
1039 let action_failures = execution.failures.len();
1040
1041 let mut packets_sent = 0usize;
1042 for protocol_result in execution.protocol_results {
1043 let ActionResult::Custom { name, data } = &protocol_result else {
1044 continue;
1045 };
1046 if name != actions::RAWIP_PACKET_RESULT {
1047 continue;
1048 }
1049
1050 let destination = data
1051 .get("destination")
1052 .and_then(|v| v.as_str())
1053 .and_then(|s| s.parse::<IpAddr>().ok());
1054 let ttl = data
1055 .get("ttl")
1056 .and_then(|v| v.as_u64())
1057 .map(|v| v.min(255) as u8);
1058 let payload = match data.get("payload_hex").and_then(|v| v.as_str()) {
1059 Some(h) => match hex::decode(h) {
1060 Ok(bytes) => bytes,
1061 Err(e) => {
1062 Log::new(Some(&status_tx)).error(format!(
1063 "rawip could not re-read its own executor output: {e}"
1064 ));
1065 continue;
1066 }
1067 },
1068 None => continue,
1069 };
1070
1071 match state.emit(destination, ttl, &payload, peer).await {
1072 Ok((n, where_to)) => {
1073 packets_sent += 1;
1074 app_state
1078 .update_connection_stats(
1079 server_id,
1080 connection_id,
1081 None,
1082 Some(n as u64),
1083 None,
1084 Some(1),
1085 )
1086 .await;
1087 Log::new(Some(&status_tx))
1088 .debug(format!("rawip sent {n} payload bytes to {where_to}"));
1089 Log::new(Some(&status_tx)).trace(format!(
1090 "rawip sent (hex): {}",
1091 truncate_for_log(&hex::encode(&payload), 512)
1092 ));
1093 }
1094 Err(e) => {
1095 Log::new(Some(&status_tx)).error(format!("rawip send failed: {e}"));
1096 }
1097 }
1098 }
1099
1100 let decision = if packets_sent > 0 {
1105 "model_reply"
1106 } else if explicit_silence {
1107 "model_reject"
1108 } else if answered_nothing {
1109 "model_silent"
1110 } else if action_failures > 0 {
1111 "fail_closed_action_error"
1112 } else {
1113 "fail_closed_no_reply"
1114 };
1115
1116 let summary = format!(
1117 "rawip proto={} from {} decision={} sent={}",
1118 state.config.protocol_number, source, decision, packets_sent
1119 );
1120 if decision.starts_with("fail_closed_") {
1121 Log::new(Some(&status_tx)).error(format!("{summary} (nothing on the wire)"));
1122 } else {
1123 Log::new(Some(&status_tx)).info(summary);
1124 }
1125 }
1126 Err(e) => {
1127 let category = crate::utils::WireFailure::classify(&e);
1137 Log::new(Some(&status_tx)).error(format!(
1138 "rawip proto={} from {} decision=fail_closed_llm_error category={} \
1139 (no packet sent: a generic IP protocol has no error frame, and inventing \
1140 one would be a guess at a protocol netget does not implement): {e}",
1141 state.config.protocol_number,
1142 source,
1143 if category.is_overloaded() {
1144 "overloaded"
1145 } else {
1146 "unavailable"
1147 },
1148 ));
1149 }
1150 }
1151
1152 Ok(())
1153 }
1154
1155 async fn record_connection(
1156 app_state: &AppState,
1157 server_id: crate::state::ServerId,
1158 connection_id: ConnectionId,
1159 remote_addr: SocketAddr,
1160 local_addr: SocketAddr,
1161 bytes_received: usize,
1162 ) {
1163 use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
1164
1165 let now = crate::utils::clock::Instant::now();
1166 app_state
1167 .add_connection_to_server(
1168 server_id,
1169 ConnectionState {
1170 id: connection_id,
1171 remote_addr,
1172 local_addr,
1173 bytes_sent: 0,
1174 bytes_received: bytes_received as u64,
1175 packets_sent: 0,
1176 packets_received: 1,
1177 last_activity: now,
1178 status: ConnectionStatus::Active,
1179 status_changed_at: now,
1180 protocol_info: ProtocolConnectionInfo::empty(),
1181 },
1182 )
1183 .await;
1184 debug!("rawip tracked packet from {remote_addr} as {connection_id}");
1185 }
1186}