Skip to content

DDoS Protection

Antonios Voulvoulis edited this page Jul 11, 2026 · 5 revisions

DDoS Protection

Type: Module Domain: Network and transport-layer attack mitigation Since: v1.80.x Config: conf.d/ddos/main.conf (.local override) Runtime: kernel-enforced (nftables meters/limits/penalty rules); the daemon proxies penalty set-writes and provides management/telemetry — packet-level enforcement does not require the daemon Last verified: v1.218.7


Purpose

See also: Glossary | Health Model | Architecture | Known Limitations

DDoS protection is a network/transport-layer module that filters malformed packets, enforces per-IP connection limits, applies SYN rate limiting, and implements a graduated penalty ladder for repeat offenders. All enforcement is kernel-only via nftables rules — the daemon is not required for packet-level protection.


How It Works

DDoS protection operates through four helper chains in the nftables input pipeline. Each chain handles a distinct class of traffic pressure:

Chain Pipeline Phase Purpose
ddos_sanity Phase 0 (HYGIENE) Drops malformed packets (Xmas, NULL, non-SYN new, fragmented)
ddos_penalty Phase 3 (ESTABLISHED) Graduated penalty ladder for IPs in penalty sets
ddos_prefix Phase 5 (SERVICE) Per /24 prefix SYN and connection rate limiting (IPv4). Per /64 prefix in IPv6.
ddos_protection Phase 5 (SERVICE) Per-IP connection limits for SMTP, DNS, ICMP, UDP. Rate limiting.

Enforcement pipeline

The kernel evaluates an inbound packet against the chains in order; the first matching guard drops it, otherwise it proceeds to service admission. The thresholds shown are the shipped defaults (conf.d/ddos/classic.conf, per-host configurable). As of v1.197.0:

flowchart TD
    P([Inbound packet]) --> S{ddos_sanity<br/>Phase 0 hygiene}
    S -- "Xmas / NULL / non-SYN-new / fragmented" --> D1[DROP]
    S -- clean --> PEN{ddos_penalty<br/>Phase 3 ESTABLISHED ladder}
    PEN -- "source in ban set" --> D2[DROP]
    PEN -- "in rate-limit set, over limit" --> D3[DROP]
    PEN -- "within limit / not penalised" --> CT{Per-IP connection limits<br/>ct count · Phase 4}
    CT -- "SSH &gt; 15 concurrent" --> D4[DROP]
    CT -- "HTTP/HTTPS &gt; 200 concurrent" --> D5[DROP]
    CT -- "Mail/SMTP &gt; 30 concurrent" --> D6[DROP]
    CT -- "within limits" --> SYN{per-IP service-port<br/>SYN meter · 25/s burst 50}
    SYN -- "over rate" --> D7[DROP]
    SYN -- "within rate" --> PFX{ddos_prefix SYN/conn<br/>/24 IPv4 · /64 IPv6 · 100/s burst 200}
    PFX -- "prefix SYN/conn flood" --> D8[DROP]
    PFX -- "ok" --> PROT{ddos_protection<br/>SMTP/DNS/ICMP/UDP flood}
    PROT -- "service flood" --> D9[DROP]
    PROT -- "ok" --> ADM([Service admission · Phase 5])
Loading

The textual Traffic flow below is the precise per-rule reference; the diagram is a navigational overview of the same path. Enforcement is kernel-only — see Kernel-only enforcement.

Traffic flow

Packet arrives at input chain
    │
    ├── Phase 0: jump ddos_sanity
    │   Xmas/NULL/non-SYN-new/frag → DROP
    │   Clean → return to input chain
    │
    ├── Phase 3: jump ddos_penalty
    │   IP in ban set → DROP
    │   IP in rate-limit set + exceeds limit → DROP
    │   IP in rate-limit set + within limit → return
    │   IP not in penalty sets → return
    │
    ├── Phase 4: (after established/ICMP accept)
    │   SSH: > 15 concurrent per IP → DROP (input_ct_ssh_drop)
    │   HTTP: > 200 concurrent per IP → DROP (input_ct_http_drop)
    │   Mail: > 30 concurrent per IP → DROP (input_ct_mail_drop)
    │   SYN to service port: rate meter → ACCEPT or DROP (input_syn_rate_exceeded)
    │   jump ddos_prefix → /24 SYN + conn rate limiting
    │   jump ddos_protection → SMTP/DNS/ICMP/UDP flood protection
    │
    ▼
  Continue to service admission (Phase 5)

