monotux.tech

Using dnsmasq & nftables together to create DNS block lists

dnsmasq, nftables, networking, DNS

I recently(ish) discovered yet another amazing feature in dnsmasq – it can add resolved IP addresses to nftsets! This is useful if you want to create an allow/block list based on DNS records instead of IP addresses.

In my case, I wanted to limit HTTPS access to a few domains out on the internet for some machines in my home lab.

Overview #

Conceptually, it works like this:

  1. Someone looks up example.com using our dnsmasq as its resolver
  2. dnsmasq resolves example.com, adds it’s IP address to the nftset as specified (and replies to the client, of course!)
  3. The client tries to connect to example.com
  4. nftables contains a ruleset which decides if the connection is allowed or not, checking the nftset dnsmasq added the resolved IP to
  5. The client is allowed or denied access to example.com

To make this work, we need a few things:

  • Using a dnsmasq with support for netfilter1
  • A list of domains to allow or block access to
  • A nft set in your nftables configuration

nftables #

First of all, we need to define a set to collect addresses in. The nftables documentation for sets shows we can use either ipv4_addr or ipv6_addr to collect IP addresses, so create one for each family if needed. It’s also safe to omit the timeout!2

table inet firewall {
      set mgmtUpdateIPv4 {
        type ipv4_addr; timeout 4h;
      }

      # Remove if you don't use IPv6!
      set mgmtUpdateIPv6 {
        type ipv6_addr; timeout 4h;
      }

      # the rest of your ruleset
}

Then you can use this named set to allow or block access in rulesets:

# Allow HTTP/HTTPS if the destination address is in the mgmtUpdateIPv4/6 set
chain update_ok {
    tcp dport {80,443} ip  daddr @mgmtUpdateIPv4 ct state new accept
    tcp dport {80,443} ip6 daddr @mgmtUpdateIPv6 ct state new accept
}

In my ruleset I include the above (jump update_ok) if I want that subnet to be able to reach the (update) servers in the named set.

Next step is to configure dnsmasq to add IP addresses to these sets.

dnsmasq #

Lets say we want to allow access to deb.debian.org & security.debian.org for all our Debian machines, add below to your dnsmasq.conf:

nftset=/deb.debian.org/security.debian.org/4#inet#firewall#mgmtUpdateIPv4
nftset=/deb.debian.org/security.debian.org/6#inet#firewall#mgmtUpdateIPv6

Note that you have to adjust the right hand side of the line to align with your configuration. My firewall is inet, is named firewall and the last part is the named sets we created in the previous section. The first digit signifies if it’s IPv4 or IPv6.

Bonus #

You can check which IP addresses are in the named set using the nft command:

# nft list set inet firewall mgmtUpdateIPv4
table inet firewall {
        set mgmtUpdateIPv4 {
                type ipv4_addr
                timeout 4h
                elements = { 18.192.94.96 expires 2h58m41s432ms, 52.58.254.253 expires 2h58m41s433ms }
        }
}

  1. You can run dnsmasq --version and check if ipset and nftset are included under compile time options ↩︎

  2. It makes some sense to use a timeout for this, as resources hidden in a CDN might change it’s address every now and then, and if you rarely restart your firewall this might end up allowing access to unintended resources. ↩︎