Skip to main content

netget/server/tuntap/
packet.rs

1//! Pure packet decoding, building and filtering for the TUN/TAP endpoint.
2//!
3//! **Nothing in this file performs I/O.** That is deliberate: creating a TUN interface needs
4//! root on every platform, so the transport can never run in the test suite, while everything
5//! that decides *what NetGet says and to whom* lives here and is tested against literal packet
6//! bytes. See `src/server/tuntap/CLAUDE.md` for the split and what it does and does not prove.
7//!
8//! Four things live here:
9//!
10//! * [`PacketInformation`] — the 4-byte header macOS `utun` prepends to every packet and Linux
11//!   does not (unless `IFF_NO_PI` is left unset). This is the single most common source of
12//!   "why is every packet off by four bytes", so it is an explicit, named, tested mode rather
13//!   than an assumption.
14//! * [`decode`] — a frame in, structured header fields out. Never raw bytes: the model is given
15//!   `{"protocol": "tcp", "source_port": 443, "tcp_flags": ["syn"]}`, not base64.
16//! * [`build_packet`] — the inverse, for the `send_packet` action. The model describes a packet
17//!   by its fields and NetGet lays out the headers and computes every checksum.
18//! * [`PacketFilter`] — the deterministic gate that decides which packets are even *allowed* to
19//!   become an event. This is load-bearing: a per-packet LLM call is hopelessly slow, so the
20//!   filter runs first, in native code, on every packet.
21
22use crate::utils::truncate_for_log;
23use serde_json::{json, Value};
24use std::fmt;
25use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
26
27/// Bytes of a payload surfaced to the model in `payload_preview`.
28///
29/// A preview, not the payload: the point of the event is the decoded header, and a model
30/// cannot do anything useful with a kilobyte of TLS ciphertext.
31pub const PAYLOAD_PREVIEW_BYTES: usize = 64;
32
33/// Ethernet header length (TAP only).
34pub const ETHERNET_HEADER_LEN: usize = 14;
35
36/// Length of the platform packet-information header, where one exists.
37pub const PACKET_INFORMATION_LEN: usize = 4;
38
39// ---------------------------------------------------------------------------
40// Errors
41// ---------------------------------------------------------------------------
42
43/// Why a frame could not be decoded.
44///
45/// Decode failures are *normal* on a real interface — a truncated read, a protocol NetGet does
46/// not model, a link-layer frame that is not IP. They are counted and dropped, never answered.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum DecodeError {
49    /// The frame was shorter than the header it claims to carry.
50    TooShort {
51        /// What was being read when the frame ran out.
52        what: &'static str,
53        /// Bytes needed.
54        need: usize,
55        /// Bytes available.
56        have: usize,
57    },
58    /// The IP version nibble was neither 4 nor 6.
59    UnsupportedIpVersion(u8),
60    /// The platform packet-information header did not describe an IP packet.
61    BadPacketInformation {
62        /// The four bytes that were read.
63        header: [u8; 4],
64    },
65    /// A TAP frame carried an EtherType this endpoint does not decode.
66    UnsupportedEtherType(u16),
67    /// An IPv4 header declared an IHL below the 20-byte minimum.
68    BadIhl(u8),
69}
70
71impl fmt::Display for DecodeError {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            Self::TooShort { what, need, have } => write!(
75                f,
76                "frame too short for {what}: need {need} bytes, have {have}"
77            ),
78            Self::UnsupportedIpVersion(v) => write!(f, "unsupported IP version {v}"),
79            Self::BadPacketInformation { header } => write!(
80                f,
81                "packet-information header {:02x?} does not describe an IP packet",
82                header
83            ),
84            Self::UnsupportedEtherType(t) => write!(f, "unsupported EtherType 0x{t:04x}"),
85            Self::BadIhl(ihl) => write!(f, "IPv4 IHL {ihl} is below the 5-word minimum"),
86        }
87    }
88}
89
90impl std::error::Error for DecodeError {}
91
92/// Why a `send_packet` action could not be turned into bytes.
93///
94/// Every one of these is a *refusal*, and the caller drops the packet. NetGet never guesses at
95/// a field the model left out in a way that would put a different packet on the wire than the
96/// model described.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct BuildError(pub String);
99
100impl fmt::Display for BuildError {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.write_str(&self.0)
103    }
104}
105
106impl std::error::Error for BuildError {}
107
108fn bad(msg: impl Into<String>) -> BuildError {
109    BuildError(msg.into())
110}
111
112// ---------------------------------------------------------------------------
113// Link mode and packet information
114// ---------------------------------------------------------------------------
115
116/// Layer the interface operates at.
117///
118/// TUN is layer 3 — the frame *is* an IP packet. TAP is layer 2 — the frame is an Ethernet
119/// frame with an IP packet inside it.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum LinkMode {
122    /// Layer 3: bare IP packets.
123    Tun,
124    /// Layer 2: Ethernet frames.
125    Tap,
126}
127
128impl LinkMode {
129    /// Parse the `mode` startup parameter.
130    pub fn parse(s: &str) -> Result<Self, String> {
131        match s.trim().to_ascii_lowercase().as_str() {
132            "tun" | "l3" | "layer3" => Ok(Self::Tun),
133            "tap" | "l2" | "layer2" => Ok(Self::Tap),
134            other => Err(format!(
135                "mode must be \"tun\" (layer 3, bare IP) or \"tap\" (layer 2, Ethernet), got {other:?}"
136            )),
137        }
138    }
139
140    /// The name used in parameters, logs and event data.
141    pub fn as_str(&self) -> &'static str {
142        match self {
143            Self::Tun => "tun",
144            Self::Tap => "tap",
145        }
146    }
147}
148
149/// The platform's per-packet prefix.
150///
151/// # The off-by-four
152///
153/// macOS `utun` *always* prepends four bytes to every packet in both directions: the address
154/// family as a big-endian `u32` (`AF_INET` = 2, `AF_INET6` = 30). Linux `/dev/net/tun` prepends
155/// nothing when the device was opened with `IFF_NO_PI`, and a different four bytes when it was
156/// not: two bytes of flags followed by the big-endian EtherType (`0x0800` / `0x86DD`).
157///
158/// Getting this wrong does not fail loudly. The IP version nibble lands in the middle of the
159/// prefix, so the packet decodes as garbage rather than erroring, and the symptom is "every
160/// field is wrong" rather than "the header is misaligned".
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum PacketInformation {
163    /// No prefix — Linux with `IFF_NO_PI`, and what the `tun` crate hands us after it has
164    /// normalised the platform difference itself.
165    None,
166    /// macOS `utun`: four bytes of big-endian address family.
167    MacOsUtun,
168    /// Linux without `IFF_NO_PI`: two bytes of flags, two bytes of big-endian EtherType.
169    LinuxTunPi,
170}
171
172/// macOS `AF_INET`.
173const AF_INET: u32 = 2;
174/// macOS `AF_INET6`. (Linux's is 10; this constant is only used for `utun`.)
175const AF_INET6_DARWIN: u32 = 30;
176/// EtherType for IPv4.
177pub const ETHERTYPE_IPV4: u16 = 0x0800;
178/// EtherType for IPv6.
179pub const ETHERTYPE_IPV6: u16 = 0x86DD;
180
181impl PacketInformation {
182    /// Parse the `packet_information` startup parameter.
183    ///
184    /// `auto` maps to [`PacketInformation::None`]: the `tun` crate strips and re-adds the
185    /// platform prefix itself (`posix::Tun::new` sets a read/write `offset` of
186    /// [`PACKET_INFORMATION_LEN`] whenever packet information is enabled), so by the time a
187    /// frame reaches NetGet it is already a bare packet. The other values exist for a raw fd
188    /// handed in from outside that machinery, and for the tests that pin the layout.
189    pub fn parse(s: &str) -> Result<Self, String> {
190        match s.trim().to_ascii_lowercase().as_str() {
191            "auto" | "none" => Ok(Self::None),
192            "macos_utun" | "macos" | "utun" => Ok(Self::MacOsUtun),
193            "linux_pi" | "linux" => Ok(Self::LinuxTunPi),
194            other => Err(format!(
195                "packet_information must be one of \"auto\", \"none\", \"macos_utun\", \
196                 \"linux_pi\", got {other:?}"
197            )),
198        }
199    }
200
201    /// Bytes this mode prepends.
202    pub fn header_len(&self) -> usize {
203        match self {
204            Self::None => 0,
205            Self::MacOsUtun | Self::LinuxTunPi => PACKET_INFORMATION_LEN,
206        }
207    }
208
209    /// Remove the prefix, returning the packet itself.
210    ///
211    /// The prefix is *validated*, not skipped: a header that does not name IPv4 or IPv6 means
212    /// the configured mode does not match the device, and silently advancing four bytes there
213    /// is exactly the failure this type exists to prevent.
214    pub fn strip<'a>(&self, frame: &'a [u8]) -> Result<&'a [u8], DecodeError> {
215        match self {
216            Self::None => Ok(frame),
217            Self::MacOsUtun | Self::LinuxTunPi => {
218                if frame.len() < PACKET_INFORMATION_LEN {
219                    return Err(DecodeError::TooShort {
220                        what: "packet-information header",
221                        need: PACKET_INFORMATION_LEN,
222                        have: frame.len(),
223                    });
224                }
225                let mut header = [0u8; 4];
226                header.copy_from_slice(&frame[..PACKET_INFORMATION_LEN]);
227                let ok = match self {
228                    Self::MacOsUtun => {
229                        let af = u32::from_be_bytes(header);
230                        af == AF_INET || af == AF_INET6_DARWIN
231                    }
232                    Self::LinuxTunPi => {
233                        let proto = u16::from_be_bytes([header[2], header[3]]);
234                        proto == ETHERTYPE_IPV4 || proto == ETHERTYPE_IPV6
235                    }
236                    Self::None => unreachable!(),
237                };
238                if !ok {
239                    return Err(DecodeError::BadPacketInformation { header });
240                }
241                Ok(&frame[PACKET_INFORMATION_LEN..])
242            }
243        }
244    }
245
246    /// The prefix that must precede an outgoing packet, if any.
247    pub fn prefix_for(&self, ipv6: bool) -> Option<[u8; 4]> {
248        match self {
249            Self::None => None,
250            Self::MacOsUtun => Some(if ipv6 {
251                AF_INET6_DARWIN.to_be_bytes()
252            } else {
253                AF_INET.to_be_bytes()
254            }),
255            Self::LinuxTunPi => {
256                let proto = if ipv6 { ETHERTYPE_IPV6 } else { ETHERTYPE_IPV4 };
257                let [a, b] = proto.to_be_bytes();
258                Some([0, 0, a, b])
259            }
260        }
261    }
262
263    /// Prepend the platform prefix to a freshly built packet, in place.
264    pub fn prepend(&self, packet: &mut Vec<u8>) -> Result<(), BuildError> {
265        let ipv6 = match packet.first() {
266            Some(b) => match b >> 4 {
267                4 => false,
268                6 => true,
269                v => return Err(bad(format!("cannot prefix a packet of IP version {v}"))),
270            },
271            None => return Err(bad("cannot prefix an empty packet")),
272        };
273        if let Some(prefix) = self.prefix_for(ipv6) {
274            packet.splice(0..0, prefix);
275        }
276        Ok(())
277    }
278}
279
280// ---------------------------------------------------------------------------
281// Protocol and ICMP naming
282// ---------------------------------------------------------------------------
283
284/// IANA protocol number → name, for the protocols this endpoint can talk about.
285///
286/// The event carries **both** the name and the number. A model reasons far better about
287/// `"tcp"` than about `6`, and the number keeps the event lossless for everything unnamed.
288pub fn ip_protocol_name(number: u8) -> &'static str {
289    match number {
290        0 => "hopopt",
291        1 => "icmp",
292        2 => "igmp",
293        4 => "ipv4",
294        6 => "tcp",
295        17 => "udp",
296        41 => "ipv6",
297        43 => "ipv6-route",
298        44 => "ipv6-frag",
299        47 => "gre",
300        50 => "esp",
301        51 => "ah",
302        58 => "icmpv6",
303        59 => "ipv6-nonxt",
304        60 => "ipv6-opts",
305        89 => "ospf",
306        112 => "vrrp",
307        132 => "sctp",
308        _ => "unknown",
309    }
310}
311
312/// Name → IANA protocol number, the inverse of [`ip_protocol_name`] for the names a model is
313/// likely to write. Returns `None` for a name with no number.
314pub fn ip_protocol_number(name: &str) -> Option<u8> {
315    Some(match name.trim().to_ascii_lowercase().as_str() {
316        "hopopt" => 0,
317        "icmp" => 1,
318        "igmp" => 2,
319        "tcp" => 6,
320        "udp" => 17,
321        "gre" => 47,
322        "esp" => 50,
323        "ah" => 51,
324        "icmpv6" | "ipv6-icmp" => 58,
325        "ospf" => 89,
326        "vrrp" => 112,
327        "sctp" => 132,
328        _ => return None,
329    })
330}
331
332/// ICMPv4 type number → name.
333pub fn icmpv4_type_name(t: u8) -> &'static str {
334    match t {
335        0 => "echo_reply",
336        3 => "destination_unreachable",
337        4 => "source_quench",
338        5 => "redirect",
339        8 => "echo_request",
340        9 => "router_advertisement",
341        10 => "router_solicitation",
342        11 => "time_exceeded",
343        12 => "parameter_problem",
344        13 => "timestamp",
345        14 => "timestamp_reply",
346        _ => "unknown",
347    }
348}
349
350/// ICMPv6 type number → name.
351pub fn icmpv6_type_name(t: u8) -> &'static str {
352    match t {
353        1 => "destination_unreachable",
354        2 => "packet_too_big",
355        3 => "time_exceeded",
356        4 => "parameter_problem",
357        128 => "echo_request",
358        129 => "echo_reply",
359        133 => "router_solicitation",
360        134 => "router_advertisement",
361        135 => "neighbor_solicitation",
362        136 => "neighbor_advertisement",
363        _ => "unknown",
364    }
365}
366
367/// Name → ICMP type number, per IP version. `None` when the name is not known.
368pub fn icmp_type_number(name: &str, ipv6: bool) -> Option<u8> {
369    let name = name.trim().to_ascii_lowercase();
370    let n = if ipv6 {
371        match name.as_str() {
372            "destination_unreachable" => 1,
373            "packet_too_big" => 2,
374            "time_exceeded" => 3,
375            "parameter_problem" => 4,
376            "echo_request" => 128,
377            "echo_reply" => 129,
378            "router_solicitation" => 133,
379            "router_advertisement" => 134,
380            "neighbor_solicitation" => 135,
381            "neighbor_advertisement" => 136,
382            _ => return None,
383        }
384    } else {
385        match name.as_str() {
386            "echo_reply" => 0,
387            "destination_unreachable" => 3,
388            "source_quench" => 4,
389            "redirect" => 5,
390            "echo_request" => 8,
391            "router_advertisement" => 9,
392            "router_solicitation" => 10,
393            "time_exceeded" => 11,
394            "parameter_problem" => 12,
395            "timestamp" => 13,
396            "timestamp_reply" => 14,
397            _ => return None,
398        }
399    };
400    Some(n)
401}
402
403/// The TCP flag bits, low to high, with the names the event and `send_packet` both use.
404pub const TCP_FLAG_NAMES: [(u8, &str); 8] = [
405    (0x01, "fin"),
406    (0x02, "syn"),
407    (0x04, "rst"),
408    (0x08, "psh"),
409    (0x10, "ack"),
410    (0x20, "urg"),
411    (0x40, "ece"),
412    (0x80, "cwr"),
413];
414
415/// Expand a TCP flags byte into the names set in it.
416pub fn tcp_flag_names(bits: u8) -> Vec<&'static str> {
417    TCP_FLAG_NAMES
418        .iter()
419        .filter(|(bit, _)| bits & bit != 0)
420        .map(|(_, name)| *name)
421        .collect()
422}
423
424/// Collapse flag names back into the byte. Unknown names are refused rather than ignored —
425/// a typo that silently sends a packet with no flags is worse than a rejection.
426pub fn tcp_flag_bits(names: &[String]) -> Result<u8, BuildError> {
427    let mut bits = 0u8;
428    for name in names {
429        let lower = name.trim().to_ascii_lowercase();
430        match TCP_FLAG_NAMES.iter().find(|(_, n)| *n == lower) {
431            Some((bit, _)) => bits |= bit,
432            None => {
433                return Err(bad(format!(
434                    "unknown TCP flag {name:?}; valid flags are {}",
435                    TCP_FLAG_NAMES
436                        .iter()
437                        .map(|(_, n)| *n)
438                        .collect::<Vec<_>>()
439                        .join(", ")
440                )))
441            }
442        }
443    }
444    Ok(bits)
445}
446
447// ---------------------------------------------------------------------------
448// Decoded representation
449// ---------------------------------------------------------------------------
450
451/// The Ethernet header of a TAP frame.
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub struct EthernetHeader {
454    /// Destination MAC, colon-separated lowercase hex.
455    pub destination_mac: String,
456    /// Source MAC, colon-separated lowercase hex.
457    pub source_mac: String,
458    /// EtherType.
459    pub ethertype: u16,
460}
461
462/// Format six bytes as `aa:bb:cc:dd:ee:ff`.
463pub fn format_mac(bytes: &[u8]) -> String {
464    bytes
465        .iter()
466        .map(|b| format!("{b:02x}"))
467        .collect::<Vec<_>>()
468        .join(":")
469}
470
471/// Parse `aa:bb:cc:dd:ee:ff` (or the `-` separated form) into six bytes.
472pub fn parse_mac(s: &str) -> Result<[u8; 6], BuildError> {
473    let parts: Vec<&str> = s.split([':', '-']).collect();
474    if parts.len() != 6 {
475        return Err(bad(format!(
476            "MAC address {s:?} must have six colon-separated octets"
477        )));
478    }
479    let mut out = [0u8; 6];
480    for (i, p) in parts.iter().enumerate() {
481        out[i] = u8::from_str_radix(p, 16)
482            .map_err(|_| bad(format!("MAC address {s:?} has a non-hex octet {p:?}")))?;
483    }
484    Ok(out)
485}
486
487/// Transport-layer detail, as far as this endpoint decodes it.
488#[derive(Debug, Clone, PartialEq, Eq)]
489pub enum Transport {
490    /// TCP, with the fields a policy decision is actually made on.
491    Tcp {
492        /// Source port.
493        source_port: u16,
494        /// Destination port.
495        destination_port: u16,
496        /// Sequence number.
497        seq: u32,
498        /// Acknowledgement number.
499        ack: u32,
500        /// Raw flag bits.
501        flags: u8,
502        /// Advertised window.
503        window: u16,
504    },
505    /// UDP.
506    Udp {
507        /// Source port.
508        source_port: u16,
509        /// Destination port.
510        destination_port: u16,
511    },
512    /// ICMP or ICMPv6.
513    Icmp {
514        /// Type number.
515        icmp_type: u8,
516        /// Code.
517        code: u8,
518        /// Echo identifier, where the type carries one.
519        id: Option<u16>,
520        /// Echo sequence, where the type carries one.
521        sequence: Option<u16>,
522    },
523    /// Anything else — the IP protocol number is still reported.
524    Other,
525}
526
527/// A decoded packet: every field the event surfaces, and nothing raw.
528#[derive(Debug, Clone, PartialEq, Eq)]
529pub struct DecodedPacket {
530    /// Present only in TAP mode.
531    pub ethernet: Option<EthernetHeader>,
532    /// 4 or 6.
533    pub ip_version: u8,
534    /// Source address.
535    pub source: IpAddr,
536    /// Destination address.
537    pub destination: IpAddr,
538    /// IANA protocol number of the payload.
539    pub protocol: u8,
540    /// IPv4 TTL / IPv6 hop limit.
541    pub ttl: u8,
542    /// Total IP packet length in bytes, as the header declares it.
543    pub total_length: usize,
544    /// Decoded transport detail.
545    pub transport: Transport,
546    /// Bytes of transport payload actually present in the frame.
547    pub payload_len: usize,
548    /// A short preview of the payload, in the encoding named by [`Self::payload_encoding`].
549    pub payload_preview: String,
550    /// `"utf8"` or `"hex"` — chosen by NetGet and always stated, never left for the reader to
551    /// guess. This is the producer declaring what it did, which is the opposite of an executor
552    /// sniffing what it was given.
553    pub payload_encoding: &'static str,
554}
555
556impl DecodedPacket {
557    /// The protocol's name, e.g. `"tcp"`.
558    pub fn protocol_name(&self) -> &'static str {
559        ip_protocol_name(self.protocol)
560    }
561
562    /// True when this is IPv6.
563    pub fn is_ipv6(&self) -> bool {
564        self.ip_version == 6
565    }
566
567    /// The ports, when the transport has any.
568    pub fn ports(&self) -> Option<(u16, u16)> {
569        match self.transport {
570            Transport::Tcp {
571                source_port,
572                destination_port,
573                ..
574            }
575            | Transport::Udp {
576                source_port,
577                destination_port,
578            } => Some((source_port, destination_port)),
579            _ => None,
580        }
581    }
582
583    /// One line a human (or a model) can read at a glance.
584    pub fn summary(&self) -> String {
585        let base = match &self.transport {
586            Transport::Tcp {
587                source_port,
588                destination_port,
589                flags,
590                seq,
591                ..
592            } => format!(
593                "{}:{} > {}:{} TCP [{}] seq={}",
594                self.source,
595                source_port,
596                self.destination,
597                destination_port,
598                tcp_flag_names(*flags).join(","),
599                seq
600            ),
601            Transport::Udp {
602                source_port,
603                destination_port,
604            } => format!(
605                "{}:{} > {}:{} UDP",
606                self.source, source_port, self.destination, destination_port
607            ),
608            Transport::Icmp {
609                icmp_type,
610                code,
611                id,
612                sequence,
613            } => {
614                let name = if self.is_ipv6() {
615                    icmpv6_type_name(*icmp_type)
616                } else {
617                    icmpv4_type_name(*icmp_type)
618                };
619                let mut s = format!(
620                    "{} > {} {} {} code={}",
621                    self.source,
622                    self.destination,
623                    if self.is_ipv6() { "ICMPv6" } else { "ICMP" },
624                    name,
625                    code
626                );
627                if let (Some(id), Some(seq)) = (id, sequence) {
628                    s.push_str(&format!(" id={id} seq={seq}"));
629                }
630                s
631            }
632            Transport::Other => format!(
633                "{} > {} {}",
634                self.source,
635                self.destination,
636                self.protocol_name()
637            ),
638        };
639        format!("{base} len={}", self.total_length)
640    }
641
642    /// The `tuntap_packet_received` event payload.
643    ///
644    /// Structured header fields only. The payload appears as a short, explicitly-encoded
645    /// preview beside its true length — never as the packet's bytes.
646    pub fn to_event_data(&self) -> Value {
647        let mut data = json!({
648            "direction": "inbound",
649            "ip_version": self.ip_version,
650            "source": self.source.to_string(),
651            "destination": self.destination.to_string(),
652            "protocol": self.protocol_name(),
653            "protocol_number": self.protocol,
654            "total_length": self.total_length,
655            "payload_length": self.payload_len,
656            "payload_preview": self.payload_preview,
657            "payload_encoding": self.payload_encoding,
658            "summary": self.summary(),
659        });
660        let obj = data.as_object_mut().expect("json! built an object");
661
662        if self.is_ipv6() {
663            obj.insert("hop_limit".into(), json!(self.ttl));
664        } else {
665            obj.insert("ttl".into(), json!(self.ttl));
666        }
667
668        if let Some(eth) = &self.ethernet {
669            obj.insert(
670                "ethernet".into(),
671                json!({
672                    "source_mac": eth.source_mac,
673                    "destination_mac": eth.destination_mac,
674                    "ethertype": format!("0x{:04x}", eth.ethertype),
675                }),
676            );
677        }
678
679        match &self.transport {
680            Transport::Tcp {
681                source_port,
682                destination_port,
683                seq,
684                ack,
685                flags,
686                window,
687            } => {
688                obj.insert("source_port".into(), json!(source_port));
689                obj.insert("destination_port".into(), json!(destination_port));
690                obj.insert("seq".into(), json!(seq));
691                obj.insert("ack".into(), json!(ack));
692                obj.insert("window".into(), json!(window));
693                obj.insert("tcp_flags".into(), json!(tcp_flag_names(*flags)));
694            }
695            Transport::Udp {
696                source_port,
697                destination_port,
698            } => {
699                obj.insert("source_port".into(), json!(source_port));
700                obj.insert("destination_port".into(), json!(destination_port));
701            }
702            Transport::Icmp {
703                icmp_type,
704                code,
705                id,
706                sequence,
707            } => {
708                let name = if self.is_ipv6() {
709                    icmpv6_type_name(*icmp_type)
710                } else {
711                    icmpv4_type_name(*icmp_type)
712                };
713                obj.insert("icmp_type".into(), json!(name));
714                obj.insert("icmp_type_number".into(), json!(icmp_type));
715                obj.insert("icmp_code".into(), json!(code));
716                if let Some(id) = id {
717                    obj.insert("icmp_id".into(), json!(id));
718                }
719                if let Some(seq) = sequence {
720                    obj.insert("icmp_sequence".into(), json!(seq));
721                }
722            }
723            Transport::Other => {}
724        }
725
726        data
727    }
728}
729
730// ---------------------------------------------------------------------------
731// Decoding
732// ---------------------------------------------------------------------------
733
734fn need(what: &'static str, need: usize, have: usize) -> DecodeError {
735    DecodeError::TooShort { what, need, have }
736}
737
738/// Decode one frame read from the interface.
739///
740/// `pi` strips (and validates) the platform prefix; `mode` decides whether an Ethernet header
741/// is expected in front of the IP packet.
742pub fn decode(
743    frame: &[u8],
744    pi: PacketInformation,
745    mode: LinkMode,
746) -> Result<DecodedPacket, DecodeError> {
747    let frame = pi.strip(frame)?;
748
749    let (ethernet, ip) = match mode {
750        LinkMode::Tun => (None, frame),
751        LinkMode::Tap => {
752            if frame.len() < ETHERNET_HEADER_LEN {
753                return Err(need("Ethernet header", ETHERNET_HEADER_LEN, frame.len()));
754            }
755            let ethertype = u16::from_be_bytes([frame[12], frame[13]]);
756            if ethertype != ETHERTYPE_IPV4 && ethertype != ETHERTYPE_IPV6 {
757                return Err(DecodeError::UnsupportedEtherType(ethertype));
758            }
759            (
760                Some(EthernetHeader {
761                    destination_mac: format_mac(&frame[0..6]),
762                    source_mac: format_mac(&frame[6..12]),
763                    ethertype,
764                }),
765                &frame[ETHERNET_HEADER_LEN..],
766            )
767        }
768    };
769
770    if ip.is_empty() {
771        return Err(need("IP header", 1, 0));
772    }
773
774    match ip[0] >> 4 {
775        4 => decode_ipv4(ethernet, ip),
776        6 => decode_ipv6(ethernet, ip),
777        v => Err(DecodeError::UnsupportedIpVersion(v)),
778    }
779}
780
781fn decode_ipv4(ethernet: Option<EthernetHeader>, ip: &[u8]) -> Result<DecodedPacket, DecodeError> {
782    if ip.len() < 20 {
783        return Err(need("IPv4 header", 20, ip.len()));
784    }
785    let ihl = ip[0] & 0x0f;
786    if ihl < 5 {
787        return Err(DecodeError::BadIhl(ihl));
788    }
789    let header_len = ihl as usize * 4;
790    if ip.len() < header_len {
791        return Err(need("IPv4 options", header_len, ip.len()));
792    }
793    let total_length = u16::from_be_bytes([ip[2], ip[3]]) as usize;
794    let ttl = ip[8];
795    let protocol = ip[9];
796    let source = IpAddr::V4(ipv4_at(ip, 12));
797    let destination = IpAddr::V4(ipv4_at(ip, 16));
798
799    let rest = &ip[header_len..];
800    let (transport, payload) = decode_transport(protocol, false, rest)?;
801
802    let (payload_preview, payload_encoding) = preview(payload);
803
804    Ok(DecodedPacket {
805        ethernet,
806        ip_version: 4,
807        source,
808        destination,
809        protocol,
810        ttl,
811        total_length,
812        transport,
813        payload_len: payload.len(),
814        payload_preview,
815        payload_encoding,
816    })
817}
818
819fn ipv4_at(buf: &[u8], at: usize) -> Ipv4Addr {
820    Ipv4Addr::new(buf[at], buf[at + 1], buf[at + 2], buf[at + 3])
821}
822
823fn decode_ipv6(ethernet: Option<EthernetHeader>, ip: &[u8]) -> Result<DecodedPacket, DecodeError> {
824    if ip.len() < 40 {
825        return Err(need("IPv6 header", 40, ip.len()));
826    }
827    let payload_length = u16::from_be_bytes([ip[4], ip[5]]) as usize;
828    let protocol = ip[6];
829    let ttl = ip[7];
830    let mut src = [0u8; 16];
831    src.copy_from_slice(&ip[8..24]);
832    let mut dst = [0u8; 16];
833    dst.copy_from_slice(&ip[24..40]);
834
835    let rest = &ip[40..];
836    // Extension headers are deliberately not walked: this endpoint reports the *next header* it
837    // finds, and a packet carrying extension headers decodes as that protocol with no ports.
838    // Pretending to have parsed a chain that was never implemented would be worse than saying so.
839    let (transport, payload) = decode_transport(protocol, true, rest)?;
840    let (payload_preview, payload_encoding) = preview(payload);
841
842    Ok(DecodedPacket {
843        ethernet,
844        ip_version: 6,
845        source: IpAddr::V6(Ipv6Addr::from(src)),
846        destination: IpAddr::V6(Ipv6Addr::from(dst)),
847        protocol,
848        ttl,
849        total_length: 40 + payload_length,
850        transport,
851        payload_len: payload.len(),
852        payload_preview,
853        payload_encoding,
854    })
855}
856
857fn decode_transport(
858    protocol: u8,
859    ipv6: bool,
860    rest: &[u8],
861) -> Result<(Transport, &[u8]), DecodeError> {
862    match protocol {
863        6 => {
864            if rest.len() < 20 {
865                return Err(need("TCP header", 20, rest.len()));
866            }
867            let data_offset = (rest[12] >> 4) as usize * 4;
868            let header_len = data_offset.max(20).min(rest.len());
869            Ok((
870                Transport::Tcp {
871                    source_port: u16::from_be_bytes([rest[0], rest[1]]),
872                    destination_port: u16::from_be_bytes([rest[2], rest[3]]),
873                    seq: u32::from_be_bytes([rest[4], rest[5], rest[6], rest[7]]),
874                    ack: u32::from_be_bytes([rest[8], rest[9], rest[10], rest[11]]),
875                    flags: rest[13],
876                    window: u16::from_be_bytes([rest[14], rest[15]]),
877                },
878                &rest[header_len..],
879            ))
880        }
881        17 => {
882            if rest.len() < 8 {
883                return Err(need("UDP header", 8, rest.len()));
884            }
885            Ok((
886                Transport::Udp {
887                    source_port: u16::from_be_bytes([rest[0], rest[1]]),
888                    destination_port: u16::from_be_bytes([rest[2], rest[3]]),
889                },
890                &rest[8..],
891            ))
892        }
893        1 | 58 => {
894            if rest.len() < 4 {
895                return Err(need("ICMP header", 4, rest.len()));
896            }
897            let icmp_type = rest[0];
898            let code = rest[1];
899            let echo = if ipv6 {
900                matches!(icmp_type, 128 | 129)
901            } else {
902                matches!(icmp_type, 0 | 8 | 13 | 14)
903            };
904            let (id, sequence, body_at) = if echo && rest.len() >= 8 {
905                (
906                    Some(u16::from_be_bytes([rest[4], rest[5]])),
907                    Some(u16::from_be_bytes([rest[6], rest[7]])),
908                    8,
909                )
910            } else {
911                (None, None, 4.min(rest.len()))
912            };
913            Ok((
914                Transport::Icmp {
915                    icmp_type,
916                    code,
917                    id,
918                    sequence,
919                },
920                &rest[body_at..],
921            ))
922        }
923        _ => Ok((Transport::Other, rest)),
924    }
925}
926
927/// Build the payload preview and name the encoding used.
928///
929/// A payload that is entirely printable ASCII is shown as text, because that is what makes an
930/// HTTP request or a DNS-over-TCP name legible to a model; anything else is hex. **NetGet
931/// decides and then states the answer** in `payload_encoding`, so no reader has to guess and
932/// no executor has to sniff.
933fn preview(payload: &[u8]) -> (String, &'static str) {
934    let head = &payload[..payload.len().min(PAYLOAD_PREVIEW_BYTES)];
935    let printable = !head.is_empty()
936        && head
937            .iter()
938            .all(|b| matches!(b, 0x20..=0x7e | b'\t' | b'\r' | b'\n'));
939    if printable {
940        let text: String = head.iter().map(|b| *b as char).collect();
941        // truncate_for_log rather than slicing: byte-index truncation on a multi-byte boundary
942        // has panicked this codebase before, and the helper is the project-wide answer.
943        (truncate_for_log(&text, PAYLOAD_PREVIEW_BYTES), "utf8")
944    } else {
945        (
946            truncate_for_log(&hex::encode(head), PAYLOAD_PREVIEW_BYTES * 2),
947            "hex",
948        )
949    }
950}
951
952// ---------------------------------------------------------------------------
953// The escalation filter
954// ---------------------------------------------------------------------------
955
956/// One condition a packet must satisfy.
957#[derive(Debug, Clone, PartialEq, Eq)]
958enum Term {
959    V4,
960    V6,
961    Protocol(u8),
962    PortAny(u16),
963    ProtocolPort(u8, u16),
964    TcpSyn,
965    From(IpAddr),
966    To(IpAddr),
967    Host(IpAddr),
968}
969
970impl Term {
971    fn matches(&self, p: &DecodedPacket) -> bool {
972        match self {
973            Self::V4 => p.ip_version == 4,
974            Self::V6 => p.ip_version == 6,
975            Self::Protocol(n) => p.protocol == *n,
976            Self::PortAny(port) => p.ports().is_some_and(|(s, d)| s == *port || d == *port),
977            Self::ProtocolPort(n, port) => {
978                p.protocol == *n && p.ports().is_some_and(|(s, d)| s == *port || d == *port)
979            }
980            Self::TcpSyn => matches!(
981                p.transport,
982                Transport::Tcp { flags, .. } if flags & 0x02 != 0 && flags & 0x10 == 0
983            ),
984            Self::From(addr) => p.source == *addr,
985            Self::To(addr) => p.destination == *addr,
986            Self::Host(addr) => p.source == *addr || p.destination == *addr,
987        }
988    }
989}
990
991/// The deterministic gate in front of everything else.
992///
993/// **This is the design's load-bearing part.** Every packet routed to the interface is decoded
994/// in native code and tested against this filter. Only a packet that matches is allowed to
995/// become an event at all — and only then can a handler, or in the last resort the model, be
996/// consulted. Without it, one `ping` is one LLM call per second and a TCP handshake is three in
997/// a few milliseconds.
998///
999/// # Grammar
1000///
1001/// * `all` — every decodable packet.
1002/// * `none` — nothing. The interface still runs; every packet is counted and dropped.
1003/// * otherwise: comma-separated alternatives, ORed. Each alternative is one or more terms
1004///   joined by `+`, ANDed:
1005///   - `v4`, `v6`
1006///   - a protocol name (`icmp`, `icmpv6`, `tcp`, `udp`, `gre`, `esp`, `ospf`, …) or
1007///     `ip-proto-<n>`
1008///   - `tcp:<port>` / `udp:<port>` — either endpoint carries that port
1009///   - `port:<n>` — either endpoint, either transport
1010///   - `tcp-syn` — a TCP segment with SYN set and ACK clear. **The most useful term in the
1011///     language**: it fires once per connection attempt instead of once per packet, so a whole
1012///     TCP flow costs one event.
1013///   - `from:<addr>`, `to:<addr>`, `host:<addr>`
1014///
1015/// `tcp-syn+to:10.7.0.2, icmp` reads as "a new connection to 10.7.0.2, or any ping".
1016#[derive(Debug, Clone, PartialEq, Eq)]
1017pub struct PacketFilter {
1018    /// The original expression, kept for logs and for `metadata()`-style reporting.
1019    source: String,
1020    /// `None` means "match everything"; an empty Vec means "match nothing".
1021    alternatives: Option<Vec<Vec<Term>>>,
1022}
1023
1024impl PacketFilter {
1025    /// Parse a filter expression. See the type documentation for the grammar.
1026    pub fn parse(expr: &str) -> Result<Self, String> {
1027        let source = expr.trim().to_string();
1028        let lower = source.to_ascii_lowercase();
1029        if lower.is_empty() {
1030            return Err(
1031                "packet_filter must not be empty; use \"none\" to escalate nothing, \
1032                        or \"all\" to escalate everything"
1033                    .to_string(),
1034            );
1035        }
1036        if lower == "all" {
1037            return Ok(Self {
1038                source,
1039                alternatives: None,
1040            });
1041        }
1042        if lower == "none" {
1043            return Ok(Self {
1044                source,
1045                alternatives: Some(Vec::new()),
1046            });
1047        }
1048
1049        let mut alternatives = Vec::new();
1050        for group in lower.split(',') {
1051            let group = group.trim();
1052            if group.is_empty() {
1053                return Err(format!(
1054                    "packet_filter {expr:?} has an empty alternative between commas"
1055                ));
1056            }
1057            let mut terms = Vec::new();
1058            for term in group.split('+') {
1059                terms.push(parse_term(term.trim(), expr)?);
1060            }
1061            alternatives.push(terms);
1062        }
1063        Ok(Self {
1064            source,
1065            alternatives: Some(alternatives),
1066        })
1067    }
1068
1069    /// The expression as written.
1070    pub fn as_str(&self) -> &str {
1071        &self.source
1072    }
1073
1074    /// True when this filter can never match, so nothing will ever escalate.
1075    pub fn matches_nothing(&self) -> bool {
1076        matches!(&self.alternatives, Some(v) if v.is_empty())
1077    }
1078
1079    /// Does this packet get to become an event?
1080    pub fn matches(&self, packet: &DecodedPacket) -> bool {
1081        match &self.alternatives {
1082            None => true,
1083            Some(alts) => alts
1084                .iter()
1085                .any(|terms| terms.iter().all(|t| t.matches(packet))),
1086        }
1087    }
1088}
1089
1090fn parse_term(term: &str, whole: &str) -> Result<Term, String> {
1091    let err = |detail: String| format!("packet_filter {whole:?}: {detail}");
1092
1093    if term.is_empty() {
1094        return Err(err("empty term between '+' separators".into()));
1095    }
1096    match term {
1097        "v4" | "ipv4" => return Ok(Term::V4),
1098        "v6" | "ipv6" => return Ok(Term::V6),
1099        "tcp-syn" | "tcp_syn" => return Ok(Term::TcpSyn),
1100        _ => {}
1101    }
1102    if let Some(rest) = term.strip_prefix("ip-proto-") {
1103        let n: u8 = rest
1104            .parse()
1105            .map_err(|_| err(format!("{rest:?} is not an IP protocol number (0-255)")))?;
1106        return Ok(Term::Protocol(n));
1107    }
1108    for (prefix, ctor) in [
1109        ("from:", Term::From as fn(IpAddr) -> Term),
1110        ("to:", Term::To as fn(IpAddr) -> Term),
1111        ("host:", Term::Host as fn(IpAddr) -> Term),
1112    ] {
1113        if let Some(rest) = term.strip_prefix(prefix) {
1114            let addr: IpAddr = rest
1115                .parse()
1116                .map_err(|_| err(format!("{rest:?} is not an IP address")))?;
1117            return Ok(ctor(addr));
1118        }
1119    }
1120    if let Some(rest) = term.strip_prefix("port:") {
1121        let port: u16 = rest
1122            .parse()
1123            .map_err(|_| err(format!("{rest:?} is not a port number (0-65535)")))?;
1124        return Ok(Term::PortAny(port));
1125    }
1126    if let Some((name, port)) = term.split_once(':') {
1127        let number = ip_protocol_number(name)
1128            .ok_or_else(|| err(format!("{name:?} is not a known protocol name")))?;
1129        if number != 6 && number != 17 {
1130            return Err(err(format!("{name:?} has no ports to match on")));
1131        }
1132        let port: u16 = port
1133            .parse()
1134            .map_err(|_| err(format!("{port:?} is not a port number (0-65535)")))?;
1135        return Ok(Term::ProtocolPort(number, port));
1136    }
1137    match ip_protocol_number(term) {
1138        Some(n) => Ok(Term::Protocol(n)),
1139        // `all` and `none` are deliberately absent from this list, and the list used to
1140        // name them. They are whole-expression keywords, handled in `PacketFilter::parse`
1141        // before anything is split, so `tcp:80,all` is refused by an error that told the
1142        // reader `all` was valid — and the reader here is usually the model, correcting a
1143        // filter from exactly this text. An `all` inside a group means nothing anyway: it
1144        // would make the whole expression match everything, which is what writing `all` on
1145        // its own says.
1146        None if matches!(term, "all" | "none") => Err(err(format!(
1147            "{term:?} is the whole packet_filter, not one term of a list: write \
1148             packet_filter {term:?} on its own rather than combining it with others"
1149        ))),
1150        None => Err(err(format!(
1151            "{term:?} is not a filter term; expected v4, v6, tcp-syn, a protocol name, \
1152             ip-proto-<n>, tcp:<port>, udp:<port>, port:<n>, from:<addr>, to:<addr> or \
1153             host:<addr>. \"all\" and \"none\" are whole expressions, not terms"
1154        ))),
1155    }
1156}
1157
1158// ---------------------------------------------------------------------------
1159// Building
1160// ---------------------------------------------------------------------------
1161
1162/// The one's-complement sum used by IPv4, ICMP, TCP and UDP.
1163pub fn checksum16(bytes: &[u8]) -> u16 {
1164    let mut sum: u32 = 0;
1165    let mut chunks = bytes.chunks_exact(2);
1166    for c in &mut chunks {
1167        sum += u16::from_be_bytes([c[0], c[1]]) as u32;
1168    }
1169    if let [last] = chunks.remainder() {
1170        sum += (*last as u32) << 8;
1171    }
1172    while sum >> 16 != 0 {
1173        sum = (sum & 0xffff) + (sum >> 16);
1174    }
1175    !(sum as u16)
1176}
1177
1178fn pseudo_header(source: IpAddr, destination: IpAddr, protocol: u8, length: usize) -> Vec<u8> {
1179    let mut out = Vec::with_capacity(40);
1180    match (source, destination) {
1181        (IpAddr::V4(s), IpAddr::V4(d)) => {
1182            out.extend_from_slice(&s.octets());
1183            out.extend_from_slice(&d.octets());
1184            out.push(0);
1185            out.push(protocol);
1186            out.extend_from_slice(&(length as u16).to_be_bytes());
1187        }
1188        (IpAddr::V6(s), IpAddr::V6(d)) => {
1189            out.extend_from_slice(&s.octets());
1190            out.extend_from_slice(&d.octets());
1191            out.extend_from_slice(&(length as u32).to_be_bytes());
1192            out.extend_from_slice(&[0, 0, 0, protocol]);
1193        }
1194        _ => unreachable!("caller checked the address families match"),
1195    }
1196    out
1197}
1198
1199fn get_str<'a>(action: &'a Value, key: &str) -> Option<&'a str> {
1200    action.get(key).and_then(|v| v.as_str())
1201}
1202
1203fn get_u64(action: &Value, key: &str) -> Option<u64> {
1204    action.get(key).and_then(|v| v.as_u64())
1205}
1206
1207fn required_addr(action: &Value, key: &str) -> Result<IpAddr, BuildError> {
1208    let raw = get_str(action, key).ok_or_else(|| bad(format!("send_packet requires \"{key}\"")))?;
1209    raw.parse()
1210        .map_err(|_| bad(format!("\"{key}\" is not an IP address: {raw:?}")))
1211}
1212
1213/// Decode the `payload` field according to the explicit `payload_encoding` field.
1214///
1215/// **The encoding is never sniffed.** `"48656c6c6f"` is simultaneously valid text and valid
1216/// hex, and only the sender knows which it meant — the exact bug `CLAUDE.md` records against
1217/// `send_tcp_data`. The field defaults to `utf8`, and hex is decoded strictly (even length,
1218/// hex digits only) so a malformed value is a refusal rather than a different packet.
1219pub fn decode_payload(action: &Value) -> Result<Vec<u8>, BuildError> {
1220    let Some(payload) = action.get("payload") else {
1221        return Ok(Vec::new());
1222    };
1223    if payload.is_null() {
1224        return Ok(Vec::new());
1225    }
1226    let text = payload
1227        .as_str()
1228        .ok_or_else(|| bad("\"payload\" must be a string"))?;
1229    let encoding = get_str(action, "payload_encoding").unwrap_or("utf8");
1230    match encoding.trim().to_ascii_lowercase().as_str() {
1231        "utf8" | "utf-8" | "text" => Ok(text.as_bytes().to_vec()),
1232        "hex" => hex::decode(text.trim()).map_err(|e| {
1233            bad(format!(
1234                "\"payload\" is not valid hex ({e}); payload_encoding said \"hex\""
1235            ))
1236        }),
1237        other => Err(bad(format!(
1238            "payload_encoding must be \"utf8\" or \"hex\", got {other:?}"
1239        ))),
1240    }
1241}
1242
1243/// Build the bytes for a `send_packet` action.
1244///
1245/// The model describes the packet by its fields; NetGet lays out the headers and computes the
1246/// IPv4 header checksum, the ICMP/ICMPv6 checksum and the TCP/UDP checksum over the correct
1247/// pseudo-header. When `source_mac` and `destination_mac` are both present the result is an
1248/// Ethernet frame (TAP); otherwise it is a bare IP packet (TUN).
1249pub fn build_packet(action: &Value) -> Result<Vec<u8>, BuildError> {
1250    let source = required_addr(action, "source")?;
1251    let destination = required_addr(action, "destination")?;
1252
1253    let declared_version = get_u64(action, "ip_version");
1254    let ipv6 = match (source, destination) {
1255        (IpAddr::V4(_), IpAddr::V4(_)) => false,
1256        (IpAddr::V6(_), IpAddr::V6(_)) => true,
1257        _ => {
1258            return Err(bad(
1259                "\"source\" and \"destination\" must be the same IP version",
1260            ))
1261        }
1262    };
1263    if let Some(v) = declared_version {
1264        let expected = if ipv6 { 6 } else { 4 };
1265        if v != expected {
1266            return Err(bad(format!(
1267                "ip_version says {v} but the addresses are IPv{expected}"
1268            )));
1269        }
1270    }
1271
1272    let protocol = match action.get("protocol") {
1273        None => return Err(bad("send_packet requires \"protocol\"")),
1274        Some(Value::String(s)) => ip_protocol_number(s).ok_or_else(|| {
1275            bad(format!(
1276                "unknown protocol {s:?}; use a name such as \"icmp\", \"icmpv6\", \"tcp\", \
1277                 \"udp\", or a number"
1278            ))
1279        })?,
1280        Some(Value::Number(n)) => {
1281            let v = n
1282                .as_u64()
1283                .ok_or_else(|| bad("\"protocol\" must be a non-negative number"))?;
1284            u8::try_from(v).map_err(|_| bad("\"protocol\" must be 0-255"))?
1285        }
1286        Some(other) => {
1287            return Err(bad(format!(
1288                "\"protocol\" must be a name or a number, got {other}"
1289            )))
1290        }
1291    };
1292
1293    if ipv6 && protocol == 1 {
1294        return Err(bad(
1295            "protocol \"icmp\" is IPv4-only; use \"icmpv6\" with IPv6 addresses",
1296        ));
1297    }
1298    if !ipv6 && protocol == 58 {
1299        return Err(bad(
1300            "protocol \"icmpv6\" is IPv6-only; use \"icmp\" with IPv4 addresses",
1301        ));
1302    }
1303
1304    let payload = decode_payload(action)?;
1305
1306    let transport = match protocol {
1307        1 | 58 => build_icmp(action, ipv6, &payload)?,
1308        6 => build_tcp(action, &payload)?,
1309        17 => build_udp(action, &payload)?,
1310        _ => payload.clone(),
1311    };
1312
1313    let mut transport = transport;
1314    // Checksums that need the addresses live here, where both are known.
1315    match protocol {
1316        58 => {
1317            let ph = pseudo_header(source, destination, 58, transport.len());
1318            let mut all = ph;
1319            all.extend_from_slice(&transport);
1320            let sum = checksum16(&all);
1321            transport[2..4].copy_from_slice(&sum.to_be_bytes());
1322        }
1323        6 | 17 => {
1324            let ph = pseudo_header(source, destination, protocol, transport.len());
1325            let mut all = ph;
1326            all.extend_from_slice(&transport);
1327            let sum = checksum16(&all);
1328            let at = if protocol == 6 { 16 } else { 6 };
1329            transport[at..at + 2].copy_from_slice(&sum.to_be_bytes());
1330        }
1331        _ => {}
1332    }
1333
1334    let ttl = match (get_u64(action, "ttl"), get_u64(action, "hop_limit")) {
1335        (Some(v), _) | (None, Some(v)) => {
1336            u8::try_from(v).map_err(|_| bad("\"ttl\"/\"hop_limit\" must be 0-255"))?
1337        }
1338        (None, None) => 64,
1339    };
1340
1341    let mut packet = if ipv6 {
1342        build_ipv6_header(source, destination, protocol, ttl, transport.len())?
1343    } else {
1344        build_ipv4_header(action, source, destination, protocol, ttl, transport.len())?
1345    };
1346    packet.extend_from_slice(&transport);
1347
1348    // TAP: wrap in an Ethernet header when the model supplied both MACs. The engine, which is
1349    // the only thing that knows the interface's layer, refuses the mismatched combinations —
1350    // this function is deliberately mode-agnostic so its examples are executable anywhere.
1351    match (
1352        get_str(action, "source_mac"),
1353        get_str(action, "destination_mac"),
1354    ) {
1355        (Some(src), Some(dst)) => {
1356            let mut frame = Vec::with_capacity(ETHERNET_HEADER_LEN + packet.len());
1357            frame.extend_from_slice(&parse_mac(dst)?);
1358            frame.extend_from_slice(&parse_mac(src)?);
1359            frame.extend_from_slice(
1360                &(if ipv6 { ETHERTYPE_IPV6 } else { ETHERTYPE_IPV4 }).to_be_bytes(),
1361            );
1362            frame.extend_from_slice(&packet);
1363            Ok(frame)
1364        }
1365        (None, None) => Ok(packet),
1366        _ => Err(bad(
1367            "an Ethernet frame needs both \"source_mac\" and \"destination_mac\"; supply \
1368             neither for a layer-3 (TUN) packet",
1369        )),
1370    }
1371}
1372
1373fn build_ipv4_header(
1374    action: &Value,
1375    source: IpAddr,
1376    destination: IpAddr,
1377    protocol: u8,
1378    ttl: u8,
1379    payload_len: usize,
1380) -> Result<Vec<u8>, BuildError> {
1381    let (IpAddr::V4(s), IpAddr::V4(d)) = (source, destination) else {
1382        unreachable!("caller checked the family");
1383    };
1384    let total = 20 + payload_len;
1385    if total > u16::MAX as usize {
1386        return Err(bad(format!(
1387            "packet is {total} bytes, larger than IPv4 allows"
1388        )));
1389    }
1390    let identification = get_u64(action, "identification")
1391        .map(|v| u16::try_from(v).map_err(|_| bad("\"identification\" must be 0-65535")))
1392        .transpose()?
1393        .unwrap_or(0);
1394
1395    let mut h = Vec::with_capacity(20);
1396    h.push(0x45); // version 4, IHL 5 — no options are ever emitted
1397    h.push(0); // DSCP/ECN
1398    h.extend_from_slice(&(total as u16).to_be_bytes());
1399    h.extend_from_slice(&identification.to_be_bytes());
1400    h.extend_from_slice(&0x4000u16.to_be_bytes()); // Don't Fragment, offset 0
1401    h.push(ttl);
1402    h.push(protocol);
1403    h.extend_from_slice(&[0, 0]); // checksum placeholder
1404    h.extend_from_slice(&s.octets());
1405    h.extend_from_slice(&d.octets());
1406    let sum = checksum16(&h);
1407    h[10..12].copy_from_slice(&sum.to_be_bytes());
1408    Ok(h)
1409}
1410
1411fn build_ipv6_header(
1412    source: IpAddr,
1413    destination: IpAddr,
1414    protocol: u8,
1415    hop_limit: u8,
1416    payload_len: usize,
1417) -> Result<Vec<u8>, BuildError> {
1418    let (IpAddr::V6(s), IpAddr::V6(d)) = (source, destination) else {
1419        unreachable!("caller checked the family");
1420    };
1421    // `build_ipv4_header` refuses an oversized packet; this one narrowed with `as u16` and
1422    // emitted a header declaring a wrapped payload length. Without a jumbogram hop-by-hop
1423    // option, 65535 is the whole of what an IPv6 payload length can say (RFC 8200 §3), so
1424    // there is no correct value to substitute — refuse.
1425    let declared = u16::try_from(payload_len).map_err(|_| {
1426        bad(format!(
1427            "payload is {payload_len} bytes; IPv6's payload-length field is two bytes, and \
1428             anything larger needs a jumbogram option this builder does not emit"
1429        ))
1430    })?;
1431    let mut h = Vec::with_capacity(40);
1432    h.extend_from_slice(&0x6000_0000u32.to_be_bytes()); // version 6, no traffic class or label
1433    h.extend_from_slice(&declared.to_be_bytes());
1434    h.push(protocol);
1435    h.push(hop_limit);
1436    h.extend_from_slice(&s.octets());
1437    h.extend_from_slice(&d.octets());
1438    Ok(h)
1439}
1440
1441fn icmp_type_of(action: &Value, ipv6: bool) -> Result<u8, BuildError> {
1442    match action.get("icmp_type") {
1443        None => Err(bad(
1444            "an ICMP packet requires \"icmp_type\" (a name such as \"echo_reply\", or a number)",
1445        )),
1446        Some(Value::String(s)) => icmp_type_number(s, ipv6).ok_or_else(|| {
1447            bad(format!(
1448                "unknown ICMP{} type {s:?}",
1449                if ipv6 { "v6" } else { "" }
1450            ))
1451        }),
1452        Some(Value::Number(n)) => {
1453            let v = n
1454                .as_u64()
1455                .ok_or_else(|| bad("\"icmp_type\" must be a non-negative number"))?;
1456            u8::try_from(v).map_err(|_| bad("\"icmp_type\" must be 0-255"))
1457        }
1458        Some(other) => Err(bad(format!(
1459            "\"icmp_type\" must be a name or a number, got {other}"
1460        ))),
1461    }
1462}
1463
1464fn build_icmp(action: &Value, ipv6: bool, payload: &[u8]) -> Result<Vec<u8>, BuildError> {
1465    let icmp_type = icmp_type_of(action, ipv6)?;
1466    let code = get_u64(action, "icmp_code")
1467        .map(|v| u8::try_from(v).map_err(|_| bad("\"icmp_code\" must be 0-255")))
1468        .transpose()?
1469        .unwrap_or(0);
1470
1471    let echo = if ipv6 {
1472        matches!(icmp_type, 128 | 129)
1473    } else {
1474        matches!(icmp_type, 0 | 8)
1475    };
1476
1477    let mut msg = Vec::with_capacity(8 + payload.len());
1478    msg.push(icmp_type);
1479    msg.push(code);
1480    msg.extend_from_slice(&[0, 0]); // checksum placeholder
1481
1482    if echo {
1483        let id = get_u64(action, "icmp_id")
1484            .map(|v| u16::try_from(v).map_err(|_| bad("\"icmp_id\" must be 0-65535")))
1485            .transpose()?
1486            .unwrap_or(0);
1487        let seq = get_u64(action, "icmp_sequence")
1488            .map(|v| u16::try_from(v).map_err(|_| bad("\"icmp_sequence\" must be 0-65535")))
1489            .transpose()?
1490            .unwrap_or(0);
1491        msg.extend_from_slice(&id.to_be_bytes());
1492        msg.extend_from_slice(&seq.to_be_bytes());
1493    } else {
1494        msg.extend_from_slice(&[0, 0, 0, 0]); // unused / MTU / pointer, per type
1495    }
1496    msg.extend_from_slice(payload);
1497
1498    if !ipv6 {
1499        // ICMPv4 has no pseudo-header, so the checksum is final here. ICMPv6 needs the
1500        // addresses and is finished by the caller.
1501        let sum = checksum16(&msg);
1502        msg[2..4].copy_from_slice(&sum.to_be_bytes());
1503    }
1504    Ok(msg)
1505}
1506
1507fn port(action: &Value, key: &str) -> Result<u16, BuildError> {
1508    let v = get_u64(action, key)
1509        .ok_or_else(|| bad(format!("this packet requires \"{key}\" (0-65535)")))?;
1510    u16::try_from(v).map_err(|_| bad(format!("\"{key}\" must be 0-65535")))
1511}
1512
1513fn build_udp(action: &Value, payload: &[u8]) -> Result<Vec<u8>, BuildError> {
1514    // UDP's length field is two bytes and covers the header. `as u16` on an oversized payload
1515    // wrote a datagram *declaring* a wrapped length — well-formed to look at, and describing
1516    // a different packet than the one it carried.
1517    let length = u16::try_from(8 + payload.len()).map_err(|_| {
1518        bad(format!(
1519            "a UDP datagram is {} bytes including its header, and UDP's length field is two \
1520             bytes (65535 maximum)",
1521            8 + payload.len()
1522        ))
1523    })?;
1524    let mut msg = Vec::with_capacity(8 + payload.len());
1525    msg.extend_from_slice(&port(action, "source_port")?.to_be_bytes());
1526    msg.extend_from_slice(&port(action, "destination_port")?.to_be_bytes());
1527    msg.extend_from_slice(&length.to_be_bytes());
1528    msg.extend_from_slice(&[0, 0]); // checksum placeholder — filled with the pseudo-header
1529    msg.extend_from_slice(payload);
1530    Ok(msg)
1531}
1532
1533fn build_tcp(action: &Value, payload: &[u8]) -> Result<Vec<u8>, BuildError> {
1534    let flags = match action.get("flags") {
1535        None => 0x10, // a bare ACK is the only defensible default
1536        Some(Value::Array(items)) => {
1537            let names: Vec<String> = items
1538                .iter()
1539                .map(|v| {
1540                    v.as_str()
1541                        .map(str::to_string)
1542                        .ok_or_else(|| bad("\"flags\" entries must be strings"))
1543                })
1544                .collect::<Result<_, _>>()?;
1545            tcp_flag_bits(&names)?
1546        }
1547        Some(other) => {
1548            return Err(bad(format!(
1549                "\"flags\" must be an array of flag names, got {other}"
1550            )))
1551        }
1552    };
1553    // Refused, not narrowed. Every other numeric field here goes through `try_from`; these
1554    // two did not, so `{"seq": 4294967296}` silently became 0 — and a TCP sequence number is
1555    // exactly the kind of value where "a different number" and "the number asked for" are
1556    // indistinguishable to everyone downstream.
1557    let seq = get_u64(action, "seq")
1558        .map(|v| u32::try_from(v).map_err(|_| bad("\"seq\" must be 0-4294967295")))
1559        .transpose()?
1560        .unwrap_or(0);
1561    let ack = get_u64(action, "ack")
1562        .map(|v| u32::try_from(v).map_err(|_| bad("\"ack\" must be 0-4294967295")))
1563        .transpose()?
1564        .unwrap_or(0);
1565    let window = get_u64(action, "window")
1566        .map(|v| u16::try_from(v).map_err(|_| bad("\"window\" must be 0-65535")))
1567        .transpose()?
1568        .unwrap_or(65535);
1569
1570    let mut msg = Vec::with_capacity(20 + payload.len());
1571    msg.extend_from_slice(&port(action, "source_port")?.to_be_bytes());
1572    msg.extend_from_slice(&port(action, "destination_port")?.to_be_bytes());
1573    msg.extend_from_slice(&seq.to_be_bytes());
1574    msg.extend_from_slice(&ack.to_be_bytes());
1575    msg.push(5 << 4); // data offset 5 words, no options
1576    msg.push(flags);
1577    msg.extend_from_slice(&window.to_be_bytes());
1578    msg.extend_from_slice(&[0, 0]); // checksum placeholder
1579    msg.extend_from_slice(&[0, 0]); // urgent pointer
1580    msg.extend_from_slice(payload);
1581    Ok(msg)
1582}