NFTables Quick Reference

View Source

Import Options

NFTables provides two ways to import expression functions:

  • use NFTables - Automatically imports all Expr modules (convenient)
  • Selective imports - Explicitly import only what you need (examples below use this approach)

Both are equally valid. This guide uses selective imports for clarity.

Match Expressions

The Expr module provides tooling for building match expressions.

How Expressions Works

Expressions are represented by an %Expr{} struct. This struct can be piped through a series of functions to build and manipulate the structure. The structure at the end of the pipeline represents the expressions which will be used to match packets.

  1. Initialize - Create empty expression builder
  2. Accumulate - Each function adds JSON expression to list
  3. Execute - Send via Builder Pattern (automatically extracts expression list)

Visual Example

import NFTables.Expr
import NFTables.Expr.{Port, TCP, Verdicts}
alias NFTables.{Builder, Local, Requestor}

# Step 1: Initialize - generate a new Expr struct, not necessary, you can start with step 2
match_expr = expr()
# %Expr{fmaily: :inet, comment: nil, protocol: nil, expr_list: []}

# Step 2: Accumulate expressions
match_expr
|> tcp()
# %Expr{
#   family: :inet, comment: nil, protocol: :tcp,
#   expr_list: [
#     %{match: %{ op: "==", left: %{payload: %{protocol: "ip", field: "protocol"}}, right: "tcp" } } 
#   ] }

|> dport(22)
# %Expr{
#   family: :inet, comment: nil, protocol: :tcp,
#   expr_list: [
#     %{ match: %{ op: "==", left: %{payload: %{protocol: "ip", field: "protocol"}}, right: "tcp" } },
#     %{ match: %{ op: "==", left: %{payload: %{protocol: "tcp", field: "dport"}}, right: 22 } }
#   ] }

|> accept()
# %Expr{
#   family: :inet, comment: nil, protocol: :tcp,
#   expr_list: [
#     %{ match: %{ op: "==", left: %{payload: %{protocol: "ip", field: "protocol"}}, right: "tcp" } },
#     %{ match: %{ op: "==", left: %{payload: %{protocol: "tcp", field: "dport"}}, right: 22 } }
#     %{ accept: nil }
#   ] }

# Step 3: Commit - Builder automatically extracts expression list
|> then(fn rule ->
  NFTables.add(rule: rule, table: "filter", chain: "INPUT", family: :inet)
  |> NFTables.submit(pid)
end)

Internal Flow

Expr.expr() - new expression
    
expression building
    
NFTables.add(rule: rule ) - Automatically extracts expression list and adds to configuration
    
NFTables.submit() - Send to NFTables.Port
    
Local.submit() - JSON encoding
    
NFTables.Port
    
libnftables
    
Kernel

Complex Rule Examples

Example 1: SSH Protection

What it does:

  • Match TCP port 22 (SSH)
  • Only NEW connections
  • Rate limit to 5/minute with burst
  • Log violations
  • Drop excessive attempts
import NFTables.Expr
alias NFTables.{Builder, Local, Requestor}

expr = 
  tcp()
  |> dport(22)
  |> ct_state([:new])
  |> rate_limit(5, :minute, burst: 10)
  |> log("SSH_RATELIMIT: ", level: :warn)
  |> drop()

NFTables. NFTables.add(rule: expr, table: "filter", chain: "INPUT", family: :inet)
|> Local.submit(pid)

Example 2: Port Forwarding (DNAT)

What it does:

  • Match external port 8080
  • Only NEW connections
  • Forward to internal server 10.0.0.10:80
expr = 
  tcp()
  |> dport(8080)
  |> ct_state([:new])
  |> dnat_to("10.0.0.10", port: 80)

NFTables. NFTables.add(rule: expr, table: "nat", chain: "prerouting", family: :inet)
|> Local.submit(pid)

Example 3: IP Blocklist

What it does:

  • Check if source IP in blocklist set
  • Count matches
  • Log blocked IPs
  • Drop packet
expr = 
  set("blocklist", :saddr)
  |> counter()
  |> log("BLOCKED_IP: ", level: :info)
  |> drop()

NFTables. NFTables.add(rule: expr, table: "filter", chain: "INPUT", family: :inet)
|> Local.submit(pid)

Example 4: SYN Proxy (DDoS Protection)

What it does:

  • Match HTTPS port (443)
  • Only SYN packets
  • NEW connections
  • Enable SYN proxy
  • Accept legitimate traffic
expr = 
  tcp()
  |> dport(443)
  |> tcp_flags([:syn], [:syn, :ack, :rst, :fin])
  |> ct_state([:new])
  |> counter()
  |> synproxy(mss: 1460, wscale: 7, timestamp: true, sack_perm: true)
  |> accept()

NFTables. NFTables.add(rule: expr, table: "filter", chain: "INPUT", family: :inet)
|> Local.submit(pid)

Key Concepts

Dual-Arity API

All Match functions support both starting new rules and continuing existing ones:

# Start with expr()
tcp() |> dport(22) |> accept()

# Or start without expr() using arity-1
tcp() |> dport(22) |> accept()

# Or use an existing builder
builder = expr()
builder = tcp(builder)
builder = dport(builder, 22)
builder = accept(builder)

Advanced Features Quick Reference

Flowtables (Hardware Acceleration)

# Create flowtable
Builder.new(family: :inet)
|> NFTables.add(table: "filter")
|> NFTables.add(
  flowtable: "fastpath",
  hook: :ingress,
  priority: 0,
  devices: ["eth0", "eth1"]
)
|> NFTables.submit(pid: pid)

# Offload established connections
expr()
|> state([:established, :related])
|> flow_offload()

Meters (Per-Key Rate Limiting)

alias NFTables.Expr.Meter

# Per-IP rate limiting
expr()
|> meter_update(
  Meter.payload(:ip, :saddr),
  "limits",
  10,
  :second
)
|> accept()

# Composite key (IP + port)
expr()
|> meter_add(
  Meter.composite_key([
    Meter.payload(:ip, :saddr),
    Meter.payload(:tcp, :dport)
  ]),
  "conn_limits",
  100,
  :second,
  burst: 200
)

Raw Payload (Deep Packet Inspection)

# Match DNS port via raw payload
udp()
|> payload_raw(:th, 16, 16, 53)  # Transport header, offset 16, 16 bits, value 53
|> drop()

# TCP SYN flag check with mask
tcp()
|> payload_raw_masked(:th, 104, 8, 0x02, 0x02)
|> counter()

# HTTP method detection
tcp()
|> dport(80)
|> payload_raw(:ih, 0, 32, "GET ")  # First 4 bytes
|> log("HTTP GET: ")

Payload Bases:

  • :ll - Link layer (Ethernet)
  • :nh - Network header (IP)
  • :th - Transport header (TCP/UDP)
  • :ih - Inner header (tunneled packets)

Transparent Proxy (TPROXY)

# Mark existing transparent sockets
socket_transparent()
|> set_mark(1)
|> accept()

# Redirect to local proxy
tcp()
|> dport(80)
|> tproxy(to: 8080)

# With specific address
tcp()
|> dport(443)
|> tproxy(to: 8443, addr: "127.0.0.1")

Specialized Protocols

# SCTP (WebRTC, telephony) - use generic dport/sport
sctp()
|> dport(9899)
|> accept()

# DCCP (streaming media) - use generic dport/sport
dccp()
|> sport(5000)
|> dport(6000)
|> counter()

# GRE (VPN tunnels)
gre()
|> gre_version(0)
|> gre_key(12345)
|> accept()

# Port ranges supported for SCTP/DCCP
sctp()
|> dport(9000..9999)
|> accept()

OS Fingerprinting (OSF)

Requirements:

nfnl_osf -f /usr/share/pf.os
# Match Linux systems
osf_name("Linux")
|> log("Linux detected: ")
|> accept()

# Match with strict TTL
osf_name("Windows", ttl: :strict)
|> set_mark(2)

# Match OS version
osf_name("Linux")
|> osf_version("3.x")
|> counter()

# Security policy
tcp()
|> dport(22)
|> osf_name("Linux")
|> accept()

TTL Modes: :loose (default), :skip, :strict Common OS: "Linux", "Windows", "MacOS", "FreeBSD", "OpenBSD", "unknown"

Actions vs Verdicts

