NFTables.Expr.Structs (NFTables v0.8.2)

View Source

Low-level helper functions for building nftables expression structures.

This module provides the foundational API for constructing the raw expression data structures used by nftables. These are the building blocks used by the higher-level NFTables.Expr module and its sub-modules.

NOTE: All functions return maps with atom keys (not string keys). The JSON encoding happens later in the Builder/Local pipeline.

Most users should use NFTables.Expr instead of calling these functions directly.

Expression Format

nftables uses expressions that consist of:

  • Matches: Compare packet fields against values
  • Statements: Perform actions (counter, log, limit, mark, etc.)
  • Verdicts: Terminal decisions (accept, drop, reject, etc.)

Examples

# Simple IP match
Expr.payload_match("ip", "saddr", "192.168.1.1")
#=> %{match: %{
#     left: %{payload: %{protocol: "ip", field: "saddr"}},
#     right: "192.168.1.1",
#     op: "=="
#   }}

# Connection tracking match
Expr.ct_match("state", ["established", "related"])
#=> %{match: %{
#     left: %{ct: %{key: "state"}},
#     right: ["established", "related"],
#     op: "in"
#   }}

# Verdict
Expr.verdict("drop")
#=> %{drop: nil}

Reference

Official nftables documentation: https://wiki.nftables.org/wiki-nftables/index.php/JSON_API

Summary

Functions

Build a bitwise AND match.

Build a counter statement.

Build a connection tracking match expression.

Build a connection tracking original direction match.

Build a CT set statement (set connection tracking value).

Build a DNAT (Destination NAT) statement.

Build a goto verdict (goto another chain, no return).

Build a jump verdict (jump to another chain).

Build a limit statement (rate limiting).

Build a log statement.

Build a masquerade statement.

Build a meta expression match.

Build a meta set statement (set packet mark, priority, etc.).

Build an OSF (OS Fingerprinting) match expression.

Build an OSF match expression with value comparison.

Build a payload match expression.

Build a payload match with prefix (CIDR notation).

Build a raw payload expression for offset-based matching.

Build a raw payload match expression.

Build a reject verdict with optional type.

Build a set membership match.

Build a set update operation (for meters).

Build a SNAT (Source NAT) statement.

Build a socket match expression.

Build a socket match expression with value comparison.

Build a verdict expression.

Functions

bitwise_and_match(left_expr, mask, value)

@spec bitwise_and_match(map(), term(), term()) :: map()

Build a bitwise AND match.

Used for TCP flags, fragmentation checks, etc.

Examples

# TCP flags: Check if SYN is set (mask includes SYN, ACK, RST, FIN)
bitwise_and_match(
  %{payload: %{protocol: "tcp", field: "flags"}},
  ["syn", "ack", "rst", "fin"],
  ["syn"]
)

counter()

@spec counter() :: map()

Build a counter statement.

Counts packets and bytes.

Examples

counter()
#=> %{counter: nil}

ct_match(key, value, op \\ nil)

@spec ct_match(String.t(), term(), String.t() | nil) :: map()

Build a connection tracking match expression.

Parameters

  • key - CT key ("state", "status", "mark", "bytes", "packets", etc.)
  • value - Value to match
  • op - Comparison operator (default: "in" for lists, "==" for single values)

Examples

# Match established/related connections
ct_match("state", ["established", "related"])

# Match connection mark
ct_match("mark", 42, "==")

# Match connection bytes
ct_match("bytes", 1000000, ">")

ct_original_match(field, value)

@spec ct_original_match(String.t(), String.t()) :: map()

Build a connection tracking original direction match.

Examples

ct_original_match("saddr", "192.168.1.1")
#=> Match original source address

ct_set(key, value)

@spec ct_set(String.t(), term()) :: map()

Build a CT set statement (set connection tracking value).

Examples

ct_set("mark", 42)
ct_set("helper", "ftp")

dnat(addr, opts \\ [])

@spec dnat(
  String.t(),
  keyword()
) :: map()

Build a DNAT (Destination NAT) statement.

Options

  • :port - Port or port range
  • :family - Address family ("ip" or "ip6", default: "ip")

Examples

dnat("192.168.1.10")
dnat("192.168.1.10", port: 8080)
dnat("192.168.1.10", port: [8080, 8090])
dnat("2001:db8::10", family: "ip6")

goto(chain_name)

@spec goto(String.t()) :: map()

Build a goto verdict (goto another chain, no return).

Examples

goto("custom_chain")
#=> %{goto: %{target: "custom_chain"}}

jump(chain_name)

@spec jump(String.t()) :: map()

Build a jump verdict (jump to another chain).

Examples

jump("custom_chain")
#=> %{jump: %{target: "custom_chain"}}

limit(rate, per, opts \\ [])

@spec limit(integer(), String.t(), keyword()) :: map()

Build a limit statement (rate limiting).

Options

  • :rate - Rate number (required)
  • :per - Time unit ("second", "minute", "hour", "day") (required)
  • :burst - Burst packets (optional)
  • :inv - Invert match (rate over) (optional)

