Nftables - Filtering traffic at IP addresses level based on relevant domain name

Hi,

I got an nftables filtering of traffic based on sets of IP address filled via DNS queries, that is similar to the fw3 ipset implementation https://openwrt.org/docs/guide-user/firewall/fw3_configurations/dns_ipset but with fw4 in 22.03.

Here the configuration, I would like to get some comment and then add a page in the wiki.

Goal : Filter traffic in fw4 based on the destination IP address of the packets, getting the list of addresses from their domain names.

Prerequisites :

  1. You need a firewall zone without forwarding to wan, so that no traffic to the internet is allowed by default.
  2. Have dig and grep installed

Overall approach : We will add a rule to the forwarding chain of the firewall zone mentioned in the prerequisite, the rule will allow traffic to specified IP addresses associated to their domains.

In /etc/rc.local add the below code to create the nft set in which we will save the IP addresses, the proposed code is ipv4 only but can be extended to cover ipv6

# Filter wildlan by IP addresses
## Create a set for "inet fw4" table with name "blackhole" that can include "ipv4_addr"
nft add set inet fw4 blackhole { type ipv4_addr \;}

## Add element to "blackhole" from file urls.txt
for address in $(dig a -f /etc/sets-ipdns/wildlan-urls.list +short | grep -v '\.$'); do
	nft add element inet fw4 blackhole {$address}
done

## Allow packes in "blackhole" (all others are denied by default)
nft insert rule inet fw4 forward_wildlan position 424 ip daddr @blackhole accept

The list of domains to which traffic will be allowed shall be included in a the custom file /etc/sets-ipdns/wildlan-urls.list, to create the file

cd /etc
mkdir sets-ipdns
cd sets-ipdns
vim wildlan-urls.list

List inside vim the domain names that shall be allowed.

At this point it has to be verified the handle to which insert the new firewall rule included in rc.local, in the above example is added before the handle 424.

To verify your handle, assuming you are using the standard table inet fw4

nft -n -a list table inet fw4

This will list the chains inside inet fw4, within those you need to identify the forward chain of the firewall zone mentioned in the prerequisites. In the below example the zone is called wildlan.

chain forward_wildlan { # handle 20
     ct status 0x20 accept comment "!fw4: Accept port forwards" # handle 423
     jump reject_to_wildlan # handle 424
}

The command nft insert rule inet fw4 forward_wildlan position 424 ip daddr @blackhole accept will result in

chain forward_wildlan { # handle 20
     ct status 0x20 accept comment "!fw4: Accept port forwards" # handle 423
     ip daddr @blackhole accept # handle 450
     jump reject_to_wildlan # handle 424
}

Based on this forward chain only the traffic with destination to the IP addresses included in @blackhole will be allowed. At this stage the @blackhole sets is still empty.

The rc.local is executed at boot time, so that @blackhole will be filled with IP addresses. That set shall be periodically updated for two reasons:

  1. The IP addresses may change
  2. In case of DNS Load Balancing, the same DNS query will result in different IP addresses (all valid) based on time of request.

In the Scheduled Task in Luci or in /etc/crontabs/root we execute every 15 minutes a script to update the sets

15 * * * * /etc/sets-ipdns/update-sets.sh

In the /etc/sets-ipdns/update-sets.sh include the update of sets code from /etc/rc.local

## Add element to "blackhole" from file urls.txt
for address in $(dig a -f /etc/sets-ipdns/wildlan-urls.list +short | grep -v '\.$'); do
	nft add element inet fw4 blackhole {$address}
done

Enable the script and reboot

chmod +x /etc/sets-ipdns/update-sets.sh
reboot

After the reboot, verify the content of the @blackhole nft list set inet fw4 blackhole and the result should be a list of ipv4 addresses.

The final crosscheck is to verify that addresses listed in /etc/sets-ipdns/wildlan-urls.list can be accessed, no other domains should be accessible unless the same IP address is shared between multiple domains (that happen with CDNs).

In case anyone reading this topic in future will wonder, here the wiki page https://openwrt.org/docs/guide-user/firewall/filtering_traffic_at_ip_addresses_by_dns

well isn't this far harder than before?
this is exactly the reason why i'm not adopting FW4. With FW3 i have a long list of ipsets to filter web access to my iot devices, why are we trying to make things harder?
i don't even know if the fw4-compliant dnsmasq-full is out and if it manages ipsets..

It depends on what you are referring, the wiki example referenced in this new wiki page share exactly the same approach, with a list of domains translated into list of IPs.

The fw4 equivalent of ipset are called sets, with dnsmasq 2.87 will be introduced --nftset as equivalent of --ipset (https://github.com/openwrt/openwrt/pull/4977).

So I would say that is not harder than before, is just moving to something different.

i was referring exactly to the equivalent of ipset that - please correct me if i'm wrong - today is still not available and we don't even have an expected availability.
I was planning to start from scratch to abandon 21.02 and go back to master, but it seems it's not the case..

There won't be a standalone ipset replacement, the successor is nftables' builtin set support. See https://wiki.nftables.org/wiki-nftables/index.php/Ipset for some further information.

The equivalent of ipset is available and is called sets, is no longer an external package but is bundled with nftables. What is missing is the equivalent of dnsmasq --ipset option, that will be avaiable in 2.8.7 as --nftset option.

So, if you are filling your ipset from a list of domains, the functionalities are already there. If you are filling your ipset from a subset of queries resolved by dnsmasq you still have to wait.