Actions (non-terminal - rule continues):

  • counter() - Count packets
  • log(prefix, opts) - Log to syslog
  • rate_limit(rate, per, opts) / limit(...) - Rate limiting
  • meter_update(key, set, rate, per) - Per-key rate limiting
  • meter_add(key, set, rate, per) - Per-key limits (add only)
  • set_mark(mark) - Mark packets
  • set_connmark(mark) - Mark connections
  • set_ct_label(label) - Set CT label
  • set_dscp(dscp) - Set DSCP value
  • continue() - Explicit continue

Verdicts (terminal - rule stops):

  • accept() - Accept packet
  • drop() - Drop silently
  • reject(type) - Drop with ICMP error
  • jump(chain) - Jump to chain
  • goto(chain) - Goto chain
  • return_from_chain() / return() - Return from jump
  • tproxy(opts) - Transparent proxy redirect
  • snat_to(ip) / dnat_to(ip) - NAT
  • masquerade() - Masquerade NAT
  • redirect_to(port) - Port redirection
  • notrack() - Disable connection tracking
  • flow_offload() - Hardware offload
  • synproxy() - SYN proxy protection
  • queue_to_userspace(num) - Send to userspace

Convenience Aliases

save buf Shorter function names for common operations:

Full NameAliasExample
source_ip/2source/2source("192.168.1.1")
dest_ip/2dest/2dest("10.0.0.1")
source_port/2sport/2sport(1024)
dest_port/2dport/2dport(80)
dest_port/2port/2port(22)
ct_state/2state/2state([:established])
rate_limit/3limit/3limit(10, :minute)

Protocol Helpers

Quick protocol matching:

tcp()    # Match TCP protocol
udp()    # Match UDP protocol
icmp()   # Match ICMP protocol
sctp()   # Match SCTP protocol (WebRTC, telephony)
dccp()   # Match DCCP protocol (streaming)
gre()    # Match GRE protocol (VPN tunnels)

Match Modules

Functionality is organized into sub-modules:

  • IP - IP addresses (source/dest)
  • Port - TCP/UDP ports
  • TCP - Protocol-specific (flags, TTL)
  • Layer2 - MAC, interfaces, VLAN
  • CT - Connection tracking
  • Advanced - ICMP, marks, sets, raw payload, socket, OSF
  • Protocols - SCTP, DCCP, GRE (specialized protocols)
  • Meter - Per-key rate limiting with dynamic sets
  • Actions - Counter, log, rate limit, marks
  • NAT - SNAT, DNAT, masquerade
  • Verdicts - accept, drop, reject, jump, TPROXY, flow offload

Common Patterns

Accept Established Connections

expr =
  state([:established, :related])
  |> accept()

NFTables. NFTables.add(rule: expr, table: "filter", chain: "INPUT", family: :inet)
|> Local.submit(pid)

Rate Limit Service

expr =
  tcp()
  |> dport(80)
  |> limit(100, :second, burst: 200)
  |> accept()

Log and Drop

expr =
  source("192.168.1.100")
  |> log("BLOCKED: ", level: :warn)
  |> drop()

NAT Gateway

expr =
  oif("eth0")
  |> masquerade()

NFTables. NFTables.add(rule: expr, table: "nat", chain: "postrouting", family: :inet)
|> Local.submit(pid)

Connection Limit

expr =
  tcp()
  |> dport(80)
  |> ct_state([:new])
  |> limit_connections(100)
  |> drop()

High-Level Policy Helpers

Use Policy module for common firewall patterns:

alias NFTables.{Policy, Builder}

# These use the Expr API internally (composable)
:ok =
  Policy.accept_loopback()
  |> Policy.accept_established()
  |> Policy.drop_invalid()
  |> Policy.allow_ssh(rate_limit: 10)
  |> Policy.allow_http()
  |> Policy.allow_https()
  |> NFTables.submit(pid: pid)

Architecture Summary

import NFTables.Expr
alias NFTables.{Builder, Local, Requestor}
    
expr() - Initialize pure builder
    
|> tcp() |> dport(22) |> accept() - Build expressions
    
NFTables.add(rule: rule_struct, ...) - Automatically extract and add to configuration
    
Local.submit(pid) - Send to kernel
    
JSON encoding
    
NFTables.Port
    
libnftables
    
Kernel