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,--filefilename โ 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,--definename=value โ Define a variable. You can only combine this option with-f.-i,--interactiveโ Read input from an interactive readline CLI. You can usequitto exit, or use the EOF marker, normally this isCTRL-D.-I,--includepathdirectory โ 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-cto 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/passwdand/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 usingadd,insertorreplacecommands, print notifications just likenft monitor.-j,--jsonโ Format output in JSON. Seelibnftables-json(5)for a schema description.-d,--debuglevel โ Enable debugging output. The debug level can be any ofscanner,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 thepreroutinghook. 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 theinputhook.forwardโ Packets forwarded to a different host are processed by theforwardhook.outputโ Packets sent by local processes are processed by theoutputhook.postroutingโ All packets leaving the system are processed by thepostroutinghook.ingressโ All packets entering the system are processed by this hook. It's invoked before layer 3 protocol handlers, hence before thepreroutinghook, and it can be used for filtering and policing.ingressis only available for the Inet family (since Linux kernel5.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 theinputhook.outputโ Packets send by the local system are processed by theoutputhook.
๐ฆ 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 aftertc ingressand 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 beforetc 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 theaddcommand, 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,hookandpriorityparameters 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.
- Families:
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).
- Families:
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.
- Families:
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
netdevfamily supports merely two combinations, namelyfiltertype withingresshook andfiltertype withegresshook. Base chains in this family also require theDEVICEparameter to be present since they exist per interface only. - The
arpfamily supports only theinputandoutputhooks, both in chains of typefilter. - The
inetfamily also supports theingresshook (since Linux kernel 5.10), to filter IPv4 and IPv6 packet at the same location as thenetdevingress hook. Thisinethook allows you to share sets and maps between the usualprerouting,input,forward,output,postroutingand thisingresshook.
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: allmangleโ value-150โ families:ip,ip6,inetโ hooks: alldstnatโ value-100โ families:ip,ip6,inetโ hooks:preroutingfilterโ value0โ families:ip,ip6,inet,arp,netdevโ hooks: allsecurityโ value50โ families:ip,ip6,inetโ hooks: allsrcnatโ value100โ families:ip,ip6,inetโ hooks:postrouting
Table 7. Standard priority names and hook compatibility for the bridge family
dstnatโ value-300โ hooks:preroutingfilterโ value-200โ hooks: alloutโ value100โ hooks:outputsrcnatโ value300โ 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 asaddexcept the rule is inserted at the beginning of the chain, or before the specified rule.replaceโ Similar toadd, 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.*.forwardingornet.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
jumpandgoto). 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 usesdropas its policy, then that same base chain (or a regular chain directly or indirectly called by it) must contain at least one matchingacceptrule, 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
returnverdict 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
gotoverdicts โ 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,returnandcontinueโ as well as statements that imply a verdict (likerejector 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 acceptthecounterstatement is processed, but in... accept counterit 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,marktypeofโ data type of set element. type: expression to derive the data type fromflagsโ 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,sgc-intervalโ garbage collection interval, only available when timeout or flag timeout are active. type: string, decimal followed by unit. units are:d,h,m,selementsโ elements contained by the set. type: set data typesizeโ 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],memoryauto-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 ofperformance[default] ormemoryโ๏ธ
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 theadd,updateordeletekeywords ๐ฅintervalโ set must be able to store intervals (ranges). Cannot be combined with thedynamicflag. 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 flagtimeoutโฐ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. ๐
Unlikeiptables, helper assignment needs to be performed after the conntrack lookup has completed, for example with the default0hook 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 integerl3protoโ 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 integertimeoutโ timeout value for the expectation. type: unsigned integersizeโ size value for expectation. type: unsigned integerl3protoโ 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 argumentsusedโ initial value of used quota. type: two arguments, unsigned integer (64 bit) and string:bytes,kbytes,mbytescommentโ 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 expressionbit[,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,ackmatches packets that have the SYN flag, the ACK flag, or both SYN and ACK set. Other flags are ignored. Meanwhiletcp 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,relatedandct state { established, related }match exactly the same packets, because the bits ofct stateare 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)"
LINK LAYER ADDRESS TYPE ๐ฆ
- 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โ value1missingโ value0
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โ0destination-unreachableโ3source-quenchโ4redirectโ5echo-requestโ8router-advertisementโ9router-solicitationโ10time-exceededโ11parameter-problemโ12timestamp-requestโ13timestamp-replyโ14info-requestโ15info-replyโ16address-mask-requestโ17address-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โ1packet-too-bigโ2time-exceededโ3parameter-problemโ4echo-requestโ128echo-replyโ129mld-listener-queryโ130mld-listener-reportโ131mld-listener-doneโ132mld-listener-reductionโ132nd-router-solicitโ133nd-router-advertโ134nd-neighbor-solicitโ135nd-neighbor-advertโ136nd-redirectโ137router-renumberingโ138ind-neighbor-solicitโ141ind-neighbor-advertโ142mld2-listener-reportโ143
๐ ok so noticemld-listener-doneandmld-listener-reductionboth map to132โ 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โ value1๐establishedโ value2๐คrelatedโ value4๐ธnewโ value8โจuntrackedโ value64๐
๐งพ Table 23. conntrack direction (ct_dir)
originalโ value0๐ซถreplyโ value1๐
๐งพ Table 24. conntrack status (ct_status)
expectedโ value1โ Expected connection; conntrack helper set it up ๐ผseen-replyโ value2โ Conntrack has seen packets in both directions ๐assuredโ value4โ Conntrack entry will not be removed if hash table is full ๐confirmedโ value8โ Initial packet processed โsnatโ value16โ Original source address differs from reply destination ๐ญdnatโ value32โ Original destination differs from reply source ๐ฏseq-adjustโ value64โtcpsequence number rewrite due to conntrack helper orsynproxy๐งฎsnat-doneโ value128โ tried to find matchingsnat/masqueraderule ๐ต๏ธdnat-doneโ value256โ tried to find matchingdnat/redirectrule ๐dyingโ value512โ Connection about to be deleted โฐ๏ธfixed-timeoutโ value1024โ entry expires even if traffic is active โฐhelperโ value8192โ connection is monitored by conntrack helper ๐งoffloadโ value16384โ connection is offloaded to a flow table ๐hw-offloadโ value32768โ connection is offloaded to hardware ๐ฅ๏ธ
๐งพ Table 25. conntrack event bits (ct_event)
newโ value1โจrelatedโ value2๐destroyโ value4๐ฅreplyโ value8โฉ๏ธassuredโ value16๐ก๏ธprotoinfoโ value32๐helperโ value64๐งmarkโ value128๐๏ธseqadjโ value256๐ขsecmarkโ value512๐ท๏ธlabelโ value1024๐
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โ0responseโ1dataโ2ackโ3dataackโ4closereqโ5closeโ6resetโ7syncโ8syncackโ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 givinginteger (32-bit)โจnfprotoโ the real hook protocol family. useful only in theinettable, 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_typepriorityโ TC packet priority, iconic honestly. type:tc_handlemarkโ the packet mark. type:markiifโ input interface index. type:iface_indexiifnameโ input interface name (the one you actually remember ๐ซถ). type:ifnameiiftypeโ input interface type. type:iface_typeoifโ output interface index. type:iface_indexoifnameโ output interface name. type:ifnameoiftypeโ output interface hardware type. type:iface_typesdifโ slave device input interface index. type:iface_indexsdifnameโ slave device interface name. type:ifnameskuidโ the UID tied to the originating socket. type:uidskgidโ the GID tied to the originating socket. type:gidrtclassidโ routing realm. type:realmibrnameโ input bridge interface name. type:ifnameobrnameโ output bridge interface name. type:ifnamepkttypeโ packet type. type:pkt_typecpuโ the cpu number that's processing the packet. type:integer (32 bit)iifgroupโ incoming device group. type:devgroupoifgroupโ outgoing device group. type:devgroupcgroupโ control groupnet_cls.classid(and for matching on cgroupv2, seesocket 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)orpacket receptionโ type:stringdayโ Day of week. Type:Integer (8 bit)orstringhourโ Hour of day. Type:String value in the form HH:MM or HH:MM:SS. Values are expected to be less than24:00, although for technical reasons,23:59:60is accepted, too.
Table 28. Meta expression specific types โจ
iface_indexโ Interface index (32 bitnumber). Can be specified numerically or as name of an existing interface. either way bestie, it works ๐ซถifnameโ Interface name (16 bytestring). Does not have to exist. no cap, it can just be a vibeiface_typeโ Interface type (16 bitnumber).uidโ User ID (32 bitnumber). Can be specified numerically or as user name.gidโ Group ID (32 bitnumber). Can be specified numerically or as group name.realmโ Routing Realm (32 bitnumber). Can be specified numerically or as symbolic name defined in/etc/iproute2/rt_realms.devgroup_typeโ Device group (32 bitnumber). 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 bytestring). See TYPES inip-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 between0and6. Strings are matched case-insensitively, and a full match is not expected (e.g."Mon"would match"Monday"). When an integer is given,0is Sunday and6is 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:00and17:00:00would 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 theIP_TRANSPARENTsocket option in the found socket. It can be0or1. 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 (like0.0.0.0or::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 atpf.osfile. 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 thetyperesult.
๐ 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 isiface_indexcheckโ Output interface check, result typebooleanoifnameโ Output interface name, result typeifnametypeโ Address type, result typefib_addrtype(peepnft describe fib_addrtypeif 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:realmnexthopโ Routing nexthop โ type:ipv4_addr/ipv6_addrmtuโ 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_addrdaddrโ Destination address of the tunnel โ type:ipv4_addr/ipv6_addr
Note: When usingxfrm_interface, this expression is not useable inoutputhook as the plain packet does not traverse it with IPsec info attached โ use a chain inpostroutinghook 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_addrsaddrโ Source MAC address โether_addrtypeโ 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_typehlenโ Hardware address len โinteger (8 bit)plenโ Protocol address len โinteger (8 bit)operationโ Operation โarp_opsaddr etherโ Ethernet sender address โether_addrdaddr etherโ Ethernet target address โether_addrsaddr ipโ IPv4 sender address โipv4_addrdaddr 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 scalingdscpโ Differentiated Services Code Point โ type:dscpecnโ Explicit Congestion Notification โ type:ecnlengthโ 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_protochecksumโ IP header checksum โ type:integer (16 bit)saddrโ Source address โ type:ipv4_addrdaddrโ Destination address โ type:ipv4_addr
โ ๏ธ Be careful with matching onip 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 than65535(seeman ip-link(8), specificallygro_ipv4_max_sizeandgso_ipv4_max_size), thenip lengthmight be0for such jumbo packets.meta lengthlets 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_typecodeโ 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_typemrtโ 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:dscpecnโ Explicit Congestion Notification โ type:ecnflowlabelโ Flow label โ type:integer (20 bit)lengthโ Payload length โ type:integer (16 bit)nexthdrโ Nexthdr protocol โ type:inet_protohoplimitโ Hop limit โ type:integer (8 bit)saddrโ Source address โ type:ipv6_addrdaddrโ Destination address โ type:ipv6_addr
careful with matching onip6 lengthbestie, 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 than65535(seeman ip-link(8), specificallygro_max_sizeandgso_max_size), thenip6 lengthmight be0for such jumbo packets.meta lengthallows 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_typecodeโ 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_addrdaddrโ 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_servicedportโ Destination port โinet_servicelengthโ 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_servicedportโ Destination port โinet_servicechecksumโ 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_servicedportโ Destination port โinet_servicevtagโ 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" energylengthโ 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 numberstreamโ 16 bits wide โ lives in data โ stream identifierssnโ 16 bits wide โ lives in data โ stream sequence numberppidโ 32 bits wide โ lives in data โ payload protocol identifierinit-tagโ 32 bits wide โ lives in init, init-ack โ initiate taga-rwndโ 32 bits wide โ lives in init, init-ack, sack โ advertised receiver window creditnum-outbound-streamsโ 16 bits wide โ lives in init, init-ack โ number of outbound streamsnum-inbound-streamsโ 16 bits wide โ lives in init, init-ack โ number of inbound streamsinitial-tsnโ 32 bits wide โ lives in init, init-ack โ initial transmit sequence numbercum-tsn-ackโ 32 bits wide โ lives in sack, shutdown โ cumulative transmission sequence number acknowledgednum-gap-ack-blocksโ 16 bits wide โ lives in sack โ number of Gap Ack Blocks includednum-dup-tsnsโ 16 bits wide โ lives in sack โ number of duplicate transmission sequence numbers receivedlowest-tsnโ 32 bits wide โ lives in ecne, cwr โ lowest transmission sequence numberseqnoโ 32 bits wide โ lives in asconf-ack, asconf โ sequence numbernew-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, typeinet_service๐dportโ the destination port, typeinet_service๐typeโ the packet type, typedccp_pkttype๐
๐ AUTHENTICATION HEADER EXPRESSION ๐
ah {nexthdr | hdrlength | reserved | spi | sequence}
Table 50. AH header expression ๐
nexthdrโ next header protocol, typeinet_proto๐ธhdrlengthโ AH header length, typeinteger (8 bit)๐ธreservedโ reserved area, typeinteger (16 bit)๐ธspiโ Security Parameter Index, typeinteger (32 bit)๐ธsequenceโ sequence number, typeinteger (32 bit)๐ธ
๐ซง ENCRYPTED SECURITY PAYLOAD HEADER EXPRESSION ๐ซง
esp {spi | sequence}
Table 51. ESP header expression โจ
spiโ Security Parameter Index, typeinteger (32 bit)๐sequenceโ sequence number, typeinteger (32 bit)๐
๐๏ธ IPCOMP HEADER EXPRESSION ๐๏ธ
comp {nexthdr | flags | cpi}
Table 52. IPComp header expression ๐
nexthdrโ next header protocol, typeinet_proto๐ฆflagsโ flags, typebitmask๐ฆcpiโ compression Parameter Index, typeinteger (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, typeinteger (5 bit)โญversionโ gre version field, 0 for GRE and 1 for PPTP, typeinteger (3 bit)โญprotocolโ EtherType of encapsulated packet, typeinteger (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, typeinteger (16 bit)๐vniโ Virtual Network ID (VNI), typeinteger (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, typeinteger (8 bit)โจvniโ Virtual Network ID (VNI), typeinteger (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:nftwill not add dependencies for Raw payload expressions. so if you wanna match protocol fields of a transport header with protocol number5, you gotta manually exclude packets that have a different transport header โ for instance by usingmeta l4proto 5before 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,sizewindowโ TCP Window Scaling. fields:length,countsack-permโ TCP SACK permitted. field:lengthsackโ TCP Selective Acknowledgement (alias of block 0). fields:length,left,rightsack0โ TCP Selective Acknowledgement (block 0). fields:length,left,rightsack1โ TCP Selective Acknowledgement (block 1). fields:length,left,rightsack2โ TCP Selective Acknowledgement (block 2). fields:length,left,rightsack3โ TCP Selective Acknowledgement (block 3). fields:length,left,righttimestampโ TCP Timestamps. fields:length,tsval,tsecrmptcpโ 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,addrraโ Router Alert. fields:length,valuerrโ Record Route. fields:length,ptr,addrssrrโ 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, typect_state๐ธdirectionโ which way the packet is going relative to the connection, typect_dir๐ฆstatusโ the status of the connection, typect_statusmarkโ the connection mark, typemark๐expirationโ how long until the connection expires, typetimeโณhelperโ the helper that's associated with the connection, typestring๐งlabelโ a connection tracking label bit, or a symbolic name defined inconnlabel.confin the nftables include path, typect_label๐ท๏ธl3protoโ the Layer 3 protocol of the connection, typenf_protosaddrโ the source address of the connection for the given direction, typeipv4_addr/ipv6_addr๐daddrโ the destination address of the connection for the given direction, typeipv4_addr/ipv6_addrprotocolโ the Layer 4 protocol of the connection for the given direction, typeinet_protoproto-srcโ the Layer 4 protocol source for the given direction, typeinteger (16 bit)proto-dstโ the Layer 4 protocol destination for the given direction, typeinteger (16 bit)packetsโ the packet count seen in the given direction, or the sum of original and reply, typeinteger (64 bit)๐ฆbytesโ the byte count seen, check the description for thepacketskeyword, typeinteger (64 bit)avgpktโ the average bytes per packet, check the description for thepacketskeyword, typeinteger (64 bit)๐zoneโ the conntrack zone, typeinteger (16 bit)countโ the number of current connections, typeinteger (32 bit)idโ the connection id, typect_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 โ anacceptverdict in a chain of theforwardhook still allows you to drop the packet in anotherforwardhook base chain (or a chain called from it) that has a higher priority number, or in a chain attached to thepostroutinghook. ๐ญ
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 implicitreturnverdict 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 asjumpexcept 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 viagotoverdicts), 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,warnis 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โ0host-unreachableโ1prot-unreachableโ2port-unreachableโ3frag-neededโ4net-prohibitedโ9host-prohibitedโ10admin-prohibitedโ13
Table 64. keywords may be used to reject when specifying the ICMPv6 code
no-routeโ0admin-prohibitedโ1addr-unreachableโ3port-unreachableโ4policy-failโ5reject-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โ0port-unreachableโ1host-unreachableโ2admin-prohibitedโ3
The common default ICMP code to reject is port-unreachable. Periodt.
Note that inbridgefamily,rejectstatement is only allowed in base chains which hook intoinputorprerouting.
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 stringmarkโ Connection tracking mark โ value:marklabelโ Connection tracking label โ value:labelzoneโ 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_handlemarkโ Packet mark โ value:markpkttypeโ packet type โ value:pkt_typenftraceโ ruleset packet tracing on/off. Usemonitor tracecommand to watch traces โ value:0,1brouteโ 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,natstatements require bothpreroutingandpostroutingbase 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 asfully-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 theIP_TRANSPARENToption is bound to.portโ the port that the listening socket with theIP_TRANSPARENToption 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 is0. 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, readlibnetfilter_queuedocumentation 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
setnamemust already exist in the given table and must have been created with one or both of thedynamicand thetimeoutflags. - The
dynamicflag is required if the set statement expression includes a stateful object. - The
timeoutflag 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 optionCONFIG_NETFILTER_NETLINK_HOOKenabled, either as a module or builtin. The module is namednfnetlink_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 ๐
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
- ubuntu resolute: nftables 1.1.6-1 ยท GPL-2.0-only
- ubuntu stonking: nftables 1.1.6-1 ยท GPL-2.0-only
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms