-
Notifications
You must be signed in to change notification settings - Fork 0
Firewall Anchor Architecture
As of v1.62.0 | Contract document | Last updated: 2026-04-02
NFTBan's firewall uses a fixed rule structure called the Anchor Skeleton. Every packet entering the system traverses exactly 7 phases in a fixed order. Modules (DDoS, portscan, botguard, etc.) plug into this structure at defined insertion points. They cannot change the order, skip phases, or insert rules outside their designated zone.
This page documents the contract — how packets flow, where modules are allowed, and what invariants the system enforces.
- The Problem Anchors Solve
- Phase Model
- Anchor Markers
- Module Insertion Points
- Rule Order Walkthrough
- Ordering Invariants
- Structural Invariants
- Safety Invariants
- Address Family Parity
- Verification Commands
- What Anchors Are NOT
- Version History
NFTBan modules insert jump rules into the main input chain at runtime. Before v1.62.0, each module found its insertion point by grepping for a set or meter reference in the live chain (e.g., @whitelist_ipv4, syn_meter_v4). This worked but was fragile:
- Set names could change across versions
- A module inserting at the wrong position could shadow other rules
- No machine-readable way to verify "is the chain structure correct?"
- Health checks had to know implementation details (which set = which phase)
Anchors solve this by placing named counter rules with comments at phase boundaries. These markers are:
- Present in the template (survive rebuilds)
- Visible in
nft listoutput (verifiable at runtime) - Stable across versions (comment strings are the contract)
- Zero-cost (counter increment is CPU-negligible)
Every packet entering NFTBan's input chain traverses 7 phases in this exact order:
Phase 0: HYGIENE — Drop broken packets (ct state invalid)
Phase 1: TRUSTED — Accept known-good traffic (loopback, whitelist)
Phase 2: BAN — Drop known-bad traffic (blacklist, per-IP port access)
Phase 3: ESTABLISHED — Accept packets belonging to existing connections
Phase 4: DETECT — Rate limits, SYN metering, connection tracking limits
Phase 5: SERVICE — Accept new connections to allowed ports
Phase 6: FINAL — Log and drop everything else (policy drop)
This order is not negotiable. Specifically:
- Bans (phase 2) execute before established (phase 3). A banned IP's existing TCP connections are immediately killed. This is deliberate — moving established before bans would let banned IPs keep open connections alive. See:
CVE-2025-NFTBAN-001 protectioncomment in template. - Whitelist (phase 1) executes before blacklist (phase 2). A whitelisted IP can never be banned.
- Detectors (phase 4) execute after established (phase 3). Established connections bypass rate limits.
Each phase boundary has a named counter rule with a machine-readable comment:
counter name anchor_hygiene comment "NFTBAN_ANCHOR:ANCHOR_HYGIENE"
counter name anchor_trusted comment "NFTBAN_ANCHOR:ANCHOR_TRUSTED"
counter name anchor_ban comment "NFTBAN_ANCHOR:ANCHOR_BAN"
counter name anchor_established comment "NFTBAN_ANCHOR:ANCHOR_ESTABLISHED"
counter name anchor_detect comment "NFTBAN_ANCHOR:ANCHOR_DETECT"
counter name anchor_service comment "NFTBAN_ANCHOR:ANCHOR_SERVICE"
counter name anchor_final comment "NFTBAN_ANCHOR:ANCHOR_FINAL"
These lines appear in both the ip nftban and ip6 nftban input chains (14 markers total). They are defined in install/nftables/nftables.conf.tpl and survive firewall rebuilds.
The anchor counter values tell you how many packets reached each phase — useful for diagnostics.
Each module inserts its jump rule before a specific anchor. The jump sends matching packets to the module's subchain for processing.
| Module | Subchain | Inserts Before | Phase |
|---|---|---|---|
| DDoS sanity | ddos_sanity |
ANCHOR_TRUSTED |
1 |
| DDoS ban enforce | ddos_ban_enforce |
ANCHOR_BAN |
2 |
| DDoS penalty | ddos_penalty |
ANCHOR_ESTABLISHED |
3 |
| SYNPROXY | ddos_synproxy |
ANCHOR_ESTABLISHED |
3 |
| Portscan detection | portscan_detection |
ANCHOR_DETECT |
4 |
| DDoS classic | ddos_protection |
ANCHOR_SERVICE |
5 |
| DDoS prefix | ddos_prefix |
ANCHOR_SERVICE |
5 |
| HTTP Bot Guard | http_bot_guard |
ANCHOR_SERVICE |
5 |
Rule: A module's jump MUST have a lower handle number than its anchor. If a jump appears after its anchor, the module is functionally dead — packets have already passed the phase boundary.
Multiple modules can share an anchor. When they do, they are inserted in enable-order (each new jump goes before the anchor, pushing earlier jumps up). Within a phase, module order is not guaranteed and modules must be independent.
This is the canonical input chain with anchors (IPv4 shown, IPv6 is structurally identical):
chain input {
type filter hook input priority 0; policy drop;
# ── Phase 0: HYGIENE ──────────────────────────────────────
counter name anchor_hygiene comment "NFTBAN_ANCHOR:ANCHOR_HYGIENE"
ct state invalid → drop
# ── Phase 1: TRUSTED ──────────────────────────────────────
counter name anchor_trusted comment "NFTBAN_ANCHOR:ANCHOR_TRUSTED"
← ddos_sanity jump inserts here
iif "lo" → accept
@whitelist_ipv4 → accept
# ── Phase 2: BAN ──────────────────────────────────────────
counter name anchor_ban comment "NFTBAN_ANCHOR:ANCHOR_BAN"
← ddos_ban_enforce jump inserts here
@blacklist_manual_ipv4 → drop
@blacklist_ipv4 → drop
@port_allow_tcp_ipv4 → accept (per-IP)
@port_allow_udp_ipv4 → accept (per-IP)
# ── Phase 3: ESTABLISHED ──────────────────────────────────
counter name anchor_established comment "NFTBAN_ANCHOR:ANCHOR_ESTABLISHED"
← ddos_penalty jump inserts here
← ddos_synproxy jump inserts here
ct state established,related → accept
ICMP essentials → accept
# ── Phase 4: DETECT ───────────────────────────────────────
counter name anchor_detect comment "NFTBAN_ANCHOR:ANCHOR_DETECT"
← portscan_detection jump inserts here
CT limits (SSH, HTTP, MAIL) → drop
# As of v1.145 the SSH brute-force CT limit is set-driven:
# ct state new tcp dport @ssh_ports ct count over N → drop
# @ssh_ports holds every detected sshd listener port (TCP-only,
# IPv4/IPv6 mirrored), so runtime SSH-port changes update set
# membership without re-rendering the rule.
SYN meter → accept/drop
# ── Phase 5: SERVICE ──────────────────────────────────────
counter name anchor_service comment "NFTBAN_ANCHOR:ANCHOR_SERVICE"
← ddos_protection jump inserts here
← ddos_prefix jump inserts here
← http_bot_guard jump inserts here
@tcp_ports_in → accept
@udp_ports_in → accept
# ── Phase 6: FINAL ────────────────────────────────────────
counter name anchor_final comment "NFTBAN_ANCHOR:ANCHOR_FINAL"
log + policy drop
}
These invariants are enforced by CI (validate-chain-order.sh) and runtime health checks. Violation of a CRITICAL invariant means the firewall is not safe.
| ID | Rule | Severity |
|---|---|---|
| INV-O-001 | ANCHOR_HYGIENE before ANCHOR_TRUSTED | ERROR |
| INV-O-002 | ANCHOR_TRUSTED before ANCHOR_BAN | ERROR |
| INV-O-003 | ANCHOR_BAN before ANCHOR_ESTABLISHED | CRITICAL |
| INV-O-004 | ANCHOR_ESTABLISHED before ANCHOR_DETECT | ERROR |
| INV-O-005 | ANCHOR_DETECT before ANCHOR_SERVICE | ERROR |
| INV-O-006 | ANCHOR_SERVICE before ANCHOR_FINAL | ERROR |
| INV-O-007 | Each module jump handle < its anchor handle | ERROR |
| INV-O-008 | IPv4/IPv6 family parity for each module | ERROR |
INV-O-003 is CRITICAL because reversing ban/established order lets banned IPs keep existing connections alive.
| ID | Rule | Severity |
|---|---|---|
| INV-S-001 |
ip nftban and ip6 nftban tables exist |
CRITICAL |
| INV-S-002 |
input chain exists with hook input in both families |
CRITICAL |
| INV-S-003 | All 7 anchor markers exist exactly once per family | ERROR |
| INV-S-004 | For each enabled module, its subchain exists | ERROR |
| INV-S-005 | For each enabled module, jump <chain> exists in input |
ERROR |
| INV-S-006 | No duplicate anchor markers | ERROR |
| INV-S-007 | No duplicate module jumps | ERROR |
| ID | Rule | Severity |
|---|---|---|
| INV-F-001 | Whitelist accept before blacklist drop | CRITICAL |
| INV-F-002 | SSH port present in tcp_ports_in (reachability) and, as of v1.145, in ssh_ports (brute-force rate-limit) |
WARNING |
| INV-F-003 | At least one whitelist entry exists (anti-lockout) | WARNING |
NFTBan uses separate ip nftban and ip6 nftban tables (not inet). The anchor skeleton is identical in both families. Every anchor marker, every module jump, and every structural rule must exist in both tables.
If a module jump exists in IPv4 but not IPv6 (or vice versa), this is a parity violation (INV-O-008). The health check reports this as ERROR.
List all anchors in live chain:
nft -a list chain ip nftban input | grep NFTBAN_ANCHOR
nft -a list chain ip6 nftban input | grep NFTBAN_ANCHORExpected output (7 lines per family):
counter name anchor_hygiene packets 0 bytes 0 comment "NFTBAN_ANCHOR:ANCHOR_HYGIENE" # handle 4
counter name anchor_trusted packets 0 bytes 0 comment "NFTBAN_ANCHOR:ANCHOR_TRUSTED" # handle 5
counter name anchor_ban packets 0 bytes 0 comment "NFTBAN_ANCHOR:ANCHOR_BAN" # handle 6
counter name anchor_established packets 0 bytes 0 comment "NFTBAN_ANCHOR:ANCHOR_ESTABLISHED" # handle 7
counter name anchor_detect packets 0 bytes 0 comment "NFTBAN_ANCHOR:ANCHOR_DETECT" # handle 8
counter name anchor_service packets 0 bytes 0 comment "NFTBAN_ANCHOR:ANCHOR_SERVICE" # handle 9
counter name anchor_final packets 0 bytes 0 comment "NFTBAN_ANCHOR:ANCHOR_FINAL" # handle 10
Validate module placement:
scripts/validate-chain-order.sh
scripts/validate-chain-order.sh --jsonHealth check (includes anchor validation):
nftban health --json | jq '.checks[] | select(.name == "module_jump_placement")'- Not a performance feature. Anchor counters add negligible overhead. They exist for correctness verification, not optimization.
- Not a routing mechanism. Anchors don't change how packets flow. They mark phase boundaries so tools can verify the structure.
-
Not module configuration. Enabling/disabling a module is done through
nftban ddos enable, not by adding/removing anchors. Anchors are always present. -
Not inet table migration. NFTBan uses separate
ip/ip6tables. Anchors work identically with either scheme. Migration toinetis a separate effort (v2.x).
| Version | Change |
|---|---|
| v1.62.0 | Anchor markers added to template. Zero behavioral change. |
| v1.62.1 | Jump renderers migrate to comment-based anchoring (set-based fallback retained). |
| v1.61.0 | Handle-based nft insert position for module jumps. No anchor markers yet. |
| Pre-v1.61 | Fragment-file add rule append pattern. No position control. |
NFTBan Wiki
Getting Started
Architecture
Modules
- BotGuard (HTTP L7)
- BotScan (Web Access-Log)
- DDoS Protection (L3/L4)
- Portscan Detection
- Login Monitoring
- Blacklist & Threat Intelligence
- Suricata IDS Integration
- DNS Tunnel Suspicion
Operator Reference
- CLI Commands Reference
- Configuration Reference
- Systemd Units & Timers
- Optimization & Tuning
- Security Operations Guide
- GeoIP Database Guide
- FHS Compliance
- Troubleshooting: Smoke & Selftest
Verification & Trust
- Glossary & Vocabulary
- Known Limitations
- Metrics & Evidence Model
- Binary Verification (SLSA)
- Security Architecture
Reference
Legal