Score:2

iptables/nftables: how to exclude all forwarded traffic from connection tracking on a router?

ua flag

A Linux box has multiple network interfaces. IP forwarding is enabled for IPv4 and IPv6.

I would like to protect the services running on the router itself via a stateful firewall. For that, connection tracking needs to be enabled. At the same time, I would like to exclude all traffic that is forwarded from one interface to another from connection tracking.

For the stateful firewall, I would typically use the INPUT and OUTPUT chains of the filter table. Forwarded traffic would go to the FORWARD chain. But AFAIK there is no way to mark traffic as untracked in the FORWARD chain. Such logic has to go to the PREROUTING chain in the raw table. But, I believe, in the PREROUTING chain it has not yet been decided whether traffic is forwarded or not.

Connection tracking has many disadvantages, such as packet drops when the list of tracked connection has reached its maximum size.

What is the easiest way to exclude forwarded traffic (and only that forwarded traffic) from connection tracking?

A.B avatar
cl flag
A.B
Can you give your network layout (`ip -br link; ip -br address; ip -4 route; ip -6 route`) and specify if there's any NAT to be expected?
ua flag
Just consider a router with two interfaces, eth0 and eth1 with addresses 192.168.1.1/24 and 192.168.2.1/24.
Score:1
cl flag
A.B

For a generic ruleset, one can ask nftables to do a route lookup in advance using the fib expression instead of waiting for the routing stack to do it. This allows to involve the (future) output interface despite not existing yet (routing decision didn't happen), at the cost of an extra lookup. Then if the results tells the packet will be routed, prevent tracking to happen using a notrack statement.

FIB EXPRESSIONS

fib {saddr | daddr | mark | iif | oif} [. ...] {oif | oifname | type}

A fib expression queries the fib (forwarding information base) to obtain information such as the output interface index a particular address would use. The input is a tuple of elements that is used as input to the fib lookup functions.

NOTRACK STATEMENT

The notrack statement allows to disable connection tracking for certain packets.

notrack

Note that for this statement to be effective, it has to be applied to packets before a conntrack lookup happens. Therefore, it needs to sit in a chain with either prerouting or output hook and a hook priority of -300 or less.

So one should do a "simple" route check from prerouting, using only the destination address as selector and check for the existence of an output interface (non-routable packets or packets intended for the host won't resolve any). There's an exception for the lo (loopback) interface to keep it tracked: while it represents local traffic, a packet sent (through the output path) from host to itself comes back through prerouting path and does have an output interface of lo too. As the outgoing packet already created a conntrack entry, better keep this consistent.

nft add table ip stateless
nft add chain ip stateless prerouting '{ type filter hook prerouting priority -310; policy accept; }'
nft add rule ip stateless prerouting iif != lo fib daddr oif exists notrack

Replacing the ip family with the inet combo family should extend the same generic behavior to IPv4+IPv6.

To be more specific one could specify the future output interface with fib daddr oif eth1 for example, which is more or less the equivalent of oif eth1, but also available in prerouting.

Of course if the topology is known in advance it's possible to avoid a FIB lookup by using one or a few rules based on address tests since the routes are then known in advance by the administrator. Benchmarking the results might be needed to know if this is more interesting than keeping a generic method.

For example, with OP's provided information, replacing the previous rule with:

nft add rule ip stateless prerouting 'ip daddr != { 192.168.1.1, 192.168.2.1, 127.0.0.0/8 } notrack'

should have a near-equivalent effect. 127.0.0.0/8 is present for the same reasons as above with the lo interface.

Handling of broadcast (like 192.168.1.255 received on eth0) and multicast (like link-local 224.0.0.1 received on an interface) might not work the same in both methods nor as expected and would possibly require additional rules for specific needs, especially with the 2nd method. As tracking broadcast and multicast is rarely useful, because a reply source won't (and can't) be the original broadcast or multicast address destination so the conntrack entry will never "see" bidirectional traffic, it usually doesn't matter much for stateful rules.


Notes

  • This will usually not be compatible with stateful NAT.

    My understanding is that DNAT toward a remote host will get its reply traffic not de-NATed and fail, and that forwarded SNAT won't trigger since there was no conntrack entry created. Rarely used SNAT in input path should be fine, and a combo of DNAT+SNAT (using a local address source) might also work since then in both original and reply directions there's a local destination involved so a conntrack entry should then always be correctly created or looked up.

  • standard ruleset

    Actual rules using iptables or nftables (in its own different table) can then be done as usual, including stateful rules for the host itself. As routed traffic won't create conntrack entries, rules if any involving such traffic should stick to be only stateless and not use any ct expression because it would never match.

  • verifying behavior

    One can check the overall behavior even without proper firewall rules by:

    • using a dummy ct rule to be sure the conntrack facility gets registered in the current network namespace.

      nft add table ip mytable
      nft add chain ip mytable mychain '{ type filter hook prerouting priority -150; policy accept; }'
      nft add rule ip mytable mychain ct state new
      
    • use the conntrack tool to follow events:

      conntrack -E
      
    • generate traffic from remote

      NEW conntrack entries will be then created for traffic to be received by the router, but not for routed traffic.

ua flag
That fib expression is exactly what I was looking for. Checking whether daddr is a local address might work. But given that some addresses (e.g. some IPv6) may change over time, this seems unpractical to me.
ua flag
What if there was an unreachable route? Would that traffic also `fib daddr oif exists` also match?
A.B avatar
cl flag
A.B
If there's no route there's no output interface: no notrack. But actually while rules before the routing decision will have `ct state new` match such packets, conntrack won't commit it because it's dropped midway (in the actual routing decision): `conntrack -E` won't display it at all. So goal still achieved. (Beside this, during tests I couldn't manage to get `oif daddr type xxx` to match as expected when there's no route.)
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.