# Importing nftables rules

> Convert an nftables ruleset to an XDP filter with moat import nftables: which chains are read, how sets, meters and limits translate, and what stays a comment.

Source: https://moatfw.dev/docs/import-nftables

`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.

```sh
$ 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](https://moatfw.dev/docs/import-iptables#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:

```moat
# 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`, `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](https://moatfw.dev/docs/import-iptables#warnings-to-read) 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](https://moatfw.dev/docs/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](https://moatfw.dev/docs/import-iptables#migrating-safely).