Examples

limit(10, "minute")
limit(100, "second", burst: 200)
limit(5, "minute", burst: 10, inv: true)  # Rate over 5/min

log(prefix, opts \\ [])

@spec log(
  String.t(),
  keyword()
) :: map()

Build a log statement.

Options

  • :prefix - Log prefix string (required)
  • :level - Syslog level ("emerg", "alert", "crit", "err", "warn", "notice", "info", "debug")
  • :flags - Log flags list (["tcp sequence", "tcp options", "ip options", "skuid", "ether", "all"])

Examples

log("SSH_ATTEMPT: ")
log("DROPPED: ", level: "warn")
log("AUDIT: ", level: "info", flags: ["all"])

masquerade(opts \\ [])

@spec masquerade(keyword()) :: map()

Build a masquerade statement.

Examples

masquerade()
masquerade(port: [1024, 65535])

meta_match(key, value, op \\ "==")

@spec meta_match(String.t(), term(), String.t()) :: map()

Build a meta expression match.

Meta expressions match packet metadata (not packet contents).

Parameters

  • key - Meta key ("mark", "iif", "oif", "length", "protocol", etc.)
  • value - Value to match
  • op - Comparison operator (default: "==")

Examples

# Match packet mark
meta_match("mark", 100)

# Match input interface
meta_match("iifname", "eth0")

# Match packet length
meta_match("length", 1000, ">")

meta_set(key, value)

@spec meta_set(String.t(), term()) :: map()

Build a meta set statement (set packet mark, priority, etc.).

Examples

meta_set("mark", 100)
meta_set("priority", 1)

osf_match(key, ttl \\ "loose")

@spec osf_match(String.t(), String.t()) :: map()

Build an OSF (OS Fingerprinting) match expression.

OSF performs passive operating system detection by analyzing TCP SYN packet characteristics. Requires the pf.os fingerprint database to be loaded.

Parameters

  • key - What to match: "name" (OS name) or "version" (OS version)
  • ttl - TTL matching mode (default: "loose"):
    • "loose" - Allow TTL variations
    • "skip" - Ignore TTL completely
    • "strict" - Require exact TTL match

Examples

# Match OS name
osf_match("name")
#=> %{osf: %{key: "name", ttl: "loose"}}

# Match OS version with strict TTL
osf_match("version", "strict")
#=> %{osf: %{key: "version", ttl: "strict"}}

Requirements

The pf.os database must be loaded before using OSF:

nfnl_osf -f /usr/share/pf.os

Supported OS Names

Common values include: "Linux", "Windows", "MacOS", "FreeBSD", "OpenBSD"

osf_match_value(key, value, ttl \\ "loose", op \\ "==")

@spec osf_match_value(String.t(), term(), String.t(), String.t()) :: map()

Build an OSF match expression with value comparison.

Examples

# Match Linux systems
osf_match_value("name", "Linux")
#=> %{match: %{left: %{osf: %{key: "name", ttl: "loose"}}, right: "Linux", op: "=="}}

# Match specific OS version
osf_match_value("version", "3.x", "strict")
#=> %{match: %{left: %{osf: %{key: "version", ttl: "strict"}}, right: "3.x", op: "=="}}

payload_match(protocol, field, value, op \\ "==")

@spec payload_match(String.t(), String.t(), term(), String.t()) :: map()

Build a payload match expression.

Matches a protocol field against a value.

Parameters

  • protocol - Protocol name ("ip", "ip6", "tcp", "udp", "icmp", etc.)
  • field - Field name ("saddr", "daddr", "sport", "dport", etc.)
  • value - Value to match (string, integer, or list)
  • op - Comparison operator (default: "==")

Examples

# IPv4 source address
payload_match("ip", "saddr", "192.168.1.1")

# TCP destination port
payload_match("tcp", "dport", 80)

# Port range
payload_match("tcp", "dport", %{range: [1024, 65535]})

# Not equal
payload_match("ip", "saddr", "10.0.0.0/8", "!=")

payload_match_prefix(protocol, field, addr, prefix_len)

@spec payload_match_prefix(String.t(), String.t(), String.t(), integer()) :: map()

Build a payload match with prefix (CIDR notation).

Examples

payload_match_prefix("ip", "saddr", "192.168.1.0", 24)
#=> Matches 192.168.1.0/24

payload_match_range(protocol, field, min_val, max_val)

@spec payload_match_range(String.t(), String.t(), term(), term()) :: map()

Build a payload range match.

Examples

payload_match_range("tcp", "dport", 1024, 65535)
#=> Matches ports 1024-65535

payload_raw(base, offset, length)

@spec payload_raw(atom(), non_neg_integer(), pos_integer()) :: map()

Build a raw payload expression for offset-based matching.

Raw payload matching allows matching arbitrary bytes at specific offsets, bypassing protocol-specific parsing. Essential for custom protocols or DPI.

Parameters

  • base - Base reference point:
    • :ll - Link layer (Ethernet header start)
    • :nh - Network header (IP header start)
    • :th - Transport header (TCP/UDP header start)
    • :ih - Inner header (for tunneled packets)
  • offset - Bit offset from base (not byte offset!)
  • length - Number of bits to extract

