# Importing iptables rules

> Convert iptables rules to an XDP filter with moat import iptables: INPUT, raw and mangle PREROUTING, ipsets with --ipsets, what translates, what stays a comment, and how to migrate safely.

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

If the host already filters with iptables, you don't have to start from
an empty file. `moat import iptables` reads what `iptables-save` prints
and writes the equivalent moat rules, with every iptables line kept as a
comment above the rule it became.

```sh
$ iptables-save | moat import iptables -o rules.moat
$ { iptables-save; ip6tables-save; } | moat import iptables -o rules.moat
```

The second form imports IPv4 and IPv6 together. `moat import iptables
[FILE] [-o OUT] [--family 4|6]` reads FILE, or stdin when FILE is left out,
and writes to OUT, or stdout. Input it cannot read exits with 2.

It reads the chains a packet arriving for this host goes through, in the
kernel's order: PREROUTING of the raw table, PREROUTING of the mangle
table, then INPUT of the filter table. Their rules are named `raw_N`,
`mangle_N` and `input_N`. Other chains (FORWARD, OUTPUT, the nat table)
are not read; at the end the importer lists those that have rules, with
how many, as in `nat PREROUTING: 2 rules`.

## An example

Given this INPUT chain:

```sh
*filter
:INPUT DROP [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -m set --match-set blocklist src -j DROP
-A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
-A INPUT -s 203.0.113.0/24 -p tcp --dport 22 -j ACCEPT
-A INPUT -p udp --dport 27015 -m hashlimit --hashlimit-above 50/sec --hashlimit-burst 100 --hashlimit-mode srcip --hashlimit-name q -j DROP
COMMIT
```

moat writes (header comments shortened):

```moat
version 1

# The INPUT policy is DROP, but moat has no connection state: ...
default pass

# ipset blocklist, assumed to hold addresses or networks ...
#   moat set sync ipset_blocklist FILE --iface IF
set ipset_blocklist prefixes

# not translated (connection state is not visible to XDP): -A INPUT -m conntrack ...
# an accept above could not be translated: check this rule does not drop what it let in
# iptables: -A INPUT -m set --match-set blocklist src -j DROP
rule input_2 { when ip4 and ip.src in ipset_blocklist then drop }
# iptables: -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
rule input_3 { when ip4 and tcp and tcp.dport in {80, 443} then pass }
# iptables: -A INPUT -s 203.0.113.0/24 -p tcp --dport 22 -j ACCEPT
rule input_4 {
    when ip4 and ip.src in {203.0.113.0/24} and tcp and tcp.dport == 22
    then pass
}
# an accept above could not be translated: check this rule does not drop what it let in
# iptables: -A INPUT -p udp --dport 27015 -m hashlimit --hashlimit-above 50/sec ...
rule input_5 {
    when ip4 and udp and udp.dport == 27015
    then limit 50/s per ip.src burst 100
}
```

On stderr it prints a summary and the warnings described below.

## Translated

| iptables | moat |
|----------|------|
| `-s`, `-d`, `-m iprange --src-range/--dst-range` | `ip.src == ADDR` for one host, `ip.src in {...}` for prefixes and ranges |
| `-p tcp/udp/icmp/icmpv6` with a port or header test | `tcp`, `udp`, `icmp`, `icmp6` |
| `-p udp` alone, `! -p tcp` | `ip.proto == 17`, `ip.proto != 6`, which match fragments too, as iptables does |
| `--dport`, `--sport`, `-m multiport` | `tcp.dport == 22` for one port, `tcp.dport in {...}` for several |
| `--syn`, `--tcp-flags MASK COMP` | conditions on `tcp.flags` |
| `--icmp-type`, `--icmpv6-type` | `icmp.type`, `icmp.code` |
| `-m set --match-set NAME src/dst` | `ip.src in ipset_NAME` and an empty `set ipset_NAME prefixes` |
| `-m ttl`, `-m hl` | `ip.ttl` |
| `-m length --length` | `ip.len` |
| `-m hashlimit --hashlimit-above R` with `-j DROP` | `limit R per ip.src[/N] burst B` |
| `-m string --string`, `--hex-string` (with `--algo`, `--to`) | `payload contains`, with a note and a warning |
| `-m comment --comment` | a comment above the rule |
| `-j ACCEPT`, `-j DROP`, `-j REJECT` | `pass`, `drop`, `drop` (XDP cannot answer) |

`-m string` needs `-p tcp` or `-p udp` and a text of 1 to 32 bytes, and a
file can search for at most 16 different texts. iptables searched the
whole packet, up to `--to`; moat searches the first 256 bytes of the
payload, so the translated rule carries a note and a warning saying so.

## ACCEPT in raw and mangle

In iptables, ACCEPT in raw or mangle only ends that table: the packet
still goes through the tables after it. So when a later table has rules,
such an ACCEPT does not become `pass`. Instead the rules below it in the
same table get a `not (...)` guard, so they leave those packets alone,
and the packet goes on to the next table's rules.

