Importing Importing nftables rules
Importing nftables rules turn an nft ruleset into a moat file.
moat import nftables reads the JSON that nft -j list ruleset prints,
not the text form, and writes the equivalent moat rules, with every nft
rule kept as a comment above the rule it became. moat import nft is the
same command.
$ nft -j list ruleset | moat import nftables -o rules.moat
moat import nftables [FILE] [-o OUT] reads FILE, or stdin when FILE is
left out, and writes to OUT, or stdout. The family comes from the rules
themselves, so --family does not apply. Input that is not
nft -j list ruleset JSON exits with 2.
Chains it reads
moat reads the filter base chains on the ingress (netdev or inet),
prerouting and input hooks, ordered by hook and then by priority, the
order the kernel runs them in. Rules are named after their chain,
ingress_N, prerouting_N or input_N, with the table in front when
two chains read share a name. At the end the importer lists the chains
it did not read, with how many rules they have.
accept ends only the chain it is in. When a later chain read has rules,
an accept does not become pass: the rules below it in the same chain
get a not (...) guard, and the later chains still see the packet. This
is the same as ACCEPT in raw and mangle
for iptables.
An example
From a ruleset with an inet filter table, a raw prerouting chain and a meter, moat writes, among other rules:
# set blocklist of table inet filter
const blocklist = {198.51.100.0/24, 203.0.113.7}
# nft: ip saddr 10.0.0.0/8 tcp dport 22 accept
# accept here only skips the rest of chain prerouting of table ip raw; the
# rules below in it leave these packets alone
# nft: udp length < 20 drop
rule prerouting_3 {
when ip4 and udp and udp.len < 20 and not (ip4 and ip.src in {10.0.0.0/8} and tcp and tcp.dport == 22)
then drop
}
# nft: tcp dport { 22, 80, 443, 8000-8100 } accept comment "web and ssh"
rule input_5 { when tcp and tcp.dport in {22, 80, 443, 8000-8100} then pass }
# an accept above could not be translated: check this rule does not drop what it let in
# nft: udp dport 27015 update @flooders { ip saddr limit rate over 50/second burst 100 packets } drop
rule input_8 {
when udp and udp.dport == 27015 and ip4
then limit 50/s per ip.src burst 100
}
# nft: ip daddr 192.0.2.1 udp dport 30120 @th,64,32 4294967295 accept
rule input_14 {
when ip4 and ip.dst == 192.0.2.1 and udp and udp.dport == 30120 and payload[0:4] == 0xffffffff
then pass
}
# not translated (connection state is not visible to XDP): ct state established,related accept
# not translated (jump to chain extra: moat has no chains; copy its rules here by hand): jump extra
Translated
| nftables | moat |
|---|---|
ip saddr, ip6 daddr with addresses, prefixes, ranges and sets | ip.src in {...}; address ranges become prefixes |
ip protocol, ip6 nexthdr, meta l4proto | ip.proto == N, as in ip.proto == 17 for udp |
meta nfproto | ip4, ip6 |
ports, ip ttl, ip6 hoplimit, ip length, udp length | the matching fields, with values, ranges and sets |
icmp type, icmpv6 type and code, by name | icmp.type, icmp.code |
ether type, vlan id | eth.type, vlan.id |
tcp flags syn,rst | any of them: (tcp.flags has {syn} or tcp.flags has {rst}) |
tcp flags == syn,rst | exactly those: tcp.flags == {syn, rst} |
tcp flags & (mask) == value | has and not has |
tcp flags ! a,b | none of those flags set |
| a set of flag combinations | the alternatives joined with or |
@ih,OFF,LEN and @th,OFF,LEN after UDP, compared with a number | payload[o:l] == 0x..., whole bytes, at most 8 |
limit rate over R/second|minute|hour burst B with drop | limit, see below |
meter m { ip saddr limit rate over R ... } drop | limit R per ip.src burst B |
update @m { ip saddr limit rate over R ... } drop | the same; depending on its version, nft prints a meter in one form or the other |
update @set { ip saddr } | add ip.src to set, with its ttl |
add @set { ip saddr } without a timeout | add ip.src to set |
accept, drop, reject | pass, drop, drop (XDP cannot answer) |
After @th on UDP, moat subtracts the 8-byte UDP header, since its
payload starts after the transport header.
ip.proto also matches fragments, as nft does, where the udp test
would not. For ip6 nexthdr the rule carries a comment: nft compares the
first next header, moat the one after the extension headers. ip saddr
in a meter or a set update makes the rule IPv4 only, ip6 saddr IPv6
only. A rule with no test at all becomes when ip in an inet, ip or ip6
chain, since those hooks only see IP (so ARP is left alone), and
when eth on netdev ingress. ICMPv6 codes are read by their ICMPv6
names and numbers.
A limit in packets translates when the rate is a whole number per
second. A limit in bytes translates exactly as bits per second; its burst
in bytes is one period's worth of the rate plus the nft burst, which is
how the kernel sizes the bucket, and a burst of 0 is allowed. A limit without over accepts below its rate, where
moat's limit acts above it, so it stays as a comment.
Sets
Named sets with fixed elements become constants, const NAME = {...}.
Sets that change at run time, with the dynamic or timeout flag or
without elements, become set NAME addresses, with a ttl when the set
has a timeout. A set a meter updates holds that meter's state, which moat
keeps inside limit, so a rule testing membership of it stays as a
comment.
Not translated
These stay in the file as # not translated (REASON): RULE:
- connection state (
ct): XDP runs before connection tracking; - interfaces (
iif,iifnameand the like): moat applies to the interface it is loaded on; meta pkttype,notrack;jumpandgoto: moat has no chains, so copy the rules of the target chain by hand. They count as an accept that could not be translated, so the drops after them get the usual warning;add @set { ... }when the set or the element has a timeout: nft's add does not renew an entry that is already there, and moat's does;limit ratewithoutover;- a match written after a
limitor set statement in the same rule; - any other statement or match moat cannot express exactly.
The warnings work as for iptables: drops after an accept that could not be translated get a comment and a warning, and a drop that would cut everything such an accept let through is left out.
Tables written by iptables-nft, such as Docker's or ufw's on an nft system, show their extensions as xt without their options. Those rules stay as comments, and a warning says to import the output of iptables-save with moat import iptables instead.
A policy of drop is imported as default pass, with a comment, for the
same reason as with iptables: moat has no connection state. Load the
result with --audit first, as in
migrating safely.