Skip to content

Firewall Anchor Architecture

Antonios Voulvoulis edited this page Jun 5, 2026 · 2 revisions

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.


Table of Contents

  1. The Problem Anchors Solve
  2. Phase Model
  3. Anchor Markers
  4. Module Insertion Points
  5. Rule Order Walkthrough
  6. Ordering Invariants
  7. Structural Invariants
  8. Safety Invariants
  9. Address Family Parity
  10. Verification Commands
  11. What Anchors Are NOT
  12. Version History

The Problem Anchors Solve

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 list output (verifiable at runtime)
  • Stable across versions (comment strings are the contract)
  • Zero-cost (counter increment is CPU-negligible)

Phase Model

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 protection comment 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.

Anchor Markers

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.


Module Insertion Points

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.


Rule Order Walkthrough

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
}

Ordering Invariants

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.


Structural Invariants

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

Safety Invariants

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

Address Family Parity

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.


Verification Commands

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_ANCHOR

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

Health check (includes anchor validation):

nftban health --json | jq '.checks[] | select(.name == "module_jump_placement")'

What Anchors Are NOT

  • 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/ip6 tables. Anchors work identically with either scheme. Migration to inet is a separate effort (v2.x).

Version History

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.

Clone this wiki locally