Kernel-only enforcement

Once rules are loaded, DDoS protection works without the daemon. If nftband stops, all rate limits, connection limits, and penalty rules continue enforcing. The penalty ladder is scored by the shell maintenance timer (nftban_ddos_classic.sh); the daemon only proxies the resulting set-write through its single nftables-write authority (IPC) and does not perform the scoring itself. Schema generation and reload are handled by the CLI. Packet-level decisions are entirely kernel-side.

This is fundamentally different from BotGuard and LoginMon, which require the daemon for scoring and set population.


Kernel Objects

Chains (4 required when ENABLED)

Chain Rules (typical) Purpose
ddos_sanity 5 Malformed packet filtering
ddos_penalty 6 Graduated penalty ladder (ban/drop/rate-limit tiers)
ddos_prefix 4 Per-prefix SYN and connection flood protection
ddos_protection 8 Per-service connection limits and flood protection

Each chain must have > 0 rules. A chain with 0 rules is DEGRADED (B80-3).

Named Counters (enforcement evidence)

Counter Family Evidence Meaning
input_ct_ssh_drop ip + ip6 ENFORCEMENT SSH per-IP connection limit exceeded (> 15 concurrent)
input_ct_http_drop ip + ip6 ENFORCEMENT HTTP per-IP connection limit exceeded (> 200 concurrent)
input_ct_mail_drop ip + ip6 ENFORCEMENT Mail per-IP connection limit exceeded (> 30 concurrent)
input_syn_rate_exceeded ip + ip6 ENFORCEMENT SYN rate exceeded or non-service port SYN
input_syn_prefix_drop ip6 only ENFORCEMENT IPv6 /64 prefix SYN rate exceeded (anti-rotation)

Each counter maps to exactly one enforcement decision. Unlike the shared blacklist counter, DDoS counters are independently attributable.

Counter > 0 = ENFORCING (positive evidence). Counter = 0 = NEUTRAL (not failure).

Where the connection-limit counters live. The input_ct_ssh_drop, input_ct_http_drop, and input_ct_mail_drop counters are part of the always-on base schema (rendered into the input chain at install), not the DDoS module. They enforce per-IP connection limits (SSH 15, HTTP/HTTPS 200, Mail 30) even when DDOS_ENABLED="false". They are listed here because they sit in the same input pipeline the DDoS chains feed into, but they do not depend on DDoS being enabled.

Penalty Sets (populated via the maintenance timer)

Set Purpose
ddos_ban_1h IPs banned for 1 hour (repeat offenders)
ddos_drop_5m IPs dropped for 5 minutes
ddos_limit_5m IPs rate-limited for 5 minutes
ddos_limit_10s IPs rate-limited for 10 seconds

Penalty sets are timeout-based. The penalty ladder is computed by the shell maintenance timer (nftban_ddos_classic.sh), which scores strikes and selects the target penalty set. The daemon only proxies the resulting set-write through its single nftables-write authority (IPC); it does not perform the scoring. Existing penalty entries keep enforcing via kernel timeouts if the daemon stops.

Meters (per-IP rate tracking)

Meter Purpose
syn_meter_v4 / syn_meter_v6 Per-IP SYN rate tracking for service ports
ddos_prefix_syn Per /24 prefix SYN rate
ddos_prefix_conn Per /24 prefix connection rate
ddos_dns_udp Per-IP DNS/UDP rate
ddos_icmp_flood Per-IP ICMP rate
ddos_udp_flood Per-IP UDP rate

Meters are nftables dynamic sets, not named counters. They do not provide direct evidence. Enforcement from meter-triggered rules must be inferred from the associated drop counters (e.g., input_syn_rate_exceeded).


Configuration

Key File Default Meaning
DDOS_ENABLED conf.d/ddos/main.conf "false" Master enable/disable
DDOS_MODE conf.d/ddos/main.conf "auto" Mode: auto, classic, hybrid

Override via main.conf.local (survives upgrades).

Connection limits (in nftables rules, not config)

Service Limit Counter
SSH (port 22) 15 concurrent per IP input_ct_ssh_drop
HTTP/HTTPS (ports 80, 443) 200 concurrent per IP input_ct_http_drop
Mail (ports 25, 465, 587) 30 concurrent per IP input_ct_mail_drop
SYN rate (service ports) 25/second burst 50 input_syn_rate_exceeded

These limits are set in the nftables schema at rebuild time. Changing them requires a schema rebuild (nftban firewall rebuild).


State Model (v1.81)

