NFTables.Expr.Meter (NFTables v0.8.2)
View SourcePer-key rate limiting using dynamic sets (meters).
Meters provide stateful rate limiting on a per-key basis (e.g., per-IP address). They use dynamic sets to track keys and enforce rate limits independently for each key.
This replaces iptables' hashlimit functionality with a more flexible approach.
Import
import NFTables.Expr.MeterOverview
A meter consists of:
- A dynamic set to store keys
- A key expression (what to track: IP, port, tuple, etc.)
- A limit statement (rate limit per key)
- Optional timeout (how long to keep inactive keys)
Workflow
- Create a dynamic set with
NFTables.add(set: ...) - Use meter expressions in rules to track and limit per-key
- nftables automatically manages set entries with timeouts
Examples
import NFTables.Match
import NFTables.Match.Meter
alias NFTables.Builder
# Step 1: Create dynamic set
Builder.new(family: :inet)
|> NFTables.add(table: "filter")
|> NFTables.add(
set: "ssh_ratelimit",
table: "filter",
type: :ipv4_addr,
flags: [:dynamic],
timeout: 60, # Expire after 60s inactivity
size: 10000 # Max 10k tracked IPs
)
|> NFTables.submit(pid: pid)
# Step 2: Use meter in rule
ssh_rule = rule()
|> tcp()
|> dport(22)
|> ct_state([:new])
|> meter_update(
payload(:ip, :saddr), # Track by source IP
"ssh_ratelimit", # Set name
3, # 3 connections
:minute, # per minute
burst: 5 # Allow burst of 5
)
|> accept()
Builder.new()
|> NFTables.add(rule: ssh_rule, table: "filter", chain: "input", family: :inet)
|> NFTables.submit(pid: pid)Set Types for Keys
:ipv4_addr- Track by IPv4 address:ipv6_addr- Track by IPv6 address:inet_proto- Track by protocol number:inet_service- Track by port number- Composite types:
{:concat, [:ipv4_addr, :inet_service]}- Track by IP+port tuple
Use Cases
- SSH brute-force protection: Limit connections per IP
- HTTP flood protection: Limit requests per source
- Port scan detection: Limit new connections per IP
- Fair bandwidth sharing: Limit throughput per user/IP
- SYN flood protection: Limit SYN packets per source
For more information, see the nftables meters wiki.
Summary
Functions
Build a composite key expression from multiple fields.
Add meter with add operation.
Add meter with update operation.
Convenience function: creates payload expression for common keys.
Functions
Build a composite key expression from multiple fields.
Examples
# Track by source IP + destination port
composite_key([
payload(:ip, :saddr),
payload(:tcp, :dport)
])
# Track by src IP + dst IP + protocol
composite_key([
payload(:ip, :saddr),
payload(:ip, :daddr),
payload(:ip, :protocol)
])
@spec meter_add( NFTables.Expr.t(), term(), String.t(), non_neg_integer(), atom(), keyword() ) :: NFTables.Expr.t()
Add meter with add operation.
Uses "add" operation which fails if the element already exists. Less common than update - use when you need to distinguish first-time vs repeat.
Examples
# Track first connection per IP
builder
|> meter_add(payload(:ip, :saddr), "new_ips", 1, :minute)
@spec meter_update( NFTables.Expr.t(), term(), String.t(), non_neg_integer(), atom(), keyword() ) :: NFTables.Expr.t()
Add meter with update operation.
Uses "update" operation which updates existing entries or adds new ones. This is the most common meter operation.
Parameters
builder- Match builderkey_expr- Expression for key (single or list for composite keys)set_name- Name of the dynamic setrate- Rate limit (number of events)per- Time unit (:second, :minute, :hour, :day, :week)opts- Options::burst- Burst size (default: 0)
Examples
# Per-IP SSH rate limiting
builder
|> meter_update(payload(:ip, :saddr), "ssh_limits", 3, :minute, burst: 5)
# Composite key: per source-destination pair
builder
|> meter_update(
[payload(:ip, :saddr), payload(:ip, :daddr)],
"flow_limits",
100,
:second
)
# Per-port limiting
builder
|> meter_update(payload(:tcp, :dport), "port_limits", 50, :second)
Convenience function: creates payload expression for common keys.
Examples
# Source IP (IPv4)
payload(:ip, :saddr)
#=> %{payload: %{protocol: "ip", field: "saddr"}}
# Source IP (IPv6)
payload(:ip6, :saddr)
# Source port (TCP)
payload(:tcp, :sport)
# Destination port (UDP)
payload(:udp, :dport)