Turning the nftables#
Fri, 02 Jun 2023 23:16:32 +0000
In the course of Liminix hacking it has become apparent that I need to understand the new Linux packet filtering ("firewall") system known as nftables
The introductory documentation for nftables is a textbook example of pattern 1 in Julia Evans Patterns in confusing explanations document. I have, nevertheless, read enough of it that I now think I understand what is going on, and am ready to attempt the challenge of describing
nftables without comparing to ip{tables,chains,fw}
We start with a picture:
This picture shows the flow of a network packet through the Linux kernel. Incoming packets are received from the driver on the far left and flow up to the aplication layer at the top, or rightwards to the be transmitted through the driver on the right. Locally generated packets start at the top and flow right.
The round-cornered rectangles depict hooks, which are the places where we can use nftables to intercept the flow and handle packets specially. For example:
- if we want to drop packets before they reach userspace (without affecting forwarding) we could do that in the "input" hook.
- if we want to do NAT - i.e. translate the network addresses embedded in packets from an internal 192.168.. (RFC 1918) network to a real internet address, we'd do that in the "postrouting" hook (and so that we get replies, we'd also do the opposite translation in the "prerouting" hook)
- if we're being DDoSed, maybe we want to drop packets in the "ingress" hook before they get any further.
The picture is actually part of the docs and I think it should be on the first page.
Chains and rules
A chain (more specifically, a "base chain") is registered with one of the hooks in the diagram, meaning that all the packets seen at that point will be sent to the chain. There may be multiple chains registered to the same hook: they get run in priority order (numerically lowest to highest), and packets accepted by an earlier chain are passed to the next one.
Each chain contains rules. A rule has a match - some criteria to decide which packets it applies to - and an action which says what should be done when the match succeeds.
A chain has a policy (accept
or drop
) which says what happens if a packet gets to the end of the chain without matching any rules.
You can also create chains which aren't registered with hooks, but are called by other chains that are. These are termed "regular chains" (as distinct from "base chains"). A rule with a jump
action will execute all the rules in the chain that's jumped to, then resume processing the calling chain. A rule with a goto
action will execute the new chain's rules in place of the rest of the current chain, and then the packet will be accepted or dropped as per the policy of the base chain.
[ Open question: the doc claims that a regular chain may also have a policy, but doesn't describe how/whether the policy applies when processing reaches the end of the called chain. I think this omission may be because it is incorrect in the first claim: a very sketchy reading of the source code suggests that you can't specify policy when creating a chain unless you also specify the hook. Also, it hurts my brain to think about it. ]
Chain types
A chain has a type, which is one of filter
, nat
or route
.
filter
does as the name suggests: filters packets.nat
is used for NAT - again, as the name suggests. It differs fromfilter
in that only the first packet of a given flow hits this chain; subsequent packets bypass it.route
allows changes to the content or metadata of the packet (e.g. setting the TTL, or packet mark/conntrack mark/priority) which can then be tested by policy-based routing (see ip-rule(8)) to send the packet somewhere non-usual. After the route chain runs, the kernel re-evaluates the packet routing decision - this doesn't happen for other chain types.route
only works in theoutput
hook.
Tables
Chains are contained in tables, which also contain sets, maps, flowtables, and stateful objects. The things in a table must all be of the same family, which is one of
ip
- IPv4 trafficip6
- IPv6 trafficinet
- IPv4 and IPv6 traffic. Rules in an inet chain may match ipv4 or ipv6 or higher-level protocols: an ipv6 packet won't be tested against an ipv4 rule (or vice versa) but a rule for a layer 3 protocol (e.g. UDP) will be tried against both. (Some people [who?] claim this family is less useful than you might first think it would be and in practice you just end up writing separate but similar chains forip
andip6
)arp
- note per the diagram that there is a disjoint set of hooks for ARP traffic, which allow only chains inarp
tablesbridge
- similarly, another set of hooks for bridge trafficnetdev
- for chains attached to theingress
andegress
hooks, which are tied to a single network interface and see all traffic on that interface. This hook/chain type gives great power but correspondingly great faff levels, because the packets are still pretty raw. For example, the ingress chain runs before fragmented datagrams have been reassembled, so you can't match e.g. UDP destination port as it might not be present in the first fragment.
There's a handy summary in the docs describing which chains work with which families and which tables.
What next?
I hope that makes sense. I hope it's correct :-). I haven't explained anything about the syntax or CLI tools because there are perfectly good docs for that already which you now have the background to understand.
Now I'm going to read the script I cargo-culted when I wanted to see if Liminix packet forwarding was working, and replace/update it to perform as an adequate and actually useful firewall