configure my linux as a router, how do enable port forwarding with nftables?

Solution 1:

Once the first packet of a new flow (thus state NEW) traverses the nat prerouting chain, dnat happens and with the new destination the packet is routed and traverses the filter forward chain.

Then this rule drops it:

        iif "enp3s0" oif "enp1s0f0" counter packets 0 bytes 0 drop

A rule to allow this first packet (which is not in established state) is needed.

This could be inserted before the drop rule in a first naive way:

iif "enp3s0" oif "enp1s0f0" ip daddr 192.168.1.2 tcp dport 22 accept

and because of the specific topology: the 192.168.1.0/24 LAN isn't routable so is not reachable by default from Internet, it would probably be good enough (technically the next hop router could cheat and reach 192.168.1.2:22 directly without NAT). But if the system wasn't doing any masquerade (and there was a routable LAN instead of 192.168.1.0/24) this would leave the service reachable directly.

There's actually a simpler and safer method which is also more generic in case some other ports are also dnat-ed and all such dnat rules are all to be allowed: add a rule that allows any packet that underwent a dnat transformation. It's more detailed in the equivalent iptables-extensions' conntrack match:

DNAT

A virtual state, matching if the original destination differs from the reply source.

Just insert this instead in filter forward before the last drop rule:

ct status dnat accept

or to be a bit more precise:

iif enp3s0 oif enp1s0f0 ct status dnat accept

up to very precise:

iif enp3s0 oif enp1s0f0 ct state new ct status dnat ip daddr 192.168.1.2 tcp dport 22 accept

This status can only appear because of a previous dnat rule done on a flow, so it validates the intent: accept.


Notes:

  • there is no need to use different tables for different hook types (filter and nat) as long as it's about the same family (ip here).

    That's an habit inherited from iptables that might limit possibilities. For example, the scope of a set is a table. Using the same set between a filter chain and a nat chain requires them to be in the same table (where the set is defined). Sadly many examples even from the wiki still use naming conventions mimicing iptables.

  • while it doesn't matter here, the historical priority for nat prerouting isn't 100 but -100 (aka dstnat).

    It would only matter if there were other tables also including nat prerouting chains or if iptables nat rules were used together (and in such case it would be advised to use -101 or -99 rather than exactly -100), to determine which rules take precedence.