moat docs ยท language 1
GitHub

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

nftablesmoat
ip saddr, ip6 daddr with addresses, prefixes, ranges and setsip.src in {...}; address ranges become prefixes
ip protocol, ip6 nexthdr, meta l4protoip.proto == N, as in ip.proto == 17 for udp
meta nfprotoip4, ip6
ports, ip ttl, ip6 hoplimit, ip length, udp lengththe matching fields, with values, ranges and sets
icmp type, icmpv6 type and code, by nameicmp.type, icmp.code
ether type, vlan ideth.type, vlan.id
tcp flags syn,rstany of them: (tcp.flags has {syn} or tcp.flags has {rst})
tcp flags == syn,rstexactly those: tcp.flags == {syn, rst}
tcp flags & (mask) == valuehas and not has
tcp flags ! a,bnone of those flags set
a set of flag combinationsthe alternatives joined with or
@ih,OFF,LEN and @th,OFF,LEN after UDP, compared with a numberpayload[o:l] == 0x..., whole bytes, at most 8
limit rate over R/second|minute|hour burst B with droplimit, see below
meter m { ip saddr limit rate over R ... } droplimit R per ip.src burst B
update @m { ip saddr limit rate over R ... } dropthe 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 timeoutadd ip.src to set
accept, drop, rejectpass, 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, iifname and the like): moat applies to the interface it is loaded on;
  • meta pkttype, notrack;
  • jump and goto: 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 rate without over;
  • a match written after a limit or 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.