Packages
Elixir library for working with Linux nftables rules. Provides high-level APIs for building tables, chains, rules, maps and sets. Works with NFTables.Port for communicating with the kernel firewall.
Current section
Files
Jump to
Current section
Files
lib/nftables/expr/actions.ex
defmodule NFTables.Expr.Actions do
@moduledoc """
Action and packet modification functions for Expr.
Provides functions for counter, logging, rate limiting, packet/connection marking,
CT operations, and packet header modifications (DSCP, TTL, hop limit).
These actions modify packets or connection state rather than matching conditions.
## Import
import NFTables.Expr.Actions
## Examples
# Counter and logging
tcp() |> dport(22) |> counter() |> log("SSH: ") |> accept()
# Rate limiting
tcp() |> dport(80) |> limit(100, :second, burst: 20) |> accept()
# Packet marking for QoS
udp() |> dport(5060) |> set_dscp(:ef) |> set_mark(1) |> accept()
# Connection marking
ct_state([:new]) |> set_mark(100) |> save_mark() |> accept()
state([:established]) |> restore_mark() |> accept()
For more information, see the [nftables statements wiki](https://wiki.nftables.org/wiki-nftables/index.php/Quick_reference-nftables_in_10_minutes#Statements).
"""
alias NFTables.Expr
# Basic actions
@doc "Add counter expression"
@spec counter(Expr.t()) :: Expr.t()
def counter(builder \\ Expr.expr()) do
expr = Expr.Structs.counter()
Expr.add_expr(builder, expr)
end
@doc """
Add log expression.
## Options
- `:level` - Syslog level (default: no level specified)
- `:emerg` - Emergency
- `:alert` - Alert
- `:crit` - Critical
- `:err` - Error
- `:warning` or `:warn` - Warning
- `:notice` - Notice
- `:info` - Info
- `:debug` - Debug
## Examples
# Basic logging
builder |> log("DROPPED: ")
# With syslog level
builder |> log("AUDIT: ", level: :warning)
builder |> log("CRITICAL: ", level: :crit)
"""
@spec log(Expr.t(), String.t(), keyword()) :: Expr.t()
def log(builder \\ Expr.expr(), prefix, opts \\ []) do
level = Keyword.get(opts, :level)
level_map = %{
emerg: "emerg",
alert: "alert",
crit: "crit",
err: "err",
warning: "warn",
warn: "warn",
notice: "notice",
info: "info",
debug: "debug"
}
json_opts =
if level do
[level: Map.get(level_map, level, to_string(level))]
else
[]
end
expr = Expr.Structs.log(prefix, json_opts)
Expr.add_expr(builder, expr)
end
@doc """
Add rate limiting.
## Example
builder |> rate_limit(10, :minute)
builder |> rate_limit(100, :second)
"""
@spec rate_limit(Expr.t(), non_neg_integer(), atom(), keyword()) :: Expr.t()
def rate_limit(builder \\ Expr.expr(), rate, unit, opts \\ []) do
unit_str =
case unit do
:second -> "second"
:minute -> "minute"
:hour -> "hour"
:day -> "day"
:week -> "week"
other -> to_string(other)
end
json_opts =
if burst = Keyword.get(opts, :burst) do
[burst: burst]
else
[]
end
expr = Expr.Structs.limit(rate, unit_str, json_opts)
Expr.add_expr(builder, expr)
end
@doc """
Convenience alias for rate_limit/4. Add rate limiting.
Supports dual-arity: can start a new expression or continue an existing one.
## Examples
# Basic rate limiting
limit(10, :minute)
# With burst
tcp() |> dport(22) |> limit(10, :minute, burst: 5)
# Continue existing expression
builder |> limit(100, :second)
"""
@spec limit(Expr.t(), non_neg_integer(), atom(), keyword()) :: Expr.t()
def limit(builder \\ Expr.expr(), rate, unit, opts \\ []),
do: rate_limit(builder, rate, unit, opts)
# Marking actions
@doc """
Set packet mark.
Useful for policy routing and traffic shaping.
## Example
builder |> set_mark(100)
"""
@spec set_mark(Expr.t(), non_neg_integer()) :: Expr.t()
def set_mark(builder \\ Expr.expr(), mark) when is_integer(mark) and mark >= 0 do
expr = Expr.Structs.meta_set("mark", mark)
Expr.add_expr(builder, expr)
end
@doc """
Set connection mark.
Connection marks persist across all packets in a connection.
## Example
builder |> set_connmark(42)
"""
@spec set_connmark(Expr.t(), non_neg_integer()) :: Expr.t()
def set_connmark(builder \\ Expr.expr(), mark) when is_integer(mark) and mark >= 0 do
expr = Expr.Structs.ct_set("mark", mark)
Expr.add_expr(builder, expr)
end
@doc """
Restore connection mark to packet mark.
Copies the connection mark to the packet mark. This ensures all packets
in a connection have the same mark, useful for policy routing and QoS.
## Example
# Restore connmark for established connections
builder
|> ct_state([:established, :related])
|> restore_mark()
|> accept()
## Use Case
In multi-WAN routing or QoS scenarios:
1. First packet: classify and set connmark
2. Subsequent packets: restore connmark to mark
3. All packets in connection use same route/QoS tier
"""
@spec restore_mark(Expr.t()) :: Expr.t()
def restore_mark(builder \\ Expr.expr()) do
# meta mark set ct mark
expr = %{
"mangle" => %{
"key" => %{"meta" => %{"key" => "mark"}},
"value" => %{"ct" => %{"key" => "mark"}}
}
}
Expr.add_expr(builder, expr)
end
@doc """
Save packet mark to connection mark.
Copies the packet mark to the connection mark. This persists the
classification for the entire connection.
## Example
# Classify new connection and save mark
builder
|> ct_state([:new])
|> dscp(46)
|> set_mark(1)
|> save_mark()
|> accept()
## Use Case
In traffic classification:
1. Match conditions and set packet mark
2. Save mark to connmark for persistence
3. Later packets restore connmark via restore_mark()
"""
@spec save_mark(Expr.t()) :: Expr.t()
def save_mark(builder \\ Expr.expr()) do
# ct mark set meta mark
expr = %{
"mangle" => %{
"key" => %{"ct" => %{"key" => "mark"}},
"value" => %{"meta" => %{"key" => "mark"}}
}
}
Expr.add_expr(builder, expr)
end
# CT actions
@doc """
Set connection tracking label.
Assigns a label to the connection for advanced stateful tracking.
Labels are 128-bit bitmaps allowing complex classification.
## Example
# Label suspicious connections
builder
|> source_ip("203.0.113.0/24")
|> set_ct_label("suspicious")
|> accept()
# Set numeric label bit
builder
|> tcp()
|> dport(22)
|> set_ct_label(5)
|> accept()
## Use Cases
- Complex multi-stage stateful tracking
- Connection classification across chains
- Security event correlation
"""
@spec set_ct_label(Expr.t(), String.t() | non_neg_integer()) :: Expr.t()
def set_ct_label(builder \\ Expr.expr(), label) when is_binary(label) or is_integer(label) do
expr = Expr.Structs.ct_set("label", label)
Expr.add_expr(builder, expr)
end
@doc """
Assign connection tracking helper.
Assigns a CT helper (FTP, SIP, etc.) to the connection for application
layer gateway functionality.
## Example
# Assign FTP helper
builder
|> tcp()
|> dport(21)
|> ct_state([:new])
|> set_ct_helper("ftp")
|> accept()
# Assign SIP helper
builder
|> udp()
|> dport(5060)
|> set_ct_helper("sip")
|> accept()
## Use Cases
- FTP active mode support
- SIP/VoIP NAT traversal
- H.323 video conferencing
- TFTP file transfers
"""
@spec set_ct_helper(Expr.t(), String.t()) :: Expr.t()
def set_ct_helper(builder \\ Expr.expr(), helper) when is_binary(helper) do
expr = Expr.Structs.ct_set("helper", helper)
Expr.add_expr(builder, expr)
end
@doc """
Assign connection to tracking zone.
Places the connection in a specific CT zone for isolation.
Useful for multi-tenant or namespace scenarios.
## Example
# Assign to zone 1
builder
|> iif("tenant1")
|> set_ct_zone(1)
|> accept()
# Assign to tenant-specific zone
builder
|> source_ip("192.168.100.0/24")
|> set_ct_zone(100)
|> accept()
## Use Cases
- Multi-tenant isolation
- Network namespace separation
- Overlapping IP address spaces
- Container network isolation
"""
@spec set_ct_zone(Expr.t(), non_neg_integer()) :: Expr.t()
def set_ct_zone(builder \\ Expr.expr(), zone) when is_integer(zone) and zone >= 0 do
expr = Expr.Structs.ct_set("zone", zone)
Expr.add_expr(builder, expr)
end
# Packet modification
@doc """
Set DSCP (Differentiated Services Code Point) value.
Modifies the DSCP field in the IP header for QoS remarking.
## DSCP Values
- 46 (`:ef`) - Expedited Forwarding (VoIP voice)
- 34 (`:af41`) - Assured Forwarding 4/1 (Video)
- 26 (`:af31`) - Assured Forwarding 3/1 (Signaling)
- 18 (`:af21`) - Assured Forwarding 2/1 (Streaming)
- 10 (`:af11`) - Assured Forwarding 1/1 (Bulk)
- 0 (`:cs0`) - Class Selector 0 (Best Effort)
## Example
# Remark HTTP traffic as bulk
builder
|> tcp()
|> dport(80)
|> set_dscp(10)
|> accept()
# Mark VoIP as expedited forwarding
builder
|> udp()
|> dport(5060)
|> set_dscp(46)
|> accept()
# Use atom
builder
|> tcp()
|> dport(22)
|> set_dscp(:af31)
|> accept()
"""
@spec set_dscp(Expr.t(), atom() | non_neg_integer()) :: Expr.t()
def set_dscp(builder \\ Expr.expr(), dscp) do
dscp_val =
case dscp do
:ef -> 46
:af41 -> 34
:af31 -> 26
:af21 -> 18
:af11 -> 10
:cs0 -> 0
num when is_integer(num) and num >= 0 and num <= 63 -> num
_ -> raise ArgumentError, "Invalid DSCP value: #{inspect(dscp)}"
end
expr = %{
"mangle" => %{
"key" => %{"payload" => %{"protocol" => "ip", "field" => "dscp"}},
"value" => dscp_val
}
}
Expr.add_expr(builder, expr)
end
@doc """
Set IP TTL (Time To Live) value.
Modifies the TTL field in the IPv4 header.
## Example
# Set TTL to 64
builder |> set_ttl(64) |> accept()
# Normalize TTL
builder |> set_ttl(128) |> accept()
## Use Cases
- TTL normalization (anti-fingerprinting)
- Extending TTL for specific traffic
- Router hop limit enforcement
"""
@spec set_ttl(Expr.t(), non_neg_integer()) :: Expr.t()
def set_ttl(builder \\ Expr.expr(), ttl) when is_integer(ttl) and ttl >= 0 and ttl <= 255 do
expr = %{
"mangle" => %{
"key" => %{"payload" => %{"protocol" => "ip", "field" => "ttl"}},
"value" => ttl
}
}
Expr.add_expr(builder, expr)
end
@doc """
Set IPv6 hop limit value.
IPv6 equivalent of TTL. Modifies the hop limit field in the IPv6 header.
## Example
# Set hop limit to 64
builder |> set_hoplimit(64) |> accept()
# Normalize hop limit
builder |> set_hoplimit(255) |> accept()
"""
@spec set_hoplimit(Expr.t(), non_neg_integer()) :: Expr.t()
def set_hoplimit(builder \\ Expr.expr(), hoplimit)
when is_integer(hoplimit) and hoplimit >= 0 and hoplimit <= 255 do
expr = %{
"mangle" => %{
"key" => %{"payload" => %{"protocol" => "ip6", "field" => "hoplimit"}},
"value" => hoplimit
}
}
Expr.add_expr(builder, expr)
end
@doc """
Increment IP TTL by 1.
## Example
# Extend TTL by 1
builder |> increment_ttl() |> accept()
"""
@spec increment_ttl(Expr.t()) :: Expr.t()
def increment_ttl(builder \\ Expr.expr()) do
expr = %{
"mangle" => %{
"key" => %{"payload" => %{"protocol" => "ip", "field" => "ttl"}},
"value" => %{
"+" => [
%{"payload" => %{"protocol" => "ip", "field" => "ttl"}},
1
]
}
}
}
Expr.add_expr(builder, expr)
end
@doc """
Decrement IP TTL by 1.
## Example
# Reduce TTL by 1
builder |> decrement_ttl() |> accept()
"""
@spec decrement_ttl(Expr.t()) :: Expr.t()
def decrement_ttl(builder \\ Expr.expr()) do
expr = %{
"mangle" => %{
"key" => %{"payload" => %{"protocol" => "ip", "field" => "ttl"}},
"value" => %{
"-" => [
%{"payload" => %{"protocol" => "ip", "field" => "ttl"}},
1
]
}
}
}
Expr.add_expr(builder, expr)
end
@doc """
Increment IPv6 hop limit by 1.
## Example
# Extend hop limit by 1
builder |> increment_hoplimit() |> accept()
"""
@spec increment_hoplimit(Expr.t()) :: Expr.t()
def increment_hoplimit(builder \\ Expr.expr()) do
expr = %{
"mangle" => %{
"key" => %{"payload" => %{"protocol" => "ip6", "field" => "hoplimit"}},
"value" => %{
"+" => [
%{"payload" => %{"protocol" => "ip6", "field" => "hoplimit"}},
1
]
}
}
}
Expr.add_expr(builder, expr)
end
@doc """
Decrement IPv6 hop limit by 1.
## Example
# Reduce hop limit by 1
builder |> decrement_hoplimit() |> accept()
"""
@spec decrement_hoplimit(Expr.t()) :: Expr.t()
def decrement_hoplimit(builder \\ Expr.expr()) do
expr = %{
"mangle" => %{
"key" => %{"payload" => %{"protocol" => "ip6", "field" => "hoplimit"}},
"value" => %{
"-" => [
%{"payload" => %{"protocol" => "ip6", "field" => "hoplimit"}},
1
]
}
}
}
Expr.add_expr(builder, expr)
end
@doc """
Enable SYN proxy for DDoS protection.
Implements SYN cookie-based protection against SYN flood attacks.
The firewall handles the TCP handshake, protecting backend servers.
## Options
- `:mss` - Maximum segment size (default: auto)
- `:wscale` - Window scaling (default: auto)
- `:sack_perm` - SACK permitted (default: auto)
- `:timestamp` - TCP timestamp (default: auto)
## Example
# Basic synproxy
builder
|> tcp()
|> dport(80)
|> tcp_flags([:syn], [:syn, :ack, :rst, :fin])
|> synproxy()
# With custom MSS
builder
|> tcp()
|> dport(443)
|> tcp_flags([:syn], [:syn, :ack, :rst, :fin])
|> synproxy(mss: 1460)
# Full options
builder
|> tcp()
|> dport(22)
|> tcp_flags([:syn], [:syn, :ack, :rst, :fin])
|> synproxy(mss: 1460, wscale: 7, sack_perm: true, timestamp: true)
## Use Cases
- SYN flood DDoS protection
- High-volume web servers
- Public-facing services
- Attack mitigation
## WARNING
- Only use on SYN packets (tcp_flags required)
- May break some TCP options
- Backend servers see firewall as client
"""
@spec synproxy(Expr.t(), keyword()) :: Expr.t()
def synproxy(builder \\ Expr.expr(), opts \\ []) do
synproxy_expr = %{}
synproxy_expr =
if mss = Keyword.get(opts, :mss) do
Map.put(synproxy_expr, "mss", mss)
else
synproxy_expr
end
synproxy_expr =
if wscale = Keyword.get(opts, :wscale) do
Map.put(synproxy_expr, "wscale", wscale)
else
synproxy_expr
end
synproxy_expr =
if Keyword.get(opts, :sack_perm) do
Map.put(synproxy_expr, "sack-perm", true)
else
synproxy_expr
end
synproxy_expr =
if Keyword.get(opts, :timestamp) do
Map.put(synproxy_expr, "timestamp", true)
else
synproxy_expr
end
synproxy_expr = if map_size(synproxy_expr) == 0, do: nil, else: synproxy_expr
expr = %{"synproxy" => synproxy_expr}
Expr.add_expr(builder, expr)
end
@doc """
Set TCP Maximum Segment Size (MSS).
Modifies or clamps the TCP MSS option. Useful for fixing PMTU issues
with PPPoE or VPN connections.
## Example
# Clamp MSS to 1400 (for PPPoE)
builder
|> tcp_flags([:syn], [:syn, :ack, :rst, :fin])
|> set_tcp_mss(1400)
|> accept()
# Clamp to PMTU
builder
|> oif("pppoe0")
|> tcp_flags([:syn], [:syn, :ack, :rst, :fin])
|> set_tcp_mss(:pmtu)
|> accept()
## Use Cases
- PPPoE connections (typically 1492 MTU → 1452 MSS)
- VPN tunnels with reduced MTU
- Fixing PMTU black holes
- WAN interface MSS clamping
"""
@spec set_tcp_mss(Expr.t(), non_neg_integer() | :pmtu) :: Expr.t()
def set_tcp_mss(builder \\ Expr.expr(), mss)
def set_tcp_mss(builder, :pmtu) do
# TCP MSS clamping to PMTU
expr = %{
"mangle" => %{
"key" => %{"tcp option" => %{"name" => "maxseg", "field" => "size"}},
"value" => %{"rt" => "mtu"}
}
}
Expr.add_expr(builder, expr)
end
def set_tcp_mss(builder, mss) when is_integer(mss) and mss > 0 and mss <= 65535 do
# TCP MSS clamping to specific value
expr = %{
"mangle" => %{
"key" => %{"tcp option" => %{"name" => "maxseg", "field" => "size"}},
"value" => mss
}
}
Expr.add_expr(builder, expr)
end
@doc """
Redirect to local transparent proxy (TPROXY).
Redirects packets to a local socket without changing the destination address.
Used for transparent proxy setups where the proxy needs to see the original
destination.
## Parameters
- `builder` - Match builder
- `opts` - Options:
- `:to` - Port number to redirect to (required)
- `:addr` - Local IP address to redirect to (optional)
- `:family` - Address family (`:ipv4` or `:ipv6`, optional)
## Examples
# Redirect HTTP to local transparent proxy on port 8080
rule()
|> tcp()
|> dport(80)
|> tproxy(to: 8080)
|> accept()
# With specific address
rule()
|> tcp()
|> dport(80)
|> tproxy(to: 8080, addr: "127.0.0.1")
# IPv6 transparent proxy
rule()
|> tcp()
|> dport(443)
|> tproxy(to: 8443, addr: "::1", family: :ipv6)
## Use Cases
- Transparent HTTP/HTTPS proxies
- Deep packet inspection
- Content filtering
- Traffic monitoring without changing destinations
## Requirements
- Requires special routing and iptables setup
- Socket must have IP_TRANSPARENT option
- Usually combined with socket_transparent() matching
- Requires CAP_NET_ADMIN capability
## Typical Transparent Proxy Setup
# 1. Mark packets with existing transparent socket
prerouting_mark = rule()
|> tcp()
|> socket_transparent()
|> set_mark(1)
|> accept()
# 2. Redirect unmarked packets to proxy
prerouting_tproxy = rule()
|> tcp()
|> dport(80)
|> mark(0)
|> tproxy(to: 8080)
# 3. Accept marked packets in input
input_accept = rule()
|> mark(1)
|> accept()
"""
@spec tproxy(Expr.t(), keyword()) :: Expr.t()
def tproxy(builder \\ Expr.expr(), opts) do
port = Keyword.fetch!(opts, :to)
addr = Keyword.get(opts, :addr)
family = Keyword.get(opts, :family)
tproxy_map = %{port: port}
tproxy_map = if addr, do: Map.put(tproxy_map, :addr, addr), else: tproxy_map
tproxy_map = if family, do: Map.put(tproxy_map, :family, to_string(family)), else: tproxy_map
expr = %{tproxy: tproxy_map}
Expr.add_expr(builder, expr)
end
end