ng-policy

A FreeBSD netgraph kernel node for fast, stateless, identity-aware policy enforcement on Ethernet frames — precompiled endpoint rules, swapped in atomically as policy generations.

Cwipnetgraph · kernel module · microsegmentation · firewallfreebsd from gosource on github

Why

Microsegmentation needs a gate between each workload and the network that can say yes or no to every frame at line rate, and can take a new rule set without stalling traffic. ng_policy is that gate, as a netgraph node type (policy) in the kernel.

It is deliberately the fast path only. Rules, endpoint registrations and an IPv4-to-endpoint address map arrive over a netgraph control socket already compiled. Turning tenant, security-group or label intent into endpoint IDs and IPv4 rules is a userland job, out of scope. The kernel knows endpoint IDs, addresses, protocols, ports, ICMP type and code, actions and generation numbers — nothing about tenants, VPCs, jails, VMs or clusters, and a grep over the sources proves it.

How

  • An endpoint is a pair of hooks. A workload side (ep-<id>) and a fabric side (fab-<id>), registered together. An allowed frame always leaves on the other hook of the same endpoint, so the node is a policy gate, not a switch: it never consults a routing table or learns topology from traffic. Hooks bind lazily — connect before or after registering, either order works.
  • Fail-closed by default. A fresh node starts at generation 0 with default_action = DROP and mode = ENFORCE, and blocks everything until a policy is committed. An unbound hook drops too, and counts it.
  • Atomic generations. Rules and addresses load in bulk inside a transaction; the commit builds an immutable snapshot and swaps one pointer. Stale-generation commits are rejected, and an abandoned transaction is reaped at its deadline.
  • First match by priority, through an index. Rules sit sorted by priority and insertion order; a per-source-endpoint hash and a separate ANY list are merge-walked so the lookup visits candidates in exactly the order a full scan would, touching only rules that could match. Three tests exist just to check that equivalence.
  • Three modes. DISABLED, PERMISSIVE (count violations, forward anyway) and ENFORCE. ARP, fragments and unsupported EtherTypes each have their own configurable action and never reach L4 matching.
  • Counters with an invariant. Per node, per endpoint (credited to the ingress endpoint) and per rule, discarded with their generation. packets_total == allowed + dropped + no_peer is checked under concurrent policy replacement.

v0.1 is stateless

An ALLOW from A to B says nothing about B to A. There’s no connection table and no “allow the reply”: protocol=TCP means match the TCP ports, not track the connection. The first single-host proof had only A→B rules — pings and SYNs went out and then hung forever, because the replies hit default-deny. A control plane compiling a symmetric-looking intent must emit two rules, not one.

Numbers

On a shared 6-vCPU QEMU guest running FreeBSD 14.3: building a 100,000-rule generation takes about 100–130 ms, all of it before the swap. The part that holds the node exclusively — the pointer swap — measured 0–1 µs at every size. Lookup latency stayed flat from 10 to 10,000 rules, which is the evidence that the index is actually consulted. Line-rate dataplane throughput needs real NICs and hasn’t been measured; the docs say so.

Verified, and by what

The README keeps a table of thirty properties, each paired with the test or session that demonstrates it — and where nothing proves a row, the row says so. 62 kernel tests run against the loaded module through the real control ABI, exactly as a daemon would. A seeded fuzzer covers both the control-message and packet-parsing paths, a stress script cycles nodes and module loads, and vmstat -m must show nothing left behind. The paging tests were checked against deliberately planted off-by-one bugs before being trusted.

A single-host proof wires epair and tap interfaces through the node with stock ngctl, ifconfig and VNET jails — and found that addresses on the same host bypass the wire through lo0, so each endpoint has to live in its own network stack. tun can’t attach at all: it has no Ethernet framing, so ng_ether never gives it a hook.

Status

No release yet. The module, man page ng_policy(4), tests and nine design documents are in the repo; port/ is a skeleton for net/ng-policy that needs a release tarball before distinfo can exist. Stateful matching is planned only after the stateless core has proven stable. go-freebsd-netgraph grew out of building it: its guarantees are the failures hit here on raw libnetgraph.

make
doas kldload ./sys/netgraph/ng_policy/ng_policy.ko
doas ./tests/ngp_test              # 62 run, 0 failed
doas ./tests/ngp_fuzz 12345 200000 # control- and packet-path fuzzing
doas kldunload ng_policy
vmstat -m | grep -c ng_policy      # expect 0 — nothing leaked
$ git clone https://github.com/zombocoder/ng_policy-kmod
← all projects