```sh
*raw
-A PREROUTING -s 192.0.2.0/24 -j ACCEPT
-A PREROUTING -p udp --dport 53 -m string --string "ANY" --algo bm --to 200 -j DROP
*filter
-A INPUT -p tcp --dport 22 -j ACCEPT
```

becomes

```moat
# iptables searched the packet up to byte 200, moat searches the first 256 bytes of the payload
rule raw_2 {
    when ip4 and udp and udp.dport == 53 and payload contains "ANY" and not (ip4 and ip.src in {192.0.2.0/24})
    then drop
}
rule input_1 { when ip4 and tcp and tcp.dport == 22 then pass }
```

## Not translated

These stay in the file as `# not translated (REASON): LINE`:

- connection state (`-m state`, `-m conntrack`): XDP runs before
  connection tracking;
- `-m connlimit`: it counts open connections, which XDP does not see;
- `-m recent`: it keeps its own list of sources. An addresses set with
  `add` and a `ttl` does the same, written by hand;
- `-m string` with `--icase` (moat compares bytes exactly), with
  `--from` other than 0, or negated;
- `-m limit` and `hashlimit --hashlimit-upto`: they accept below the rate,
  while a moat `limit` acts above it. Rewrite them as `hashlimit
  --hashlimit-above` with DROP, or by hand;
- `! --icmp-type any`, which never matches;
- targets that do not end the chain (LOG, SET, MARK, CT, NOTRACK and the
  like). They do not count as accepts, so a final DROP after them is
  kept;
- jumps to other chains (`-j MYCHAIN`, `-g`), RETURN and other
  targets. A jump to a chain counts as an accept that could not be
  translated, so the drops after it get the warning below;
- `-i` and `-o`: moat applies to the interface it is loaded on, and
  loopback traffic never passes through it;
- negations moat cannot express exactly, and any match not listed above.

## Warnings to read

The importer prints how many rules it translated, each line it left as a
comment with the reason, the chains it did not read, and these warnings:


- **An accept that could not be translated.** Typically the `ESTABLISHED`
  rule at the top. Every drop rule after it is marked with a comment,
  because in iptables that accept would have let some of those packets
  through first.
- **A catch-all drop after such an accept.** A rule like `-A INPUT -j
  DROP` would drop the established connections and the SSH sessions the
  untranslated accepts allowed, so it is left out with a warning.
- **A drop that would cut everything such an accept let through.** Take
  `-p udp --dport 30120 -m state --state ESTABLISHED -j ACCEPT` followed
  by `-p udp --dport 30120 -j DROP`. Without its state test the accept
  matches UDP to port 30120, and the drop tests nothing more than that, so
  enforcing it would cut the established traffic too. A drop whose tests
  are all among an untranslated accept's own, once the state test is
  removed, is left out with a warning.
- **INPUT policy DROP.** The file still says `default pass`. moat has no
  connection state, so a drop default would also drop the replies to
  connections the host opens itself. Keep iptables behind moat for
  stateful filtering, or switch to `default drop` only once every flow
  the host needs is allowed above it.

## ipsets

iptables-save does not include ipset contents. Save them too and give
both to the importer:

```sh
$ ipset save > sets.save
$ iptables-save > rules.v4
$ moat import iptables rules.v4 --ipsets sets.save -o rules.moat
moat import: wrote ipset_blocklist.txt
```

Every ipset the rules use whose type holds addresses or networks
(`hash:ip`, `hash:net`, `bitmap:ip`) is written as a set file next to the
output, `ipset_NAME.txt`, one entry per line; a range becomes the
networks that cover it exactly, and a set with `netmask N` gives /N
networks. The set is declared from that file, so
it is filled at every load:

```moat
# ipset blocklist (hash:net), 2 entries from ipset save
set ipset_blocklist from "ipset_blocklist.txt"
```

The set is left empty, with a warning, when its type holds more than
addresses (`hash:ip,port`, for example), when it has `nomatch` entries,
or when the rules use it and the save output does not have it. Entries
with a timeout are copied but become permanent, also with a warning.
`--ipsets` needs `-o`, since it writes files next to the rules, and does
not apply to nftables.

Without `--ipsets`, each set becomes an empty `set ipset_NAME prefixes`
to fill once the rules are loaded. Either way, `moat set sync` refreshes
a set later from a newer list:

```sh
$ ipset list blocklist | sed '1,/^Members:/d' | cut -d' ' -f1 > blocklist.txt
$ moat set sync ipset_blocklist blocklist.txt --iface eth0
```

## Migrating safely

1. Import, then read every `not translated` comment and warning.
2. `moat check rules.moat`, and [write a few cases](https://moatfw.dev/docs/testing) for
   the traffic that must get through (SSH from your address, your
   service ports).
3. `sudo moat load rules.moat --iface eth0 --audit`: nothing is dropped
   yet; `moat stats --iface eth0` shows what each rule would drop.
4. When the "would drop" counts look right, `sudo moat audit off --iface
   eth0`.

> moat sees every packet arriving on the interface, before routing and
> NAT, including traffic forwarded to virtual machines and containers, which
> an INPUT chain never sees. Keep iptables running behind moat: moat drops
> the floods cheaply, iptables keeps the stateful rules.
