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_MFset or a non-zero fragment offset), the protocol, and the L4 facts fromihl * 4on. 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 readsProtocol = 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.