Appendix 6.B NTFE ABI Notes
§6.A is generated from pkm/uapi/pkm/ntfe.h and holds only what a
compiler can measure. This appendix holds the rest.
6.B.1 Stability #
The ABI is experimental while NTFE grows (PEI-598): no stability
promise until the design ships. peios_ntfe_status.abi carries
PEIOS_NTFE_ABI_VERSION; a consumer checks it before trusting any other
field or record layout. Version 2 (the machinery slice) added the
event's reject_kind, the store confessions and counter_cells and
reporting_level in the status, and the counters ioctl. Version 3 (the
Flow layer) added the LOCAL_OUT seat and Flow layer ids, the
REJUDGED event flag, the Flow and refusal counters in the status, and
the flows ioctl. Version 4 (the identity facts) added the endpoints'
identities to the event and the flow record, the IDENTITY_UNRESOLVED
event flag and the identity_unresolved counter (in a reserved slot,
so the status kept its size). Version 5 renamed the device and every
symbol from pnp to ntfe, let more than one file hold the device open,
and added changes_noted, changes_walked and contexts to the status:
the first took one of the two reserved words version 4 left, the other
two are new, so the status grew by two words and keeps one reserved. pnpd and the kernel ship together on the
experimental edition, so the check is a guard, not a negotiation.
6.B.2 /dev/peios-ntfe #
A misc device, created mode 0600 and owned by uid 0 — but on Peios mode
bits and uids decide nothing (DAC is neutralised): KACS decides an
open() by the opener's token against the node's security descriptor
(§3.9.5). On devtmpfs that descriptor is inherited from the seeded
root's, which grants GENERIC_ALL to SYSTEM alone, so the device is
SYSTEM's unless trusted userspace sets another.
| Operation | Semantics | Errors |
|---|---|---|
open() | Any number of openers. | — |
read(buf, len) | Returns whole struct peios_ntfe_event records only — never a partial one — up to 64 per call, oldest first, consuming them. Blocks on an empty ring unless O_NONBLOCK. | EINVAL — len smaller than one record; EBUSY — another open file is the stream's reader; EAGAIN — empty and non-blocking; EINTR; ENOMEM; EFAULT — the records it took are lost (below) |
poll() | POLLIN | POLLRDNORM when at least one event waits. | — |
ioctl(PEIOS_NTFE_IOC_STATUS, struct peios_ntfe_status *) | Fills the status snapshot. Cumulative counters since boot. | EFAULT |
ioctl(PEIOS_NTFE_IOC_COUNTERS, struct peios_ntfe_counters_query *) | buf/buf_len describe a user buffer of struct peios_ntfe_counter_rec; on return count is how many were written and total how many cells exist. A short buffer is not an error — the two numbers disagree. Best-effort snapshot: cells may change between records. | EFAULT; ENOMEM |
ioctl(PEIOS_NTFE_IOC_FLOWS, struct peios_ntfe_flows_query *) | Same contract over struct peios_ntfe_flow_rec: count written, total live flows the walk saw. Records are copied out between hash buckets, so the dump is a best-effort picture of a table that changes under it. | EFAULT; ENOMEM |
ioctl(PEIOS_NTFE_IOC_LISTENERS, struct peios_ntfe_listeners_query *) | Same contract over struct peios_ntfe_listener_rec: every TCP socket in the listening state and every bound UDP / UDP-Lite socket of the root network namespace, with the identity KACS stamped on it (§6.9). total is how many the walk saw. A hash bucket holding more sockets than one copy-out batch is walked again from where the batch filled, so with room for every record count equals total. | EFAULT; ENOMEM |
| other ioctls | — | ENOTTY |
Sequence numbers are monotonic per boot. A gap between consecutive
records read is the number of events the ring overwrote while the
reader was away, plus any a faulting read() consumed; events_dropped in the status is the running
total of the overwrites. A read() that fails with EFAULT has
already taken its records off the ring: they are lost, and counted
nowhere but in that gap.
6.B.3 Event fields #
attributedis the winning rule's path relative to its layer key, UTF-8, NUL-terminated, truncated toPEIOS_NTFE_EV_ATTR_LEN− 1 bytes (95), which may split a multi-byte character. Two reserved values:backstop(nothing yielded) andfail-closed(evaluation failed).effectspacks the effect counts the evaluation yielded —tags | counts << 8 | reports << 16 | prompts << 24, each saturating at 255. What the stores then applied is in the status confessions, not in the event.reject_kindis meaningful only whenverdictisPEIOS_NTFE_EV_VERDICT_REJECT; a degraded reject (flags & PEIOS_NTFE_EV_F_REJECT_DEGRADED) still carries the kind the rule chose.PEIOS_NTFE_EV_F_REJUDGEDmarks aFlow-layer evaluation that replaced a stale sentence (policy change or time edge). A packet answered by a current sentence produces no event at all.layer2 isFlow;seat4 isLOCAL_OUT. AFlowevent'sdirectionis the endpoint judged: the originator's side for a normal flow, each seat's own for a loopback flow's two judgments.- Addresses: the first 4 bytes when
addr_familyis 4, all 16 when 6, undefined when 0. Ports are 0 when the fact was absent — checkprotocol. flow_state0 means the fact was absent (the ingress seat), not that the flow was untracked; untracked is 5.- The identity fields (ABI 4) are set on
Flowevents only and zero elsewhere.local_kind/remote_kindarePEIOS_NTFE_EV_LOCAL_*:ABSENT(0) for a non-Flow event, and forremotewhenever the other end is not local;remoteis filled only on a loopback flow. For aPROGRAMend,*_guid,*_pidand*_commare the process facts at the socket's stamp,*_userthe token's user SID and*_serviceits per-service SID, both binary and self-sized (byte 1 is the sub-authority count; all zero = absent — a user program has no service SID).*_unresolvedsays the end could not be attributed and was reported as the kernel's or as absent.
6.B.4 Counter records #
nameis the stream name (a rule'sCOUNT(Name)), NUL-terminated;hashis its FNV-1a identity as the kernel keys tables.keyspecsays which ofsrc_addr,dst_addr,ifindexare meaningful for this cell's key; the others are zero.familyis 4, 6, or 0 when no address fact is keyed.window_secs[i]/window_value[i]fori < n_windowsare the table's windows and this cell's current value in each;totalis cumulative since the cell was created (or migrated — see §6.6);last_secsisCLOCK_REALTIMEseconds of the last write, for staleness.- The dump lists every table's cells consecutively; group by
(name, keyspec)to reconstruct tables.
6.B.5 Flow records #
idis conntrack's own id for the entry (nf_ct_get_id()), stable for the flow's life and the key to use across dumps.src_addr/dst_addr, the ports and the ICMP fields are the original-direction tuple — the originator first. For ICMP,src_portcarries the echo id andicmp_type/icmp_codethe type and code;dst_portis 0.direction,ifindexandloopbackare meaningful only whenjudgedis 1: they were recorded at the Flow layer's first judgment. A flow withjudged == 0has had no Flow judgment recorded: every packet of it that reached the flow dispatch (§6.8) found no Flow forest (a permissive generation) or failed closed, or none did — or it has no extension at all, and so nowhere to record one (flow_uncached; its sentences and identities read empty too).- The sentences are parallel arrays indexed by slot (UAPI records hold
scalars only): slot 0 is the flow's sentence; slot 1 is only ever
filled for a loopback flow (its inbound endpoint). A slot with
sentence_generation == 0is empty.sentence_expires_at0 means never.sentence_rule_hashis FNV-1a-64 (offset0xcbf29ce484222325, prime0x100000001b3) of the whole attributing path relative to the layer key, however long — not of the event's truncatedattributed.backstophashes like any other path;fail-closedis never a sentence's, since a failed evaluation is not cached. packets/bytesare conntrack's accounting, original then reply; NTFE turns conntrack accounting on at init (net.netfilter.nf_conntrack_acct).timeout_secsis the entry's remaining lifetime as conntrack sees it;start_secsisCLOCK_REALTIMEseconds when conntrack created it.tag_hash/tag_valuehold up toPEIOS_NTFE_FLOW_MAX_TAGS(8) present tags by name hash and value;n_tagsis the flow's total, so a value above 8 means some are not listed.- The identities (ABI 4) are per sentence slot, recorded at the flow's
first judgment and fixed:
owner_kind[slot]isPEIOS_NTFE_EV_LOCAL_*(ABSENT= not resolved;ABSENTwithowner_unresolved[slot]set = judged but unattributed, such as a loopback sender the outbound seat never recorded); the per-slot arrays are flattened at a fixed stride —owner_guid16 bytes per slot,owner_comm16,owner_user68,owner_service32 — so slot 1's user SID starts at byte 68. Slot 1 is filled only for a loopback flow.
6.B.6 Listener records #
- One record per socket prepared to receive: TCP in
LISTEN, and UDP / UDP-Lite bound to a port (connectedwhen it also has a peer and so receives from one address only).addrall zero is the wildcard;ifindexisSO_BINDTODEVICE, 0 for any;reuseportmarks a member of aSO_REUSEPORTgroup, of which each member is listed.v6onlysays anAF_INET6socket refuses v4-mapped traffic — a v6 socket without it answers on both families. - The owner fields are those of the flow record's slot, for the socket's
current stamp:
owner_kindisPROGRAMorKERNEL(a socket nobody stamped readsKERNELwithowner_unresolvedset). - The walk is the root namespace's tables only, copied out between hash buckets: a best-effort list of a set that changes under it.
6.B.7 Bounds not in the header #
6.B.8 Build configuration #
CONFIG_PEIOS_NTFE (bool) depends on SECURITY_PKM, NETFILTER,
NETFILTER_INGRESS, NETFILTER_EGRESS and NF_CONNTRACK=y — NTFE is built in and reads flow
facts on the packet path, so conntrack must be too. CONFIG_PEIOS_NTFE_KUNIT
builds the kernel-resident tests (pkm_kunit_ntfe), defaulting to
SECURITY_PKM_KUNIT. The production fragment (build/config/pkm.fragment)
enables NTFE and configures the nf_tables/xtables family out;
kernel/verify-kernel-config.sh asserts both.