rules¶
The rules file holds per-connection rules — the explicit exceptions evaluated before the
policy defaults. It carries two kinds of entry:
- filter rules — accept, drop, or reject a specific class of connection;
DNATrules — port-forward an inbound connection to an internal host.
Each non-comment line is one entry; columns are whitespace-separated; blank lines and #
comments are ignored. The file is preprocessed first, so
params substitution and the ?if/?FORMAT/?SECTION directives apply. A - in any optional
column means "unspecified".
Filter rules¶
#ACTION SOURCE DEST [PROTO] [DEST PORT] [SOURCE PORT] [ORIGINAL DEST] [RATE LIMIT] [USER-GROUP] [MARK] [CONNLIMIT]
| Column | Index | Required | Value |
|---|---|---|---|
ACTION |
0 | yes | A built-in verdict (ACCEPT/DROP/REJECT) or a macro/action name (see Actions). |
SOURCE |
1 | yes | zone or zone:host — the originating zone, optionally narrowed to a host/CIDR. |
DEST |
2 | yes | zone or zone:host — the destination zone, optionally narrowed to a host/CIDR. |
PROTO |
3 | no | tcp, udp, icmp, or ipv6-icmp (case-insensitive). |
DEST PORT |
4 | no | Destination port(s). Requires a PROTO. |
SOURCE PORT |
5 | no | Source port(s). Requires a PROTO. |
ORIGINAL DEST |
6 | no | Not implemented. Must be - if a later column is used. |
RATE LIMIT |
7 | no | <rate>/<interval>[:<burst>] — rate-limits new connections (see RATE LIMIT and CONNLIMIT below). |
USER-GROUP |
8 | no | Not implemented. Must be - if a later column is used. |
MARK |
9 | no | Not implemented. Must be - if a later column is used. |
CONNLIMIT |
10 | no | <count> — caps simultaneous connections (see RATE LIMIT and CONNLIMIT below). |
Columns are positional: to reach RATE LIMIT or CONNLIMIT, every intervening unimplemented
column must be -. A non-- value in ORIGINAL DEST, USER-GROUP, or MARK is a hard error,
and any twelfth column is a hard error too — nothing past CONNLIMIT is recognized.
Filter rules are placed ahead of the policy defaults in the same base chain, so an explicit
verdict wins over the zone-pair default. Chain selection and interface-based zone matching are
identical to policy: source firewall zone → output, dest
firewall zone → input, else forward.
SOURCE / DEST and zone:host narrowing¶
The bare zone form matches by interface, exactly as a policy does. The zone:host form adds an
address match — a source host narrows on saddr, a destination host on daddr. host is an
IPv4/IPv6 address or a CIDR (192.0.2.0/24, 2001:db8::5). The literal's family determines the
match (ip vs ip6) and is itself the family guard — no extra family match is added.
+setname — named-set matching¶
A SOURCE or DEST column may be a named-set reference instead of a zone: +setname matches
any host in the declared set setname, and !+setname matches any host not in it. The set
must be declared in the sets file, which fixes its address family (ipv4/ipv6/both):
#ACTION SOURCE DEST PROTO DEST PORT
ACCEPT +goodguys fw tcp 22
DROP !+goodguys fw
ACCEPT net +web tcp 443
The set's declared family scopes the rule the same way an address literal does: a ipv4-only set
narrows the rule to IPv4, an ipv6-only set to IPv6, and a both set leaves it dual-stack. A set
referenced against a conflicting family — an ipv4 set with ipv6-icmp, or against a
2001:db8::/32 host on the other side — is a hard error, as is a reference to a set that was never
declared.
PROTO and ports¶
PROTO is stored in nftables' canonical lowercase. A port column requires a protocol; a port
without a proto is a hard error.
| Port form | Example | Compiles to |
|---|---|---|
| single | 22 |
a scalar match |
| list | 80,443 |
an anonymous set { 80, 443 } |
| range | 8000:8100 |
a range 8000-8100 |
A list element may itself be a range, and a non-numeric token (a service name like ssh) passes
through verbatim for nftables to resolve. With ports, one payload match is emitted per column
(dport before sport) and no separate protocol match — nftables folds the protocol dependency
back in on load.
icmp pins a rule to IPv4 and ipv6-icmp to IPv6 (with an ICMP type such as echo-request in
the DEST PORT column). A rule that mixes IPv4 and IPv6 hints — say an IPv4 host on one side and
ipv6-icmp — is a hard error.
RATE LIMIT and CONNLIMIT¶
Both columns gate the rule's verdict: traffic under the limit takes the verdict, traffic over it falls through to whatever is evaluated next (a later rule, or the zone-pair policy).
RATE LIMIT (column 7) is <rate>/<interval>[:<burst>]:
| Field | Value |
|---|---|
rate |
A positive integer. |
interval |
One of sec, min, hour, day (maps to nftables' second/minute/hour/day). |
burst |
Optional positive integer packet-burst allowance; nftables' default applies when omitted. |
10/min:20 limits the rule to 10 new connections per minute with a burst allowance of 20. It
rate-limits new connections only — established/related traffic is already fast-pathed ahead of
this rule (see ?SECTION), so packets 2+ of a flow never
touch the limiter. It compiles to an nft limit rate statement placed immediately before the
verdict. Shorewall's named/shared limiters (a name or s:/d: selector before the rate) are
not implemented and are rejected.
CONNLIMIT (column 10) is a bare <count> — a positive integer cap on the number of
simultaneous connections matching the rule. It compiles to an nft ct count over <count>
statement placed immediately before the verdict (after RATE LIMIT when both are present). The
masked/grouped <count>:<mask> per-source form is not implemented and is rejected.
#ACTION SOURCE DEST PROTO DEST PORT SOURCE PORT ORIGINAL DEST RATE LIMIT USER-GROUP MARK CONNLIMIT
ACCEPT loc net tcp 22 - - 10/min:20 - - -
ACCEPT loc net tcp 22 - - - - - 4
The first rule accepts up to 10 new SSH connections per minute (burst 20); the second caps SSH to 4 simultaneous connections.
Actions and macros¶
The ACTION column is either a built-in verdict or the name of a macro/action, resolved before
validation:
- Built-in verdicts:
ACCEPT→accept,DROP→drop,REJECT→reject. - Built-in macros/actions:
Web(accept inbound TCP 80 and 443) andDropSmb(silently drop SMB/NetBIOS chatter on UDP 137–139/445 and TCP 139/445). - Site definitions: an
action.<Name>file in the config directory defines a custom action; its name may then be used in theACTIONcolumn. A site definition overrides a built-in of the same name.
A macro/action expands to one rule per body line, each narrowed by the call site's source/dest/proto/ports. An unknown action name, or a narrowing whose port/proto intersection is empty, is a hard error citing the offending line.
?SECTION — connection-state ordering¶
A ?SECTION <NAME> directive on its own line sets the connection-state section for the filter
rules that follow. The recognized names, and the order rules are emitted in regardless of where
they appear in the file, are:
| Section | Matches | Notes |
|---|---|---|
ESTABLISHED |
ct state established |
ACCEPT-only (see below). |
RELATED |
ct state related |
ACCEPT-only (see below). |
INVALID |
ct state invalid |
|
NEW |
new connections | The default for a rule written before any ?SECTION. |
Within a section, file order is preserved. An unrecognized section name is a hard error.
ESTABLISHED / RELATED are accept-only
The base filter chains already fast-path ct state {established, related} accept at the top,
so a DROP or REJECT in the ESTABLISHED or RELATED section would be unreachable. The
validator rejects that dead case up front; an ACCEPT there is a redundant no-op.
INVALID and NEW are unaffected.
Filter example¶
#ACTION SOURCE DEST PROTO DEST PORT
ACCEPT net fw tcp 22
Web net fw
ACCEPT loc net tcp 80,443
ACCEPT loc:192.0.2.0/24 net udp 53
ACCEPT net fw icmp echo-request
ACCEPT net fw ipv6-icmp echo-request
This accepts inbound SSH to the firewall, exposes a web service via the Web macro, lets the
LAN reach web and DNS out, and allows IPv4 and IPv6 ping to the firewall. Each rule is reached
before the policy defaults.
DNAT / port-forwarding¶
A DNAT entry forwards an inbound connection to an internal host (a port-forward):
DNAT SOURCE ZONE:HOST[:PORT] [PROTO] [DEST PORT]
| Column | Required | Value |
|---|---|---|
DNAT |
yes | The literal action. |
SOURCE |
yes | The external zone (zone, or all). |
ZONE:HOST[:PORT] |
yes | Target: the internal ZONE, the internal HOST address, and an optional :PORT remap. |
PROTO |
no | tcp/udp/… |
DEST PORT |
no | The external port(s) matched on the way in. Requires a PROTO. |
The target column must include a host (zone:host) or it is a hard error. Any sixth column
is rejected. ?SECTION does not apply to DNAT rows.
A v4 DNAT compiles to two rules:
- a
natpreroutingrule that matches the inbound interface and the externalDEST PORT, then rewrites the destination toHOST(adnattarget); - a
filterforwardrule that admits the translated connection toHOSTthrough the fail-closed forward chain (thenatrule alone would be dropped).
The dedicated inet nat table is emitted only when the config actually has NAT entries.
The :PORT remap¶
An optional :PORT on the target rewrites the destination port. When present, the dnat
target and the forward accept use the remapped internal port, while prerouting still matches
the external DEST PORT. With no remap, the internal host is reached on the same external port.
IPv6 targets take no NAT¶
NAT is IPv4-only. A DNAT whose target is an IPv6 literal does no translation: it compiles
to a plain forward accept to that global IPv6 address (a service is exposed by being globally
routable, not NATed). No nat table or prerouting rule is emitted for it.
Port-forward example¶
Expose services on internal hosts, using documentation address ranges only:
#ACTION SOURCE DEST PROTO DEST PORT
DNAT net loc:192.0.2.10 tcp 80
DNAT net loc:198.51.100.20 tcp 8000:8100
DNAT net loc:203.0.113.30:8022 tcp 22
DNAT net loc:2001:db8::5 tcp 443
Reading top to bottom:
- inbound TCP 80 from
netis forwarded to192.0.2.10:80; - inbound TCP 8000–8100 is forwarded to
198.51.100.20on the same range; - inbound TCP 22 is forwarded to
203.0.113.30and remapped to port 8022; - inbound TCP 443 to the IPv6 host
2001:db8::5is accepted directly (no NAT).
Each forward is admitted through the forward chain, so it works even though the default forward
policy is drop.
See also¶
policy— the inter-zone defaults these rules are layered ahead of.snat— source NAT / masquerading (the outbound counterpart toDNAT).zones/interfaces— where zones and their interfaces are declared.