5

For IPv4, it is easy to create a rule that only accepts connections from hosts of the same subnet, for example (assuming my computer is 192.168.42.2, and the incoming connection is 192.168.42.20):

table ip firewall {
    chain incoming {
        type filter hook input priority 0; policy drop;
        ip saddr 192.168.42.0/24 tcp dport 8080 accept
    }
}

How does one do this for IPv6? I know there's always the link-local address and theoretically this rule should work:

ip6 saddr fe80::/64 tcp dport 8080 accept

The problem now is that I have mDNS setup, and the address it returns is the globally-routable address, something like 2001:db8::1234. Because of that, the packets I receive from other hosts (despite being on the same subnet) all have an ip6 saddr with the 2001:db8 prefix which gets blocked by the firewall.

I cannot simply add a rule that matches 2001:db8::/64, because that prefix comes from the ISP and changes from time to time. Setting up a ULA so that I get a predictable prefix doesn't seem possible either, since the router is ISP-mandated and the configuration interface it has for IPv6 is painfully hollow.

So... this is why I am looking for something that is akin to this:

ip6 saddr & ffff:ffff:ffff:ffff:: == ip6 daddr & ffff:ffff:ffff:ffff:: tcp dport 8080 accept

But nftables doesn't seem to accept that. Is there something I can do to workaround this, or am I missing something?

0

1 Answer 1

4

The feature doesn't exist ... yet?

Currently, nftables can only use one register (in its virtual state machine): it applies bitwise operations on the left hand side (LHS) to compare the result with a constant or a set on the right hand side (RHS). It cannot use two variable operands (meaning: both from packet) in LHS and RHS.

There's WIP about improving this in these patch series (they are not accepted yet): nf-next libnftnl nftables:

Currently bitwise boolean operations (AND, OR and XOR) can only have one variable operand. [...] We add support for evaluating these operations directly in kernel space on one register and either an immediate value or a second register.

This will probably still take additional iterations and some time before it is made available (the idea has been floating around since 2019 and maybe earlier, but it's still not available). Once done, one can image that OP's precise rule:

ip6 saddr & ffff:ffff:ffff:ffff:: == ip6 daddr & ffff:ffff:ffff:ffff:: tcp dport 8080 accept

would work as expected.


Workaround

That said, what could be done today?

One can use an external tool that reacts to the host changing address and updates a set so it has the host's IPv6 network/netmask as content. A set is fine to use as RHS. In the end it's not dynamic as in variable operands but it's still dynamic enough for the need.

Add a set to OP's ruleset (I won't put a full ruleset, this is beyond the scope of the question. Just remember that usually ct state related,established accept as well as allowing the loopback interface should be present and that a table of family inet rather than ip6 could merge some rules for IPv4 plus IPv6 when relevant).

ip6firewall.nft:

table ip6 firewall        #for idempotence
delete table ip6 firewall #for idempotence

table ip6 firewall {
    set myip6net {
        typeof ip6 saddr
        flags interval
    }

    chain acceptmyip6netsrc {
        ip6 saddr @myip6net counter accept
    }

}

This could be called from a base input chain with:

tcp dport 8080 jump acceptmyip6netsrc

I'll assume the network interface name is eth0. The script below uses a very simple event loop with ip monitor and will keep running: use it as a service, not in crontab. It will trigger whenever an address event happens (most of the time uselessly when a Router Advertisement that refreshes timeouts and changes nothing happens). ip monitor's output isn't easy to parse, so just ignore it and use ip -json addr to retrieve actual values. The script has room for improvement but does the job.

Requires tools which are usually available in distributions:

  • jq for efficient JSON parsing
  • netmask (handles correctly any abbreviation of an IPv6 address, so 2001:db8::4:5:6:7:8/64 is correctly transformed into 2001:db8:0:4::/64).

updatemyip6net.sh:

#!/bin/sh

{ echo init; ip -6 -o monitor address dev eth0; } | while read dummy; do
    myip6addr=$(ip -json -6 addr show dev eth0 scope global |
        jq -j '.[].addr_info[] | if .local then .local,"/",.prefixlen,"\n", halt else empty end'
    )
    myip6net=$(netmask $myip6addr)

    nft -f - <<EOF
        flush set ip6 firewall myip6net
        add element ip6 firewall myip6net { $myip6net }
EOF

done

Above,

ip -json -6 addr show dev eth0 scope global | jq -j '.[].addr_info[] | if .local then .local,"/",.prefixlen,"\n", halt else empty end'

is quite long, but replacing it with the simpler:

ip -j -6 route get 2001:4860:4860::8888 | jq -r '.[].prefsrc'

doesn't get the netmask, and hardcoding /64 everywhere should be avoided.

3
  • 1
    Thanks for the great answer! It's good to actually know that I wasn't missing something and that nftables indeed doesn't support non-constant RHS :(
    – Haden
    Commented Jun 12, 2023 at 17:12
  • 1
    while it's certainly a more expensive, couldn't one simply update the set each time in a preceding rule
    – T Nierath
    Commented Jun 26, 2023 at 20:50
  • 1
    @TNierath Indeed, one could create a set with dynamic+timeout flags and for each received packet set the set's element(s) to the destination net + short timeout (say a few seconds) and in the same extended rule compare the source net with the set. Yes this would work. More overhead, but can all be running without userspace assistance. One still has to hardcode /64 (the netmask can't be infered from the packet path contrary to the interface's address), but that's a common value.
    – A.B
    Commented Jun 26, 2023 at 21:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .