NFT(8)

๐Ÿฆ‹ NFT(8) ๐Ÿฆ‹

nft โ€” Administration tool of the nftables framework for packet filtering and classification


๐Ÿ’– NAME ๐Ÿ’–

okay bestie, listen. nft is the Administration tool of the nftables framework for packet filtering and classification. that's the whole vibe. that's the job. ๐Ÿ’…


โœจ SYNOPSIS โœจ

nft [ -nNscaeSupyjtT ] [ -I directory ] [ -f filename | -i | cmd ...]
nft -h
nft -v

๐ŸŒธ DESCRIPTION ๐ŸŒธ

so. nft is the command line tool you use to set up, maintain, and inspect packet filtering and classification rules in the Linux kernel, inside the nftables framework. she's the main character of kernel firewalling, no cap. ๐Ÿซถ

the Linux kernel subsystem doing all this is known as nf_tables, and โ€˜nfโ€™ stands for Netfilter. that's it, that's the lore. ๐Ÿงš


๐ŸŽ€ OPTIONS ๐ŸŽ€

the command accepts several different options which are documented here in groups, for better understanding of their meaning. you can get info about options by running nft --help. โœจ

General options:

  • -h, --help โ€” Show help message and all options.
  • -v, --version โ€” Show version.
  • -V โ€” Show long version information, including compile-time configuration.

Ruleset input handling options that specify to how to load rulesets:

  • -f, --file filename โ€” Read input from filename. If filename is -, read from stdin. The directory path to this file is inserted at the beginning the list of directories to be searched for included files (see -I/--includepath).
  • -D, --define name=value โ€” Define a variable. You can only combine this option with -f.
  • -i, --interactive โ€” Read input from an interactive readline CLI. You can use quit to exit, or use the EOF marker, normally this is CTRL-D.
  • -I, --includepath directory โ€” Add the directory to the list of directories to be searched for included files. This option may be specified multiple times.
  • -c, --check โ€” Check commands validity without actually applying the changes. ๐Ÿ‘€
  • -o, --optimize โ€” Optimize your ruleset. You can combine this option with -c to inspect the proposed optimizations.

Ruleset list output formatting that modify the output of the list ruleset command:

  • -a, --handle โ€” Show object handles in output.
  • -s, --stateless โ€” Omit stateful information of rules and stateful objects.
  • -t, --terse โ€” Omit contents of sets from output.
  • -S, --service โ€” Translate ports to service names as defined by /etc/services.
  • -N, --reversedns โ€” Translate IP address to names via reverse DNS lookup. This may slow down your listing since it generates network traffic.
  • -u, --guid โ€” Translate numeric UID/GID to names as defined by /etc/passwd and /etc/group.
  • -n, --numeric โ€” Print fully numerical output.
  • -y, --numeric-priority โ€” Display base chain priority numerically.
  • -p, --numeric-protocol โ€” Display layer 4 protocol numerically.
  • -T, --numeric-time โ€” Show time, day and hour values in numeric format.

Command output formatting:

  • -e, --echo โ€” When inserting items into the ruleset using add, insert or replace commands, print notifications just like nft monitor.
  • -j, --json โ€” Format output in JSON. See libnftables-json(5) for a schema description.
  • -d, --debug level โ€” Enable debugging output. The debug level can be any of scanner, parser, eval, netlink, mnl, proto-ctx, segtree, all. You can combine more than one by separating by the , symbol, for example -d eval,mnl.

๐Ÿฆ‹ INPUT FILE FORMATS ๐Ÿฆ‹

๐ŸŒˆ LEXICAL CONVENTIONS ๐ŸŒˆ

Input is parsed line-wise. When the last character of a line, just before the newline character, is a non-quoted backslash (\), the next line is treated as a continuation. Multiple commands on the same line can be separated using a semicolon (;).

