nftables Deployment
The dhcp-processor daemon installs the inspection chains, enforcement sets, per-msg-type routing, and counters automatically every time it starts. You do not deploy the rules by hand on a normal install.
How the Ruleset Reaches the Kernel
Section titled “How the Ruleset Reaches the Kernel”The daemon reads its active firewall ruleset from the database on startup and applies it to the kernel atomically. The Firewall Manager (chapter 20) is the supported way to view, edit, or switch rulesets.
The flow:
install.shseeds the database with the shipped default rulesets (one or more named profiles).- On every start,
dhcp-processorreads the currently active ruleset from the database, builds theinet dhcp_inspectiontable, and applies it to the kernel in a single transaction. - Operator changes made through the Firewall Manager GUI are written back to the database; the daemon picks them up on apply (or on next restart, depending on the change type). See chapter 20 for the operator workflow.
- The Flow Visualizer (chapter 21) shows the same ruleset graphically, including how each message-type chain traverses the enforcement sets — use it to understand what the currently loaded ruleset will actually do to a packet.
You never need to invoke nft by hand to deploy or modify the production ruleset. Low-level nft commands are still useful for verification and debugging (covered in the next section), but they should not be your editing surface.
Multiple rulesets can be active at the same time. The database can hold several named rulesets and any subset of them can be marked active concurrently. The daemon applies them in sequence using standard nftables semantics — they are not idempotent, the order of application matters, and the system does not check for collisions between rulesets. It is the operator’s responsibility to design rulesets that do not contradict or shadow one another. If you are not sure how the pieces will interact, keep a single ruleset active. Chapter 19 explains how application order is determined and what the safe composition patterns look like.
Where the Shipped Shell Script Fits
Section titled “Where the Shipped Shell Script Fits”The repository still includes nft-config/nft-v2.sh and the JSON profiles under nft-config/. These are reference artifacts, not the production deployment path:
nft-v2.shis the human-readable source of the shipped default ruleset. Use it to read what the defaults look like, or as a recovery tool if the database is unreachable.nft-config/dualstack_*_throttling.json(low / medium / strict) are the shipped throttling profiles.install.shwrites them into the database; Firewall Manager exposes them as selectable profiles afterwards.- Older per-protocol-stack files under
nft-config/obsoleted/should not be loaded against the current ruleset.
If you ever need to push the shipped baseline back into the kernel directly (for offline debugging on a host where the daemon cannot start), sudo ./nft-config/nft-v2.sh still works. It flushes the existing inet dhcp_inspection table and replaces it with the shipped default. Treat this as an escape hatch, not a normal operation.
What the Daemon Installs
Section titled “What the Daemon Installs”Whatever ruleset is loaded, it always has the same structural skeleton — only the rate-limit thresholds, timeouts, and set sizes vary between profiles. The skeleton:
Top-level chains (hook-attached):
| Chain | Hook | Priority | Purpose |
|---|---|---|---|
dhcp_inspect | prerouting | -100 | Captures DHCP packets, queues to NFQueue (bypass fanout) |
drop_mirrored | prerouting | -99 | Drops mirrored TZSP traffic on ports 1067 / 1547 after analysis |
route_by_type | prerouting | -98 | vmap on mark high byte → jumps to per-msg-type chain |
dhcp_inspect_output | output | -100 | Captures outbound DHCP responses, queues to NFQueue |
route_output_by_type | output | -98 | vmap on mark high byte → jumps to outbound per-msg-type chain |
mac_actions | prerouting | 90 | LLM-set safety net for untyped marks |
dhcp_block | prerouting | 100 | Per-client rate-limit safety net |
Per-msg-type chains (jumped to by route_by_type / route_output_by_type):
| High byte | Inbound chain | Outbound chain |
|---|---|---|
0x01 DISCOVER (v4) | v4_discover_chain | — |
0x02 OFFER (v4) | — | v4_offer_out_chain |
0x03 REQUEST (v4) | v4_request_chain | — |
0x04 DECLINE (v4) | v4_decline_chain | — |
0x05 ACK (v4) | — | v4_ack_out_chain |
0x06 NAK (v4) | — | v4_nak_out_chain |
0x07 RELEASE (v4) | v4_release_chain | — |
0x08 INFORM (v4) | v4_inform_chain | — |
0x11 SOLICIT (v6) | v6_solicit_chain | — |
0x12 ADVERTISE (v6) | — | v6_advertise_out_chain |
0x13–0x1B (v6 client) | one per type | — |
0x17 REPLY (v6) | — | v6_reply_out_chain |
0x1A RECONFIGURE (v6) | — | v6_reconfigure_out_chain |
0x1C RELAY-FORW (v6) | v6_relay_forw_chain | — |
0x1D RELAY-REPL (v6) | — | v6_relay_repl_out_chain |
Plus default_enforce_chain which route_by_type jumps to for marks with no type bits but a non-zero client mark.
Inside each per-msg-type chain the order is fixed: strip type bits → check LLM sets (allow → block → deny → throttle → monitor) → check blocked_macs → per-client rate limit using tracked_marks and blocked_macs. See Flow Visualizer for the full ladder.
Sets (defaults for the shipped profiles; per-ruleset configurable in Firewall Manager):
| Set | Type | Default timeout | Size cap |
|---|---|---|---|
blocked_macs | mark | 2 min | 2.5M |
tracked_marks | mark | 1 min | 2.5M |
llm_blocked_marks | mark | 2 h | 1M |
llm_denied_marks | mark | 2 h | 1M |
llm_throttled_marks | mark | 7 d | 1M |
llm_allowed_marks | mark | 30 d | 1M |
llm_monitored_marks | mark | 14 d | 1M |
Counters: named counters for every DHCP message type (<type>_total, <type>_a, <type>_d), plus system counters all_dhcp_packets, inbound_to_nf, outbound_to_nf, mirrored_dropped, tracked_a, freshly_blocked_macs, default_enforce_a.
Queue Configuration
Section titled “Queue Configuration”The inbound dhcp_inspect chain queues packets to queue num 1-4; the outbound dhcp_inspect_output chain queues to queue num 1-8. The daemon binds to the matching range based on a single config.yaml setting:
nfqueue: queue_count: 8 # IMPORTANT: must match the nft queue range. Default 8.queue_count is the only queue-related YAML field the binary parses (there is no queue_start). The daemon numbers queues from 1 upward and writes the matching queue num 1-N ranges into the ruleset it installs, so the YAML value and the kernel ruleset stay in sync automatically. You only need to touch this if you have a specific reason to change the worker count.
Important: queue counts must match. The number of queues declared in the nftables ruleset must match the number of queues the daemon binds to. If nftables spreads packets across queues that the application is not reading, those packets will be missing from event storage, graphs, the WebSocket stream, and every downstream analysis — the appliance reports false-low traffic for affected clients. Packets are not dropped, however: the kernel’s NFQueue
bypassflag forwards anything that is not picked up in time, so the DHCP transaction itself completes normally. The only cost is added latency — one NFQueue read timeout per unpicked packet (default 50 ms). In other words, mismatched queue counts degrade observability, not availability. Because the daemon-installed ruleset always matches its ownqueue_count, you can only get into this state by editing the kernel ruleset by hand (or by running the legacy shell script with a different value) after the daemon has started.
Verifying Deployment
Section titled “Verifying Deployment”After deploying rules, verify the table, chains, and sets are created correctly.
List the Table
Section titled “List the Table”sudo nft list table inet dhcp_inspectionExpected output (trimmed) shows the table with all chains and sets:
table inet dhcp_inspection { counter all_dhcp_packets { packets 0 bytes 0 } ... set blocked_macs { type mark flags dynamic,timeout timeout 2m size 2500000 } ... chain dhcp_inspect { type filter hook prerouting priority -100; policy accept; udp dport != { 67, 68, 546, 547, 1067, 1547 } accept ... } chain route_by_type { type filter hook prerouting priority -98; policy accept; meta mark & 0xff000000 vmap { ... } } ...}Check Specific Counters
Section titled “Check Specific Counters”After running for a while, verify packets are being processed:
sudo nft list counters table inet dhcp_inspectionExpected output shows non-zero packet counts:
counter all_dhcp_packets { packets 6108 bytes 2066174}counter inbound_to_nf { packets 6108 bytes 2066174}If all_dhcp_packets is zero, DHCP traffic is not reaching the host. Check your network positioning (see Deployment Modes -> Common Topologies).
Check Set Contents
Section titled “Check Set Contents”View which marks are currently in a set:
sudo nft list set inet dhcp_inspection blocked_macsExpected output when MACs have been blocked:
set blocked_macs { type mark flags dynamic,timeout timeout 2m size 2500000 elements = { 0x00aabbcc timeout 2m expires 1m45s, 0x00112233 timeout 2m expires 0m30s }}Set entries store the 24-bit client mark (last 3 bytes of the MAC); the high byte (DHCP message type) is stripped by the per-msg-type chain before the set lookup. An empty elements section means no MACs are currently blocked.
Firewall Profiles — Your Source of Truth
Section titled “Firewall Profiles — Your Source of Truth”The shipped throttling profiles cover the typical enforcement intensities. They are loaded into the database at install time and exposed through Firewall Manager afterwards. Treat the profiles in the database as the source of truth for what your appliance is enforcing.
| File | Description |
|---|---|
nft-v2.json | JSON export of the shipped default ruleset |
dualstack_low_throttling.json | Profile with low dual-stack throttle rates |
dualstack_medium_throttling.json | Profile with medium dual-stack throttle rates |
dualstack_strict_throttling.json | Profile with strict dual-stack throttle rates |
These files are nft -j list ruleset exports. The dualstack_*_throttling.json files are what install.sh writes into the database as switchable profiles. The supported way to switch between them is the Firewall Manager (see chapter 20). Hand-editing the JSON files on disk and re-running the shell script is the offline fallback only.
Older releases shipped per-protocol-stack profiles (ipv4_*_throttling.json); those are now under nft-config/obsoleted/ and should not be loaded against the current ruleset.
Working With Profiles Safely
Section titled “Working With Profiles Safely”A handful of habits keep the firewall configuration recoverable:
- Test every change before it goes wide. Activate a candidate profile on a representative subset (or a staging appliance) and watch the counters, the Flow Visualizer, and a sample of clients before promoting it everywhere.
- Never edit an existing profile in place. When you want to alter behaviour, create a new profile — copy the current one, change what you need, save it under a new name — and switch over. The original stays available for instant rollback. Editing a profile in place destroys your rollback target.
- Back up the appliance regularly. A full installation backup is best (database, configuration, license artifacts). At minimum, export your firewall profiles from Firewall Manager on a schedule so you can re-import them after a rebuild.
- “Deleted” profiles are not really deleted. When you delete a profile from Firewall Manager, the row is hidden in the GUI but remains in the database indefinitely. You can undelete (restore) it from Firewall Manager at any time, so an accidental deletion is not catastrophic — but do not rely on this as your backup strategy; export profiles regardless.
Common Issues
Section titled “Common Issues”dhcp_inspection Table Missing After Daemon Start
Section titled “dhcp_inspection Table Missing After Daemon Start”If sudo nft list table inet dhcp_inspection returns “No such file or directory” after the daemon has started, the daemon failed to install its ruleset. Check:
- The daemon is actually running:
systemctl status dhcp-processor. - The journal for nftables-related errors:
journalctl -u dhcp-processor | grep -i nft. - The database is reachable — the daemon cannot fetch its ruleset without ClickHouse.
systemctl status clickhouse-server. - The
nf_tableskernel module is loaded (lsmod | grep nf_tables). If missing:sudo modprobe nf_tablesand restart the daemon.
Queue Mismatch
Section titled “Queue Mismatch”If the daemon logs “failed to bind to queue” errors, something has changed the kernel ruleset out from under it (typically a hand-edited reload of nft-v2.sh with a different queue range, or another tool flushing the ruleset). Restart dhcp-processor to reinstall its own ruleset, which always matches the nfqueue.queue_count value in config.yaml.
Packets Not Being Processed
Section titled “Packets Not Being Processed”If counters show packets arriving (all_dhcp_packets > 0) but the application reports no events:
- Check the daemon is running:
systemctl status dhcp-processor. - Check the journal for queue-binding messages near startup.
- Confirm the
bypassflag is present in the active rule — the daemon-installed ruleset usesqueue num 1-4 bypass fanoutinbound and1-8 bypass fanoutoutbound. Withoutbypass, packets are silently dropped when the daemon is not running.
”Flush ruleset” Side Effects
Section titled “”Flush ruleset” Side Effects”The daemon does not call flush ruleset; it only installs and updates its own inet dhcp_inspection table, leaving other tables on the host untouched. The legacy nft-v2.sh shell script, however, does begin with flush ruleset and will wipe every other nftables table on the host. Run the script only as an offline recovery tool, and only on a host where no other nftables rules need to survive.
Surrounding nftables Configuration Fighting the Daemon
Section titled “Surrounding nftables Configuration Fighting the Daemon”The recurring failure modes:
- Inbound restrictions blocking client DHCP. A
preroutingorinputrule from an unrelated table that drops UDP from “unknown” sources will silently swallow DISCOVER / SOLICIT packets before thedhcp_inspectchain ever sees them. Counters stay at zero, no events appear, clients fail to acquire leases. The DPI looks dead; the real cause is an upstream ACL. - Outbound restrictions toward the DHCP server. Egress filters that limit traffic from the appliance to the server’s IP / port — common in tightened “default deny” baselines — prevent relayed requests from reaching the server. Inbound counters tick, outbound packets disappear, the DHCP server logs nothing.
- Mistakes in the overall flow. Custom chains hooked at the same or higher priority as DPI’s own chains can drop, mangle, or re-route packets out from under the inspector. Symptoms range from missing events to broken Option 82 to packets traversing the wrong interface.
- Server replies blocked on the return path. Nftables or conntrack drops the OFFER / ACK / REPLY coming back from the DHCP server because the operator forgot that the appliance is in the conversation. Clients see DISCOVER → silence; the server actually replied, but the reply never made it back.
- Mixing multiple firewall front-ends.
nftables,iptables(iniptables-nftcompatibility mode),ufw, andfirewalldall ultimately program the same kernel tables, often without knowing about each other. The shipped appliance assumes only DPI is configuring the firewall. Ifufw enableorfirewalldstarts on boot in parallel, you will get duplicate or contradictory rules, mysterious drops, and a ruleset that does not match anything in the database. Disable every other firewall front-end on the appliance host.
How to stay out of trouble:
- Treat the DPI host as a single-purpose appliance. Do not co-host it with a general-purpose router / NAT box / corporate firewall.
- If you must combine DPI with other rules, do it inside the same
inet dhcp_inspectiontable the daemon owns, and review the merge in Firewall Manager (chapter 20) rather than from the shell. - Mask the OS-level firewall services you are not using:
systemctl mask ufw firewalld iptables(whichever apply to your distro). - After any unrelated firewall change on the host, re-run the verification commands above — if
all_dhcp_packetsstops incrementing, the new rule is the suspect, not the daemon.