NFTables Quick Reference
View SourceImport 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.
- Initialize - Create empty expression builder
- Accumulate - Each function adds JSON expression to list
- 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
↓
KernelComplex 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 packetslog(prefix, opts)- Log to syslograte_limit(rate, per, opts)/limit(...)- Rate limitingmeter_update(key, set, rate, per)- Per-key rate limitingmeter_add(key, set, rate, per)- Per-key limits (add only)set_mark(mark)- Mark packetsset_connmark(mark)- Mark connectionsset_ct_label(label)- Set CT labelset_dscp(dscp)- Set DSCP valuecontinue()- Explicit continue
Verdicts (terminal - rule stops):
accept()- Accept packetdrop()- Drop silentlyreject(type)- Drop with ICMP errorjump(chain)- Jump to chaingoto(chain)- Goto chainreturn_from_chain()/return()- Return from jumptproxy(opts)- Transparent proxy redirectsnat_to(ip)/dnat_to(ip)- NATmasquerade()- Masquerade NATredirect_to(port)- Port redirectionnotrack()- Disable connection trackingflow_offload()- Hardware offloadsynproxy()- SYN proxy protectionqueue_to_userspace(num)- Send to userspace
Convenience Aliases
save buf Shorter function names for common operations:
| Full Name | Alias | Example |
|---|---|---|
source_ip/2 | source/2 | source("192.168.1.1") |
dest_ip/2 | dest/2 | dest("10.0.0.1") |
source_port/2 | sport/2 | sport(1024) |
dest_port/2 | dport/2 | dport(80) |
dest_port/2 | port/2 | port(22) |
ct_state/2 | state/2 | state([:established]) |
rate_limit/3 | limit/3 | limit(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