Skip to content

Architecture Overview

Antonios Voulvoulis edited this page Apr 15, 2026 · 12 revisions

Architecture Overview

Type: Architecture Scope: System structure, component wiring, and truth authority Since: v1.80.x Terminology: Glossary & Vocabulary


Purpose

This page describes what exists and how it is wired. It covers the structural components of NFTBan, their relationships, and the authority model that determines which component is trusted for what.

For how health is derived from this structure, see the Health Model. For term definitions, see the Glossary.


System Components

NFTBan is a hybrid Go + Shell system with four distinct layers:

┌────────────────────────────────────────────────────────┐
│  KERNEL LAYER (nftables)                    AUTHORITY  │
│  tables, chains, sets, counters, rules                 │
│  packet acceptance and drop decisions are enforced here │
└────────────────────────────┬───────────────────────────┘
                             │ reads (nft -j list ruleset)
┌────────────────────────────┴───────────────────────────┐
│  GO LAYER (nftband daemon + nftban-validate binary)    │
│  daemon: ban/unban execution, loginmon, IPC, scoring   │
│  validator: kernel state verification, health output   │
└────────────────────────────┬───────────────────────────┘
                             │ reads validator JSON
┌────────────────────────────┴───────────────────────────┐
│  SHELL LAYER (nftban CLI)                              │
│  schema generation, configuration, operator interface  │
│  presentation only — does NOT compute truth            │
└────────────────────────────┬───────────────────────────┘
                             │ reads config files
┌────────────────────────────┴───────────────────────────┐
│  CONFIG LAYER (/etc/nftban/)                           │
│  operator intent — what SHOULD happen                  │
│  does NOT represent what IS happening                  │
└────────────────────────────────────────────────────────┘

Layer roles

Layer What it does What it does NOT do
Kernel Enforces packet decisions. Holds sets, counters, chains. Does not interpret or report.
Go daemon (nftband) Writes to kernel (ban/unban, set population). Runs loginmon pipeline, BotGuard scoring. Does not own kernel truth — it writes, kernel holds.
Go validator (nftban-validate) Reads kernel state. Derives health. Emits JSON. Zero side effects. Does not modify kernel, config, or runtime.
Shell CLI (nftban) Operator interface. Schema generation. Config management. Does not compute health truth independently (INV-CONS-001). For truth-critical commands (status, health), it renders validator output only.
Config (/etc/nftban/) Stores operator intent. .local overrides base. Does not represent runtime state.

Truth Authority Model

NFTBan uses a strict truth hierarchy. When sources disagree, higher priority wins.

Priority Source Role
1 (highest) Kernel (nft list ruleset) What is actually enforcing right now
2 Validator (nftban-validate --json) Derives health state from kernel data. Authoritative for health interpretation, not for underlying truth.
3 CLI output (nftban status, nftban health truth) Renders validator output. Never computes independently.
4 (lowest) Config files (/etc/nftban/conf.d/) Operator intent. May disagree with kernel (see residual state).

Invariant INV-CONS-001: CLI output must agree with validator output. If the validator says DEGRADED, the CLI must say DEGRADED. No softening, no reinterpretation, no cosmetic overrides.


Kernel Invariants

These rules hold at all times and cannot be overridden by userspace components:

  1. Kernel state is the single source of truth. What nft list ruleset returns is what is actually enforcing. No daemon, CLI, or config file can change this fact — they can only change kernel state through nft commands.
  2. All packet decisions are enforced in kernel. Accept, drop, and bypass verdicts happen in nftables. Userspace cannot intercept or override a kernel packet decision after it is made.
  3. Userspace writes, kernel holds. The daemon and CLI can add/remove set elements and reload the schema, but once loaded, the kernel owns the state. If the daemon crashes, kernel state persists.
  4. Set timeouts are kernel-managed. Timeout-based set entries (bans, BotGuard classifications) expire in kernel regardless of daemon state.

System Contract (Canonical Invariants)

