Skip to content

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.

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:

  1. install.sh seeds the database with the shipped default rulesets (one or more named profiles).
  2. On every start, dhcp-processor reads the currently active ruleset from the database, builds the inet dhcp_inspection table, and applies it to the kernel in a single transaction.
  3. 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.
  4. 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.

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.sh is 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.sh writes 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.

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):

ChainHookPriorityPurpose
dhcp_inspectprerouting-100Captures DHCP packets, queues to NFQueue (bypass fanout)
drop_mirroredprerouting-99Drops mirrored TZSP traffic on ports 1067 / 1547 after analysis
route_by_typeprerouting-98vmap on mark high byte → jumps to per-msg-type chain
dhcp_inspect_outputoutput-100Captures outbound DHCP responses, queues to NFQueue
route_output_by_typeoutput-98vmap on mark high byte → jumps to outbound per-msg-type chain
mac_actionsprerouting90LLM-set safety net for untyped marks
dhcp_blockprerouting100Per-client rate-limit safety net

Per-msg-type chains (jumped to by route_by_type / route_output_by_type):

High byteInbound chainOutbound 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
0x130x1B (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):

SetTypeDefault timeoutSize cap
blocked_macsmark2 min2.5M
tracked_marksmark1 min2.5M
llm_blocked_marksmark2 h1M
llm_denied_marksmark2 h1M
llm_throttled_marksmark7 d1M
llm_allowed_marksmark30 d1M
llm_monitored_marksmark14 d1M

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.

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 bypass flag 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 own queue_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.

After deploying rules, verify the table, chains, and sets are created correctly.

Terminal window
sudo nft list table inet dhcp_inspection

Expected 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 { ... }
}
...
}

After running for a while, verify packets are being processed:

Terminal window
sudo nft list counters table inet dhcp_inspection

Expected 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).

View which marks are currently in a set:

Terminal window
sudo nft list set inet dhcp_inspection blocked_macs

Expected 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.

FileDescription
nft-v2.jsonJSON export of the shipped default ruleset
dualstack_low_throttling.jsonProfile with low dual-stack throttle rates
dualstack_medium_throttling.jsonProfile with medium dual-stack throttle rates
dualstack_strict_throttling.jsonProfile 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.

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.

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:

  1. The daemon is actually running: systemctl status dhcp-processor.
  2. The journal for nftables-related errors: journalctl -u dhcp-processor | grep -i nft.
  3. The database is reachable — the daemon cannot fetch its ruleset without ClickHouse. systemctl status clickhouse-server.
  4. The nf_tables kernel module is loaded (lsmod | grep nf_tables). If missing: sudo modprobe nf_tables and restart the daemon.

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.

If counters show packets arriving (all_dhcp_packets > 0) but the application reports no events:

  1. Check the daemon is running: systemctl status dhcp-processor.
  2. Check the journal for queue-binding messages near startup.
  3. Confirm the bypass flag is present in the active rule — the daemon-installed ruleset uses queue num 1-4 bypass fanout inbound and 1-8 bypass fanout outbound. Without bypass, packets are silently dropped when the daemon is not running.

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 prerouting or input rule from an unrelated table that drops UDP from “unknown” sources will silently swallow DISCOVER / SOLICIT packets before the dhcp_inspect chain 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 (in iptables-nft compatibility mode), ufw, and firewalld all ultimately program the same kernel tables, often without knowing about each other. The shipped appliance assumes only DPI is configuring the firewall. If ufw enable or firewalld starts 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:

  1. Treat the DPI host as a single-purpose appliance. Do not co-host it with a general-purpose router / NAT box / corporate firewall.
  2. If you must combine DPI with other rules, do it inside the same inet dhcp_inspection table the daemon owns, and review the merge in Firewall Manager (chapter 20) rather than from the shell.
  3. Mask the OS-level firewall services you are not using: systemctl mask ufw firewalld iptables (whichever apply to your distro).
  4. After any unrelated firewall change on the host, re-run the verification commands above — if all_dhcp_packets stops incrementing, the new rule is the suspect, not the daemon.