Examples

# Extract 32 bits at network header offset 96 (source IP)
payload_raw(:nh, 96, 32)
#=> %{payload: %{base: "nh", offset: 96, len: 32}}

# Extract 16 bits at transport header offset 16 (dest port)
payload_raw(:th, 16, 16)

# Extract 8 bits at network header offset 0 (IP version + IHL)
payload_raw(:nh, 0, 8)

Notes

  • Offsets are in bits, not bytes (multiply byte offset by 8)
  • Length is also in bits (e.g., 32 bits = 4 bytes)
  • Network byte order (big endian) is assumed

payload_raw_match(base, offset, length, value, op \\ "==")

@spec payload_raw_match(atom(), non_neg_integer(), pos_integer(), term(), String.t()) ::
  map()

Build a raw payload match expression.

Convenience function combining payload_raw/3 with a match.

Parameters

  • base - Base reference (:ll, :nh, :th, :ih)
  • offset - Bit offset from base
  • length - Number of bits
  • value - Value to match against
  • op - Comparison operator (default: "==")

Examples

# Match source IP (32 bits at nh+96)
payload_raw_match(:nh, 96, 32, <<192, 168, 1, 1>>)

# Match destination port 53
payload_raw_match(:th, 16, 16, 53)

# Match DNS port with not-equal
payload_raw_match(:th, 16, 16, 53, "!=")

reject(type \\ nil)

@spec reject(String.t() | nil) :: map()

Build a reject verdict with optional type.

Examples

reject()
reject("tcp reset")
reject("icmpx port-unreachable")

set_add_operation(elem, set_name, statements)

@spec set_add_operation(term(), String.t(), [map()]) :: map()

Build a set add operation.

Similar to set_update but uses "add" operation instead of "update". "add" fails if element already exists, "update" updates existing or adds new.

Examples

set_add_operation(
  %{payload: %{protocol: "ip", field: "saddr"}},
  "tracked_ips",
  [counter()]
)

set_match(protocol, field, set_name)

@spec set_match(String.t(), String.t(), String.t()) :: map()

Build a set membership match.

Checks if a value is in a named set.

Examples

# Check if source IP is in blocklist
set_match("ip", "saddr", "@blocklist")

# Check if destination port is in allowed_ports set
set_match("tcp", "dport", "@allowed_ports")

set_update(elem, set_name, statements)

@spec set_update(term(), String.t(), [map()]) :: map()

Build a set update operation (for meters).

Set update operations add elements to a set with associated statements (like limit). Used for per-key rate limiting (meters).

Parameters

  • elem - Element expression(s) to add to set (single value or list for composite keys)
  • set_name - Name of the set (without @ prefix)
  • statements - List of statement expressions to associate with the element

Examples

# Per-IP rate limiting
set_update(
  %{payload: %{protocol: "ip", field: "saddr"}},
  "ssh_ratelimit",
  [limit(3, "minute", burst: 5)]
)

# Composite key (src + dst IP)
set_update(
  [
    %{payload: %{protocol: "ip", field: "saddr"}},
    %{payload: %{protocol: "ip", field: "daddr"}}
  ],
  "flow_limits",
  [limit(100, "second")]
)

snat(addr, opts \\ [])

@spec snat(
  String.t(),
  keyword()
) :: map()

Build a SNAT (Source NAT) statement.

Options

  • :port - Port or port range
  • :family - Address family ("ip" or "ip6", default: "ip")

Examples

snat("203.0.113.1")
snat("203.0.113.1", port: 1024)
snat("203.0.113.1", port: [1024, 65535])
snat("2001:db8::1", family: "ip6")

socket_match(key)

@spec socket_match(String.t()) :: map()

Build a socket match expression.

Socket matching allows matching packets based on socket attributes. Useful for transparent proxy setups and process-based filtering.

Parameters

  • key - Socket attribute to match:
    • "transparent" - Match transparent sockets (for TPROXY)
    • "mark" - Match socket mark
    • "wildcard" - Match wildcard sockets

Examples

# Match packets with transparent socket
socket_match("transparent")
#=> %{socket: %{key: "transparent"}}

# Match socket mark
socket_match("mark")
#=> %{socket: %{key: "mark"}}

socket_match_value(key, value, op \\ "==")

@spec socket_match_value(String.t(), term(), String.t()) :: map()

Build a socket match expression with value comparison.

Examples

# Match transparent socket (value = 1)
socket_match_value("transparent", 1)
#=> %{match: %{left: %{socket: %{key: "transparent"}}, right: 1, op: "=="}}

verdict(verdict_name)

@spec verdict(String.t()) :: map()

Build a verdict expression.

Supported Verdicts

  • "accept" - Accept the packet
  • "drop" - Drop the packet
  • "continue" - Continue to next rule
  • "return" - Return from chain

Examples

verdict("accept")
#=> %{accept: nil}

verdict("drop")
#=> %{drop: nil}