NFTBan guarantees the following invariants when the system is healthy:

  1. Kernel contains nftables schema:

    • Tables: ip nftban, ip6 nftban
    • Base chains: input (drop policy), forward, output
    • Required sets: whitelist, blacklist (both types), service ports
  2. Input chain follows the 7-anchor pipeline: HYGIENE → TRUSTED → BAN → ESTABLISHED → DETECT → SERVICE → FINAL

  3. Validator agrees with kernel: nftban-validate reports PROTECTED or IDLE when structure is correct

  4. CLI reflects validator truth: nftban health output matches nftban-validate output (INV-CONS-001)

  5. Config expresses intent, kernel expresses reality: ENABLED in config + MISSING in kernel = DEGRADED (VAL-CONS-001)

If any invariant is violated, the system state is DEGRADED or DOWN.


Metrics & Evidence Classes

NFTBan exposes three metric classes that serve different purposes:

Class Source What it proves Examples
Structural Validator System integrity (objects exist, ordered correctly) Tables, chains, anchors, sets present
Enforcement Kernel counters Real protection activity (packets processed) input_blacklist_drop, input_syn_rate_exceeded
Operational Timers / services Data freshness and system maintenance Feed sync, GeoIP updates, exporter runs
  • Counters prove enforcement happened
  • Validator proves structure is correct
  • Metrics exporter exposes both for monitoring systems (Prometheus, JSON)

For the full counter catalog, see Metrics & Evidence Model. For evidence interpretation rules, see Glossary.


Table Structure

NFTBan uses two nftables tables, one per address family:

Table Family Purpose
ip nftban IPv4 All IPv4 enforcement
ip6 nftban IPv6 All IPv6 enforcement (mirrors IPv4 structure)

Both tables are required on dual-stack hosts. On IPv4-only hosts, only the ip nftban table is required.

Each table contains:

  • Base chains with hooks (input, forward, output)
  • Helper chains for module-specific logic (ddos_sanity, http_bot_guard, etc.)
  • Named sets for IP lists (whitelist, blacklist, BotGuard classification sets)
  • Named counters for enforcement evidence
  • Meters for per-IP rate tracking

Anchor Pipeline (Input Chain)

The input chain follows a fixed 7-phase pipeline. Every inbound packet traverses these phases in order. Anchor counters mark phase boundaries.

Packet arrives
    │
    ▼
ANCHOR_HYGIENE ─── Phase 1: Sanity
    │  Invalid state → DROP (input_invalid_drop)
    │  ddos_sanity chain → malformed packet filtering
    │
    ▼
ANCHOR_TRUSTED ─── Phase 2: Trust
    │  Loopback → ACCEPT
    │  Whitelist → ACCEPT (input_whitelist_accept)
    │
    ▼
ANCHOR_BAN ──────── Phase 3: Ban Enforcement
    │  Manual blacklist → DROP (input_blacklist_manual_drop)
    │  Feed/geoban blacklist → DROP (input_blacklist_drop)
    │  Per-IP port access → ACCEPT
    │  ddos_penalty chain → repeat offender ladder
    │
    ▼
ANCHOR_ESTABLISHED  Phase 4: Connection Tracking
    │  Established/related → ACCEPT (input_established_accept)
    │  ICMP essential → ACCEPT
    │
    ▼
ANCHOR_DETECT ───── Phase 5: Detection
    │  Portscan detection chain → LOG
    │  SSH connection limit → DROP (input_ct_ssh_drop)
    │  HTTP connection limit → DROP (input_ct_http_drop)
    │  Mail connection limit → DROP (input_ct_mail_drop)
    │  SYN rate meter → ACCEPT (rate OK) or DROP (exceeded)
    │  ddos_prefix chain → IPv6 prefix aggregation
    │  ddos_protection chain → classic DDoS protection
    │
    ▼
ANCHOR_SERVICE ──── Phase 6: Service Admission
    │  TCP service ports → ACCEPT (input_service_tcp_accept)
    │  UDP service ports → ACCEPT (input_service_udp_accept)
    │
    ▼
ANCHOR_FINAL ────── Phase 7: Default Drop
    │  Everything else → DROP (policy drop)
    │
    ▼
  Packet dropped (default policy)

