6.3 The snapshot

Every judgment reads a snapshot: a fixed-size, stack-allocated struct peios_ntfe_snapshot built once per seat by peios_ntfe_snapshot_from_skb() and never mutated during evaluation. That immutability is a ratified law, not an implementation detail — nothing a rule writes is visible to the same evaluation's matching, so temporal feedback (the next packet sees it) is the only feedback there is.

6.3.1 Validity bits #

Many facts have meaningful zero values (port 0, TTL 0, VLAN 0), so presence is carried separately in has, a bitmask of PEIOS_NTFE_HAS_*: ethertype, MACs, source MAC alone, VLAN, TTL, DSCP, fragment, ports, TCP flags, ICMP, time, the flow's start time, and the network context. Address facts use addr_family (0, 4 or 6) as their validity; the protocol is valid iff a family is. Flow state uses its own ABSENT value; flow_related is valid iff flow is. The bridge turns each clear bit into None on the Rust side, and the absent-fact law makes every condition over a None false.

6.3.2 Extraction #

Seat facts first: seat, direction, interface (ifindex and name), the network context of that interface (below), whether the device is the loopback (IFF_LOOPBACK — the flow dispatch's "two endpoints" test), the packet length as the stack sees it (skb->len — not the wire length), and the wall clock (ktime_get_real_seconds() through time64_to_tm(), UTC, with tm_wday re-based so the DayOfWeek fact is ISO: 1 = Monday .. 7 = Sunday), kept alongside as epoch seconds (t_secs) for the time-flip arithmetic (§6.4) and the sentence expiry check (§6.8).

Then the frame: the ethertype from skb->protocol. The VLAN id is the frame's tag if skb_vlan_tag_present(), else the device's if is_vlan_dev() — the VLAN is a fact of the frame at the device seats and of the device at the IP seats, where inbound the VLAN code has already stripped the tag and re-parented the packet onto the VLAN device, and outbound the tag is pushed only when that device transmits. (Before the Flow slice the IP seats read the stripped tag and Vlan was absent on VLAN interfaces there.) The MAC pair if the MAC header is set and the device is ARPHRD_ETHER; failing that, at an IP seat on an Ethernet device — a locally generated packet with no link header yet — the source alone, from the device's own address (HAS_SRC_MAC): present so every flow carries the same fact set, and not useful. The destination is unknown until neighbour resolution, after the seat: absent.

Then IP, from skb_network_offset():

  • IPv4 — addresses, TTL, DSCP (tos >> 2), the fragment flag (IP_MF set or a non-zero fragment offset), the protocol, and the L4 facts from ihl * 4 on. A non-first fragment (non-zero offset) has no L4 facts — its payload begins mid-datagram — though it still reads the protocol its header names.
  • IPv6 — addresses, hop limit, DSCP from the traffic class, and a bounded walk (eight hops) of the extension-header chain: hop-by-hop, routing and destination options are skipped by (hdrlen + 1) * 8; a fragment header sets the fragment fact, and a non-first fragment ends the walk with no L4 facts (there are none to read). The protocol fact is the header the walk stops at — so an MLD report behind a hop-by-hop header reads Protocol = icmpv6, as a rule would expect. Every way out of the walk names one: a non-first fragment reads the protocol its fragment header names, as a later IPv4 fragment reads its own; an extension header too short to read is itself the protocol; and after eight hops the next header is the protocol, with L4 facts if it is not another extension header.

Then L4, by protocol: ports for TCP, UDP and SCTP; the flag byte for TCP (TcpFlags is the TCP header's byte at offset 13, FIN in bit 0 through CWR in bit 7, the same encoding tcp_flag_byte() uses); type and code for ICMP and ICMPv6. Every header read goes through skb_header_pointer(), so a packet whose headers are paged or truncated yields absent facts rather than a fault.

6.3.3 Flow state and the flow #

At the ingress seat conntrack has not run and the flow state is ABSENT; at LOCAL_IN, LOCAL_OUT and egress, nf_ct_get() gives the entry and ctinfo maps to the fact: IP_CT_ESTABLISHED and its reply → established; IP_CT_RELATED and reply → related; IP_CT_NEW → new; anything else, or no entry at all, → untracked. The invalid value is reserved: distinguishing an incoherent packet needs conntrack's own verdict, which this seat does not receive.

The snapshot also carries flow: the struct nf_conn * itself (never a template), or NULL. This is the tag store's and the sentence's scope — TAG writes land on that entry's extension, tag reads come from it, and the Flow layer's verdict is cached there. It is the one pointer in the snapshot that outlives the extraction, and it is valid because the skb holds a reference to the entry for the duration of the hook.

With the flow come two facts that exist only on a flow: flow_related (ct->master != NULL — the flow was expected by another) and the flow's start time, read from the extension's start_secs (stamped when conntrack created the entry) through the same time64_to_tm() lowering as the clock, into the s_* fields (HAS_START). These are the Flow layer's Related and Start.* facts. flow_reply says whether this packet travels in the flow's reply direction — the Flow layer's view builder (§6.8) uses it to turn the packet's tuple back into the flow's.

6.3.4 What the core sees #

NtfeSnapshotC in kacs/ntfe_runtime.rs mirrors the C struct field for field (#[repr(C)]; keep them in lockstep). snapshot_from_c() lifts it into the core's Snapshot — an Option per fact — and the bridge then fills in the two machinery fact tables: tags, as (name hash, value) pairs for every tag name the forest can read that the flow carries, and counter_views, as (view index, value) for every view the forest reads that the store can answer for this packet. Both are resolved before evaluation, against the forest being evaluated, which is why the forest carries its name sets (§6.5).

Three visibility laws are enforced here, and again in the core, which builds a condition over a fact its layer never has never to hold (§6.5): a RawPacket forest is given no tags whatever the flow says (tags flow upward only, and RawPacket is the lowest layer; Packet and Flow forests read them); an ingress snapshot has no flow, so no tags exist to give; and the flow-only facts (Related, Start.*, and the identity facts) are given to a Flow forest alone — everywhere else they are absent by law, exactly as ingestion's lint says (§6.5). The clock is given to every layer, and so is the trace of consulted time conditions (§6.4); only the Flow seat acts on it.

6.3.5 The network context #

Three string fields are not read from the packet either: network_id, network_name and network_trust, the Network.* facts. peios_ntfe_context_fill() (context.c) looks the device's name up in the active context table — the kernel's reading of netd's inventory, one entry per interface standing on an identified network, built by ingestion (§6.5) and published under RCU — and copies the entry's three strings in, setting HAS_NETWORK. The bridge lifts the id whenever the bit is set, and the name and trust only when non-empty (a record the operator has not labelled). No entry, no bit: an interface no network has been identified on carries no context, and every condition over the three facts is false there. The strings are bounded (40, 64 and 32 bytes with their NUL, PEIOS_NTFE_NETWORK_*_LEN); a longer registry value — a record's Name or Trust, or the Network an interface's Status names — is truncated at ingestion and the truncation logged once. The id of a record is its key name under Networks\, not a value, and is not truncated: a name of 40 bytes or more is ignored, record and all (§6.5).

The fields are the three Network.* facts the interface layer — the fourth layer, which netd builds and judges in userspace — conditions on beside its own (see the interface layer): netd fills them from the interface record when it judges an interface, the kernel from the table when it judges a packet, so Network.Trust.Equal reads the same record in either layer.

Flow refreshes these facts on a cache miss after capturing its cache invalidation generation (§6.8), because this packet snapshot can predate a context publication. Packet and RawPacket continue to use their traversal snapshot; their verdicts are not retained as Flow sentences.

6.3.6 The identity fields #

The snapshot's last fields are not extracted from the packet at all. The Flow layer's view builder (§6.8) sets them from the flow's extension: for the local end, and on loopback for the other end, the kind (program, kernel, shared, none), the process GUID, pid and comm, and a borrowed pointer to the KACS token the socket was stamped with — the extension holds the reference for the flow's life, so the pointer is valid for the hook. The bridge lifts a program end into a Principal the core's Local.* and Remote.* conditions question (§6.9); every other snapshot leaves the fields zero, which lifts to absent.

Edit this page