A hash sign (#) begins a comment. All following characters on the same line are ignored. ๐Ÿ’€ (yes, it just ghosts them)

Identifiers begin with an alphabetic character (a-z,A-Z), followed by zero or more alphanumeric characters (a-z,A-Z,0-9) and the characters slash (/), backslash (\), underscore (_) and dot (.). Identifiers using different characters or clashing with a keyword need to be enclosed in double quotes (").

๐ŸŽ€ INCLUDE FILES ๐ŸŽ€

include filename

Other files can be included by using the include statement. The directories to be searched for include files can be specified using the -I/--includepath option. You can override this behaviour either by prepending โ€˜./โ€™ to your path to force inclusion of files located in the current working directory (i.e. relative path) or / for file location expressed as an absolute path.

If -I/--includepath is not specified, then nft relies on the default directory that is specified at compile time. You can retrieve this default directory via the -h/--help option. ๐Ÿ”

Include statements support the usual shell wildcard symbols (,?,[]). Having no matches for an include statement is not an error, if wildcard symbols are used in the include statement. This allows having potentially empty include directories for statements like include "/etc/firewall/rules/". The wildcard matches are loaded in the collation order of the C locale. Files beginning with dot (.) are not matched by include statements.

๐Ÿ’œ SYMBOLIC VARIABLES ๐Ÿ’œ

define variable = expr
undefine variable
redefine variable = expr
$variable

Symbolic variables can be defined using the define statement. Variable references are expressions and can be used to initialize other variables. The scope of a definition is the current block and all blocks contained within. Symbolic variables can be undefined using the undefine statement, and modified using the redefine statement.

Using symbolic variables. โœจ

define int_if1 = eth0
define int_if2 = eth1
define int_ifs = { $int_if1, $int_if2 }
redefine int_if2 = wlan0
undefine int_if2

filter input iif $int_ifs accept

โœจ ADDRESS FAMILIES โœจ

okay so address families are literally the whole vibe-check for your packets โ€” they decide which kind of packets get processed. For every address family, the kernel has these little things called hooks tucked into specific stages of the packet-processing paths, and if rules exist for those hooks, they invoke nftables. no hooks, no rules, no drama ๐Ÿ’…

  • ip โ€” the IPv4 address family, bestie.
  • ip6 โ€” the IPv6 address family.
  • inet โ€” the Internet address family, handling IPv4 and IPv6.
  • arp โ€” the ARP address family, handling IPv4 ARP packets.
  • bridge โ€” the Bridge address family, handling packets that traverse a bridge device.
  • netdev โ€” the Netdev address family, handling packets on ingress and egress.

All nftables objects live in address family specific namespaces, so every single identifier includes an address family. And if you specify an identifier without one? The ip family is used by default. periodt. ๐Ÿ’–


๐ŸŒˆ IPV4/IPV6/INET ADDRESS FAMILIES

The IPv4/IPv6/Inet address families handle IPv4 packets, IPv6 packets, or both. They've got five hooks at different packet processing stages in the network stack. iconic behavior honestly.

Table 1. IPv4/IPv6/Inet address family hooks

  • prerouting โ€” All packets entering the system are processed by the prerouting hook. It's invoked before the routing process and is used for early filtering or for changing packet attributes that affect routing.
  • input โ€” Packets delivered to the local system are processed by the input hook.
  • forward โ€” Packets forwarded to a different host are processed by the forward hook.
  • output โ€” Packets sent by local processes are processed by the output hook.
  • postrouting โ€” All packets leaving the system are processed by the postrouting hook.
  • ingress โ€” All packets entering the system are processed by this hook. It's invoked before layer 3 protocol handlers, hence before the prerouting hook, and it can be used for filtering and policing. ingress is only available for the Inet family (since Linux kernel 5.10).

๐ŸŽ€ ARP ADDRESS FAMILY

The ARP address family handles ARP packets received and sent by the system. It's commonly used to mangle ARP packets for clustering. slay.

Table 2. ARP address family hooks

  • input โ€” Packets delivered to the local system are processed by the input hook.
  • output โ€” Packets send by the local system are processed by the output hook.

๐Ÿฆ‹ BRIDGE ADDRESS FAMILY

The bridge address family handles Ethernet packets traversing bridge devices.

The list of supported hooks is identical to the IPv4/IPv6/Inet address families above. copy-paste energy, but make it kernel-level. โœจ


๐Ÿงš NETDEV ADDRESS FAMILY

The Netdev address family handles packets from the device ingress and egress path. This family lets you filter packets of any ethertype โ€” think ARP, VLAN 802.1q, VLAN 802.1ad (Q-in-Q), and also IPv4 and IPv6 packets. so many options, we love a versatile queen ๐Ÿ‘‘

Table 3. Netdev address family hooks

  • ingress โ€” All packets entering the system are processed by this hook. It's invoked after the network taps (ie. tcpdump), right after tc ingress and before layer 3 protocol handlers, and it can be used for early filtering and policing.
  • egress โ€” All packets leaving the system are processed by this hook. It's invoked after layer 3 protocol handlers and before tc egress. It can be used for late filtering and policing.

Tunneled packets (such as vxlan) are processed by netdev family hooks both in decapsulated and encapsulated (tunneled) form. So a packet can be filtered on the overlay network as well as on the underlying network. double the layers, double the fun ๐Ÿซถ

Note that the order of netfilter and tc is mirrored on ingress versus egress. This ensures symmetry for NAT and other packet mangling. ๐Ÿชž

Ingress packets which are redirected out some other interface are only processed by netfilter on egress if they have passed through netfilter ingress processing before. So: ingress packets redirected by tc are not subjected to netfilter. But they are if they're redirected by netfilter on ingress. Conceptually, tc and netfilter can be thought of as layers, with netfilter layered above tc: if the packet hasn't been passed up from the tc layer to the netfilter layer, it's not subjected to netfilter on egress. iykyk ๐Ÿ’…


๐Ÿ‘‘ RULESET ๐Ÿ‘‘

{list | flush} ruleset [family]

The ruleset keyword is used to identify the whole set of tables, chains, etc. currently in place in kernel. Here are the ruleset commands that exist:

  • list โ€” Print the ruleset in human-readable format.
  • flush โ€” Clear the whole ruleset.
Note that, unlike iptables, flush will remove all tables and whatever they contain, effectively leading to an empty ruleset โ€” no packet filtering will happen anymore, so the kernel accepts any valid packet it receives. bestie that is a lot of power, be careful ๐Ÿ˜ญ

It's possible to limit list and flush to a specific address family only. For a list of valid family names, see the section called "ADDRESS FAMILIES" above. โฌ†๏ธ

By design, list ruleset command output may be used as input to nft -f. Effectively, this is the nft-equivalent of iptables-save and iptables-restore. so tidy, so reversible, obsessed. ๐Ÿ’–

๐ŸŒธ TABLES ๐ŸŒธ

{add | create} table [family] table [{ [comment comment ;] [flags flags ;] }]
{delete | destroy | list | flush} table [family] table
list tables [family]
delete table [family] handle handle
destroy table [family] handle handle

Okay bestie, tables are the โœจvibe containersโœจ of the whole operation. They hold your chains, your sets, your stateful objects โ€” the whole girlboss ecosystem ๐Ÿ’…. Every table has an address family and a name, that's how you tell them apart.

The family has to be one of these, no substitutions: ip, ip6, inet, arp, bridge, netdev. The inet family is a lil dummy family (affectionate) used to build hybrid IPv4/IPv6 tables โ€” iconic behaviour honestly. You can use the meta expression nfproto keyword to test which family (ipv4 or ipv6) the packet is getting processed in. And if you don't specify a family at all? ip is the default, no cap.

The only difference between add and create is this: add will not throw an error if that table already exists, while create absolutely will. create is the one who keeps receipts ๐Ÿ“.

Table 4. Table flags

  • dormant โ€” table is not evaluated any more (base chains are unregistered).
  • owner โ€” table is owned by the creating process.
  • persist โ€” table shall outlive the owning process.

So here's the tea ๐Ÿ“: making a table with the owner flag locks other processes out of messing with it or anything inside it. By default it gets removed when that process exits. Setting persist stops that from happening, and the resulting orphaned table will accept a new owner โ€” like, say, a restarting daemon maintaining the table. Found family, but for tables ๐Ÿซถ.

Add, change, delete a table.

# start nft in interactive mode
nft --interactive

# create a new table.
create table inet mytable

# add a new base chain: get input packets
add chain inet mytable myin { type filter hook input priority filter; }

# add a single counter to the chain
add rule inet mytable myin counter

# disable the table temporarily -- rules are not evaluated anymore
add table inet mytable { flags dormant; }

# make table active again:
add table inet mytable
  • add โ€” Add a new table for the given family with the given name.
  • delete โ€” Delete the specified table.
  • destroy โ€” Delete the specified table, it does not fail if it does not exist.
  • list โ€” List all chains and rules of the specified table.
  • flush โ€” Flush all chains and rules of the specified table.

๐Ÿ”ฅ CHAINS ๐Ÿ”ฅ

{add | create} chain [family] table chain [{ type type hook hook [DEVICE] priority priority ; [policy policy ;] [comment comment ;] }]
{delete | destroy | list | flush} chain [family] table chain
list chains [family]
delete chain [family] table handle handle
destroy chain [family] table handle handle
rename chain [family] table chain newname

DEVICE := {device DEVICE_NAME | devices = { DEVICE_LIST }}
DEVICE_LIST := DEVICE_NAME [, DEVICE_LIST]
DEVICE_NAME := string | string*

Chains are containers for rules ๐Ÿ’– โ€” they live in two flavours, base chains and regular chains. A base chain is literally an entry point for packets coming off the networking stack (main character energy), and a regular chain can be used as a jump target and exists for better rule organization. Regular chains can be anonymous too โ€” go check the VERDICT STATEMENT examples for the details, iykyk.

  • add โ€” Add a new chain in the specified table. When a hook and priority value are specified, the chain is created as a base chain and hooked up to the networking stack.
  • create โ€” Similar to the add command, but returns an error if the chain already exists.
  • delete โ€” Delete the specified chain. The chain must not contain any rules or be used as jump target.
  • destroy โ€” Delete the specified chain, it does not fail if it does not exist. The chain must not contain any rules or be used as jump target.
  • rename โ€” Rename the specified chain.
  • list โ€” List all rules of the specified chain.
  • flush โ€” Flush all rules of the specified chain.
For base chains, type, hook and priority parameters are mandatory. Not optional, not "we'll figure it out later" โ€” mandatory, bestie. ๐Ÿšจ

Table 5. Supported chain types

  • filter
    • Families: all
    • Hooks: all
    • Description: Standard chain type to use in doubt.
  • nat
    • Families: ip, ip6, inet
    • Hooks: prerouting, input, output, postrouting
    • Description: Chains of this type perform Native Address Translation based on conntrack entries. Only the first packet of a connection actually traverses this chain โ€” its rules usually define details of the created conntrack entry (NAT statements for instance).
  • route
    • Families: ip, ip6, inet
    • Hooks: output
    • Description: If a packet has traversed a chain of this type and is about to be accepted, a new route lookup is performed if relevant parts of the IP header have changed. This allows one to e.g. implement policy routing selectors in nftables.

Okay so apart from the special cases we just showed you (like nat type not supporting the forward hook, or route type only supporting the output hook), there are three further quirks worth noticing ๐Ÿ‘€:

  • The netdev family supports merely two combinations, namely filter type with ingress hook and filter type with egress hook. Base chains in this family also require the DEVICE parameter to be present since they exist per interface only.
  • The arp family supports only the input and output hooks, both in chains of type filter.
  • The inet family also supports the ingress hook (since Linux kernel 5.10), to filter IPv4 and IPv6 packet at the same location as the netdev ingress hook. This inet hook allows you to share sets and maps between the usual prerouting, input, forward, output, postrouting and this ingress hook.

The device parameter accepts a network interface name as a string, and it is required when adding a base chain that filters traffic on the ingress or egress hooks. Any ingress or egress chains will only filter traffic from the interface specified in the device parameter. And if you want one base chain doing the work of many? Use the devices parameter instead โ€” the same base chain may be used for multiple devices that way. Multi-tasking queen ๐Ÿ‘‘.

With newer kernels there's also basic support for wildcards in DEVICE_NAME by specifying an asterisk suffix. The chain will apply to all interfaces matching the given prefix. Run the list hooks command to see the current status. โญ

The priority parameter takes either a signed integer or one of the standard priority names, and it's literally the thing that decides the order chains with the same hook value get walked through โ€” and yes, that's across all tables, not just within one, bestie. The ordering is ascending: for each hook, chains with lower priority values get evaluated before the higher ones. And if two chains have the exact same priority? ๐Ÿ’€ The ordering between them is undefined. Not "sorted by table name", not "sorted by vibes" โ€” undefined. Periodt.

For nat type chains there's a lower excluding limit of -200 on priority values, because conntrack hooks at this priority and NAT requires it. So -200 itself is out, -199 and up is fine, don't fight me on this, it's the kernel's house ๐Ÿ .

Those standard priority values can be swapped out for names that are actually memorizable, which, thank god. But heads up: not every name makes sense in every family with every hook (the compatibility matrices are right below, iykyk), though their numerical value can still be used for prioritizing chains anyway. The names and values are defined based on what priorities xtables uses when it registers its default chains. Most families share the same values, but bridge is the exception โ€” bridge does its own thing, as always. ๐ŸŒˆ

Table 6. Standard priority names, family and hook compatibility matrix

  • raw โ€” value -300 โ€” families: ip, ip6, inet โ€” hooks: all
  • mangle โ€” value -150 โ€” families: ip, ip6, inet โ€” hooks: all
  • dstnat โ€” value -100 โ€” families: ip, ip6, inet โ€” hooks: prerouting
  • filter โ€” value 0 โ€” families: ip, ip6, inet, arp, netdev โ€” hooks: all
  • security โ€” value 50 โ€” families: ip, ip6, inet โ€” hooks: all
  • srcnat โ€” value 100 โ€” families: ip, ip6, inet โ€” hooks: postrouting

Table 7. Standard priority names and hook compatibility for the bridge family

  • dstnat โ€” value -300 โ€” hooks: prerouting
  • filter โ€” value -200 โ€” hooks: all
  • out โ€” value 100 โ€” hooks: output
  • srcnat โ€” value 300 โ€” hooks: postrouting

Also you can do basic arithmetic with these standard names โ€” addition and subtraction only, don't get cute โ€” to make relative prioritizing way easier. Like mangle - 5 stands for -155. So no mental math required, slay. โœจ And values get printed that way too, until the value is no further than 10 from the standard value. Once it drifts more than 10 away, it goes back to being a plain number. The way that's such a thoughtful little detail honestly ๐Ÿฅน.

Base chains also let you set the chain's policy โ€” that's what happens to packets that don't get explicitly accepted or refused by the rules inside. Supported policy values are accept (which is the default, obviously, she's chill like that) or drop. That's it, two options, choose your fighter.


๐Ÿ‘‘ RULES ๐Ÿ‘‘

{add | insert} rule [family] table chain [handle handle | index index] statement ... [comment comment]
replace rule [family] table chain handle handle statement ... [comment comment]
{delete | reset} rule [family] table chain handle handle
destroy rule [family] table chain handle handle
reset rules [family] [table [chain]]

Rules get added to chains in the given table. No family specified? Then the ip family is assumed, bestie. ๐Ÿ’… Rules are built from two kinds of components following a set of grammatical rules: expressions and statements.

So add and insert support an optional location specifier โ€” either a handle or the index (starting at zero, don't forget) of an existing rule. Internally, rule locations are always identified by handle, and the translation from index happens in userspace. Which has two potential implications if a concurrent ruleset change happens after that translation was done:

  • The effective rule index might change if a rule was inserted or deleted before the one you referred to. ๐Ÿซ 
  • If the rule you referred to got deleted, the command is rejected by the kernel, just as if an invalid handle was given.

A comment is a single word or a double-quoted (") multi-word string you can use to leave notes about the actual rule. Note: if you're using bash to add rules, you have to escape the quotation marks, like \"enable ssh for servers\". Do not skip this, or bash will eat your string and you'll be sad ๐Ÿ˜ญ.

  • add โ€” Add a new rule described by the list of statements. The rule is appended to the given chain unless a location is specified, in which case the rule is inserted after the specified rule.
  • insert โ€” Same as add except the rule is inserted at the beginning of the chain, or before the specified rule.
  • replace โ€” Similar to add, but the rule replaces the specified rule.
  • delete โ€” Delete the specified rule.
  • destroy โ€” Delete the specified rule, and it does not fail if it does not exist. No drama, no error, just gone. ๐Ÿฆ‹
  • reset โ€” Reset rule-contained state, e.g. counter and quota statement values.

Add a rule to the ip table output chain:

nft add rule filter output ip daddr 192.168.0.0/24 accept # 'ip filter' is assumed
# same command, slightly more verbose
nft add rule ip filter output ip daddr 192.168.0.0/24 accept

Delete a rule from an inet table:

# nft -a list ruleset
table inet filter {
        chain input {
                type filter hook input priority filter; policy accept;
                ct state established,related accept # handle 4
                ip saddr 10.1.1.1 tcp dport ssh accept # handle 5
          ...
# delete the rule with handle 5
nft delete rule inet filter input handle 5

๐Ÿ’ซ OVERALL EVALUATION OF THE RULESET ๐Ÿ’ซ

okay bestie, gather round, because this is the part where it all clicks ๐Ÿซถ this is the summary of how the ruleset actually gets evaluated, and honestly it's giving "cheat code for the whole game" energy.

  • Even if a packet gets accepted by the ruleset, it can STILL get thrown away by other stuff. Like, Linux generally ignores various ICMP types, and there are sysctl options out here doing their own thing, for example net.ipv{4,6}.conf.*.forwarding or net.ipv4.conf.*.rp_filter. So "accepted" is not the same as "arrived at its destination," bestie. ๐Ÿ’€
  • Tables are literally just a concept that nftables uses to structure the ruleset. They have zero relevance to how the ruleset gets evaluated. Vibe only. โœจ
  • Packets traverse the network stack, and at various hooks โ€” peep the "ADDRESS FAMILIES" section above for the lists of hooks per address family โ€” they get evaluated by any base chains attached to those hooks. ๐Ÿ“
  • Base chains can call regular chains (via jump and goto). Regular chains can call other regular chains too. Evaluation continues over in the called chain. Oh, and chains living in a different table cannot be called โ€” no cross-table collabs, sorry. ๐Ÿšซ
  • For each hook, the attached chains get evaluated in order of their priorities. Chains with lower priority values get evaluated before the ones with higher ones. And the order of chains that share the same priority value? Undefined. ๐Ÿ™ˆ So don't count on it, fr fr.
  • An accept verdict ends the evaluation of the current base chain โ€” and yes, that includes an implicit one coming from the base chain's policy. It does not matter whether that accept verdict got issued in the base chain itself or in a regular chain that was called from the base chain. The packet then advances to the next base chain. Which means: a packet is ultimately accepted if and only if no (matching) rule or base chain policy issues a drop verdict. All of this also applies to verdict-like statements that imply accept, for example the NAT statements. ๐Ÿ‘‘
  • A drop verdict immediately ends the evaluation of the WHOLE ruleset. ๐Ÿ›‘ And again, that includes an implicit one via the base chain's policy. No further chains of any hook get consulted. Which means it is not possible to have a drop verdict changed to an accept in a later chain โ€” sorry, no take-backs. This also applies to other terminal statements that imply drop, for example reject. So like: if any base chain uses drop as its policy, then that same base chain (or a regular chain directly or indirectly called by it) must contain at least one matching accept rule, or the packet will be dropped. Periodt. ๐Ÿ’…
  • Given the semantics of accept/drop, and only with respect to the ultimate decision of whether a packet is accepted or dropped, the ordering of the various base chains per hook (via their priorities) matters only in so far as any of them modifies the packet or its meta data and that has an influence on the verdicts issued by the chains. Other than that, the ordering shouldn't matter โ€” except for performance and other side effects, obviously. It also means that short-circuiting the ultimate decision is only possible via drop verdicts (respectively verdict-like statements that imply drop, for example reject). ๐Ÿง 
  • A jump verdict causes the current position to be stored in the call stack of chains, and evaluation continues at the beginning of the called regular chain. Called chains must be from the same table and cannot be base chains. When the end of the called chain is reached, an implicit return verdict gets issued. Other verdicts (respectively verdict-like statements) are processed as described above and below. ๐Ÿฆ‹
  • A goto verdict is equal to jump โ€” except the current position is not stored in the call stack of chains. That's it, that's the difference. ๐ŸŽ€
  • A return verdict ends the evaluation of the current chain, pops the most recently added position off the call stack of chains, and causes evaluation to continue after that position. But when there's no position to pop โ€” which happens when the current chain is either the base chain, or a regular chain that was reached solely via goto verdicts โ€” it ends the evaluation of the current base chain (and any regular chains called from it) using the base chain's policy as the implicit verdict. ๐ŸŒธ

okay now the examples for jump/goto/return, because visualising this is everything:

  • Example one:
base {jump}โ†’ regular-1 {jump}โ†’ regular-2

At the end of regular-2, or when a return is issued in it, evaluation continues after the jump position in regular-1. At the end of regular-1, or when a return is issued in it, evaluation continues after the jump position in base.

  • Example two:
base {jump}โ†’ regular-1 {goto}โ†’ regular-2

At the end of regular-2, or when a return is issued in it, evaluation continues after the jump position in base.

  • Example three:
base {jump}โ†’ regular-1 {jump}โ†’ regular-2 {goto}โ†’ regular-3

At the end of regular-3, or when a return is issued in it, evaluation continues after the jump position in regular-1. At the end of regular-1, or when a return is issued in it, evaluation continues after the jump position in base.

  • Example four:
base {jump}โ†’ regular-1 {goto}โ†’ regular-2 {goto}โ†’ regular-3

At the end of regular-3, or when a return is issued in it, evaluation continues after the jump position in base.


  • Verdicts โ€” that is: accept, drop, jump, goto, return and continue โ€” as well as statements that imply a verdict (like reject or the NAT statements), also end the evaluation of any later statements in their respective rules (respectively cause an error when loading such rules). For example, in ... counter accept the counter statement is processed, but in ... accept counter it is not. ๐Ÿซ 
This does not apply to the comment statement โ€” that one is always evaluated, no matter what. cute of it honestly ๐ŸŽ€

SETS ๐Ÿ’–โœจ

okay bestie, nftables gives you two whole flavors of sets and they are literally not the same vibe. let's get into it ๐Ÿซถ

Anonymous sets are sets that have no specific name. the set members are enclosed in curly braces, with commas to separate elements when you're creating the rule the set is used in. once that rule is removed, the set is removed as well. they cannot be updated, i.e. once an anonymous set is declared it cannot be changed anymore except by removing/altering the rule that uses the anonymous set. no take-backs, no edits, it's giving commitment issues ๐Ÿ’€

using anonymous sets to accept particular subnets and ports:

nft add rule filter input ip saddr { 10.0.0.0/8, 192.168.0.0/16 } tcp dport { 22, 443 } accept

Named sets are sets that need to be defined first before they can be referenced in rules. unlike anonymous sets, elements can be added to or removed from a named set at any time. sets are referenced from rules using an @ prefixed to the sets name.

using named sets to accept addresses and ports:

nft add rule filter input ip saddr @allowed_hosts tcp dport @allowed_ports accept

the sets allowed_hosts and allowed_ports need to be created first. the next section describes nft set syntax in more detail.

add set [family] table set { type type | typeof expression ; [flags flags ;] [timeout timeout ;] [gc-interval gc-interval ;] [elements = { element[, ...] } ;] [size size ;] [comment comment ;] [policy 'policy ;] [auto-merge ;] }
{delete | destroy | list | flush | reset } set [family] table set
list sets [family] [table]
delete set [family] table handle handle
{add | delete | destroy } element [family] table set { element[, ...] }

sets are element containers of a user-defined data type, they are uniquely identified by a user-defined name and attached to tables. their behaviour can be tuned with the flags that can be specified at set creation time. slay ๐Ÿ’…

see Set and Map flags below for more details.

  • add โ€” add a new set in the specified table. see the Set specification table below for more information about how to specify properties of a set.
  • delete โ€” delete the specified set.
  • destroy โ€” delete the specified set, it does not fail if it does not exist.
  • list โ€” display the elements in the specified set.
  • flush โ€” remove all elements from the specified set.
  • reset โ€” reset state in all contained elements, e.g. counter and quota statement values.

Table 8. Set specifications ๐Ÿ“‹โœจ

  • type โ€” data type of set elements. type: string: ipv4_addr, ipv6_addr, ether_addr, inet_proto, inet_service, mark
  • typeof โ€” data type of set element. type: expression to derive the data type from
  • flags โ€” set flags. type: string: constant, dynamic, interval, timeout. used to describe the sets properties.
  • timeout โ€” time an element stays in the set, mandatory if set is added to from the packet path (ruleset). type: string, decimal followed by unit. units are: d, h, m, s
  • gc-interval โ€” garbage collection interval, only available when timeout or flag timeout are active. type: string, decimal followed by unit. units are: d, h, m, s
  • elements โ€” elements contained by the set. type: set data type
  • size โ€” maximum number of elements in the set, mandatory if set is added to from the packet path (ruleset). type: unsigned integer (64 bit)
  • policy โ€” set policy. type: string: performance [default], memory
  • auto-merge โ€” automatic merge of adjacent/overlapping set elements (only for interval sets)

the gc-interval doesnโ€™t affect element timeout, but it does affect memory reclaim. a large set that rarely has elements that time out can use a higher (less frequent) garbage collection to save cpu time, whereas sets that see many updates with short-lived elements will benefit from a lower interval. lower intervals ensure the set stays below its maximum size. internally, a timed-out entry stays around until it is removed by the garbage collector, which also decrements the sets element count. this also means that it is possible to have a set that can not accept more elements, even if all elements timed out, if the gc-interval is set too large. bestie please pay attention to this one, it's giving sneaky memory trap energy ๐Ÿ˜ญ

the size defines the upper limit of the amount of elements that the set can support. mandatory for sets that are added to from the ruleset with the add and update keywords. providing the size keyword for sets that are only added to via nft add element allows for a more compact (memory conserving) set implementation selection, but it is not required. lowkey a nice-to-have, not a must, unless you're adding from the ruleset โ€” then it's mandatory fr fr ๐Ÿซถ

the optional policy keyword can be used to request a more memory-conserving set implementation.

auto-merge instructs the nftables frontend to merge adjacent and overlapping ranges. example: when the set contains range 1.2.3.1-1.2.3.4, then adding element 1.2.3.2 has no effect. adding 1.2.3.5 changes the existing range to cover 1.2.3.1-1.2.3.5. without this flag, 1.2.3.2 can not be added and 1.2.3.5 is inserted as a new entry. it's giving "why do more work when less work do trick" โœจ

equality of a value with a set is given if the value matches exactly one value in the set (which for intervals means that itโ€™s contained in any of them). see the section called โ€œBITMASK TYPEโ€ for the subtle differences between syntactically similarly looking equality checks of sets and bitmasks. iykyk, the syntax can look the same but the semantics are doing their own thing ๐Ÿ‘‘

๐Ÿ—บ๏ธโœจ MAPS โœจ๐Ÿ—บ๏ธ

add map [family] table map { type type | typeof expression [flags flags ;] [elements = { element[, ...] } ;] [size size ;] [comment comment ;] [policy 'policy ;] }
{delete | destroy | list | flush | reset } map [family] table map
list maps [family] [table]

Okay bestie, so maps are like sets but with a glow-up: they store data based on some specific key that gets used as the input ๐Ÿ—๏ธ. Every map gets its own user-defined name (so it's uniquely identified, obvi) and it's attached to a table. She's got her own little home.

  • add โ€” adds a brand new map in the specified table ๐Ÿ’–
  • delete โ€” deletes the specified map ๐Ÿซก
  • destroy โ€” deletes the specified map, and honestly she's so chill about it: it does not fail if the map doesn't even exist ๐Ÿ’…
  • list โ€” displays the elements in the specified map ๐Ÿ‘€
  • flush โ€” removes all elements from the specified map, like a total reset of the vibes ๐Ÿงน
  • reset โ€” resets state in all contained elements, e.g. counter and quota statement values ๐Ÿ”„

๐Ÿ“‹ Table 9. Map specifications ๐Ÿ’–

  • type โ€” data type of map elements. Type is a string, one of: ipv4_addr, ipv6_addr, ether_addr, inet_proto, inet_service, mark, counter, quota. And heads up: Counter and quota can't be used as keys ๐Ÿšซ
  • typeof โ€” data type of set element; you give it an expression to derive the data type from ๐Ÿง 
  • flags โ€” map flags; string, same as set flags ๐Ÿšฉ
  • elements โ€” elements contained by the map ๐Ÿ“
  • size โ€” maximum number of elements in the map; unsigned integer (64 bit) ๐Ÿ”ข
  • policy โ€” map policy; string, one of performance [default] or memory โš–๏ธ

So here's the tea: users can specify the properties/features that the set/map must support, which lets the kernel pick an optimal internal representation โœจ. If a required flag is missing, the ruleset might still work, because nftables will auto-enable features if it can infer this from the ruleset โ€” she's intuitive like that. But this may not work for all cases, however, so it's recommended to specify all required features in the set/map definition manually, fr fr ๐Ÿ“. Also, some features are mutually exclusive ๐Ÿ˜ญ. For example, it is not possible for a set to support intervals and insertion from the packet path. Pick a lane bestie.


๐Ÿšฉ Table 10. Set and Map flags ๐Ÿšฉ

  • constant โ€” set contents will never change after creation ๐Ÿ—ฟ (iconic, frozen, unbothered)
  • dynamic โ€” set must support updates from the packet path with the add, update or delete keywords ๐Ÿ”ฅ
  • interval โ€” set must be able to store intervals (ranges). Cannot be combined with the dynamic flag. It's giving incompatible ๐Ÿ’€
  • timeout โ€” set must support element timeouts (auto-removal of elements once they expire) โฐ

๐Ÿงฉโœจ ELEMENTS โœจ๐Ÿงฉ

{add | create | delete | destroy | get | reset } element [family] table set { ELEMENT[, ...] }

ELEMENT := key_expression OPTIONS [: value_expression]
OPTIONS := [timeout TIMESPEC] [expires TIMESPEC] [comment string]
TIMESPEC := [numd][numh][numm][num[s]]

Element-related commands let you change the contents of named sets and maps ๐Ÿซถ. key_expression is typically a value matching the set type. value_expression is not allowed in sets but is mandatory when adding to maps, where it matches the data part in its type definition. When you're deleting from maps, it may be specified but it's optional, because key_expression already uniquely identifies the element ๐Ÿ’….

create command is similar to add with the exception that none of the listed elements may already exist ๐Ÿšซ โ€” no duplicates, we don't do that here.

get command is useful to check if an element is contained in a set, which may be non-trivial in very large and/or interval sets ๐Ÿ‘‘. In the latter case, the containing interval is returned instead of just the element itself.

reset command resets state attached to the given element(s), e.g. counter and quota statement values ๐Ÿ”„.


๐Ÿ“‹ Table 11. Element options ๐ŸŒธ

  • timeout โ€” timeout value for sets/maps with flag timeout โฐ
  • expires โ€” the time until given element expires; useful for ruleset replication only ๐Ÿ“‹
  • comment โ€” per element comment field ๐Ÿ’ฌ

๐ŸŒŠโœจ FLOWTABLES โœจ๐ŸŒŠ

{add | create} flowtable [family] table flowtable { hook hook priority priority ; devices = { DEVICE_LIST } ; }
list flowtables [family] [table]
{delete | destroy | list} flowtable [family] table flowtable
delete flowtable [family] table handle handle

DEVICE_LIST := DEVICE_NAME [, DEVICE_LIST]
DEVICE_NAME := string | string*

Bestie, flowtables let you accelerate packet forwarding in software ๐Ÿ”ฅ๐Ÿ”ฅ. Flowtable entries are represented through a tuple composed of the input interface, source and destination address, source and destination port; and layer 3/4 protocols ๐Ÿงฌ. Each entry also caches the destination interface and the gateway address โ€” to update the destination link-layer address โ€” to forward packets. The ttl and hoplimit fields are also decremented. Hence, flowtables provide an alternative path that allows packets to bypass the classic forwarding path ๐Ÿ›ฃ๏ธ๐Ÿ’จ.

Flowtables reside in the ingress hook that is located before the prerouting hook ๐Ÿ‘€. You can select which flows you want to offload through the flow expression from the forward chain.

Flowtables are identified by their address family and their name ๐Ÿ’–. The address family must be one of ip, ip6, or inet. The inet address family is a dummy family which is used to create hybrid IPv4/IPv6 tables. When no address family is specified, ip is used by default ๐ŸŽ€.

The priority can be a signed integer or filter, which stands for 0. Addition and subtraction can be used to set relative priority, e.g. filter + 5 equals to 5 โž•.

With newer kernels there is basic support for wildcards in DEVICE_LIST by specifying an asterisk suffix โœจ. The flowtable will apply to all interfaces matching the given prefix. Use the list hooks command to see the current status ๐Ÿ‘‘

  • add โ€” adds a new flowtable for the given family with the given name ๐ŸŒŠ
  • delete โ€” deletes the specified flowtable ๐Ÿซก
  • destroy โ€” deletes the specified flowtable; it does not fail if it does not exist ๐Ÿ’…
  • list โ€” lists all flowtables ๐Ÿ“œ

โœจ STATEFUL OBJECTS ๐Ÿ’–

{add | delete | destroy | list | reset} counter [family] table object
{add | delete | destroy | list | reset} quota [family] table object
{add | delete | destroy | list} limit [family] table object
delete counter [family] table handle handle
delete quota [family] table handle handle
delete limit [family] table handle handle
destroy counter [family] table handle handle
destroy quota [family] table handle handle
destroy limit [family] table handle handle
list { counters | limits | quotas } [family] [table]
reset { counters | quotas } [family] [table]

okay so stateful objects?? they live inside tables, bestie, and each one has its own unique name so you can find it. they're literally little containers that gather up stateful info from your rules, and when you wanna reference one from a rule you just use the keywords type name โ€” like counter name. that's it, that's the whole vibe ๐Ÿ’…

  • add โ€” adds a brand new stateful object into the table you picked.
  • delete โ€” deletes the object you specified.
  • destroy โ€” deletes the object you specified, and it does NOT fail if it doesn't exist. so chill of it honestly.
  • list โ€” shows you all the stateful info that object is holding onto. nosy in the best way.
  • reset โ€” list-and-reset the stateful object. shows you the tea and then wipes it clean โœจ

๐ŸŒธ CT HELPER ๐ŸŒธ

add ct helper [family] table name { type type protocol protocol ; [l3proto family ;] }
delete ct helper [family] table name
list ct helpers

so ct helper is how you define connection tracking helpers, and then you can use them together with the ct helper set statement. type and protocol are mandatory (no skipping, bestie), and l3proto gets derived from the table family by default โ€” meaning in the inet table, the kernel will try to load both the ipv4 and ipv6 helper backends, as long as the kernel supports them. cute right?

๐ŸŽ€ Table 12. conntrack helper specifications ๐ŸŽ€

  • type โ€” name of helper type โ€” type: quoted string (e.g. "ftp")
  • protocol โ€” layer 4 protocol of the helper โ€” type: string (e.g. ip)
  • l3proto โ€” layer 3 protocol of the helper โ€” type: address family (e.g. ip)
  • comment โ€” per ct helper comment field โ€” type: string

defining and assigning ftp helper. ๐ŸŸ

Unlike iptables, helper assignment needs to be performed after the conntrack lookup has completed, for example with the default 0 hook priority. not optional, bestie, this is the whole assignment moment.
table inet myhelpers {
  ct helper ftp-standard {
     type "ftp" protocol tcp
  }
  chain prerouting {
      type filter hook prerouting priority filter;
      tcp dport 21 ct helper set "ftp-standard"
  }
}

โณ CT TIMEOUT โณ

add ct timeout  [family] table name { protocol protocol ; policy = { state: value [, ...] } ; [l3proto family ;] }
delete ct timeout [family] table name
list ct timeouts

ct timeout is what you use to update connection tracking timeout values. the timeout policies get assigned with the ct timeout set statement. protocol and policy are mandatory, and l3proto is derived from the table family by default. ๐Ÿ’œ

๐Ÿฆ‹ Table 13. conntrack timeout specifications ๐Ÿฆ‹

  • protocol โ€” layer 4 protocol of the timeout object โ€” type: string (e.g. ip)
  • state โ€” connection state name โ€” type: string (e.g. "established")
  • value โ€” timeout value for connection state โ€” type: unsigned integer
  • l3proto โ€” layer 3 protocol of the timeout object โ€” type: address family (e.g. ip)
  • comment โ€” per ct timeout comment field โ€” type: string

the tcp connection state names that can have a specific timeout value are:

close, close_wait, established, fin_wait, last_ack, retrans, syn_recv, syn_sent, time_wait and unack.

you can use sysctl -a |grep net.netfilter.nf_conntrack_tcp_timeout_ to view and change the system-wide defaults. ct timeout is the move when you want flow-specific settings without touching the global timeouts. iconic tbh ๐ŸŒˆ

for example, tcp port 53 could have much lower settings than other traffic.

the udp state names that can have a specific timeout value are replied and unreplied.

defining and assigning ct timeout policy.

table ip filter {
        ct timeout customtimeout {
                protocol tcp;
                l3proto ip
                policy = { established: 2m, close: 20s }
        }

        chain output {
                type filter hook output priority filter; policy accept;
                ct timeout set "customtimeout"
        }
}

testing the updated timeout policy.

% conntrack -E

it should display:

[UPDATE] tcp      6 120 ESTABLISHED src=172.16.19.128 dst=172.16.19.1
sport=22 dport=41360 [UNREPLIED] src=172.16.19.1 dst=172.16.19.128
sport=41360 dport=22

๐Ÿ”ฎ CT EXPECTATION ๐Ÿ”ฎ

add ct expectation  [family] table name { protocol protocol ; dport dport ; timeout timeout ; size size ; [l3proto family ;] }
delete ct expectation  [family] table name
list ct expectations

ct expectation is used to create connection expectations. expectations are assigned with the ct expectation set statement. protocol, dport, timeout and size are all mandatory โ€” every single one, no cap โ€” and l3proto is derived from the table family by default. ๐Ÿ’–

๐ŸŽ€ Table 14. conntrack expectation specifications ๐ŸŽ€

bestie these are the lil knobs you get to twist on a ct expectation object, and there are exactly six of them:

  • protocol โ€” the layer 4 protocol of the expectation object. type: string (e.g. ip) ๐Ÿ’…
  • dport โ€” destination port of the expected connection. type: unsigned integer
  • timeout โ€” timeout value for the expectation. type: unsigned integer
  • size โ€” size value for expectation. type: unsigned integer
  • l3proto โ€” the layer 3 protocol of the expectation object. type: address family (e.g. ip)
  • comment โ€” per ct expectation comment field. type: string

ok and here's the whole vibe of defining and assigning a ct expectation policy, look at her go:

table ip filter {
        ct expectation expect {
                protocol udp
                dport 9876
                timeout 2m
                size 8
                l3proto ip
        }

        chain input {
                type filter hook input priority filter; policy accept;
                ct expectation set "expect"
        }
}

COUNTER ๐Ÿ”ข

three commands, that's it, that's the whole counter life:

add counter [family] table name [{ [ packets packets bytes bytes ; ] [ comment comment ; }]
delete counter [family] table name
list counters

๐Ÿ’– Table 15. Counter specifications ๐Ÿ’–

  • packets โ€” initial count of packets. type: unsigned integer (64 bit)
  • bytes โ€” initial count of bytes. type: unsigned integer (64 bit)
  • comment โ€” per counter comment field. type: string

Using named counters, i.e. the moment it gets cute:

nft add counter filter http
nft add rule filter input tcp dport 80 counter name \"http\"

Using named counters with maps, which is honestly giving main character energy:

nft add counter filter http
nft add counter filter https
nft add rule filter input counter name tcp dport map { 80 : \"http\", 443 : \"https\" }

QUOTA ๐Ÿ“

add quota [family] table name { [over|until] bytes BYTE_UNIT [ used bytes BYTE_UNIT ] ; [ comment comment ; ] }
BYTE_UNIT := bytes | kbytes | mbytes
delete quota [family] table name
list quotas

โœจ Table 16. Quota specifications โœจ

  • quota โ€” quota limit, used as the quota name. type: two arguments, unsigned integer (64 bit) and string: bytes, kbytes, mbytes. "over" and "until" go before these arguments
  • used โ€” initial value of used quota. type: two arguments, unsigned integer (64 bit) and string: bytes, kbytes, mbytes
  • comment โ€” per quota comment field. type: string

Using named quotas:

nft add quota filter user123 { over 20 mbytes }
nft add rule filter input ip saddr 192.168.10.123 quota name \"user123\"

Using named quotas with maps, go off queen:

nft add quota filter user123 { over 20 mbytes }
nft add quota filter user124 { over 20 mbytes }
nft add rule filter input quota name ip saddr map { 192.168.10.123 : \"user123\", 192.168.10.124 : \"user124\" }

๐ŸŒˆ EXPRESSIONS ๐ŸŒˆ

ok so expressions are values, periodt. either constants like network addresses, port numbers, etc., or data actually scooped out of the packet while the ruleset is being evaluated. you can smash expressions together using binary, logical, relational and other kinds of expressions to build big complex or relational (match) expressions. they also show up as arguments to certain types of operations, like NAT, packet marking, etc. ๐Ÿซถ

every single expression has a data type, and that type is what decides the size, parsing and representation of symbolic values, plus type compatibility with other expressions. no cap.

๐Ÿ” DESCRIBE COMMAND ๐Ÿ”

describe expression | data type

the describe command spills the tea about the type of an expression and its data type. you can also hand it a data type directly, and then nft will show you even more info about that type. iconic behaviour honestly.

The describe command:

$ nft describe tcp flags
payload expression, datatype tcp_flag (TCP flag) (basetype bitmask, integer), 8 bits

predefined symbolic constants:
fin                           0x01
syn                           0x02
rst                           0x04
psh                           0x08
ack                           0x10
urg                           0x20
ecn                           0x40
cwr                           0x80

๐Ÿฆ‹ DATA TYPES ๐Ÿฆ‹

data types are the whole personality-decider here: they determine the size, parsing and representation of symbolic values, and the type compatibility of expressions. a bunch of global data types exist, and on top of that some expression types define further data types that are specific to that expression type. most data types have a fixed size, but some may have a dynamic size, f.i. the string type. and some types also have predefined symbolic constants โ€” you can list those with the nft describe command:

$ nft describe ct_state
datatype ct_state (conntrack state) (basetype bitmask, integer), 32 bits

pre-defined symbolic constants (in hexadecimal):
invalid                         0x00000001
new ...

types may be derived from lower order types, f.i. the IPv4 address type is derived from the integer type, which means an IPv4 address can also be specified as an integer value. lowkey elegant.

and in certain contexts (set and map definitions), you have to explicitly specify a data type. every type has a name, and that name is what you use for this. ๐Ÿ’…

๐Ÿ’œ INTEGER TYPE ๐Ÿ’œ

  • Name: Integer
  • Keyword: integer
  • Size: variable
  • Base type: -

no, we are not done yet bestie, there's so much more coming โ€” keep scrolling ๐ŸŒธ

The integer type is for numeric values, bestie. You can write them as decimal, hexadecimal or octal numbers, dealer's choice ๐Ÿ’…. Here's the gag though: the integer type does not have a fixed size โ€” its size is decided by the expression it's used in. It's giving โœจadaptableโœจ.


BITMASK TYPE ๐ŸŽ€

  • Name: Bitmask
  • Keyword: bitmask
  • Size: variable
  • Base type: integer

The bitmask type (bitmask) is used for bitmasks, obviously, no cap.

In expressions, the bits of a bitmask may be specified as bit[,bit]..., where each bit is the value of the bit or a pre-defined symbolic constant, if there is one (for example, ct state's bit 0x1 has the symbolic constant new).

Equality of a value with such a bitmask is given if the value has any of the bitmask's bits set (and optionally others too, she's chill like that).

The syntax expression value / mask is identical to expression and mask == value. So for example, tcp flags syn,ack / syn,ack,fin,rst is the same as tcp flags and (syn|ack|fin|rst) == syn|ack. Periodt.

Careful though, bestie: the expression bit[,bit]... is not generally the same as the expression {bit[,bit]...}, and it's the same deal with a named set. Those latter forms are lookups in a set, and they'll only match if the set contains exactly one value that matches. They are effectively the same (and matching bitmasks are typically faster ๐Ÿƒโ™€๏ธ) when all the bits are semantically mutually exclusive.

Examples, because examples ate and left no crumbs:

  • tcp flags syn,ack matches packets that have the SYN flag, the ACK flag, or both SYN and ACK set. Other flags are ignored. Meanwhile tcp flags { syn, ack } matches packets that have either only the SYN or only the ACK flag set. All other flags must be unset. Different vibes, different checks ๐Ÿ’€
  • ct state established,related and ct state { established, related } match exactly the same packets, because the bits of ct state are all mutually exclusive. So here they're twins fr fr ๐Ÿซถ

As usual, the nft describe command may be used to get details on a data type, which for bitmasks shows the symbolic names and values of the bits. Go check it, it's lowkey iconic.


STRING TYPE ๐ŸŒธ

  • Name: String
  • Keyword: string
  • Size: variable
  • Base type: -

The string type is used for character strings. A string begins with an alphabetic character (a-zA-Z) followed by zero or more alphanumeric characters or the characters /, -, _ and .. And in addition, anything enclosed in double quotes (") is recognized as a string. Yes, even the weird ones โœจ

String specification:

# Interface name
filter input iifname eth0

# Weird interface name
filter input iifname "(eth0)"

  • Name: Link layer address
  • Keyword: lladdr
  • Size: variable
  • Base type: integer

The link layer address type is used for link layer addresses. These are specified as a variable amount of groups of two hexadecimal digits separated using colons (:). So many colons, so much drama ๐Ÿ’…

Link layer address specification:

# Ethernet destination MAC address
filter input ether daddr 20:c9:d0:43:12:d9

IPV4 ADDRESS TYPE ๐Ÿ‘‘

  • Name: IPV4 address
  • Keyword: ipv4_addr
  • Size: 32 bit
  • Base type: integer

The IPv4 address type is used for IPv4 addresses. Addresses are specified in either dotted decimal, dotted hexadecimal, dotted octal, decimal, hexadecimal, octal notation or as a host name. A host name will be resolved using the standard system resolver. So many notations, she contains multitudes ๐ŸŒˆ

IPv4 address specification:

# dotted decimal notation
filter output ip daddr 127.0.0.1

# host name
filter output ip daddr localhost

IPV6 ADDRESS TYPE ๐Ÿ’œ

  • Name: IPv6 address
  • Keyword: ipv6_addr
  • Size: 128 bit
  • Base type: integer

The IPv6 address type is used for IPv6 addresses. Addresses are specified as a host name or as hexadecimal halfwords separated by colons. Addresses might be enclosed in square brackets ("[]") to differentiate them from port numbers. Not the brackets doing the heavy lifting ๐Ÿ™ˆ

IPv6 address specification:

# abbreviated loopback address
filter output ip6 daddr ::1

IPv6 address specification with bracket notation:

# without [] the port number (22) would be parsed as part of the
# ipv6 address
ip6 nat prerouting tcp dport 2222 dnat to [1ce::d0]:22

BOOLEAN TYPE โญ

  • Name: Boolean
  • Keyword: boolean
  • Size: 1 bit
  • Base type: integer

The boolean type is a syntactical helper type in userspace. Its whole job is living on the right-hand side of a (typically implicit) relational expression so it can turn the expression on the left-hand side into a boolean check (usually for existence). It's the supportive bestie of the type world ๐Ÿซถ

Table 17. The following keywords will automatically resolve into a boolean type with given value:

  • exists โ€” value 1
  • missing โ€” value 0

Table 18. Expressions that support a boolean comparison:

  • fib โ€” Check route existence.
  • exthdr โ€” Check IPv6 extension header existence.
  • tcp option โ€” Check TCP option header existence.

Boolean specification:

# match if route exists
filter input fib daddr . iif check exists

# match only non-fragmented packets in IPv6 traffic
filter input exthdr frag missing

# match if TCP timestamp option is present
filter input tcp option timestamp exists

ICMP TYPE TYPE ๐ŸŽฏ

  • Name: ICMP Type
  • Keyword: icmp_type
  • Size: 8 bit
  • Base type: integer

The ICMP Type type is used to conveniently specify the ICMP header's type field. Convenient! Iconic! Exactly what we needed ๐ŸŽ€

Table 19 โ€” the ICMP type keyword cheat sheet ๐Ÿ’–

bestie these are the keywords you can drop in when you're specifying the ICMP type:

  • echo-reply โ†’ 0
  • destination-unreachable โ†’ 3
  • source-quench โ†’ 4
  • redirect โ†’ 5
  • echo-request โ†’ 8
  • router-advertisement โ†’ 9
  • router-solicitation โ†’ 10
  • time-exceeded โ†’ 11
  • parameter-problem โ†’ 12
  • timestamp-request โ†’ 13
  • timestamp-reply โ†’ 14
  • info-request โ†’ 15
  • info-reply โ†’ 16
  • address-mask-request โ†’ 17
  • address-mask-reply โ†’ 18

ICMP Type specification.

# match ping packets
filter output icmp type { echo-request, echo-reply }

ICMP CODE TYPE ๐ŸŒธ

  • Name: ICMP Code
  • Keyword: icmp_code
  • Size: 8 bit
  • Base type: integer

The ICMP Code type is the โœจconvenientโœจ way to specify the ICMP header's code field. no cap, that's literally its whole job.


ICMPV6 TYPE TYPE ๐Ÿฆ‹

  • Name: ICMPv6 Type
  • Keyword: icmpv6_type
  • Size: 8 bit
  • Base type: integer

The ICMPv6 Type type is here to conveniently specify the ICMPv6 header's type field. v6 girlies, this one's for you ๐Ÿ’…

Table 20 โ€” keywords you can use when specifying the ICMPv6 type

  • destination-unreachable โ†’ 1
  • packet-too-big โ†’ 2
  • time-exceeded โ†’ 3
  • parameter-problem โ†’ 4
  • echo-request โ†’ 128
  • echo-reply โ†’ 129
  • mld-listener-query โ†’ 130
  • mld-listener-report โ†’ 131
  • mld-listener-done โ†’ 132
  • mld-listener-reduction โ†’ 132
  • nd-router-solicit โ†’ 133
  • nd-router-advert โ†’ 134
  • nd-neighbor-solicit โ†’ 135
  • nd-neighbor-advert โ†’ 136
  • nd-redirect โ†’ 137
  • router-renumbering โ†’ 138
  • ind-neighbor-solicit โ†’ 141
  • ind-neighbor-advert โ†’ 142
  • mld2-listener-report โ†’ 143
๐Ÿ‘€ ok so notice mld-listener-done and mld-listener-reduction both map to 132 โ€” that's not a typo on my end, that's exactly what the table says. fr fr.

ICMPv6 Type specification.

# match ICMPv6 ping packets
filter output icmpv6 type { echo-request, echo-reply }

ICMPV6 CODE TYPE ๐ŸŽ€

  • Name: ICMPv6 Code
  • Keyword: icmpv6_code
  • Size: 8 bit
  • Base type: integer

The ICMPv6 Code type is used to conveniently specify the ICMPv6 header's code field. yes queen, same energy as the v4 one, just with a lil v6 glow-up โœจ

๐Ÿงต CONNTRACK TYPES ๐Ÿงต

okay bestie, this is the part where conntrack gets its own little wardrobe of types ๐Ÿ’… and honestly? it's giving organized. Every ct expression and statement leans on these, so let's go.

๐Ÿงพ Table 21. overview of types used in ct expression and statement

  • Name: conntrack state โ€” Keyword: ct_state โ€” Size: 4 byte โ€” Base type: bitmask โœจ
  • Name: conntrack direction โ€” Keyword: ct_dir โ€” Size: 8 bit โ€” Base type: integer ๐Ÿงญ
  • Name: conntrack status โ€” Keyword: ct_status โ€” Size: 4 byte โ€” Base type: bitmask ๐ŸŽ€
  • Name: conntrack event bits โ€” Keyword: ct_event โ€” Size: 4 byte โ€” Base type: bitmask ๐Ÿฆ‹
  • Name: conntrack label โ€” Keyword: ct_label โ€” Size: 128 bit โ€” Base type: bitmask ๐Ÿท๏ธ

For each of the types above, keywords are available for convenience:


๐Ÿงพ Table 22. conntrack state (ct_state)

  • invalid โ€” value 1 ๐Ÿ’€
  • established โ€” value 2 ๐Ÿค
  • related โ€” value 4 ๐ŸŒธ
  • new โ€” value 8 โœจ
  • untracked โ€” value 64 ๐Ÿ™ˆ

๐Ÿงพ Table 23. conntrack direction (ct_dir)

  • original โ€” value 0 ๐Ÿซถ
  • reply โ€” value 1 ๐Ÿ”

๐Ÿงพ Table 24. conntrack status (ct_status)

  • expected โ€” value 1 โ€” Expected connection; conntrack helper set it up ๐Ÿผ
  • seen-reply โ€” value 2 โ€” Conntrack has seen packets in both directions ๐Ÿ‘€
  • assured โ€” value 4 โ€” Conntrack entry will not be removed if hash table is full ๐Ÿ”’
  • confirmed โ€” value 8 โ€” Initial packet processed โœ…
  • snat โ€” value 16 โ€” Original source address differs from reply destination ๐ŸŽญ
  • dnat โ€” value 32 โ€” Original destination differs from reply source ๐ŸŽฏ
  • seq-adjust โ€” value 64 โ€” tcp sequence number rewrite due to conntrack helper or synproxy ๐Ÿงฎ
  • snat-done โ€” value 128 โ€” tried to find matching snat/masquerade rule ๐Ÿ•ต๏ธ
  • dnat-done โ€” value 256 โ€” tried to find matching dnat/redirect rule ๐Ÿ”
  • dying โ€” value 512 โ€” Connection about to be deleted โšฐ๏ธ
  • fixed-timeout โ€” value 1024 โ€” entry expires even if traffic is active โฐ
  • helper โ€” value 8192 โ€” connection is monitored by conntrack helper ๐Ÿงš
  • offload โ€” value 16384 โ€” connection is offloaded to a flow table ๐Ÿ›
  • hw-offload โ€” value 32768 โ€” connection is offloaded to hardware ๐Ÿ–ฅ๏ธ

๐Ÿงพ Table 25. conntrack event bits (ct_event)

  • new โ€” value 1 โœจ
  • related โ€” value 2 ๐Ÿ”—
  • destroy โ€” value 4 ๐Ÿ’ฅ
  • reply โ€” value 8 โ†ฉ๏ธ
  • assured โ€” value 16 ๐Ÿ›ก๏ธ
  • protoinfo โ€” value 32 ๐Ÿ“‹
  • helper โ€” value 64 ๐Ÿงš
  • mark โ€” value 128 ๐Ÿ–Š๏ธ
  • seqadj โ€” value 256 ๐Ÿ”ข
  • secmark โ€” value 512 ๐Ÿท๏ธ
  • label โ€” value 1024 ๐Ÿ’–

heads up bestie: possible keywords for conntrack label type (ct_label) are read at runtime from /etc/connlabel.conf. no cap, that file is the whole vibe check for your labels. ๐ŸŒˆ

๐ŸŽ€ DCCP PKTTYPE TYPE ๐ŸŽ€

ok so here's the vibe for this one, bestie โ€” quick lil field breakdown first:

  • Name โ€” DCCP packet type
  • Keyword โ€” dccp_pkttype
  • Size โ€” 4 bit
  • Base type โ€” integer

The DCCP packet type is basically the โœจabstractโœจ of all the legal values that four-bit field in the DCCP header can hold, exactly as RFC4340 says. And here's the tea: values 10-15 are considered reserved, so you are not allowed to use them. In iptables' dccp match, those values get aliased to INVALID. With nftables tho? You just match on the numeric value range, i.e. 10-15. easy. ๐Ÿ’…

Table 26. keywords may be used when specifying the DCCP packet type
  • request โ†’ 0
  • response โ†’ 1
  • data โ†’ 2
  • ack โ†’ 3
  • dataack โ†’ 4
  • closereq โ†’ 5
  • close โ†’ 6
  • reset โ†’ 7
  • sync โ†’ 8
  • syncack โ†’ 9

๐ŸŒˆ PRIMARY EXPRESSIONS ๐ŸŒˆ

The lowest order expression is a primary expression, and it represents either a constant or a single datum pulled from a packet's payload, meta data, or a stateful module. no cap, that's the whole definition. ๐Ÿซถ

โœจ META EXPRESSIONS โœจ

meta {length | nfproto | l4proto | protocol | priority}
[meta] {mark | iif | iifname | iiftype | oif | oifname | oiftype | skuid | skgid | nftrace | rtclassid | ibrname | obrname | pkttype | cpu | iifgroup | oifgroup | cgroup | random | ipsec | iifkind | oifkind | time | hour | day }

A meta expression refers to meta data associated with a packet. ๐Ÿ’ซ

So there are two types of meta expressions: unqualified and qualified. Qualified meta expressions need the meta keyword sitting in front of the meta key. Unqualified meta expressions can be specified by using the meta key directly, or as qualified meta expressions. Either way works, she's flexible like that. ๐Ÿ˜Œ

Meta l4proto is lowkey iconic for matching a particular transport protocol that's part of either an IPv4 or an IPv6 packet. It will also skip any IPv6 extension headers present in an IPv6 packet. periodt.

meta iif, oif, iifname and oifname are used to match the interface a packet arrived on, or is about to be sent out on. ๐ŸŒธ

iif and oif match on the interface index, whereas iifname and oifname match on the interface name. And bestie, that is not the same thing โ€” assuming the rule:

filter input meta iif "foo"

Then this rule can only be added if the interface "foo" exists. Also, the rule will continue to match even if the interface "foo" is renamed to "bar". ๐Ÿคฏ

Why tho? Because internally the interface index is what's being used. So in the case of dynamically created interfaces โ€” think tun/tap or dialup interfaces (ppp for example) โ€” it might be better to use iifname or oifname instead. ๐Ÿฆ‹

In those cases, the name is used, so the interface doesn't have to exist to add such a rule. It will stop matching if the interface gets renamed, and it will match again in case the interface gets deleted and later a new interface with the same name is created. main character energy fr fr. ๐Ÿ‘‘

Like with iptables, wildcard matching on interface name prefixes is available for iifname and oifname matches by appending an asterisk (*) character. Note however that unlike iptables, nftables does not accept interface names consisting of the wildcard character only โ€” users are supposed to just skip those always-matching expressions. ๐Ÿ’€ In order to match on a literal asterisk character, one may escape it using backslash (\).

๐ŸŒธ Table 27. Meta expression types ๐ŸŒธ

okay bestie, this is the meta table โ€” all the packet-level deets you can vibe-check directly, no header parsing required. every one of these is a keyword you can just say in your ruleset ๐Ÿ’…

  • length โ€” how big the packet is, in bytes. it's giving integer (32-bit) โœจ
  • nfproto โ€” the real hook protocol family. useful only in the inet table, so don't come for it anywhere else. type: integer (32 bit)
  • l4proto โ€” the layer 4 protocol. heads up: it skips ipv6 extension headers. type: integer (8 bit)
  • protocol โ€” the EtherType protocol value. type: ether_type
  • priority โ€” TC packet priority, iconic honestly. type: tc_handle
  • mark โ€” the packet mark. type: mark
  • iif โ€” input interface index. type: iface_index
  • iifname โ€” input interface name (the one you actually remember ๐Ÿซถ). type: ifname
  • iiftype โ€” input interface type. type: iface_type
  • oif โ€” output interface index. type: iface_index
  • oifname โ€” output interface name. type: ifname
  • oiftype โ€” output interface hardware type. type: iface_type
  • sdif โ€” slave device input interface index. type: iface_index
  • sdifname โ€” slave device interface name. type: ifname
  • skuid โ€” the UID tied to the originating socket. type: uid
  • skgid โ€” the GID tied to the originating socket. type: gid
  • rtclassid โ€” routing realm. type: realm
  • ibrname โ€” input bridge interface name. type: ifname
  • obrname โ€” output bridge interface name. type: ifname
  • pkttype โ€” packet type. type: pkt_type
  • cpu โ€” the cpu number that's processing the packet. type: integer (32 bit)
  • iifgroup โ€” incoming device group. type: devgroup
  • oifgroup โ€” outgoing device group. type: devgroup
  • cgroup โ€” control group net_cls.classid (and for matching on cgroupv2, see socket cgroupv2). type: integer (32 bit)
  • random โ€” a pseudo-random number, main character energy fr fr ๐ŸŽฒ. type: integer (32 bit)
  • ipsec โ€” true if the packet was ipsec encrypted. type: boolean (1 bit)
  • iifkind โ€” input interface kind. (no type listed, she's mysterious)
  • oifkind โ€” output interface kind. (also type-less, we respect it)
  • time โ€” absolute time ofโ€ฆ type: Integer (32 bit) or
  • packet reception โ€” type: string
  • day โ€” Day of week. Type: Integer (8 bit) or string
  • hour โ€” Hour of day. Type: String value in the form HH:MM or HH:MM:SS. Values are expected to be less than 24:00, although for technical reasons, 23:59:60 is accepted, too.

Table 28. Meta expression specific types โœจ

  • iface_index โ€” Interface index (32 bit number). Can be specified numerically or as name of an existing interface. either way bestie, it works ๐Ÿซถ
  • ifname โ€” Interface name (16 byte string). Does not have to exist. no cap, it can just be a vibe
  • iface_type โ€” Interface type (16 bit number).
  • uid โ€” User ID (32 bit number). Can be specified numerically or as user name.
  • gid โ€” Group ID (32 bit number). Can be specified numerically or as group name.
  • realm โ€” Routing Realm (32 bit number). Can be specified numerically or as symbolic name defined in /etc/iproute2/rt_realms.
  • devgroup_type โ€” Device group (32 bit number). Can be specified numerically or as symbolic name defined in /etc/iproute2/group.
  • pkt_type โ€” Packet type: host (addressed to local host), broadcast (to all), multicast (to group), other (addressed to another host).
  • ifkind โ€” Interface kind (16 byte string). See TYPES in ip-link(8) for a list.
  • time โ€” Either an integer or a date in ISO format. For example: "2019-06-06 17:00". Hour and seconds are optional and can be omitted if desired. If omitted, midnight will be assumed. The following three would be equivalent: "2019-06-06", "2019-06-06 00:00" and "2019-06-06 00:00:00". Use a range expression such as "2019-06-06 10:00"-"2019-06-10 14:00" for matching a time range. When an integer is given, it is assumed to be a UNIX timestamp.
  • day โ€” Either a day of week ("Monday", "Tuesday", etc.), or an integer between 0 and 6. Strings are matched case-insensitively, and a full match is not expected (e.g. "Mon" would match "Monday"). When an integer is given, 0 is Sunday and 6 is Saturday. Use a range expression such as "Monday"-"Wednesday" for matching a week day range.
  • hour โ€” A string representing an hour in 24-hour format. Seconds can optionally be specified. For example, 17:00 and 17:00:00 would be equivalent. Use a range expression such as "17:00"-"19:00" for matching a time range.

Using meta expressions. ๐Ÿ‘‘

# qualified meta expression
filter output meta oif eth0
filter forward meta iifkind { "tun", "veth" }

# unqualified meta expression
filter output oif eth0

# incoming packet was subject to ipsec processing
raw prerouting meta ipsec exists accept

# match incoming packet from 03:00 to 14:00 local time
raw prerouting meta hour "03:00"-"14:00" counter accept

๐Ÿ’… SOCKET EXPRESSION ๐Ÿ’…

socket {transparent | mark | wildcard}
socket cgroupv2 level NUM

The socket expression is literally the detective of expressions bestie ๐Ÿ” โ€” you use it to search for an existing open TCP/UDP socket and its attributes that can be associated with a packet. It looks for an established or non-zero bound listening socket (possibly with a non-local address). ๐Ÿฆ‹

You can also use it to match on the socket cgroupv2 at a given ancestor level, e.g. if the socket belongs to cgroupv2 a/b, ancestor level 1 checks for a matching on cgroup a and ancestor level 2 checks for a matching on cgroup b. it's giving nested-groups realness fr fr ๐ŸŽ€

Table 29. Available socket attributes ๐Ÿ’– here's the full lineup, no gatekeeping:

  • transparent โ€” value of the IP_TRANSPARENT socket option in the found socket. It can be 0 or 1. Type: boolean (1 bit).
  • mark โ€” value of the socket mark (SOL_SOCKET, SO_MARK). Type: mark.
  • wildcard โ€” tells you whether the socket is wildcard-bound (like 0.0.0.0 or ::0). Type: boolean (1 bit).
  • cgroupv2 โ€” cgroup version 2 for this socket (path from /sys/fs/cgroup). Type: cgroupv2.

Using socket expression. ๐Ÿฆ‹

okay bestie, watch how it's done:

# Mark packets that correspond to a transparent socket. "socket wildcard 0"
# means that zero-bound listener sockets are NOT matched (which is usually
# exactly what you want).
table inet x {
    chain y {
        type filter hook prerouting priority mangle; policy accept;
        socket transparent 1 socket wildcard 0 mark set 0x00000001 accept
    }
}

# Trace packets that corresponds to a socket with a mark value of 15
table inet x {
    chain y {
        type filter hook prerouting priority mangle; policy accept;
        socket mark 0x0000000f nftrace set 1
    }
}

# Set packet mark to socket mark
table inet x {
    chain y {
        type filter hook prerouting priority mangle; policy accept;
        tcp dport 8080 mark set socket mark
    }
}

# Count packets for cgroupv2 "user.slice" at level 1
table inet x {
    chain y {
        type filter hook input priority filter; policy accept;
        socket cgroupv2 level 1 "user.slice" counter
    }
}

OSF EXPRESSION ๐Ÿ’€

osf [ttl {loose | skip}] {name | version}

So the osf expression does passive operating system fingerprinting โ€” like, it's literally profiling the packets, main character detective energy ๐Ÿ”. It compares some data (Window Size, MSS, options and their order, DF, and others) from packets with the SYN bit set.

Table 30. Available osf attributes โœจ

  • ttl โ€” do TTL checks on the packet to determine the operating system. Type: string.
  • version โ€” do OS version checks on the packet.
  • name โ€” name of the OS signature to match. All signatures can be found at pf.os file. Use "unknown" for OS signatures that the expression could not detect. Type: string.

Available ttl values. ๐ŸŽ€

If no TTL attribute is passed, make a true IP header and fingerprint TTL true comparison. This generally works for LANs.

  • loose: check if the IP header's TTL is less than the fingerprint one. Works for globally-routable addresses.
  • skip: do not compare the TTL at all.

Using osf expression. ๐Ÿงš

# Accept packets that match the "Linux" OS genre signature without comparing TTL.
table inet x {
    chain y {
        type filter hook input priority filter; policy accept;
        osf ttl skip name "Linux"
    }
}

FIB EXPRESSIONS ๐Ÿงญ

fib FIB_TUPLE FIB_RESULT
FIB_TUPLE := { saddr | daddr} [ . { iif | oif } . mark ]
FIB_RESULT  := { oif | oifname | check | type }

A fib expression queries the fib (forwarding information base) to obtain information such as the output interface index. fr fr it's asking the routing table for the tea โ˜•.

The first arguments to the fib expression are the input keys to be passed to the fib lookup function. One of saddr or daddr is mandatory, they are also mutually exclusive (pick one, bestie, not both ๐Ÿ™ˆ).

mark, iif and oif keywords are optional modifiers to influence the search result, see the FIB_TUPLE keyword table below for a description. The iif and oif tuple keywords are also mutually exclusive.

The last argument to the fib expression is the desired result type.

oif asks to obtain the interface index that would be used to send packets to the packets source (saddr key) or destination (daddr key). If no routing entry is found, the returned interface index is 0.

oifname is like oif, but it fills the interface name instead. This is useful to check dynamic interfaces such as ppp devices. If no entry is found, an empty interface name is returned.

type returns the address type such as unicast or multicast. A complete list of supported address types can be shown with nft describe fib_addrtype.

Table 31. FIB_TUPLE keywords ๐Ÿ”ฅ

  • daddr โ€” perform a normal route lookup: search fib for route to the destination address of the packet.
  • saddr โ€” perform a reverse route lookup: search the fib for route to the source address of the packet.
  • mark โ€” consider the packet mark (nfmark) when querying the fib.
  • iif โ€” if fib lookups provides a route then check its output interface is identical to the packets input interface.
  • oif โ€” if fib lookups provides a route then check its output interface is identical to the packets output interface. This flag can only be used with the type result.

๐Ÿ“Š Table 32. FIB_RESULT keywords

So when a fib lookup comes back at you, these are the lil keywords you can pull out of the result, plus the type each one hands you ๐Ÿ’…

  • oif โ€” Output interface index, and the result type is iface_index
  • check โ€” Output interface check, result type boolean
  • oifname โ€” Output interface name, result type ifname
  • type โ€” Address type, result type fib_addrtype (peep nft describe fib_addrtype if you want the whole list, bestie)

Okay important vibe check: the oif and oifname result is only valid in the prerouting, input and forward hooks. The type on the other hand can be queried from any one of prerouting, input, forward, output and postrouting ๐ŸŒˆ

And for type specifically, the presence of the iif keyword in the FIB_TUPLE modifiers restricts the available hooks to those where the packet is associated with an incoming interface, i.e. prerouting, input and forward. Likewise, the oif keyword in the FIB_TUPLE modifier list will limit the available hooks to forward, output and postrouting. No cap ๐Ÿซถ

Using fib expressions.

# drop packets without a reverse path
filter prerouting fib saddr . iif oif missing drop

In this example, 'saddr . iif' looks up a route to the source address of the packet and restricts matching results to the interface that the packet arrived on, then stores the output interface index from the obtained fib route result. It's giving reverse-path-realness โœจ

If no route was found for the source address/input interface combination, the output interface index is zero. Hence, this rule will drop all packets that do not have a strict reverse path (hypothetical reply packet would be sent via the interface the tested packet arrived on). Iconic behaviour honestly.

If only 'saddr oif' is used as the input key, then this rule would only drop packets where the fib cannot find a route. In most setups this will never drop packets because the default route is returned. So likeโ€ฆ know which one you meant ๐Ÿ’€

# drop packets if the destination ip address is not configured on the incoming interface
filter prerouting fib daddr . iif type != { local, broadcast, multicast } drop

This queries the fib based on the current packets' destination address and the incoming interface.

If the packet is sent to a unicast address that is configured on a different interface, then the packet will be dropped as such an address would be classified as 'unicast' type. Without the 'iif' modifier, any address configured on the local machine is 'local', and unicast addresses not configured on any interface would return the type 'unicast'.

# perform lookup in a specific 'blackhole' table (0xdead, needs ip appropriate ip rule)
filter prerouting meta mark set 0xdead fib daddr . mark type vmap { blackhole : drop, prohibit : jump prohibited, unreachable : drop }

๐ŸŒ ROUTING EXPRESSIONS

rt [ip | ip6] {classid | nexthop | mtu | ipsec}

A routing expression refers to routing data associated with a packet. She's routing-coded ๐Ÿฆ‹

๐Ÿ“Š Table 33. Routing expression types

  • classid โ€” Routing realm โ€” type: realm
  • nexthop โ€” Routing nexthop โ€” type: ipv4_addr/ipv6_addr
  • mtu โ€” TCP maximum segment size of route โ€” type: integer (16 bit)
  • ipsec โ€” route via ipsec tunnel or transport โ€” type: boolean

๐Ÿ“Š Table 34. Routing expression specific types

  • realm โ€” Routing Realm (32 bit number). Can be specified numerically or as symbolic name defined in /etc/iproute2/rt_realms.

Using routing expressions.

# IP family independent rt expression
filter output rt classid 10

# IP family dependent rt expressions
ip filter output rt nexthop 192.168.0.1
ip6 filter output rt nexthop fd00::1
inet filter output rt ip nexthop 192.168.0.1
inet filter output rt ip6 nexthop fd00::1

# outgoing packet will be encapsulated/encrypted by ipsec
filter output rt ipsec exists

๐Ÿ” IPSEC EXPRESSIONS

ipsec {in | out} [ spnum NUM ]  {reqid | spi}
ipsec {in | out} [ spnum NUM ]  {ip | ip6} {saddr | daddr}

An ipsec expression refers to ipsec data associated with a packet. Very tunnel-vision of it ๐Ÿ’œ

The in or out keyword needs to be used to specify if the expression should examine inbound or outbound policies. The in keyword can be used in the prerouting, input and forward hooks. The out keyword applies to forward, output and postrouting hooks. The optional keyword spnum can be used to match a specific state in a chain, it defaults to 0.

๐Ÿ“Š Table 35. Ipsec expression types

  • reqid โ€” Request ID โ€” type: integer (32 bit)
  • spi โ€” Security Parameter Index โ€” type: integer (32 bit)
  • saddr โ€” Source address of the tunnel โ€” type: ipv4_addr/ipv6_addr
  • daddr โ€” Destination address of the tunnel โ€” type: ipv4_addr/ipv6_addr
Note: When using xfrm_interface, this expression is not useable in output hook as the plain packet does not traverse it with IPsec info attached โ€” use a chain in postrouting hook instead. Write that down bestie, it WILL bite you ๐Ÿ˜ญ

๐Ÿ”ข NUMGEN EXPRESSION

numgen {inc | random} mod NUM [ offset NUM ]

Create a number generator. The inc or random keywords control its operation mode: In inc mode, the last returned value is simply incremented. In random mode, a new random number is returned. The value after the mod keyword specifies an upper boundary (read: modulus) which is not reached by returned numbers. The optional offset allows one to increment the returned value by a fixed offset. Slay, math โœจ

A typical use-case for numgen is load-balancing:

Using numgen expression.

# round-robin between 192.168.10.100 and 192.168.20.200:
add rule nat prerouting dnat to numgen inc mod 2 map \
        { 0 : 192.168.10.100, 1 : 192.168.20.200 }

# probability-based with odd bias using intervals:
add rule nat prerouting dnat to numgen random mod 10 map \
        { 0-2 : 192.168.10.100, 3-9 : 192.168.20.200 }

#๏ธโƒฃ HASH EXPRESSIONS

jhash {ip saddr | ip6 daddr | tcp dport | udp sport | ether saddr} [. ...] mod NUM [ seed NUM ] [ offset NUM ]
symhash mod NUM [ offset NUM ]

Use a hashing function to spit out a number โ€” that's the whole vibe here. The functions you can pick from are jhash (aka Jenkins Hash, iconic) and symhash (Symmetric Hash, equally iconic, different energy). jhash needs an expression from you so it knows which packet header parameters to hash over โ€” and yes bestie, you can concatenate them too, go off. The value you put after the mod keyword sets an upper boundary (read it as: the modulus), and here's the thing โ€” the numbers coming back will never actually reach that boundary. The optional seed is your init value, the little starter crumb the hashing function chews on first. The optional offset lets you bump the returned value up by a fixed offset, like adding a lil' base to everything.

A typical use-case for jhash and symhash is load-balancing, and honestly? It's serving.

Using hash expressions. ๐ŸŽ€

# load balance based on source ip between 2 ip addresses:
add rule nat prerouting dnat to jhash ip saddr mod 2 map \
        { 0 : 192.168.10.100, 1 : 192.168.20.200 }

# symmetric load balancing between 2 ip addresses:
add rule nat prerouting dnat to symhash mod 2 map \
        { 0 : 192.168.10.100, 1 : 192.168.20.200 }

โœจ PAYLOAD EXPRESSIONS โœจ

Payload expressions are the ones that reach right into the packet's payload and pull data out. Digging in the bag, basically. ๐Ÿ‘œ

๐ŸŒธ ETHERNET HEADER EXPRESSION ๐ŸŒธ

ether {daddr | saddr | type}

Table 36. Ethernet header expression types โ€” lowkey a whole menu ๐Ÿ’…

  • daddr โ€” Destination MAC address โ€” ether_addr
  • saddr โ€” Source MAC address โ€” ether_addr
  • type โ€” EtherType โ€” ether_type

๐Ÿฆ‹ VLAN HEADER EXPRESSION ๐Ÿฆ‹

vlan {id | dei | pcp | type}

The vlan expression is your girl for matching on the vlan header fields. BUT WAIT โ€” important tea: this expression will not work in the ip, ip6 and inet families, unless the vlan interface is configured with the reorder_hdr off setting. The default is reorder_hdr on, which will automatically rip the vlan tag off the packet before you ever see it. See ip-link(8) for more info, bestie. For those families it's genuinely easier to match the vlan interface name instead, using the meta iif or meta iifname expression. Periodt.

Table 37. VLAN header expression โ€” the whole roster ๐ŸŒˆ

  • id โ€” VLAN ID (VID) โ€” integer (12 bit)
  • dei โ€” Drop Eligible Indicator โ€” integer (1 bit)
  • pcp โ€” Priority code point โ€” integer (3 bit)
  • type โ€” EtherType โ€” ether_type

๐Ÿ’œ ARP HEADER EXPRESSION ๐Ÿ’œ

arp {htype | ptype | hlen | plen | operation | saddr { ip | ether } | daddr { ip | ether }

Table 38. ARP header expression โ€” every field, no skips, we don't do that here ๐Ÿซถ

  • htype โ€” ARP hardware type โ€” integer (16 bit)
  • ptype โ€” EtherType โ€” ether_type
  • hlen โ€” Hardware address len โ€” integer (8 bit)
  • plen โ€” Protocol address len โ€” integer (8 bit)
  • operation โ€” Operation โ€” arp_op
  • saddr ether โ€” Ethernet sender address โ€” ether_addr
  • daddr ether โ€” Ethernet target address โ€” ether_addr
  • saddr ip โ€” IPv4 sender address โ€” ipv4_addr
  • daddr ip โ€” IPv4 target address โ€” ipv4_addr

โญ IPV4 HEADER EXPRESSION โญ

ip {version | hdrlength | dscp | ecn | length | id | frag-off | ttl | protocol | checksum | saddr | daddr }

Table 39. IPv4 header expression ๐Ÿ’– โ€” here's the whole lineup, bestie:

  • version โ€” IP header version (it's 4) โ€” type: integer (4 bit)
  • hdrlength โ€” IP header length, options included โ€” type: integer (4 bit) FIXME scaling
  • dscp โ€” Differentiated Services Code Point โ€” type: dscp
  • ecn โ€” Explicit Congestion Notification โ€” type: ecn
  • length โ€” Total packet length โ€” type: integer (16 bit)
  • id โ€” IP ID โ€” type: integer (16 bit)
  • frag-off โ€” Fragment offset โ€” type: integer (16 bit)
  • ttl โ€” Time to live โ€” type: integer (8 bit)
  • protocol โ€” Upper layer protocol โ€” type: inet_proto
  • checksum โ€” IP header checksum โ€” type: integer (16 bit)
  • saddr โ€” Source address โ€” type: ipv4_addr
  • daddr โ€” Destination address โ€” type: ipv4_addr

โš ๏ธ Be careful with matching on ip length: the Linux kernel might aggregate several packets into one big packet that is larger than MTU. And like, on top of that, if GRO/GSO maximum size is larger than 65535 (see man ip-link(8), specifically gro_ipv4_max_size and gso_ipv4_max_size), then ip length might be 0 for such jumbo packets. meta length lets you match on the packet length including the IP header size. ๐Ÿซถ

๐ŸŒธ ICMP HEADER EXPRESSION ๐ŸŒธ

icmp {type | code | checksum | id | sequence | gateway | mtu}

This one refers to ICMP header fields, no cap. But heads up: when you use it in the inet, bridge or netdev families, it will cause an implicit dependency on IPv4 to be created. And if you wanna match on unusual cases like ICMP over IPv6, you have to add an explicit meta protocol ip6 match to the rule. ๐Ÿ’…

Table 40. ICMP header expression

  • type โ€” ICMP type field โ€” type: icmp_type
  • code โ€” ICMP code field โ€” type: integer (8 bit)
  • checksum โ€” ICMP checksum field โ€” type: integer (16 bit)
  • id โ€” ID of echo request/response โ€” type: integer (16 bit)
  • sequence โ€” sequence number of echo request/response โ€” type: integer (16 bit)
  • gateway โ€” gateway of redirects โ€” type: integer (32 bit)
  • mtu โ€” MTU of path MTU discovery โ€” type: integer (16 bit)

๐Ÿฆ‹ IGMP HEADER EXPRESSION ๐Ÿฆ‹

igmp {type | mrt | checksum | group}

This expression refers to IGMP header fields. Same vibe as ICMP, bestie: when you use it in inet, bridge or netdev families, it will cause an implicit dependency on IPv4 to be created. To match on unusual cases like IGMP over IPv6, one has to add an explicit meta protocol ip6 match to the rule. โœจ

Table 41. IGMP header expression

  • type โ€” IGMP type field โ€” type: igmp_type
  • mrt โ€” IGMP maximum response time field โ€” type: integer (8 bit)
  • checksum โ€” IGMP checksum field โ€” type: integer (16 bit)
  • group โ€” Group address โ€” type: integer (32 bit)

๐ŸŒˆ IPV6 HEADER EXPRESSION ๐ŸŒˆ

ip6 {version | dscp | ecn | flowlabel | length | nexthdr | hoplimit | saddr | daddr}

This expression refers to the ipv6 header fields. Caution when using ip6 nexthdr: the value only refers to the next header, i.e. ip6 nexthdr tcp will only match if the ipv6 packet does not contain any extension headers. Packets that are fragmented or e.g. contain a routing extension headers will not be matched. ๐Ÿ˜ญ Please use meta l4proto if you wish to match the real transport header and ignore any additional extension headers instead. ๐Ÿ‘‘


Table 42. IPv6 header expression ๐ŸŒ

  • version โ€” the IP header version, which for v6 is obviously (6) โ€” type: integer (4 bit)
  • dscp โ€” Differentiated Services Code Point โ€” type: dscp
  • ecn โ€” Explicit Congestion Notification โ€” type: ecn
  • flowlabel โ€” Flow label โ€” type: integer (20 bit)
  • length โ€” Payload length โ€” type: integer (16 bit)
  • nexthdr โ€” Nexthdr protocol โ€” type: inet_proto
  • hoplimit โ€” Hop limit โ€” type: integer (8 bit)
  • saddr โ€” Source address โ€” type: ipv6_addr
  • daddr โ€” Destination address โ€” type: ipv6_addr

careful with matching on ip6 length bestie, this one bites people!! The Linux kernel might aggregate several packets into one big packet that is larger than MTU. Moreover, if GRO/GSO maximum size is larger than 65535 (see man ip-link(8), specifically gro_max_size and gso_max_size), then ip6 length might be 0 for such jumbo packets. meta length allows you to match on the packet length including the IPv6 header size. so if your length match is acting weird, that's why, it's giving jumbo packet energy ๐Ÿ˜ญ

Using ip6 header expressions, like so:

# matching if first extension header indicates a fragment
ip6 nexthdr ipv6-frag

ICMPV6 HEADER EXPRESSION ๐Ÿ’œ

icmpv6 {type | code | checksum | parameter-problem | packet-too-big | id | sequence | max-delay | taddr | daddr}

This expression refers to ICMPv6 header fields. heads up: when you use it in inet, bridge or netdev families, it will cause an implicit dependency on IPv6 to be created. So to match on unusual cases like ICMPv6 over IPv4, one has to add an explicit meta protocol ip match to the rule. no cap, don't skip that step or your rule will be confused about its whole identity ๐Ÿ’…

Table 43. ICMPv6 header expression ๐Ÿฆ‹

  • type โ€” ICMPv6 type field โ€” type: icmpv6_type
  • code โ€” ICMPv6 code field โ€” type: integer (8 bit)
  • checksum โ€” ICMPv6 checksum field โ€” type: integer (16 bit)
  • parameter-problem โ€” pointer to problem โ€” type: integer (32 bit)
  • packet-too-big โ€” oversized MTU โ€” type: integer (32 bit)
  • id โ€” ID of echo request/response โ€” type: integer (16 bit)
  • sequence โ€” sequence number of echo request/response โ€” type: integer (16 bit)
  • max-delay โ€” maximum response delay of MLD queries โ€” type: integer (16 bit)
  • taddr โ€” target address of neighbor solicit/advert, redirect or MLD โ€” type: ipv6_addr
  • daddr โ€” destination address of redirect โ€” type: ipv6_addr

TCP HEADER EXPRESSION ๐ŸŽ€

tcp {sport | dport | sequence | ackseq | doff | reserved | flags | window | checksum | urgptr}

okay so that's the whole lineup of TCP header fields you can poke at, and the full rundown of what each one actually means and what type it gives you back is coming right up next, so don't go anywhere โœจ

โœจ Table 44. TCP header expression โœจ

okay so Table 44, the TCP header expression, and bestie this is literally the TCP header decoded into lil keywords you can grab. Every single one, no cap:

  • sport โ€” Source port โ€” inet_service ๐Ÿซถ
  • dport โ€” Destination port โ€” inet_service โœจ
  • sequence โ€” Sequence number โ€” integer (32 bit)
  • ackseq โ€” Acknowledgement number โ€” integer (32 bit)
  • doff โ€” Data offset โ€” integer (4 bit) FIXME scaling (yes the FIXME stays, it's part of the lore ๐Ÿ’€)
  • reserved โ€” Reserved area โ€” integer (4 bit)
  • flags โ€” TCP flags โ€” tcp_flag ๐ŸŒธ
  • window โ€” Window โ€” integer (16 bit)
  • checksum โ€” Checksum โ€” integer (16 bit)
  • urgptr โ€” Urgent pointer โ€” integer (16 bit)

โœจ UDP HEADER EXPRESSION โœจ

UDP is the chill one, lowkey. Here's the whole synopsis, verbatim:

udp {sport | dport | length | checksum}

Table 45. UDP header expression ๐Ÿฆ‹

  • sport โ€” Source port โ€” inet_service
  • dport โ€” Destination port โ€” inet_service
  • length โ€” Total packet length โ€” integer (16 bit)
  • checksum โ€” Checksum โ€” integer (16 bit)

โœจ UDP-LITE HEADER EXPRESSION โœจ

UDP-Lite said "same vibe, less baggage." Synopsis:

udplite {sport | dport | checksum}

Table 46. UDP-Lite header expression ๐ŸŽ€

  • sport โ€” Source port โ€” inet_service
  • dport โ€” Destination port โ€” inet_service
  • checksum โ€” Checksum โ€” integer (16 bit)

โœจ SCTP HEADER EXPRESSION โœจ

okay SCTP is the main character with the most lore, and it's giving nested grammar. Here's everything, exactly as written:

sctp {sport | dport | vtag | checksum}
sctp chunk CHUNK [ FIELD ]

CHUNK := data | init | init-ack | sack | heartbeat |
           heartbeat-ack | abort | shutdown | shutdown-ack | error |
           cookie-echo | cookie-ack | ecne | cwr | shutdown-complete
           | asconf-ack | forward-tsn | asconf

FIELD := COMMON_FIELD | DATA_FIELD | INIT_FIELD | INIT_ACK_FIELD |
           SACK_FIELD | SHUTDOWN_FIELD | ECNE_FIELD | CWR_FIELD |
           ASCONF_ACK_FIELD | FORWARD_TSN_FIELD | ASCONF_FIELD

COMMON_FIELD := type | flags | length
DATA_FIELD := tsn | stream | ssn | ppid
INIT_FIELD := init-tag | a-rwnd | num-outbound-streams |
                num-inbound-streams | initial-tsn
INIT_ACK_FIELD := INIT_FIELD
SACK_FIELD := cum-tsn-ack | a-rwnd | num-gap-ack-blocks |
                num-dup-tsns
SHUTDOWN_FIELD := cum-tsn-ack
ECNE_FIELD := lowest-tsn
CWR_FIELD := lowest-tsn
ASCONF_ACK_FIELD := seqno
FORWARD_TSN_FIELD := new-cum-tsn
ASCONF_FIELD := seqno

Table 47. SCTP header expression ๐Ÿ”ฅ

  • sport โ€” Source port โ€” inet_service
  • dport โ€” Destination port โ€” inet_service
  • vtag โ€” Verification Tag โ€” integer (32 bit)
  • checksum โ€” Checksum โ€” integer (32 bit)
  • chunk โ€” Search chunk in packet โ€” without FIELD, boolean indicating existence ๐Ÿ‘‘

next part pulls up and we keep it moving, no sign-off bestie ๐Ÿ’…

๐ŸŽ€ Table 48. SCTP chunk fields ๐ŸŽ€

bestie this is THE receipts table for every SCTP chunk field, like the whole lineup, no gatekeeping ๐Ÿ’– here's the tea on each one โ€” name, how many bits wide it is, which chunk(s) it lives in, and what it's actually doing there:

  • type โ€” 8 bits wide โ€” shows up in all chunks โ€” ngl it's not useful, because it's literally defined by the chunk type anyway ๐Ÿ™ˆ
  • flags โ€” 8 bits wide โ€” shows up in all chunks โ€” the semantics are defined on a per-chunk basis, so it's giving "it depends" energy
  • length โ€” 16 bits wide โ€” shows up in all chunks โ€” length of this chunk in bytes, and bestie it's excluding padding, don't forget that part ๐Ÿ’…
  • tsn โ€” 32 bits wide โ€” lives in data โ€” transmission sequence number
  • stream โ€” 16 bits wide โ€” lives in data โ€” stream identifier
  • ssn โ€” 16 bits wide โ€” lives in data โ€” stream sequence number
  • ppid โ€” 32 bits wide โ€” lives in data โ€” payload protocol identifier
  • init-tag โ€” 32 bits wide โ€” lives in init, init-ack โ€” initiate tag
  • a-rwnd โ€” 32 bits wide โ€” lives in init, init-ack, sack โ€” advertised receiver window credit
  • num-outbound-streams โ€” 16 bits wide โ€” lives in init, init-ack โ€” number of outbound streams
  • num-inbound-streams โ€” 16 bits wide โ€” lives in init, init-ack โ€” number of inbound streams
  • initial-tsn โ€” 32 bits wide โ€” lives in init, init-ack โ€” initial transmit sequence number
  • cum-tsn-ack โ€” 32 bits wide โ€” lives in sack, shutdown โ€” cumulative transmission sequence number acknowledged
  • num-gap-ack-blocks โ€” 16 bits wide โ€” lives in sack โ€” number of Gap Ack Blocks included
  • num-dup-tsns โ€” 16 bits wide โ€” lives in sack โ€” number of duplicate transmission sequence numbers received
  • lowest-tsn โ€” 32 bits wide โ€” lives in ecne, cwr โ€” lowest transmission sequence number
  • seqno โ€” 32 bits wide โ€” lives in asconf-ack, asconf โ€” sequence number
  • new-cum-tsn โ€” 32 bits wide โ€” lives in forward-tsn โ€” new cumulative transmission sequence number

๐Ÿฆ‹ DCCP HEADER EXPRESSION ๐Ÿฆ‹

and for DCCP, the whole header expression is lowkey minimalist, she's serving exactly three fields and that's it, periodt:

dccp {sport | dport | type}

so you pick one of them โ€” sport, dport or type โ€” and that's your DCCP header match. short, sweet, iconic ๐Ÿ‘‘

...and the last lil snack from the dccp menu is the packet type, so let's finish the plate ๐Ÿฝ๏ธโœจ

Table 49. DCCP header expression ๐ŸŽ€

  • sport โ€” the source port, type inet_service ๐Ÿ’…
  • dport โ€” the destination port, type inet_service ๐Ÿ’…
  • type โ€” the packet type, type dccp_pkttype ๐Ÿ’…

๐Ÿ” AUTHENTICATION HEADER EXPRESSION ๐Ÿ”

ah {nexthdr | hdrlength | reserved | spi | sequence}

Table 50. AH header expression ๐Ÿ‘‘

  • nexthdr โ€” next header protocol, type inet_proto ๐ŸŒธ
  • hdrlength โ€” AH header length, type integer (8 bit) ๐ŸŒธ
  • reserved โ€” reserved area, type integer (16 bit) ๐ŸŒธ
  • spi โ€” Security Parameter Index, type integer (32 bit) ๐ŸŒธ
  • sequence โ€” sequence number, type integer (32 bit) ๐ŸŒธ

๐Ÿซง ENCRYPTED SECURITY PAYLOAD HEADER EXPRESSION ๐Ÿซง

esp {spi | sequence}

Table 51. ESP header expression โœจ

  • spi โ€” Security Parameter Index, type integer (32 bit) ๐Ÿ’œ
  • sequence โ€” sequence number, type integer (32 bit) ๐Ÿ’œ

๐Ÿ—œ๏ธ IPCOMP HEADER EXPRESSION ๐Ÿ—œ๏ธ

comp {nexthdr | flags | cpi}

Table 52. IPComp header expression ๐ŸŒˆ

  • nexthdr โ€” next header protocol, type inet_proto ๐Ÿฆ‹
  • flags โ€” flags, type bitmask ๐Ÿฆ‹
  • cpi โ€” compression Parameter Index, type integer (16 bit) ๐Ÿฆ‹

๐Ÿงš GRE HEADER EXPRESSION ๐Ÿงš

gre {flags | version | protocol}
gre ip {version | hdrlength | dscp | ecn | length | id | frag-off | ttl | protocol | checksum | saddr | daddr }
gre ip6 {version | dscp | ecn | flowlabel | length | nexthdr | hoplimit | saddr | daddr}

okay so the gre expression is literally your girl for matching on the gre header fields โ€” and bestie, it also lets you match on the IPv4 or IPv6 packet inside the gre header, which is giving nested-inception energy ๐Ÿ˜ญ๐Ÿซถ

Table 53. GRE header expression ๐Ÿ’–

  • flags โ€” checksum, routing, key, sequence and strict source route flags, type integer (5 bit) โญ
  • version โ€” gre version field, 0 for GRE and 1 for PPTP, type integer (3 bit) โญ
  • protocol โ€” EtherType of encapsulated packet, type integer (16 bit) โญ

Matching the inner IPv4 destination address encapsulated in gre? iconic. here's how ๐Ÿ’…

netdev filter ingress gre ip daddr 9.9.9.9 counter

๐ŸŒธ GENEVE HEADER EXPRESSION ๐ŸŒธ

geneve {vni | flags}
geneve ether {daddr | saddr | type}
geneve vlan {id | dei | pcp | type}
geneve ip {version | hdrlength | dscp | ecn | length | id | frag-off | ttl | protocol | checksum | saddr | daddr }
geneve ip6 {version | dscp | ecn | flowlabel | length | nexthdr | hoplimit | saddr | daddr}
geneve tcp {sport | dport | sequence | ackseq | doff | reserved | flags | window | checksum | urgptr}
geneve udp {sport | dport | length | checksum}

The geneve expression is what you reach for to match on the geneve header fields. The geneve header encapsulates an ethernet frame within a udp packet โ€” so yes, it's layers on layers, we love a stacked queen ๐Ÿ‘‘. This expression requires that you restrict the matching to udp packets (usually at port 6081 according to IANA-assigned ports). no cap, don't skip that part ๐Ÿšจ

Table 54. GENEVE header expression ๐ŸŽ€

  • protocol โ€” EtherType of encapsulated packet, type integer (16 bit) ๐ŸŒˆ
  • vni โ€” Virtual Network ID (VNI), type integer (24 bit) ๐ŸŒˆ

Matching inner TCP destination port encapsulated in geneve โ€” yes bestie, we're going full tunnel detective ๐Ÿ”

netdev filter ingress udp dport 4789 geneve tcp dport 80 counter

๐Ÿฆ‹ GRETAP HEADER EXPRESSION ๐Ÿฆ‹

gretap {vni | flags}
gretap ether {daddr | saddr | type}
gretap vlan {id | dei | pcp | type}
gretap ip {version | hdrlength | dscp | ecn | length | id | frag-off | ttl | protocol | checksum | saddr | daddr }
gretap ip6 {version | dscp | ecn | flowlabel | length | nexthdr | hoplimit | saddr | daddr}
gretap tcp {sport | dport | sequence | ackseq | doff | reserved | flags | window | checksum | urgptr}
gretap udp {sport | dport | length | checksum}

The gretap expression is the one for matching on the encapsulated ethernet frame within the gre header. Wanna match the gre header fields themselves? that's a gre job, bestie โ€” use the gre expression for that ๐Ÿ’…

Matching inner TCP destination port encapsulated in gretap, coming right up โœจ

netdev filter ingress gretap tcp dport 80 counter

and we're still going, so don't go anywhere ๐Ÿ’–

VXLAN HEADER EXPRESSION ๐Ÿฆ‹

vxlan {vni | flags}
vxlan ether {daddr | saddr | type}
vxlan vlan {id | dei | pcp | type}
vxlan ip {version | hdrlength | dscp | ecn | length | id | frag-off | ttl | protocol | checksum | saddr | daddr }
vxlan ip6 {version | dscp | ecn | flowlabel | length | nexthdr | hoplimit | saddr | daddr}
vxlan tcp {sport | dport | sequence | ackseq | doff | reserved | flags | window | checksum | urgptr}
vxlan udp {sport | dport | length | checksum}

okay so the vxlan expression is your girl for matching on vxlan header fields. quick lore drop: the vxlan header wraps an ethernet frame up inside a udp packet ๐ŸŽ€ and this expression requires that you restrict the matching to udp packets โ€” usually at port 4789 per the IANA-assigned ports. so like, do the udp part first, bestie, or you're just out here matching vibes.

Table 55. VXLAN header expression

  • flags โ€” vxlan flags, type integer (8 bit) โœจ
  • vni โ€” Virtual Network ID (VNI), type integer (24 bit) ๐Ÿ’…

Matching inner TCP destination port encapsulated in vxlan:

netdev filter ingress udp dport 4789 vxlan tcp dport 80 counter

RAW PAYLOAD EXPRESSION ๐Ÿ’€

@base,offset,length

The raw payload expression says: load length bits starting at offset bits. and listen, bit 0 is the very first bit โ€” in the C programming language that's the topmost bit, i.e. 0x80 in the case of an octet. they're lowkey iconic for matching headers that don't have a human-readable template expression yet ๐Ÿ™ˆ

Important heads up: nft will not add dependencies for Raw payload expressions. so if you wanna match protocol fields of a transport header with protocol number 5, you gotta manually exclude packets that have a different transport header โ€” for instance by using meta l4proto 5 before the raw expression. do it, or cry about it. no cap.

Table 56. Supported payload protocol bases

  • ll โ€” Link layer, for example the Ethernet header ๐ŸŒธ
  • nh โ€” Network header, for example IPv4 or IPv6 โญ
  • th โ€” Transport Header, for example TCP ๐Ÿซถ
  • ih โ€” Inner Header / Payload, i.e. after the L4 transport level header ๐Ÿ’œ

Matching destination port of both UDP and TCP:

inet filter input meta l4proto {tcp, udp} @th,16,16 { 53, 80 }

The above can also be written as:

inet filter input meta l4proto {tcp, udp} th dport { 53, 80 }

which is more convenient, but โ€” just like the raw expression notation โ€” no dependencies are created or checked. it is the users responsibility to restrict matching to those header types that have a notion of ports. otherwise, rules using raw expressions will errnously match unrelated packets, e.g. mis-interpreting ESP packets SPI field as a port ๐Ÿ˜ญ the way that's just a footgun waiting to happen.

Rewrite arp packet target hardware address if target protocol address matches a given address:

input meta iifname enp2s0 arp ptype 0x0800 arp htype 1 arp hlen 6 arp plen 4 @nh,192,32 0xc0a88f10 @nh,144,48 set 0x112233445566 accept

EXTENSION HEADER EXPRESSIONS ๐ŸŒˆ

Extension header expressions refer to data from variable-sized protocol headers โ€” think IPv6 extension headers, TCP options and IPv4 options. love that for them.

nftables currently supports matching (finding) a given ipv6 extension header, TCP option or IPv4 option:

hbh {nexthdr | hdrlength}
frag {nexthdr | frag-off | more-fragments | id}
rt {nexthdr | hdrlength | type | seg-left}
dst {nexthdr | hdrlength}
mh {nexthdr | hdrlength | checksum | type}
srh {flags | tag | sid | seg-left}
tcp option {eol | nop | maxseg | window | sack-perm | sack | sack0 | sack1 | sack2 | sack3 | timestamp | mptcp } tcp_option_field
ip option { lsrr | ra | rr | ssrr } ip_option_field

The following syntaxes are valid only in a relational expression with boolean type on right-hand side, for checking header existence only:

exthdr {hbh | frag | rt | dst | mh}
tcp option {eol | nop | maxseg | window | sack-perm | sack | sack0 | sack1 | sack2 | sack3 | timestamp | mptcp }
ip option { lsrr | ra | rr | ssrr }
dccp option dccp_option_type

Table 57. IPv6 extension headers

  • hbh โ€” Hop by Hop ๐ŸŽ€
  • rt โ€” Routing Header โœจ
  • frag โ€” Fragmentation header ๐Ÿ’…
  • dst โ€” dst options ๐Ÿฆ‹
  • mh โ€” Mobility Header ๐Ÿ‘‘
  • srh โ€” Segment Routing Header ๐ŸŒธ

๐ŸŽ€ Table 58. TCP Options ๐ŸŽ€

ok bestie here's the full rundown of every TCP option keyword you can use, plus which fields each one gives you ๐Ÿ’–

  • eol โ€” End if option list. no fields, it's giving minimalism ๐Ÿ’…
  • nop โ€” 1 Byte TCP Nop padding option. also no fields.
  • maxseg โ€” TCP Maximum Segment Size. fields: length, size
  • window โ€” TCP Window Scaling. fields: length, count
  • sack-perm โ€” TCP SACK permitted. field: length
  • sack โ€” TCP Selective Acknowledgement (alias of block 0). fields: length, left, right
  • sack0 โ€” TCP Selective Acknowledgement (block 0). fields: length, left, right
  • sack1 โ€” TCP Selective Acknowledgement (block 1). fields: length, left, right
  • sack2 โ€” TCP Selective Acknowledgement (block 2). fields: length, left, right
  • sack3 โ€” TCP Selective Acknowledgement (block 3). fields: length, left, right
  • timestamp โ€” TCP Timestamps. fields: length, tsval, tsecr
  • mptcp โ€” Multipath TCP. field: subtype

Data types can be queried with nft describe tcp option keyword [ fieldname ].

TCP option matching also supports raw expression syntax to access arbitrary options, and honestly? iconic ๐Ÿ”ฅ

tcp option

tcp option @number,offset,length

๐ŸŒˆ Table 59. IP Options ๐ŸŒˆ

  • lsrr โ€” Loose Source Route. fields: length, ptr, addr
  • ra โ€” Router Alert. fields: length, value
  • rr โ€” Record Route. fields: length, ptr, addr
  • ssrr โ€” Strict Source Route. fields: length, ptr, addr

finding TCP options:

filter input tcp option sack-perm exists counter

matching TCP options:

filter input tcp option maxseg size lt 536

matching IPv6 exthdr:

ip6 filter input frag more-fragments 1 counter

finding IP option:

filter input ip option lsrr exists counter

finding DCCP option:

filter input dccp option 40 exists counter

๐Ÿ’œ CONNTRACK EXPRESSIONS ๐Ÿ’œ

Conntrack expressions refer to meta data of the connection tracking entry associated with a packet. basically the receipts of a connection, and they keep them ๐Ÿ“‹

There are three types of conntrack expressions. Some conntrack expressions require the flow direction before the conntrack key, others must be used directly because they are direction agnostic. The packets, bytes and avgpkt keywords can be used with or without a direction. If the direction is omitted, the sum of the original and the reply direction is returned. The same is true for the zone โ€” if a direction is given, the zone is only matched if the zone id is tied to the given direction. no cap ๐Ÿซถ

ct {state | direction | status | mark | expiration | helper | label | count | id}
ct [original | reply] {l3proto | protocol | bytes | packets | avgpkt | zone}
ct {original | reply} {proto-src | proto-dst}
ct {original | reply} {ip | ip6} {saddr | daddr}

The conntrack-specific types in this table are described in the sub-section CONNTRACK TYPES above. go peek at it if you skipped, bestie, it's literally right there โœจ

  • state โ€” the state of the connection, type ct_state ๐ŸŒธ
  • direction โ€” which way the packet is going relative to the connection, type ct_dir ๐Ÿฆ‹
  • status โ€” the status of the connection, type ct_status
  • mark โ€” the connection mark, type mark ๐Ÿ’…
  • expiration โ€” how long until the connection expires, type time โณ
  • helper โ€” the helper that's associated with the connection, type string ๐Ÿงš
  • label โ€” a connection tracking label bit, or a symbolic name defined in connlabel.conf in the nftables include path, type ct_label ๐Ÿท๏ธ
  • l3proto โ€” the Layer 3 protocol of the connection, type nf_proto
  • saddr โ€” the source address of the connection for the given direction, type ipv4_addr/ipv6_addr ๐Ÿ’–
  • daddr โ€” the destination address of the connection for the given direction, type ipv4_addr/ipv6_addr
  • protocol โ€” the Layer 4 protocol of the connection for the given direction, type inet_proto
  • proto-src โ€” the Layer 4 protocol source for the given direction, type integer (16 bit)
  • proto-dst โ€” the Layer 4 protocol destination for the given direction, type integer (16 bit)
  • packets โ€” the packet count seen in the given direction, or the sum of original and reply, type integer (64 bit) ๐Ÿ“ฆ
  • bytes โ€” the byte count seen, check the description for the packets keyword, type integer (64 bit)
  • avgpkt โ€” the average bytes per packet, check the description for the packets keyword, type integer (64 bit) ๐Ÿ“Š
  • zone โ€” the conntrack zone, type integer (16 bit)
  • count โ€” the number of current connections, type integer (32 bit)
  • id โ€” the connection id, type ct_id ๐Ÿ†”

Ok so real talk, here's the actual slay use case โ€” restricting the number of parallel connections to a server. Rate limiting but make it conntrack, periodt:

nft add set filter ssh_flood '{ type ipv4_addr; flags dynamic; }'
nft add rule filter input tcp dport 22 add @ssh_flood '{ ip saddr ct count over 2 }' reject

Like, two connections max per source IP to port 22 and then it's a wrap, bestie. ๐Ÿ’€

โœจ STATEMENTS โœจ

Okay so statements are literally the actions โ€” the things that actually happen to your packets. They can mess with control flow (think return, jump to a whole different chain, accept, or drop the packet) or they can just do stuff โ€” logging, rejecting a packet, all of that. ๐ŸŒˆ

And here's the tea: statements come in two kinds:

  • Terminal statements โ€” these unconditionally terminate evaluation of the current rule. Like, that's it, we're done here, no further questions. ๐Ÿ‘‘
  • Non-terminal statements โ€” these either only conditionally terminate evaluation, or never terminate it at all. In other words, from the rule-evaluation perspective, they're just kinda passive. Chilling in the background, not ending anything.

You can have an arbitrary amount of non-terminal statements in a rule โ€” like, go off, stack as many as you want โ€” but only a single terminal statement, and it has to be the final statement. That's the rule. No exceptions. ๐Ÿ’…

VERDICT STATEMENTS

The verdict statements are the ones that alter control flow in the ruleset and issue actual policy decisions for packets. Main character behaviour fr fr.

{accept | drop | continue | return}
{jump | goto} CHAIN

CHAIN := chain_name | { statement ... }

So accept and drop are absolute verdicts โ€” they terminate chain evaluation, as if the packet had reached the end of the base chain with the equivalent policy decision already set. Wanna know more? Check the section called "OVERALL EVALUATION OF THE RULESET". ๐Ÿ‘€

accept โ€” Terminate evaluation early. Evaluation continues in the next base chain of higher or possibly equal priority from the same hook, or in the first base chain of a later hook, if there is one. This means the packet can still get dropped in another base chain, as well as any chain called from it. For example โ€” an accept verdict in a chain of the forward hook still allows you to drop the packet in another forward hook base chain (or a chain called from it) that has a higher priority number, or in a chain attached to the postrouting hook. ๐Ÿ˜ญ

Like yes she said accept but that is NOT the end of the story, bestie. The packet can still be caught lacking later. ๐Ÿซถ

  • drop โ€” like, immediately drop the packet and terminate ruleset evaluation. That's it, no more evaluating, we're done here. And bestie, hear me when I say: it is not possible to override a drop verdict. Not a thing. Periodt.
  • jump CHAIN โ€” stash the current position in the call stack of chains and keep evaluating starting at the first rule of CHAIN. When you hit the end of CHAIN, an implicit return verdict gets issued. And if an absolute verdict gets issued (or implied by a verdict-like statement) inside CHAIN, evaluation terminates the way described above. ๐Ÿ’…
  • goto CHAIN โ€” literally the same as jump except the current position is not stored in the call stack of chains. That's the whole difference, iykyk.
  • return โ€” ends evaluation of the current chain, pops the most recently added position off the call stack of chains, and keeps evaluating after that position. And when there's no position to pop (which happens when the current chain is either the base chain or a regular chain that was reached solely via goto verdicts), it ends evaluation of the current base chain (and any regular chains called from it) using the base chain's policy as the implicit verdict.
  • continue โ€” just keep evaluating with the next rule. This is the default behaviour in case a rule issues no verdict at all. โœจ

An alternative to specifying the name of an existing, regular chain in CHAIN is to specify an anonymous chain ad-hoc. Like with anonymous sets, it can't be referenced from another rule and will be removed along with the rule containing it. ๐Ÿซถ

All the above applies analogously to statements that imply a verdict: redirect, dnat, snat and masquerade internally issue an accept verdict at the end of their respective actions. And reject and synproxy internally issue a drop verdict at the end of their respective actions. So these statements behave like their implied verdicts, but with side effects. Iconic behaviour, honestly.

Using verdict statements.

# process packets from eth0 and the internal network in from_lan
# chain, drop all packets from eth0 with different source addresses.

filter input iif eth0 ip saddr 192.168.0.0/24 jump from_lan
filter input iif eth0 drop

# jump and goto statements support anonymous chain creation
filter input iif eth0 jump { ip saddr 192.168.0.0/24 drop ; udp dport domain drop; }

๐ŸŒธ PAYLOAD STATEMENT ๐ŸŒธ

payload_expression set value

The payload statement alters packet content. You can use it, for example, to set the ip DSCP (diffserv) header field or ipv6 flow labels. Obsessed with how much this does, ngl.

Route some packets instead of bridging.

# redirect tcp:http from 192.160.0.0/16 to local machine for routing instead of bridging
# assumes 00:11:22:33:44:55 is local MAC address.
bridge input meta iif eth0 ip saddr 192.168.0.0/16 tcp dport 80 meta pkttype set host ether daddr set 00:11:22:33:44:55

Set IPv4 DSCP header field.

ip forward ip dscp set 42

๐Ÿฆ‹ EXTENSION HEADER STATEMENT ๐Ÿฆ‹

extension_header_expression set value

The extension header statement alters packet content in variable-sized headers. Currently this can be used to alter the TCP Maximum segment size of packets, similar to the TCPMSS target in iptables. Cute little crossover moment ๐Ÿ’…

Change tcp mss.

tcp flags syn tcp option maxseg size set 1360
# set a size based on route information:
tcp flags syn tcp option maxseg size set rt mtu

You can also remove tcp options via the reset keyword.

Remove tcp option.

tcp flags syn reset tcp option sack-perm

๐Ÿ’œ LOG STATEMENT ๐Ÿ’œ

log [prefix quoted_string] [level syslog-level] [flags log-flags]
log group nflog_group [prefix quoted_string] [queue-threshold value] [snaplen size]
log level audit

The log statement enables logging of matching packets. When this statement is used from a rule, the Linux kernel will print some information on all matching packets, such as header fields, via the kernel log (where it can be read with dmesg(1) or read in the syslog). The receipts are all there, bestie. ๐Ÿ˜ญ

In the second form of invocation (if nflog_group is specified), the Linux kernel will pass the packet to nfnetlink_log which will send the log through a netlink socket to the specified group. One userspace process may subscribe to the group to receive the logs โ€” see man(8) ulogd for the Netfilter userspace log daemon and libnetfilter_log documentation for details in case you would like to develop a custom program to digest your logs. Main character energy fr fr.

In the third form of invocation (if level audit is specified), the Linux kernel writes a message into the audit buffer suitably formatted for reading with auditd. Therefore no further formatting options (such as prefix or flags) are allowed in this mode. Don't even try it ๐Ÿ’€

This is a non-terminating statement, so the rule evaluation continues after the packet is logged. She logs and she keeps going, that's the vibe. โœจ

Table 61. log statement options

  • prefix โ€” Log message prefix. Type: quoted string.
  • level โ€” Syslog level of logging. Type: string: emerg, alert, crit, err, warn [default], notice, info, debug, audit. (Yep, warn is the default, no cap.)
  • group โ€” NFLOG group to send messages to. Type: unsigned integer (16 bit).
  • snaplen โ€” Length of packet payload to include in netlink message. Type: unsigned integer (32 bit).
  • queue-threshold โ€” Number of packets to queue inside the kernel before sending them to userspace. Type: unsigned integer (32 bit).

Table 62. log-flags

  • tcp sequence โ€” Log TCP sequence numbers.
  • tcp options โ€” Log options from the TCP packet header.
  • ip options โ€” Log options from the IP/IPv6 packet header.
  • skuid โ€” Log the userid of the process which generated the packet.
  • ether โ€” Decode MAC addresses and protocol.
  • all โ€” Enable all log flags listed above. All of them. The whole menu. ๐Ÿ“

Using log statement.

# log the UID which generated the packet and ip options
ip filter output log flags skuid flags ip options

# log the tcp sequence numbers and tcp options from the TCP packet
ip filter output log flags tcp sequence,options

# enable all supported log flags
ip6 filter output log flags all

๐Ÿ‘‘ REJECT STATEMENT ๐Ÿ‘‘

reject [ with REJECT_WITH ]

REJECT_WITH := icmp icmp_reject_code |
                 icmpv6 icmpv6_reject_code |
                 icmpx icmpx_reject_code |
                 tcp reset

Okay so bestie, a reject statement is literally trying to send back an error packet in response to the matched packet, and then internally it issues a drop verdict. ๐Ÿ’€ That makes it a terminating statement, with all the consequences of the latter (see the section called โ€œOVERALL EVALUATION OF THE RULESETโ€ respectively the section called โ€œVERDICT STATEMENTSโ€). This statement is only valid in base chains using the prerouting, input, forward or output hooks, and regular chains which are only called from those chains. No cap.

Table 63. Keywords may be used to reject when specifying the ICMP code

  • net-unreachable โ€” 0
  • host-unreachable โ€” 1
  • prot-unreachable โ€” 2
  • port-unreachable โ€” 3
  • frag-needed โ€” 4
  • net-prohibited โ€” 9
  • host-prohibited โ€” 10
  • admin-prohibited โ€” 13

Table 64. keywords may be used to reject when specifying the ICMPv6 code

  • no-route โ€” 0
  • admin-prohibited โ€” 1
  • addr-unreachable โ€” 3
  • port-unreachable โ€” 4
  • policy-fail โ€” 5
  • reject-route โ€” 6

The ICMPvX Code type abstraction is a set of values which overlap between ICMP and ICMPv6 Code types to be used from the inet family. Itโ€™s giving overlap realness. โœจ

Table 65. keywords may be used when specifying the ICMPvX code

  • no-route โ€” 0
  • port-unreachable โ€” 1
  • host-unreachable โ€” 2
  • admin-prohibited โ€” 3

The common default ICMP code to reject is port-unreachable. Periodt.

Note that in bridge family, reject statement is only allowed in base chains which hook into input or prerouting.

COUNTER STATEMENT ๐Ÿ’–

A counter statement sets the hit count of packets along with the number of bytes. Obsessed.

counter packets number bytes number
counter { packets number | bytes number }

CONNTRACK STATEMENT ๐Ÿ’…

The conntrack statement can be used to set the conntrack mark and conntrack labels. ๐ŸŽ€

ct {mark | event | label | zone} set value

The ct statement sets meta data associated with a connection. The zone id has to be assigned before a conntrack lookup takes place, i.e. this has to be done in prerouting and possibly output (if locally generated packets need to be placed in a distinct zone), with a hook priority of raw (-300).

Unlike iptables, where the helper assignment happens in the raw table, the helper needs to be assigned after a conntrack entry has been found, i.e. it will not work when used with hook priorities equal or before -200.

Table 66. Conntrack statement types

  • event โ€” conntrack event bits โ€” value: bitmask, integer (32 bit)
  • helper โ€” name of ct helper object to assign to the connection โ€” value: quoted string
  • mark โ€” Connection tracking mark โ€” value: mark
  • label โ€” Connection tracking label โ€” value: label
  • zone โ€” conntrack zone โ€” value: integer (16 bit)

save packet nfmark in conntrack. ๐Ÿซถ

ct mark set meta mark

set zone mapped via interface.

table inet raw {
  chain prerouting {
      type filter hook prerouting priority raw;
      ct zone set iif map { "eth1" : 1, "veth1" : 2 }
  }
  chain output {
      type filter hook output priority raw;
      ct zone set oif map { "eth1" : 1, "veth1" : 2 }
  }
}

restrict events reported by ctnetlink.

ct event set new,related,destroy

NOTRACK STATEMENT ๐Ÿฆ‹

The notrack statement allows one to disable connection tracking for certain packets. ๐Ÿšซ

notrack

Note that for this statement to be effective, it has to be applied to packets before a conntrack lookup happens. Therefore, it needs to sit in a chain with either prerouting or output hook and a hook priority of -300 (raw) or less.

See SYNPROXY STATEMENT for an example usage.

META STATEMENT ๐ŸŒธ

A meta statement sets the value of a meta expression. The existing meta fields are: priority, mark, pkttype, nftrace. Lowkey iconic.

meta {mark | priority | pkttype | nftrace | broute} set value

A meta statement sets meta data associated with a packet.

Table 67. Meta statement types

  • priority โ€” TC packet priority โ€” value: tc_handle
  • mark โ€” Packet mark โ€” value: mark
  • pkttype โ€” packet type โ€” value: pkt_type
  • nftrace โ€” ruleset packet tracing on/off. Use monitor trace command to watch traces โ€” value: 0, 1
  • broute โ€” broute on/off. packets are routed instead of being bridged โ€” value: 0, 1

โœจ LIMIT STATEMENT โœจ (aka the token bucket bestie)

limit rate [over] packet_number / TIME_UNIT [burst packet_number packets]
limit rate [over] byte_number BYTE_UNIT / TIME_UNIT [burst byte_number BYTE_UNIT]

TIME_UNIT := second | minute | hour | day
BYTE_UNIT := bytes | kbytes | mbytes

Okay so a limit statement matches at a limited rate using a token bucket filter ๐Ÿ’ง. A rule that uses this statement will match until this limit is reached. You can team it up with the log statement to get yourself some limited logging (obsessed, honestly). The optional over keyword makes it match over the specified rate.

The burst value is the one that influences the bucket size, i.e. jitter tolerance. With packet-based limit, the bucket holds exactly burst packets โ€” and by default that's five. If you specify packet burst, it must be a non-zero value, no zeros allowed bestie ๐Ÿšซ. With byte-based limit, the bucket's minimum size is the given rate's byte value, and the burst value adds to that โ€” by default zero bytes.

Table 68. limit statement values

  • packet_number โ€” Number of packets. Type: unsigned integer (32 bit) ๐Ÿ”ข
  • byte_number โ€” Number of bytes. Type: unsigned integer (32 bit) ๐Ÿ”ข

๐ŸŒˆ NAT STATEMENTS ๐ŸŒˆ

snat [[ip | ip6] [ prefix ] to] TARGET_SPEC [FLAGS]
dnat [[ip | ip6] [ prefix ] to] TARGET_SPEC [FLAGS]
masquerade [to :PORT_SPEC] [FLAGS]
redirect [to :PORT_SPEC] [FLAGS]

TARGET_SPEC := ADDR_SPEC | [ADDR_SPEC] :PORT_SPEC
ADDR_SPEC := address | address - address
PORT_SPEC := port | port - port

FLAGS  := FLAG [, FLAGS]
FLAG  := persistent | random | fully-random

The nat statements are only valid from nat chain types, periodt. ๐Ÿ’…

The snat and masquerade statements say that the source address/port of the packet should be modified. While snat is only valid in the postrouting and input chains, masquerade makes sense only in postrouting. The dnat and redirect statements are only valid in the prerouting and output chains โ€” they say that the destination address/port of the packet should be modified. You can use non-base chains which are called from base chains of nat chain type too. All future packets in this connection will also be mangled, and rules should cease being examined ๐Ÿ‘€.

The masquerade statement is a special form of snat which always uses the outgoing interface's IP address to translate to. It's particularly useful on gateways with dynamic (public) IP addresses. Iconic behavior fr fr.

The redirect statement is a special form of dnat which always translates the destination address to the local host's one. It comes in handy to intercept traffic passing a router and feeding it to a locally running daemon โ€” e.g. when building a transparent proxy or application-layer gateway ๐Ÿงš.

For TARGET_SPEC, you may specify addresses, ports, or both. If no address or no port is specified, the respective packet header field remains unchanged.

When used in the inet family (available with kernel 5.2), the dnat and snat statements require the use of the ip and ip6 keyword in case an address is provided โ€” see the examples below ๐Ÿ‘‡.

Before kernel 4.18, nat statements require both prerouting and postrouting base chains to be present, since otherwise packets on the return path won't be seen by netfilter and therefore no reverse translation will take place ๐Ÿ˜ญ.

The optional prefix keyword allows one to map n source addresses to n destination addresses. See Advanced NAT examples below.

If the address for dnat is an IPv4 loopback address (i.e. 127.0.0.0/8) the "net.ipv4.conf.*.route_localnet" sysctl for the input interface needs to be set to 1. Otherwise packets will be dropped by the routing code as "martians" ๐Ÿ‘ฝ.

Table 69. NAT statement values

  • address โ€” Specifies that the source/destination address of the packet should be modified. You may specify a mapping to relate a list of tuples composed of arbitrary expression key with address value. Type: ipv4_addr, ipv6_addr, e.g. abcd::1234, or you can use a mapping, e.g. meta mark map { 10 : 192.168.1.2, 20 : 192.168.1.3 }
  • port โ€” Specifies that the source/destination port of the packet should be modified. Type: port number (16 bit)

Table 70. NAT statement flags

  • persistent โ€” Gives a client the same source-/destination-address for each connection ๐Ÿซถ
  • random โ€” In kernel 5.0 and newer this is the same as fully-random. In earlier kernels the port mapping will be randomized using a seeded MD5 hash mix using source and destination address and destination port.
  • fully-random โ€” If used then port mapping is generated based on a 32-bit pseudo-random algorithm ๐ŸŽฒ

๐Ÿ’– Using NAT statements ๐Ÿ’–

# create a suitable table/chain setup for all further examples
add table nat
add chain nat prerouting { type nat hook prerouting priority dstnat; }
add chain nat postrouting { type nat hook postrouting priority srcnat; }

# translate source addresses of all packets leaving via eth0 to address 1.2.3.4
add rule nat postrouting oif eth0 snat to 1.2.3.4

# redirect all traffic entering via eth0 to destination address 192.168.1.120
add rule nat prerouting iif eth0 dnat to 192.168.1.120

# translate source addresses of all packets leaving via eth0 to whatever
# locally generated packets would use as source to reach the same destination
add rule nat postrouting oif eth0 masquerade

# redirect incoming TCP traffic for port 22 to port 2222
add rule nat prerouting tcp dport 22 redirect to :2222

# inet family:
# handle ip dnat:
add rule inet nat prerouting dnat ip to 10.0.2.99
# handle ip6 dnat:
add rule inet nat prerouting dnat ip6 to fe80::dead
# this masquerades both ipv4 and ipv6:
add rule inet nat postrouting meta oif ppp0 masquerade

๐Ÿฆ‹ Advanced NAT examples ๐Ÿฆ‹

# map prefixes in one network to that of another, e.g. 10.141.11.4 is mangled to 192.168.2.4,
# 10.141.11.5 is mangled to 192.168.2.5 and so on.
add rule nat postrouting snat ip prefix to ip saddr map { 10.141.11.0/24 : 192.168.2.0/24 }

# map a source address, source port combination to a pool of destination addresses and ports:
add rule nat postrouting dnat to ip saddr . tcp dport map { 192.168.1.2 . 80 : 10.141.10.2-10.141.10.5 . 8888-8999 }

# The above example generates the following NAT expression:
#
# [ nat dnat ip addr_min reg 1 addr_max reg 10 proto_min reg 9 proto_max reg 11 ]
#
# which expects to obtain the following tuple:
# IP address (min), source port (min), IP address (max), source port (max)
# to be obtained from the map. The given addresses and ports are inclusive.

# This also works with named maps and in combination with both concatenations and ranges:
table ip nat {
        map ipportmap {
                typeof ip saddr : interval ip daddr . tcp dport
                flags interval
                elements = { 192.168.1.2 : 10.141.10.1-10.141.10.3 . 8888-8999, 192.168.2.0/24 : 10.141.11.5-10.141.11.20 . 8888-8999 }
        }

โœจ this named-map magic continues in the next part, don't go anywhere โœจ

                   chain prerouting {
                           type nat hook prerouting priority dstnat; policy accept;
                           ip protocol tcp dnat ip to ip saddr map @ipportmap
                   }
           }

So @ipportmap takes network prefixes and maps them onto a range of hosts and ports, bestie. The new destination comes straight outta the range that the map element hands you. Same deal for the destination port. ๐Ÿ’ซ

And clock the interval keyword in that typeof description โ€” it's literally mandatory, because that's how nftables knows it's gotta reserve twice the storage for every single key-value pair in the map. No interval, no double storage, no vibes. ๐Ÿ™ˆ

Like, : ipv4_addr . inet_service would let you glue one address and one port to each key. But in this case, for each key you need two addresses and two ports stored (the min and the max for both of them). It's giving double-booked. ๐Ÿ˜ญ


โœจ TPROXY STATEMENT โœจ

Tproxy yeets the packet to a local socket without touching the packet header even a little bit. If any of the arguments is missing, the data from the incoming packet gets used as the parameter instead. And heads up ๐Ÿซถ โ€” tproxy matching needs another rule that makes sure the transport protocol header is actually specified. It can't just guess.

tproxy to address:port
tproxy to {address | :port}

This syntax lives in ip/ip6 tables, where the network layer protocol is already obvious. Either the IP address or the port can be specified โ€” but at least one of them is necessary. Choose your fighter. ๐Ÿ’…

tproxy {ip | ip6} to address[:port]
tproxy to :port

And this syntax is for inet tables. The ip/ip6 parameter defines which family the rule is gonna match, and the address parameter must be of that family โ€” no mixing, no catfishing. When only the port is defined, you should not specify the address family at all. In that case the rule matches for both families. Iconic, honestly. ๐ŸŒˆ

Table 71. tproxy attributes ๐Ÿ‘‘

  • address โ€” the IP address that the listening socket with the IP_TRANSPARENT option is bound to.
  • port โ€” the port that the listening socket with the IP_TRANSPARENT option is bound to.

Example ruleset for the tproxy statement ๐ŸŽ€

table ip x {
    chain y {
        type filter hook prerouting priority mangle; policy accept;
        tcp dport ntp tproxy to 1.1.1.1 accept
        udp dport ssh tproxy to :2222 accept
    }
}
table ip6 x {
    chain y {
       type filter hook prerouting priority mangle; policy accept;
       tcp dport ntp tproxy to [dead::beef] accept
       udp dport ssh tproxy to :2222 accept
    }
}
table inet x {
    chain y {
        type filter hook prerouting priority mangle; policy accept;
        tcp dport 321 tproxy to :22 accept
        tcp dport 99 tproxy ip to 1.1.1.1:999 accept
        udp dport 155 tproxy ip6 to [dead::beef]:smux accept
    }
}
Okay so important thing, bestie: the tproxy statement is non-terminal. That's on purpose! It lets you post-process packets afterwards, which means you can log packets for debugging and update the mark to make sure packets get delivered locally through policy routing rules. It's giving multitasking main character. ๐Ÿ”ฅ

Example ruleset for tproxy statement with logging and meta mark ๐Ÿ’–

table inet x {
    chain y {
        type filter hook prerouting priority mangle; policy accept;
        udp dport 9999 goto {
            tproxy to :1234 log prefix "packet tproxied: " meta mark set 1 accept
            log prefix "no socket on port 1234 or not transparent?: " drop
        }
    }
}

Since packet headers stay unchanged, packets might end up getting forwarded instead of delivered locally ๐Ÿ˜ญ. As mentioned above, you can dodge that by adding policy routing rules plus the packet mark.

Example policy routing rules for local redirection โœจ

ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
โš ๏ธ This is a change in behavior compared to the legacy iptables TPROXY target, which is terminal. So if you wanna terminate packet processing after the tproxy statement, remember to issue a verdict like in the example above. Don't forget, fr fr. ๐Ÿ’€

โœจ SYNPROXY STATEMENT โœจ

This one handles the TCP three-way-handshake in parallel, in netfilter context, so it can protect either a local system or a backend system. And ngl, this statement requires connection tracking โ€” because sequence numbers need to be translated. No conntrack, no synproxy. ๐Ÿฆ‹

synproxy [mss mss_value] [wscale wscale_value] [SYNPROXY_FLAGS]

Table 72. synproxy statement attributes ๐Ÿ‘‘

  • mss โ€” maximum segment size announced to clients. This must match the backend.
  • wscale โ€” window scale announced to clients. This must match the backend.

Table 73. synproxy statement flags ๐ŸŒธ

  • sack-perm โ€” pass the client selective acknowledgement option to the backend (it'll be disabled if it's not present).
  • timestamp โ€” pass the client timestamp option to the backend (it'll be disabled if it's not present, and it's also needed for selective acknowledgement and window scaling).

Example ruleset for the synproxy statement ๐Ÿ’…

First up, figure out the tcp options your backend is using, from an external system:

Determine tcp options used by backend, from an external system

              tcpdump -pni eth0 -c 1 'tcp[tcpflags] == (tcp-syn|tcp-ack) && port 80' &
              telnet 192.0.2.42 80
              18:57:24.693307 IP 192.0.2.42.80 > 192.0.2.43.48757:
                  Flags [S.], seq 360414582, ack 788841994, win 14480,
                  options [mss 1460,sackOK,
                  TS val 1409056151 ecr 9690221,
                  nop,wscale 9],
                  length 0

Then switch tcp_loose mode off, so conntrack will mark out-of-flow packets as state INVALID:

echo 0 > /proc/sys/net/netfilter/nf_conntrack_tcp_loose

Make the SYN packets untracked:

table ip x {
        chain y {
                type filter hook prerouting priority raw; policy accept;
                tcp flags syn notrack
        }
}

Catch the UNTRACKED (SYN packets) and INVALID (3WHS ACK packets) states and send them to SYNPROXY. This rule will respond to SYN packets with SYN+ACK syncookies, create ESTABLISHED for a valid client response (3WHS ACK packets), and drop incorrect cookies. Flags combinations you wouldn't expect during 3WHS won't match and will just continue on (e.g. SYN+FIN, SYN+ACK). And finally โ€” drop the invalid packets, because those are the out-of-flow packets that SYNPROXY didn't match. Bye. ๐Ÿ‘‹

table ip x {
        chain z {
                type filter hook input priority filter; policy accept;
                ct state invalid, untracked synproxy mss 1460 wscale 9 timestamp sack-perm
                ct state invalid drop
        }
}

โœจ FLOW STATEMENT โœจ

A flow statement lets you pick which flows you wanna accelerate by forwarding through the layer 3 network stack bypass. Lowkey magical. โœจ You have to specify the flowtable name where you want this flow offloaded:

flow add @flowtable

โœจ QUEUE STATEMENT โœจ

This statement passes the packet up to userspace using the nfnetlink_queue handler. The packet gets put into the queue identified by its 16-bit queue number. Userspace can inspect it and optionally modify it if it feels like it. And userspace must provide a drop or accept verdict โ€” no ghosting allowed. ๐Ÿ’…

In the case of accept, processing resumes with the next base chain hook, not the rule following the queue verdict. See the libnetfilter_queue documentation for the details, iykyk. ๐Ÿ“š

queue [flags QUEUE_FLAGS] [to queue_number]
queue [flags QUEUE_FLAGS] [to queue_number_from - queue_number_to]
queue [flags QUEUE_FLAGS] [to QUEUE_EXPRESSION ]

QUEUE_FLAGS := QUEUE_FLAG [, QUEUE_FLAGS]
QUEUE_FLAG  := bypass | fanout
QUEUE_EXPRESSION := numgen | hash | symhash | MAP STATEMENT

QUEUE_EXPRESSION can be used to compute a queue number at run-time using the hash or numgen expressions. It also lets you use the map statement to assign fixed queue numbers based on external inputs โ€” like the source ip address or interface names. Obsessed. ๐Ÿซถ


okay so the queue statement has a whole vibe to it, and here's the receipts ๐Ÿ’–

Table 74. queue statement values โœจ

  • queue_number โ€” sets the queue number, default is 0. type: unsigned integer (16 bit)
  • queue_number_from โ€” sets the initial queue in the range, if fanout is used. type: unsigned integer (16 bit)
  • queue_number_to โ€” sets the closing queue in the range, if fanout is used. type: unsigned integer (16 bit)

Table 75. queue statement flags ๐ŸŽ€

  • bypass โ€” lets packets go through if the userspace application cannot back off. Before using this flag, read libnetfilter_queue documentation for performance tuning recommendations. (bestie, that's not optional reading, that's required reading ๐Ÿ™ˆ)
  • fanout โ€” distributes packets between several queues.

๐ŸŒˆ DUP STATEMENT ๐ŸŒˆ

The dup statement is literally for duplicating a packet and yeeting the copy to a different destination. two packets, one origin story โœจ

dup to device
dup to address device device

Table 76. Dup statement values ๐Ÿ’…

  • address โ€” specifies that the copy of the packet should be sent to a new gateway. type: ipv4_addr, ipv6_addr, e.g. abcd::1234, or you can use a mapping, e.g. ip saddr map { 192.168.1.2 : 10.1.1.1 }
  • device โ€” specifies that the copy should be transmitted via device. type: string

Using the dup statement, a.k.a. the demo reel ๐Ÿ“:

# send to machine with ip address 10.2.3.4 on eth0
ip filter forward dup to 10.2.3.4 device "eth0"

# copy raw frame to another interface
netdev ingress dup to "eth0"
dup to "eth0"

# combine with map dst addr to gateways
dup to ip daddr map { 192.168.7.1 : "eth0", 192.168.7.2 : "eth1" }

๐Ÿฆ‹ FWD STATEMENT ๐Ÿฆ‹

The fwd statement redirects a raw packet to another interface. it is only available in the netdev family ingress and egress hooks โ€” so don't go trying it anywhere else, it's not gonna answer. It's similar to the dup statement except that no copy is made. one packet, one destination, no clones ๐Ÿ’€

You can also specify the address of the next hop and the device to forward the packet to. This updates the source and destination MAC address of the packet by transmitting it through the neighboring layer. This also decrements the ttl field of the IP packet. This provides a way to effectively bypass the classical forwarding path, thus skipping the fib (forwarding information base) lookup. slay, we're taking the shortcut ๐ŸŒธ

fwd to device
fwd [ip | ip6] to address device device

Using the fwd statement ๐Ÿ‘‘:

# redirect raw packet to device
netdev ingress fwd to "eth0"

# forward packet to next hop 192.168.200.1 via eth0 device
netdev ingress ether saddr set fwd ip to 192.168.200.1 device "eth0"

๐Ÿ’œ SET STATEMENT ๐Ÿ’œ

The set statement is used to dynamically add or update elements in a set, straight from the packet path. living in the moment fr fr โœจ

okay, rules of engagement, listen up:

  • The set setname must already exist in the given table and must have been created with one or both of the dynamic and the timeout flags.
  • The dynamic flag is required if the set statement expression includes a stateful object.
  • The timeout flag is implied if the set is created with a timeout, and is required if the set statement updates elements, rather than adding them.
  • Furthermore, these sets should specify both a maximum set size (to prevent memory exhaustion), and their elements should have a timeout (so their number will not grow indefinitely) either from the set definition or from the statement that adds or updates them.

The set statement can be used to e.g. create dynamic blacklists. iconic behaviour, honestly ๐Ÿ–ค

Dynamic updates are also supported with maps. In this case, the add or update rule needs to provide both the key and the data element (value), separated via :.

{add | update} @setname { expression [timeout timeout] [comment string] }

Example for simple blacklist ๐Ÿšซ:

# declare a set, bound to table "filter", in family "ip".
# Timeout and size are mandatory because we will add elements from packet path.
# Entries will timeout after one minute, after which they might be
# re-added if limit condition persists.
nft add set ip filter blackhole \
    "{ type ipv4_addr; flags dynamic; timeout 1m; size 65536; }"

# declare a set to store the limit per saddr.
# This must be separate from blackhole since the timeout is different
nft add set ip filter flood \
    "{ type ipv4_addr; flags dynamic; timeout 10s; size 128000; }"

# whitelist internal interface.
nft add rule ip filter input meta iifname "internal" accept

# drop packets coming from blacklisted ip addresses.
nft add rule ip filter input ip saddr @blackhole counter drop

# add source ip addresses to the blacklist if more than 10 tcp connection
# requests occurred per second and ip address.
nft add rule ip filter input tcp flags syn tcp dport ssh \
    add @flood { ip saddr limit rate over 10/second } \
    add @blackhole { ip saddr } \
    drop

# inspect state of the sets.
nft list set ip filter flood
nft list set ip filter blackhole

# manually add two addresses to the blackhole.
nft add element filter blackhole { 10.2.3.4, 10.23.1.42 }

๐ŸŒŸ MAP STATEMENT ๐ŸŒŸ

The map statement looks up data based on some specific input key. like a little lookup table but make it fashion ๐Ÿ’…

expression map { MAP_ELEMENTS }

MAP_ELEMENTS := MAP_ELEMENT [, MAP_ELEMENTS]
MAP_ELEMENT  := key : value

The key is a value returned by expression.

Using the map statement ๐Ÿ—บ๏ธ:

# select DNAT target based on TCP dport:
# connections to port 80 are redirected to 192.168.1.100,
# connections to port 8888 are redirected to 192.168.1.101
nft add rule ip nat prerouting dnat tcp dport map { 80 : 192.168.1.100, 8888 : 192.168.1.101 }

# source address based SNAT:
# packets from net 192.168.1.0/24 will appear as originating from 10.0.0.1,
# packets from net 192.168.2.0/24 will appear as originating from 10.0.0.2
nft add rule ip nat postrouting snat to ip saddr map { 192.168.1.0/24 : 10.0.0.1, 192.168.2.0/24 : 10.0.0.2 }

๐Ÿงš VMAP STATEMENT ๐Ÿงš

The verdict map (vmap) statement works analogous to the map statement, but contains verdicts as values. same energy, different payload ๐Ÿ’–

expression vmap { VMAP_ELEMENTS }

VMAP_ELEMENTS := VMAP_ELEMENT [, VMAP_ELEMENTS]
VMAP_ELEMENT  := key : verdict

Using the vmap statement ๐Ÿ”ฅ:

# jump to different chains depending on layer 4 protocol type:
nft add rule ip filter input ip protocol vmap { tcp : jump tcp-chain, udp : jump udp-chain , icmp : jump icmp-chain }

๐Ÿ’€ XT STATEMENT ๐Ÿ’€

This represents an xt statement from the xtables compat interface. It's a fallback if translation is not available or not complete. the backup dancer of statements, and we respect her ๐Ÿ™ˆ

xt TYPE NAME

TYPE := match | target | watcher

Seeing this means the ruleset (or parts of it) were created by iptables-nft and one should use that to manage it.

BEWARE: nftables won't restore these statements. not a drill, not a glitch, they are just gone ๐Ÿ˜ญ

โœจ ADDITIONAL COMMANDS โœจ

okay so these are the extra little commands that ship with nft ๐Ÿ’– they're not the main character but they absolutely ate ๐Ÿ’…


๐Ÿช LIST HOOKS ๐Ÿช

This one shows you the list of functions that have been registered for a given protocol family โ€” including the ones that got registered implicitly by kernel modules like nf_conntrack. sneaky little modules, love that for them ๐Ÿฆ‹

list hooks [family]
list hooks netdev [ device DEVICE_NAME ]

ngl, just plain list hooks is enough to show you everything that's active on the system. But heads up bestie: hooks in the netdev family are tied to a network device. If you don't give a device name, nft will query all network devices in the current network namespace. so many devices. so much tea ๐Ÿ“

Example Usage:

List all active netfilter hooks in either the ip or ip6 stack.

% nft list hooks inet
family ip {
        hook prerouting {
                -0000000400 ipv4_conntrack_defrag [nf_defrag_ipv4]
                -0000000200 ipv4_conntrack_in [nf_conntrack]
                -0000000100 nf_nat_ipv4_pre_routing [nf_nat]
        }
        hook input {
                 0000000000 chain inet filter input [nf_tables]
                +0000000100 nf_nat_ipv4_local_in [nf_nat]
[..]

The above shows a host that has nat, conntrack and ipv4 packet defragmentation enabled. For each hook location for the queried family, a list of active hooks gets shown using the format

priority identifier [module_name]

The priority value is literally what dictates the order in which the hooks get called. The list is sorted, and the lowest number runs first. no cap โœจ

The priority value of hooks registered by the kernel cannot be changed. For basechains registered by nftables, that value corresponds to the priority value you specified in the base chain definition. periodt ๐Ÿ’…

After the numerical value, you get info about the hook. For basechains defined in nftables, that includes the table family, the table name and the basechain's name. For hooks coming from kernel modules, the function name is used instead. ๐Ÿ‘‘

If a module name is given, that means the hook was registered by the kernel module with that name. You can use modinfo module name to get more info about the module. cute little detective work ๐Ÿ”

โš ๏ธ This functionality requires a kernel built with the option CONFIG_NETFILTER_NETLINK_HOOK enabled, either as a module or builtin. The module is named nfnetlink_hook. don't come crying to me if it's not there ๐Ÿ˜ญ

๐Ÿ‘€ MONITOR ๐Ÿ‘€

The monitor command lets you listen to Netlink events produced by the nf_tables subsystem. These are either related to creation and deletion of objects, or to packets for which meta nftrace was enabled. When they happen, nft prints the monitored events to stdout in either JSON or native nft format. it's giving surveillance but make it legal ๐ŸŽ€

monitor [new | destroy] MONITOR_OBJECT
monitor trace

MONITOR_OBJECT := tables | chains | sets | rules | elements | ruleset

To filter events related to a concrete object, use one of the keywords in MONITOR_OBJECT. ๐Ÿฆ‹

To filter events related to a concrete action, use the keyword new or destroy. ๐Ÿ’€

The second form of invocation takes no further options and exclusively prints events generated for packets with nftrace enabled. one-track mind, honestly iconic.

Hit ^C to finish the monitor operation. bye bestie ๐Ÿ‘‹

Listen to all events, report in native nft format.

% nft monitor

Listen to deleted rules, report in JSON format.

% nft -j monitor destroy rules

Listen to both new and destroyed chains, in native nft format.

% nft monitor chains

Listen to ruleset events such as table, chain, rule, set, counters and quotas, in native nft format.

% nft monitor ruleset

Trace incoming packets from host 10.0.0.1.

% nft add rule filter input ip saddr 10.0.0.1 meta nftrace set 1
% nft monitor trace

๐Ÿ’… ERROR REPORTING ๐Ÿ’…

When an error is detected, nft shows you the line(s) containing the error, the position of the erroneous parts in the input stream, and marks up the erroneous parts using carets (^). If the error results from the combination of two expressions or statements, the part imposing the constraints which are violated is marked using tildes (~). it's like the manpage is pointing at you. respectfully ๐Ÿซถ

For errors returned by the kernel, nft cannot detect which parts of the input caused the error, so the entire command gets marked. not a single crumb left un-blamed ๐Ÿ˜ญ

Error caused by single incorrect expression.

<cmdline>:1:19-22: Error: Interface does not exist
filter output oif eth0
                  ^^^^

Error caused by invalid combination of two expressions.

<cmdline>:1:28-36: Error: Right hand side of relational expression (==) must be constant
filter output tcp dport == tcp dport
                        ~~ ^^^^^^^^^

Error returned by the kernel.

<cmdline>:0:0-23: Error: Could not process rule: Operation not permitted
filter output oif wlan0
^^^^^^^^^^^^^^^^^^^^^^^

๐Ÿšช EXIT STATUS ๐Ÿšช

On success, nft exits with a status of 0. Slay. โœจ

Unspecified errors cause it to exit with a status of 1, memory allocation errors with a status of 2, and unable to open Netlink socket with 3. memorize the numbers bestie, iykyk ๐Ÿ’–


๐ŸŒˆ SEE ALSO ๐ŸŒˆ

libnftables(3), libnftables-json(5), iptables(8), ip6tables(8), arptables(8), ebtables(8), ip(8), tc(8)

There is an official wiki at: https://wiki.nftables.org โ€” go read it, it's lowkey great ๐Ÿ“š


๐Ÿ‘‘ AUTHORS ๐Ÿ‘‘

nftables was written by Patrick McHardy and Pablo Neira Ayuso, among many other contributors from the Netfilter community. legends, every single one of them ๐Ÿซถ


Copyright ยฉ 2008-2014 Patrick McHardy <kaber@trash.net> Copyright ยฉ 2013-2018 Pablo Neira Ayuso <pablo@netfilter.org>

nftables is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.

This documentation is licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 license, CC BY-SA 4.0 http://creativecommons.org/licenses/by-sa/4.0/.


and that's the whole page, bestie ๐ŸŽ€ we made it to the end of NFT(8) โ€” this was the manual page for nftables, dated 12/05/2025 and it's version NFT(8), from the section 8 of the manual. you read all 32 parts, you're literally the main character of Unix rn ๐Ÿ’–โœจ๐Ÿ‘‘ periodt ๐Ÿฆ‹๐ŸŒธ

๐Ÿ“ฆ where this page comes from