This module follows the 4-axis model:

  • Config: ENABLED / DISABLED
  • Structural: PRESENT / MISSING (4 chains exist + jump rules in input chain)
  • Runtime: Always PASS (kernel-only module — daemon not required for enforcement)
  • Effective: ENFORCING (any DDoS counter > 0) / IDLE (all counters = 0, NEUTRAL)

Truth table

Config Structural Runtime Effective System Contribution
DISABLED skip
DISABLED PRESENT (residual) PASS ENFORCING informational (kernel still enforcing)
ENABLED PRESENT (chains exist) PASS ENFORCING PROTECTED
ENABLED PRESENT (chains exist) PASS IDLE IDLE
ENABLED PRESENT (chains exist, rules = 0) PASS DEGRADED (empty chain, B80-3)
ENABLED MISSING PASS DEGRADED

Residual enforcement

When DDoS is DISABLED in config but chains persist in kernel from a prior enable, traffic is still being rate-limited and connection-limited. This is a valid operational state — config lags kernel reality. The validator reports this as informational, not DEGRADED.

To remove residual chains: nftban firewall rebuild (regenerates schema without DDoS chains when DISABLED).


CLI Commands

nftban ddos status          # Show DDoS module state
nftban ddos enable          # Enable DDoS protection
nftban ddos disable         # Disable DDoS protection
nftban ddos show            # Show current rules and counters

Verification (MANDATORY)

# Check if chains exist (structural)
nft list chain ip nftban ddos_sanity
nft list chain ip nftban ddos_protection
# Expected: chain with rules. Empty chain = DEGRADED.

# Check enforcement counters
nft list counter ip nftban input_ct_ssh_drop
nft list counter ip nftban input_ct_http_drop
nft list counter ip nftban input_syn_rate_exceeded
# Counter > 0 = ENFORCING. Counter = 0 = NEUTRAL (not failure).

# Check module state via validator
nftban validate --json | jq '.modules.ddos'
# Expected: {"config":"enabled","structural":"present","effective":"enforcing"}
# or effective:"idle" if all counters are zero

# Verify chain has rules (not empty)
nft list chain ip nftban ddos_protection | grep -c "comment"
# Expected: > 0. Zero = DEGRADED (B80-3).

# Check residual state (when disabled)
nft list table ip nftban | grep ddos
# If chains exist while DDOS_ENABLED="false" → valid residual

Failure Modes

DEGRADED: ENABLED but chains missing

Symptom: Validator reports structural: "missing" for DDoS. Cause: Module enabled in config but chains not loaded in kernel. Fix: nftban ddos enable or nftban firewall rebuild

DEGRADED: Chain exists but has 0 rules (B80-3)

Symptom: Finding VAL-CHAIN-004 for a DDoS chain. Cause: Failed enable or partial rebuild. Fix: nftban ddos enable

Counter at zero after long uptime

Symptom: All DDoS counters show 0 packets after days of uptime. Meaning: NEUTRAL — no traffic has exceeded the configured limits. This is valid behavior on a low-traffic or well-configured host. It does NOT mean the module is broken.

Counters reset after rebuild

All named counters reset to 0 after nftban firewall rebuild. Zero counters immediately after rebuild = NEUTRAL (expected), not evidence of failure. Anchor counters will begin incrementing as traffic flows; enforcement counters will increment when limits are exceeded.


Limitations

  • No per-chain packet counter. Individual chain traversal (how many packets entered ddos_sanity vs ddos_protection) is not measurable. Only the named enforcement counters are queryable.
  • Connection limits are in the schema, not config. Changing SSH/HTTP/Mail connection limits requires editing the schema generator and rebuilding. There is no runtime config variable for these limits.
  • New penalty escalations pause without the write path. The penalty ladder is scored by the shell maintenance timer (nftban_ddos_classic.sh), but the resulting set-write is proxied through the daemon's single nftables-write authority (IPC). Kernel enforcement of existing penalty entries continues regardless; adding new IPs to penalty sets needs both the timer and that write path. Existing entries keep enforcing via kernel timeouts if the daemon stops.
  • SYN meter and DDoS counters overlap. input_syn_rate_exceeded fires both for legitimate SYN floods AND for SYN to non-service ports. It proves enforcement occurred, not that an attack was blocked.
  • IPv6 prefix counter is separate. input_syn_prefix_drop (IPv6 only) tracks /64 prefix SYN rate violations. This counter is checked by the validator alongside IPv4 counters.

Clone this wiki locally