NetworkEnjin – Part 2: DNS and DHCP with DNSMasq

Installing DNSMasq

To install dnsmasq:

apt install dnsmasq

You will see an error along the lines of “failed to create listening socket for port 53: Address already in use” that’s because systemd-resolved.service is running. We’ll disable that later once dnsmasq is configured.

Rename the default dnsmasq config file in case you would like to reference it later:

mv /etc/dnsmasq.conf /etc/dnsmasq.default.conf

Create a new /etc/dnsmasq.conf and simply fill it with:

conf-dir=/etc/dnsmasq.d/,*.conf

In case /etc/dnsmasq.d doesn’t exist run

mkdir -p /etc/dnsmasq.d

Configuring DNS

Create /etc/dnsmasq.d/dns.conf and fill it with:

no-resolv
server={:DNSServerIP:}

This will delete tell dnsmasq to ignore what the server has it’s dns directed to whatever you set {:DNSServerIP:} to be. You can specify multiple DNS servers by having multiple server lines.

Configuring DHCP

Create /etc/dnsmasq.d/dhcp.conf and fill it with:

dhcp-authoritative
dhcp-leasefile=/opt/dnsmasq/dnsmasq.leases

no-dhcp-interface={:WANInterface:}

dhcp-option=option:router,{:LANIP:}
dhcp-option=option:dns-server,{:LANIP:}
dhcp-range={:DHCPStartIP:},{:DHCPEndIP:},8h

To configure IP reservations add this line for each reservation you would like to make

dhcp-host={:MACAddress:}, {:IPReservation:}

To resolve hostnames to a domain add this

expand-hosts
domain={:LocalDomain:}

You’ll need to create the following folder for the dhcp lease file: /opt/dnsmasq

mkdir -p /opt/dnsmasq

Disabling systemd’s resolved

Once you are happy with your config disable systemd’s resolved and start dnsmasq

systemctl stop systemd-resolved.service
systemctl disable systemd-resolved.service
systemctl start dnsmasq.service

Optional: Reduce snooping with DNS over HTTPS

Install the tools to build our HTTPS to DNS proxy

apt install git build-essential cmake libc-ares-dev libcurl4-openssl-dev libev-dev

For our HTTPS to DNS proxy we are going to be using the following GitHub project: https://github.com/aarond10/https_dns_proxy

From wherever you feel appropriate clone the project, compile it and copy it to /usr/bin

git clone https://github.com/aarond10/https_dns_proxy.git
cd https_dns_proxy
cmake .
make
sudo cp ./https_dns_proxy /usr/bin

Create a systemd service to run the proxy with

systemctl edit --full --force https_dns_proxy.service

Make the contents of that service

[Unit]
Description=HTTPS to DNS Proxy
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=3
User=nobody
Group=nogroup
ExecStart=/usr/bin/https_dns_proxy -a 127.0.0.1 -p 5053 -4 -b 8.8.8.8,8.8.4.4 -r "https://dns.google/dns-query"

[Install]
WantedBy=multi-user.target

The ExecStart line is where the magic happens. It starts https_dns_proxy listening on localhost port 5053 and looks at Google’s DoH servers.

Yes I do see irony of reducing snooping by setting my DNS to use Google’s servers. That’s a personal choice I have made, you can set it to be another provider if you like. As an example if you would rather use CloudFlare as your DNS provider change the ExecStart line to be

ExecStart=/usr/bin/https_dns_proxy -a 127.0.0.1 -p 5053 -4 -b 1.1.1.1,1.0.0.1 -r "https://cloudflare-dns.com/dns-query"

Once you save and exit out of the editor we’ll need to start and enable the service

systemctl enable https_dns_proxy.service
systemctl start https_dns_proxy.service

Edit /etc/dnsmasq.d/dns.conf and replace your upstream DNS servers with just

server=127.0.0.1#5053

Before you start testing restart dnsmasq

systemctl restart dnsmasq.service

Updating our base firewall

For DNS and DHCP to work we will need to update our firewall’s base config in /etc/nftables.conf

#!/usr/sbin/nft -f

flush ruleset

define WAN_INTERFACE = {:WANInterface:}
define LAN_SUBNET = {:LANSubnet:}

table inet filter {

	set DNSMASQ_ALLOW_UDP {
		type inet_service

		elements = {
			53, 67
		}
	}

	set DNSMASQ_ALLOW_TCP {
		type inet_service

		elements = {
			53
		}
	}

	chain outgoing {
		type filter hook output priority 100
		policy accept
	}
	
	chain incoming {
		type filter hook input priority 0
		policy drop

		ct state established,related accept
		iif lo accept
		meta l4proto {icmp, icmpv6} accept
		ip saddr $LAN_SUBNET tcp dport 22 accept

		udp sport bootpc udp dport bootps ip saddr 0.0.0.0 ip daddr 255.255.255.255 accept
		udp dport @DNSMASQ_ALLOW_UDP accept
		tcp dport @DNSMASQ_ALLOW_TCP accept
	}

	chain forwarding {
		type filter hook forward priority 0
		policy drop

		ip saddr $LAN_SUBNET oifname $WAN_INTERFACE accept
		iifname $WAN_INTERFACE ip daddr $LAN_SUBNET ct state related,established accept
	}
}

table nat {

	chain prerouting {
		type nat hook prerouting priority 0
	}

	chain postrouting {
		type nat hook postrouting priority 0
	}
}

While what has changed here is mostly self explanatory the following line may have turned some heads.

udp sport bootpc udp dport bootps ip saddr 0.0.0.0 ip daddr 255.255.255.255 accept

Essentially this line is to accept DHCP discovery packets which occur before the device has an IP on the network.

Now this is all we need for our router to handle DNS and DHCP.