nftables Operations Manual
On this page
A rule existing, matching and ultimately allowing traffic are different facts. Identify input, forward or output and table ownership first. Examples are teaching fragments, not a replacement for a host ruleset shared with Docker, Tailscale or Nikki.
| Task | Start here |
|---|---|
| Accept rule but no traffic | Path and verdict |
| Repeated config loads | Atomic batch versus replacement |
| Find the matching rule | Counters and trace |
| Forwarding / NAT | Translation and permission |
Find the hook before reading rule order
Identify ownership and hooks first
# List families and tables to identify owned objects
inet/ip/ip6 have different scopes; exclude Docker/VPN-owned tables from this change.
# Read all hooks, priorities, policies, and handles
-a prints handles for precise changes. The whole-system view reveals later drops; it is not a replacement scope.
# Focus on the owned filter table
NotFound is expected before first installation. If present, establish whether this file owns the entire table.
Establish the expected packet path
# Inspect destination routing and source selection
Use the actual destination; this local lookup does not simulate every forwarded packet’s source or mark.
# Verify that the local target port has a listener
An accept rule cannot create a missing listener. For forwarded traffic, inspect the destination host.
On one input hook, an early base-chain accept is not final acceptance; a later chain may drop. Forward/output traffic does not visit these input chains.
External local traffic uses input, transit traffic forward, locally generated traffic output. Lower base-chain priorities run first; do not depend on ordering ties. Accept ends the current chain but later base chains can drop. Ordinary chains need references; the name input alone attaches no hook.
One transaction does not imply clearing old rules
nft -f submits a batch; add-only files can accumulate rules on repeated loads. Define the owned table and replacement boundary. This sample creates an independent accept-policy table for structure, not a complete security policy or an override of other tables.
table inet ops_filter {
set admin4 {
type ipv4_addr
flags interval
elements = { 192.0.2.0/24 }
}
chain input {
type filter hook input priority 10; policy accept;
ct state established,related counter accept
iifname "lo" counter accept
meta l4proto ipv6-icmp counter accept
ip saddr @admin4 tcp dport 22 counter accept
}
}
First load: file, validation, commit, request
Save the complete example as ./ops-filter.nft, replace admin4, and confirm the table is absent. This file owns only that table. Preserve existing owned configuration and independent management access first.
# Check whether the batch can be accepted in current state
-c does not change rules. Resolve syntax, references, or kernel support errors first; it does not test SSH reachability.
# Commit the reviewed, checked rule batch
On failure inspect the error and list again; successful validation is not successful installation.
# Verify installed rules, set contents, and handles
Open new SSH/application connections independently and inspect counters. For persistence, inspect nftables.service to identify the actual loader path and install the verified file there.
Load initially only when the table is absent. For a fully owned existing table, put delete table inet ops_filter and its complete replacement in one file; check then load. Delete requires existence, so this is not an unconditional idempotent installer. Avoid global flush ruleset on shared hosts. -c is not a connectivity test; verify from another connection and retain the old config.
Edit by handle and trace a narrow flow
Modify rules by handle, not line number
# Refresh the chain’s rules and handles
Example 42 must be replaced by the verified handle. Re-read after edits rather than reusing stale identifiers.
# Append a source- and port-scoped example rule
add appends; insert defaults to the start. Inspect earlier terminal verdicts first. In this accept-policy chain, the rule mainly observes matches; it does not deny other sources.
# Replace the verified rule in place
This is an alternative for modifying an existing rule, not a mandatory step after add. Verify with a new connection and update the source configuration.
# Delete the verified unwanted rule
Re-list afterward; for an experiment, restore the original configuration and verify traffic.
Add and remove set elements at different stages
# Add a verified administration address to the set
Quotes preserve nft braces. Verify with nft list set. If an interval already covers the address, inspect existing elements first.
# Remove the element when it is no longer needed
This is later removal, not an immediate sequel. Do not delete the shared set.
Trace: mark, reproduce, then clean up
Create a new owned trace_input and trace only incoming local TCP 443 from the selected source. It cannot explain packets dropped at earlier hooks.
# Create a temporary chain before ordinary input filtering
# Enable nftrace only for the selected test traffic
# Watch trace events while reproducing from another client
Follow chains/verdicts for the same trace ID. If empty, check whether traffic reaches the marking rule. Ctrl-C stops the viewer, not the marking rule.
# Clear only this dedicated temporary chain after testing
Confirm the chain contains only this experiment before clearing it, then remove the empty chain.
# Remove the empty temporary trace chain
Re-list the table to confirm cleanup; preserve other chains and the ruleset.
Read current handles before delete/replace; line numbers are not handles. Counters require counter statements. monitor trace observes packets already marked nftrace; it does not enable tracing globally. Add a narrowly scoped nftrace rule in an owned early chain, record its handle, reproduce once and remove it.
DNAT changes destination, not forwarding permission
| Layer | Requirement |
|---|---|
| Routing | Forwarding enabled, destination and return routes available |
| DNAT | Ingress usually uses prerouting; locally generated traffic follows output |
| Forward filter | Allow required traffic using the actual post-DNAT fields |
| SNAT / masquerade | Use where source translation/return routing requires it |
table ip ops_nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "wan0" ip daddr 192.0.2.20 tcp dport 8443 dnat to 10.0.0.10:443
}
}
This IPv4 fragment needs a verified owned gateway, interface/addresses, forward rules and return path. Conntrack retains NAT bindings for existing connections, so test a new connection. iptables-nft and Docker’s native nftables backend are different modes; iptables=false is not a backend selector.