Pipeline invariants

  • ANCHOR_FINAL must always be last. If missing, the pipeline is incomplete → DOWN.
  • Anchors must be in correct order. Misordering means packets skip phases.
  • Each anchor is a named counter. anchor_hygiene > 0 proves traffic entered the pipeline.
  • Flow gaps between anchors indicate shadowing. If anchor_hygiene > 0 but anchor_detect = 0, something between phases 2-4 is intercepting all traffic (may be correct under high established-traffic ratio).

Phase-to-module mapping

Phase Modules active Enforcement type
Hygiene (1) DDoS sanity Kernel-only
Trust (2) Whitelist Kernel-only
Ban (3) Blacklist (manual + feeds + geoban), DDoS penalty Kernel-only
Established (4) Connection tracking Kernel-only
Detect (5) Portscan, DDoS (connection limits + SYN rate + prefix + classic) Kernel-only
Service (6) Service admission Kernel-only
Final (7) Default drop policy Kernel-only

BotGuard operates via a helper chain (http_bot_guard) jumped to from the detect phase for TCP ports 80/443. It is present only when BotGuard is ENABLED.

LoginMon does not have a kernel-side phase. It operates entirely in the Go daemon, writing bans to the blacklist_manual_ipv4/ipv6 sets which are enforced at phase 3 (Ban Enforcement).

Portscan detection chain logs traffic for analysis but has no dedicated kernel counter. Its effective state is reported as IDLE due to lack of observable evidence — the chain may be actively logging, but without a counter there is no kernel evidence to prove enforcement.


Helper Chains

Helper chains implement module-specific logic. They are jumped to from the input chain and return after processing.

Chain Module Required when
ddos_sanity DDoS Module ENABLED (or residual)
ddos_penalty DDoS Module ENABLED (or residual)
ddos_prefix DDoS Module ENABLED (or residual)
ddos_protection DDoS Module ENABLED (or residual)
portscan_detection Portscan Module ENABLED (or residual)
http_bot_guard BotGuard Module ENABLED only

Helper chains are module-scoped: they are required only when their module is ENABLED. Their absence when the module is DISABLED is the correct structural state, not a failure.

A helper chain that exists but has zero rules is DEGRADED (B80-3). The chain is present but non-functional.


Named Sets

Sets hold IP addresses, port numbers, and enforcement state.

Base schema sets (always required)

Set Type Purpose
whitelist_ipv4/ipv6 hash Trusted IPs — bypass all restrictions
blacklist_ipv4/ipv6 interval, timeout Feed + geoban enforcement
blacklist_manual_ipv4/ipv6 timeout Manual bans + LoginMon bans
tcp_ports_in / udp_ports_in hash Allowed service ports
port_allow_tcp_ipv4 / port_allow_udp_ipv4 hash (concat) Per-IP port access

Module sets (conditional on module ENABLED)

Set Module Purpose
http_bot_suspect/6 BotGuard IPs under classification
http_bot_pending/6 BotGuard IPs awaiting verification
http_bot_allow/6 BotGuard Verified non-bots
http_bot_grey/6 BotGuard Throttled IPs
http_bot_ban/6 BotGuard Banned bots
http_bot_emergency/6 BotGuard Emergency blocks
syn_meter_v4/v6 DDoS Per-IP SYN rate tracking

Named Counters

Every counter has one defined meaning. Counters are the primary enforcement evidence used by the validator.

Counter categories

Category Examples Evidence class
Enforcement drops input_blacklist_drop, input_ct_ssh_drop, input_syn_rate_exceeded ENFORCEMENT
Enforcement accepts input_whitelist_accept ENFORCEMENT (bypass)
Observational input_established_accept, input_service_tcp_accept OBSERVATIONAL
Pipeline anchors anchor_hygiene through anchor_final STRUCTURAL (existence) + OBSERVATIONAL (value)
Aggregates total_input_accept, total_input_drop DERIVED (not for module claims)

For the full counter catalog, see the Metrics & Evidence Model.


Go Daemon (nftband)

The Go daemon is a long-running service that writes to kernel sets and provides runtime services.

Component Purpose
Ban/unban executor Adds/removes IPs from blacklist sets
LoginMon pipeline Parses auth logs, scores IPs, issues bans
BotGuard module HTTP bot classification, set population
IPC socket Communication between CLI and daemon
Watchdog integration System pressure monitoring

The daemon is a writer, not an authority. It writes to kernel sets, but kernel state (what the sets actually contain) is the authority. If the daemon crashes, existing kernel bans persist (timeout sets are kernel-managed).

Daemon dependency varies by module:

  • BotGuard and LoginMon require the daemon for new protection actions
  • DDoS and Portscan enforce entirely in kernel without the daemon

Go Validator (nftban-validate)

The validator is a read-only binary that derives health from kernel state.

Property Value
Binary /usr/lib/nftban/bin/nftban-validate
Execution ~1ms
Side effects None (pure read-only)
Input nft -j list ruleset + systemctl is-active + config files
Output Frozen JSON schema (M81-6)

The validator implements the Health Model — the 4-axis evaluation that produces PROTECTED / IDLE / DEGRADED / DOWN.


Shell CLI (nftban)

The CLI is the operator interface. It handles:

  • Schema generation (nftban firewall rebuild)
  • Configuration management (nftban config)
  • Module enable/disable (nftban ddos enable)
  • Status display (nftban status, nftban health truth)

The CLI is a presentation layer. For truth-critical commands (status, health), it reads the Go validator's JSON output and renders it. It does not independently query kernel state for health derivation.


Config Layer (/etc/nftban/)

Configuration expresses operator intent.

Path Purpose
/etc/nftban/main.conf Master config
/etc/nftban/conf.d/{module}/main.conf Per-module base config
/etc/nftban/conf.d/{module}/main.conf.local Per-module override (survives upgrades)
/etc/nftban/whitelist.d/ Whitelist IP files
/etc/nftban/blacklist.d/ Blacklist IP files

Config load order: .local overrides base. If both exist, .local wins.

Config is intent, not truth. A module can be ENABLED in config but MISSING in kernel (consistency mismatch → DEGRADED). A module can be DISABLED in config but PRESENT in kernel (valid residual state).


Schema Generation

The nftables schema (all chains, sets, rules) is generated by the shell CLI and loaded atomically:

nftban firewall rebuild
    │
    ├── 1. Generate schema in temp namespace
    ├── 2. Validate generated schema (Go validator)
    ├── 3. If valid → atomic flush + load
    └── 4. If invalid → abort, keep existing ruleset

The schema is a logical contract that defines required kernel objects. It is not tied to a specific implementation language. Runtime authority resides in the kernel — what nft list ruleset returns is the actual enforced state. Current implementation: schema generated via shell (nft_schema.sh), migrating to Go in 1.90.x. The contract remains identical regardless of implementation. The Go validator reads the generated schema via schema_generated.go (codegen from nft_schema.sh).

Rebuild invariant: If rebuild validation fails, the existing ruleset is preserved. No partial state. This was hardened in v1.70.0 (rebuild failure is FATAL — no fallback reload).


Verification (MANDATORY)

# Verify table structure
nft list tables
# Expected: "table ip nftban" and "table ip6 nftban"

# Verify anchor pipeline order
nft list chain ip nftban input | grep ANCHOR
# Expected: HYGIENE → TRUSTED → BAN → ESTABLISHED → DETECT → SERVICE → FINAL

# Verify helper chains exist
nft list chains ip nftban | grep chain
# Shows all base + helper chains

# Verify validator reads kernel correctly
nftban-validate --json | jq '.status'
# Expected: "protected", "idle", "degraded", or "down"

# Verify CLI agrees with validator (INV-CONS-001)
nftban health truth
# Must show same status as validator JSON

Limitations

  • Single table per family. NFTBan uses ip nftban and ip6 nftban. It does not use the inet family (which would combine both). This is by design — separate tables allow per-family validation and independent set types.
  • No runtime rule modification. The daemon writes to sets, not to rules. Rule changes require a full schema rebuild (nftban firewall rebuild).
  • Schema generation is currently shell-based. The schema contract is language-agnostic. Current implementation uses nft_schema.sh. Migration to Go is part of the 1.90.x direction. The contract is unchanged by this.
  • No cluster mode. NFTBan operates on a single host. There is no distributed state synchronization between hosts.

Clone this wiki locally