I have managed to set up DNS on my router without the need for static IP addresses using dnsmasq as the main DHCP client. It was a bit of a process and quite a steep learning curve, so I figured I'd document what I did here on the forums in case anybody else finds themselves wanting this feature.
The steps to get this working are as follows:
- Install packages
- Set dnsmasq as the main DHCP server
- Configure dnsmasq to provide DHCP and DNS
- Bonus: Dynamic IPv6 firewall rules
Install Packages
The first step is to install the required packages. For this setup, the version of dnsmasq built-in to OpenWRT does not have DNS functionality. Instead, we'll want to install dnsmasq-full
opkg update && opkg install dnsmasq-full
For testing purposes, we may also want to install the dig
command
opkg install bind-dig
Set Dnsmasq as the main dhcp provider
Now we need to edit some of the OpenWRT configuration files.
First, we have to turn off odhcpd
and tweak a parameter of dnsmasq
/etc/config/dhcp
config odhcpd
...
option maindhcp '0' #This is a '1' by default
...
config dnsmasq
...
option domain 'home.example.com'
option localservice '0' #Allows Dnsmasq to resolve queries from outside the network
#Make a sensible rate-limiting firwall rule!
#You might leave this to a '1' during testing.
...
config dhcp 'lan'
...
option dhcpv6 'disabled'
...
Configure Dnsmasq to Provide DHCP and DNS
There are my options to dnsmasq that aren't available through the standard configuration files. So, we need to add some sections to the /etc/dnsmasq.conf file that contains the remainder of the configuration.
/etc/dnsmasq.conf
#DNS settings
auth-server=home.example.com,eth1.2
#On my network, I only advertise the IPv6 addresses of the clients via DNS
#but some tools like fwknopd need an IPv4 address for the router in order
#to function properly, so eth1.2 (wan) advertises IPv4-only. IPv6 link-local
#addresses are excluded from DNS responses.
auth-zone=home.example.com,br-lan/6,eth1.2/4,exclude:fd00::1/8
interface-name=home.example.com,eth1.2/4
dhcp-fqdn
#DHCPv6 Settings
enable-ra
#On my router, I use both DHCPv6 (for ease of addressing) and SLAAC
#(for privacy)
dhcp-range=set:lan6,::ff:fd00:0002,::ff:fdff:ffff,constructor:br-lan,slaac,64,12h
dhcp-range=set:guest6,::ff:fd00:0002,::ff:fdff:ffff,constructor:br-guest,slaac,64,12h
Bonus: Dynamic IPv6 firewall rules
My whole purpose of setting up Dnsmasq rather than odhcpd to begin with was because I was trying to create dynamic IPv6 firewall rules. As you're probably aware, IPv6 prefixes can change pretty frequently depending on your ISP. If the prefix were to change, your existing firewall port forwards for IPv6 will be crippled until you manually re-set them at some point.
The problem with odhcpd is that it doesn't provide script callbacks that will notify of new or renewed DHCP leases. Dnsmasq, however does provide such callbacks making automatic updating of firewall rules a relative breeze.
First we'll add a firewall rule to be dynamically updated:
/etc/config/firewall
config rule
option target 'ACCEPT'
option src 'wan'
option proto 'tcp udp'
option dest_port '3306'
option name 'Allow-MySQL-from-WAN'
option family 'ipv6'
option dest 'lan'
option dest_ip '::1' #Dummy localhost value
Upon a new or renewed lease, Dnsmasq calls the script /usr/lib/dnsmasq/dhcp-script.sh
, which, in turn calls /sbin/hotplug-call dhcp
(after setting up an environment). /sbin/hotplug-call dhcp
executes scripts in the /etc/hotplug.d/dhcp
folder. By default, there are no scripts here. Let's add one:
/etc/hotplug.d/dhcp/update-firewall.sh
#!/bin/sh
#New, existing, or updated DHCP lease
if [ "$ACTION" == "add" ] || [ "$ACTION" == "old" ] || [ "$ACTION" == "update" ]; then
#If the IP address is IPv6 and it's not a link-local address (i.e. fd00::/7)
if [ "${IPADDR%:*}" != "$IPADDR" ] && [ "${IPADDR#fd}" == "$IPADDR" ]; then
#Check the hostname
case $HOSTNAME in
laptop)
#Verify the DUID -- in case another host has the same hostname
#This value can be seen by using ip neigh (may require the ip-full package)
if [ "$MACADDR" == "00:11:22:33:44:55:66:77:88:99:aa:bb:cd:dd:ee:ff:00:11" ]; then
/usr/sbin/update-uci-firewall-rule "Allow-MySQL-from-WAN" "$IPADDR"
fi
;;
esac
fi
fi
This script simply does a sanity check to ensure we're updating the firewall rule to the correct host by matching the hostname and the DUID. Then, it calls a script that I've stored in /usr/sbin/update-uci-firewall-rule. This script does the work of locating and changing the firewall rule. The script supports both IPv4 and IPv6 rules.
/usr/sbin/update-uci-firewall-rule
#!/bin/bash
if [ $# -lt 2 ]
then
echo "Usage: $0 [rule_name] [new_ip]"
exit
fi
rulename="$1"
newip="$2"
#Determine whether the given IP is IPv4 or IPv6
newipv4=$(echo $newip | grep -c '.')
newipv6=$(echo $newip | grep -c ':')
. /lib/functions.sh
sectionname=""
sectionid=""
#These are OpenWRT shell callbacks called during config_get
#Essentially, we know we're looking for a filewall rule with
#a specified name, so once we find that rule in the firewall
#config via these callbacks, we unset the callbacks via
#reset_cb
function config_cb()
{
sectionname="$1"
sectionid="$2"
}
function option_cb()
{
if [ "$sectionname" == "rule" ]
then
if [ "$1" == "name" ]
then
if [ "$2" == "$rulename" ]
then
reset_cb
fi
fi
fi
}
config_load firewall
if [ -z "$sectionname" ] || [ -z "$sectionid" ];
then
echo "Error: no rule named \"$1\" found."
exit
fi
#The callbacks will have found the $sectionid, now
#pull the dest_ip from the rule
config_get destip $sectionid dest_ip 1>/dev/null
if [ -z "$destip" ]
then
echo "Error: no IP found for rule \"$1\"."
exit
fi
#Determine whether previously-configured IP is IPv4 or IPv6
destipv4=$(echo $destip | grep -c '.')
destipv6=$(echo $destip | grep -c ':')
#Address protocol mismatch, display a warning and update the protocol
if [ $newipv4 != $destipv4 ]
then
if [ $newipv4 ]
then
echo "Warning: The previous IP appears to be IPv6 and you are replacing it with an IPv4 address."
echo "Updating address family"
uci set firewall."$sectionid".family=ipv4
else
echo "Warning: The previous IP appears to be IPv4 and you are replacing it with an IPv6 address."
echo "Updating address family"
uci set firewall."$sectionid".family=ipv6
fi
fi
#Address protocol mismatch, display a warning and update the protocol
if [ $newipv6 != $destipv6 ]
then
if [ $newipv6 ]
then
echo "Warning: The previous IP appears to be IPv4 and you are replacing it with an IPv6 address."
echo "Updating address family"
uci set firewall."$sectionid".family=ipv6
else
echo "Warning: The previous IP appears to be IPv6 and you are replacing it with an IPv4 address."
echo "Updating address family"
uci set firewall."$sectionid".family=ipv4
fi
fi
#Update the firewall rule with the new IP
uci set firewall."$sectionid".dest_ip="$newip"
uci commit
Conclusion
Once this is all set up, you should be able to dig @[router_ip] AAAA [hostname].home.example.com
. Of course, if you change all instances of home.example.com
to a real domain and set up some glue records you can get to all the devices from the outside (provided you have port forwards set, of course).
While there is a little bit of configuration involved and a couple of custom scripts to fill in the functionality gaps (be sure to include the files in your backups!), overall it's not a terribly painful process. it has also been fairly reliable for me for several months.
There is one caveat that I have found though: If the router gets reset or there's a power failure, my Linux clients don't populate in the DNS table. Windows doesn't seem to be affected. Since the Linux machines keep ahold of their lease when disconnected, something about the lease renewal process (as opposed to the full lease negotiation process) doesn't trigger Dnsmasq to add the client to the DNS. I have solved this by deleting the lease files (/var/lib/NetworkManager/.lease on Ubuntu and /var/lib/dhcpcd5/.lease on Raspbian) and then reconnecting the network. It works but it's not the most graceful solution.