Skip to main content

netget/server/rawip/
mod.rs

1//! Generic raw IP protocol-N server.
2//!
3//! This is the **generic home for IP protocols that have no other** — GRE (47), ESP (50),
4//! AH (51), SCTP (132), and anything else the operator names by number. It binds one
5//! `SOCK_RAW` socket for one operator-chosen IP protocol number, decodes the **IP header**
6//! (which is generic: the same twenty bytes whatever rides on top), surfaces the payload as
7//! opaque bytes, and lets the model decide what to do.
8//!
9//! # The rule that defines this module
10//!
11//! **There is no per-protocol branching here, and there must never be.** No
12//! `match protocol_number { 47 => parse_gre(), 50 => parse_esp(), .. }`. The moment this file
13//! knows about GRE specifically it has become the wrong thing, and the next change adds ESP,
14//! then L2TP, and it is the centralized per-protocol logic `CLAUDE.md`'s decentralization rule
15//! forbids. A protocol that deserves real parsing deserves its own module under `src/server/`.
16//!
17//! The one table keyed by protocol number is [`ip_protocol_name`], and it maps a number to the
18//! IANA *name* so the model is told "47 (GRE)" rather than "47". A name is not behaviour: no
19//! code path branches on it, and adding an entry cannot change what the server does.
20//!
21//! # Layout
22//!
23//! The **decoder is deliberately split from the transport**: [`decode_ip_packet`] and its
24//! helpers are pure functions over a byte slice with no I/O, no state and no privilege, so
25//! they are tested against literal packet bytes (`tests/server/rawip/e2e_test.rs`). The
26//! transport below is the part that has never been executed — a raw socket needs root.
27
28pub 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
51// ============================================================================
52// Pure IP header decoding — no I/O, no privilege, no protocol-specific branching.
53// ============================================================================
54
55/// Minimum length of an IPv4 header, in bytes (RFC 791 §3.1).
56pub const IPV4_MIN_HEADER_LEN: usize = 20;
57/// Length of the fixed IPv6 header, in bytes (RFC 8200 §3).
58pub const IPV6_HEADER_LEN: usize = 40;
59
60/// How much of the payload is placed in the event handed to the model.
61///
62/// A 64 KB packet hex-encodes to 128 KB of prompt, which no model reads and every model pays
63/// for. The full length is always reported in `payload_length`, and `payload_truncated` says
64/// whether what the model sees is the whole thing.
65pub const EVENT_PAYLOAD_LIMIT: usize = 2048;
66
67/// Why a buffer could not be read as an IP packet.
68///
69/// Every variant is a **refusal**, never a panic: the bytes come off the wire from a stranger,
70/// and this decoder is the only thing between them and the server loop.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum IpDecodeError {
73    /// Fewer bytes than the version's fixed header needs.
74    TooShort {
75        got: usize,
76        need: usize,
77        version: u8,
78    },
79    /// The version nibble was neither 4 nor 6.
80    UnsupportedVersion(u8),
81    /// IPv4 IHL below the legal minimum of 5 words (20 bytes).
82    IhlTooSmall(u8),
83    /// IPv4 IHL claims a header longer than the bytes actually captured.
84    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/// The IPv4 header fields, decoded (RFC 791 §3.1).
116#[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    /// Bit 0 of the flags field. Must be zero; a peer setting it is worth surfacing.
125    pub reserved_flag: bool,
126    pub dont_fragment: bool,
127    pub more_fragments: bool,
128    /// In 8-byte units, exactly as it appears on the wire.
129    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/// The fixed IPv6 header fields, decoded (RFC 8200 §3).
140///
141/// Extension headers are **not** walked. `next_header` is reported as it appears; if it names
142/// an extension header rather than an upper-layer protocol, the payload begins with that
143/// extension header and the model is looking at it. Walking the chain would mean knowing which
144/// numbers are extension headers and how each one is sized — per-protocol knowledge this
145/// module deliberately does not hold.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct Ipv6Header {
148    pub traffic_class: u8,
149    pub flow_label: u32,
150    /// The header's own Payload Length field, in bytes. Not necessarily what was captured.
151    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/// A decoded IP header, either version.
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum IpHeader {
161    V4(Ipv4Header),
162    V6(Ipv6Header),
163}
164
165impl IpHeader {
166    /// 4 or 6.
167    pub fn version(&self) -> u8 {
168        match self {
169            IpHeader::V4(_) => 4,
170            IpHeader::V6(_) => 6,
171        }
172    }
173
174    /// The number identifying what rides on top: IPv4 `protocol`, IPv6 `next_header`.
175    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/// A decoded packet: its header, and the payload that followed it.
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct DecodedPacket {
200    pub header: IpHeader,
201    pub payload: Vec<u8>,
202    /// True when the header's length field promised more payload than was captured. A raw
203    /// socket read that fills the buffer, or a datagram cut short, both land here — the model
204    /// is told rather than being handed a short payload silently.
205    pub payload_incomplete: bool,
206}
207
208/// Decode an IP header and split off its payload.
209///
210/// Version-generic: dispatches on the version nibble only, which is not per-protocol logic —
211/// IPv4 and IPv6 are different header formats, not different protocols carried *by* IP.
212///
213/// Returns `Err` for anything that is not a well-formed header. It never panics and never
214/// indexes past the slice: a hostile or truncated packet is refused, which is the whole point
215/// of this function existing separately from the socket that produced the bytes.
216pub 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    // Trust `total_length` only as an upper bound, and only when it is self-consistent. A
280    // peer that lies about it must not be able to make us read past the buffer, and BSD raw
281    // sockets have historically delivered this field in host byte order, which would make a
282    // strict reading wrong on macOS.
283    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
342/// The IANA name for an IP protocol number, where one is well known.
343///
344/// A **table of names, not a table of behaviour.** Nothing in this module branches on the
345/// result: it is placed in the event so the model reads "47 (GRE)" instead of "47", and an
346/// unknown number is reported as a number, which is a perfectly usable answer. Adding an
347/// entry here can change no decoding, no framing and no reply — that is exactly why it does
348/// not violate the no-per-protocol-logic rule.
349///
350/// Source: IANA "Assigned Internet Protocol Numbers".
351pub 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/// How a payload is represented in an event or an action.
402///
403/// **Explicit on both directions, never sniffed on the way out.** `"48656c6c6f"` is
404/// simultaneously valid text and valid hex and only the sender knows which it meant — the
405/// `send_tcp_data` defect `CLAUDE.md` records was exactly this, documented as accepting hex
406/// and then calling `as_bytes()` on it. Inbound, the server states which encoding it chose;
407/// outbound, [`decode_payload`] honours what the action says and rejects anything else.
408#[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    /// Parse the `encoding` field of an action. Unknown values are an error, not a fallback:
423    /// silently treating `"base64"` as utf8 would put the base64 text itself on the wire.
424    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
435/// Turn action payload text into the exact bytes to put on the wire.
436///
437/// This is the half that must actually exist. An action documented as accepting hex whose
438/// executor calls `as_bytes()` puts the ASCII of the hex digits on the wire, which is the
439/// reference defect in `CLAUDE.md`'s action design rules.
440pub 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
456/// Choose how to show an opaque payload to the model, and say which was chosen.
457///
458/// Text that is genuinely readable is shown as text; anything else is hex. The choice is
459/// reported in the event as `payload_encoding`, so the model can echo the same value back in
460/// its action and the round trip is symmetric.
461pub 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
474/// Build the `rawip_packet_received` event body for a decoded packet.
475///
476/// Kept pure and separate from the socket so it can be asserted directly against literal
477/// packet bytes.
478pub 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            // The header's own field, distinct from `payload_length` above, which is how
534            // many payload bytes were actually captured.
535            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// ============================================================================
554// Configuration — every declared startup parameter is parsed here, and nowhere else.
555// ============================================================================
556
557/// Which IP version the socket speaks.
558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub enum IpVersion {
560    V4,
561    V6,
562}
563
564/// Which transport carries packets to this server.
565#[derive(Debug, Clone, Copy, PartialEq, Eq)]
566pub enum RawIpTransport {
567    /// The real thing: `SOCK_RAW` on the configured protocol number. Needs root/`CAP_NET_RAW`.
568    Raw,
569    /// A UDP socket that carries **whole IP packets as datagrams**, so the decode → event →
570    /// LLM → action path can be exercised without privilege. Never a production transport.
571    Udp,
572}
573
574/// The operator's configuration, parsed from the declared startup parameters.
575#[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    /// Parse and validate the three declared startup parameters.
584    ///
585    /// Errors are propagated with `?`, never unwrapped: these values come from the model or
586    /// an MCP client, and a panic here kills the per-request task before it can reply.
587    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
653// ============================================================================
654// Transport
655// ============================================================================
656
657/// Whatever this server writes packets with.
658enum Emitter {
659    /// A second raw socket, as ICMP does, so a reply never races the receive socket's state.
660    /// The `Mutex` is `std`'s and is never held across an `.await`: `send_raw` is synchronous.
661    Raw {
662        socket: std::sync::Mutex<Socket>,
663        version: IpVersion,
664    },
665    /// The test transport writes the payload back to the peer that sent the datagram.
666    Udp { socket: Arc<UdpSocket> },
667}
668
669struct RawIpState {
670    config: RawIpConfig,
671    emitter: Emitter,
672    local_addr: SocketAddr,
673}
674
675impl RawIpState {
676    /// Write `payload` and report how many bytes went out.
677    ///
678    /// `peer` is the datagram source on the UDP test transport and `None` on the raw one.
679    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
708/// Synchronous by construction, so no lock guard can be alive across an `.await`.
709fn 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
729/// Generic raw IP protocol-N server.
730pub struct RawIpServer;
731
732impl RawIpServer {
733    /// Bind the socket and start receiving.
734    ///
735    /// **Readiness is awaited and failure is returned.** A raw socket cannot be opened without
736    /// privilege, and a server sitting in `Running` having captured nothing is the exact
737    /// ARP/DataLink/ICMP defect the root `CLAUDE.md` records — so the socket is created here,
738    /// before any task is spawned, and its error propagates out of `spawn()` so
739    /// `server_startup` records `ServerStatus::Error`.
740    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                        // SAFETY: `recv` reported `n` bytes initialised at the front.
846                        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        // Tracked, not detached: stop_server must abort this task too.
944        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                // A packet we cannot read is dropped and logged. It is not handed to the
972                // model as "here are some bytes": the event's whole contract is that its
973                // header fields are decoded.
974                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                            // `record_connection` seeds `bytes_received` at creation and
1075                            // nothing updated `bytes_sent`, so the rail's `up` counter stayed
1076                            // at zero for the life of the server however much it emitted.
1077                            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                // The wire cannot carry the distinction — every one of these outcomes is
1101                // silence to the peer — so the log has to. The tokens are stable so an
1102                // operator can grep `decision=fail_closed_` for every packet netget failed
1103                // to answer, as distinct from one the model deliberately did not answer.
1104                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                // **Silence is the correct answer here, and it is not a compromise.**
1128                // NetGet does not know what a peer speaking an arbitrary IP protocol
1129                // expects: this server is generic precisely because the protocol above IP is
1130                // one it deliberately does not understand. There is no error frame to send,
1131                // because there is no protocol to send one in — any bytes emitted would be a
1132                // guess, and a guess a peer parses is worse than a packet it never receives.
1133                // So nothing derived from `e` reaches the socket; the failure is reported
1134                // to the operator here and nowhere else. No `WireFailure` text is written to
1135                // the wire, deliberately: there is no wire format to write it in.
1136                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}