dnsmasq is super lightweight compared to ISC’s bind/dhcp servers. On a LAN, you would something that’s flexible and very easy to configure.

A brief description about how dnsmasq works:

Each name available in /etc/hosts on the server, will be available in DNS. For hosts with dynamic IPs, the DHCP daemon that comes with dnsmasq will get their hostname and put it in DNS automatically. By default, only hostnames without a domain is accepted.

Make sure only one DHCP server is listening on the network. If you have more than one DHCP server, each of them should serve a different IP range or subnet, but only hosts that are getting IPs from dnsmasq will be able to register their hostname in DNS.

Here’s my setup:

  • Main router(modem/router combo) provided by my ISP. With an IP of 192.168.100.1

  • Second Router with a WAN IP of 192.168.100.2 and a LAN IP of 192.168.101.1

  • DHCP is disabled on the Second Router

The reason I’m adding a Second Router, is because I don’t want to change anything on the Main Router. Devices connected on the main network is unaffected. If you’re trying this as home, I recommend you add another device to your main network and start from there.

Install dnsmasq

I’m on Debian:

apt install dnsmasq

The service is started by default. Stop it:

systemctl stop dnsmasq

Set a Static IP

Set a static IP for your server in /etc/network/interfaces:

----REDACTED
auto enp0s3
iface enp0s3 inet static
	address 192.168.101.51/24
	gateway 192.168.101.1

Basic DNS and DHCP Configuration

The configuration file is /etc/dnsmasq.conf. By default, all lines are commented. You can make a backup if you want, or delete the file and re-create it with touch.

Here’s my configuration:

root@debian:~# cat /etc/dnsmasq.conf
# global options
resolv-file=/etc/resolv.conf
domain-needed
bogus-priv
expand-hosts
domain=local.lan
local=/local.lan/
listen-address=127.0.0.1
listen-address=192.168.101.51

# upstream name servers
server=8.8.8.8
server=192.168.100.1 # Main Router

# IP range for subnet 192.168.101.0
dhcp-range=zone1,192.168.101.51,192.168.101.199
# dns server
dhcp-option=zone1,6,192.168.101.51
# router/gateway
dhcp-option=zone1,3, 192.168.101.1

I gave the range the name of zone1(could be anything). You typically do this when you have multiple subnets(or in the future). 6 is for DNS and 3 is for the router. A simple DHCP configuration will usually look like this:

dhcp-range=192.168.101.51,192.168.101.199
dhcp-option=option:dns-server,192.168.101.51
dhcp-option=option:router,192.168.101.1
dhcp-option=option:netmask,255.255.255.0

Since DHCP is disabled on the second router, dnsmasq won’t communicate with it anymore. From now on, only dnsmasq will answer DHCP requests. All clients connected on the network will get information such as IP address, netmask, router, and DNS. Even though I haven’t specified a netmask, DHCP will send the netmask that is set on the host running dnsmasq.

Verify The Syntax and Allow Required Ports

Verify the syntax with dnsmasq --test:

root@debian:~# dnsmasq --test
dnsmasq: syntax check OK.

Add your loopback address in /etc/resolv.conf:

root@debian:~# cat /etc/resolv.conf
nameserver 127.0.0.1

Append your IP in /etc/hosts:

root@debian:~# echo "192.168.101.51 dns-server" >> /etc/hosts
root@debian:~#

and start dnsmasq:

root@debian:~# systemctl start dnsmasq.service
root@debian:~#

Run tests on the dnsmasq server

Query both the dns server and its FQDN with nslookup:

root@debian:~# nslookup dns-server
Server:		127.0.0.1
Address:	127.0.0.1#53

Name:	dns-server
Address: 192.168.101.51

root@debian:~#
root@debian:~# nslookup dns-server.local.lan
Server:		127.0.0.1
Address:	127.0.0.1#53

Name:	dns-server.local.lan
Address: 192.168.101.51

Looks good. Allow UDP port 67 for DHCP, and TCP/UDP port 53 for DNS:

$ ufw allow 53
$ ufw allow 67/udp

View DHCP Leases

Join hosts on the network, and view /var/lib/misc/dnsmasq.leases:

root@debian:~# cat /var/lib/misc/dnsmasq.leases
1635229265 08:00:27:bc:ea:ca 192.168.101.52 alpine 01:00:a0:24:ab:fb:9c
1635228543 20:c9:fakeone:7c:ee:8d 192.168.101.191 MacBook-Pro 01:20:c9:d0:7c:ee:8d

Two hosts connected: alpine and MacBook-Pro.

Configure gethostname() on Linux Clients

If a host is not sending its hostname to DHCP, add the following line in its dhclient.conf:

root@debian:~# cat /etc/dhcp/dhclient.conf | grep hostname
send host-name = gethostname();

Test DNS from a client

On a client or host on the network, query a website with dig to verify which DNS is being queried:

MacBook-Pro:~ kavish$ dig google.com

; <<>> DiG 9.10.6 <<>> google.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4512
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;google.com.			IN	A

;; ANSWER SECTION:
google.com.		265	IN	A	216.58.223.78

;; Query time: 239 msec
;; SERVER: 192.168.101.51#53(192.168.101.51)
;; WHEN: Tue Oct 26 09:27:33 +04 2021
;; MSG SIZE  rcvd: 55

MacBook-Pro:~ kavish$

Look for status: NOERROR, and SERVER: 192.168.101.51. Test the server’s hostname and FQDN:

MacBook-Pro:~ kavish$ dig @dns-server google.com | egrep -i "status|server"
; <<>> DiG 9.10.6 <<>> @dns-server google.com
; (1 server found)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6296
;; SERVER: 192.168.101.51#53(192.168.101.51)
MacBook-Pro:~ kavish$
MacBook-Pro:~ kavish$ dig @dns-server.local.lan google.com | egrep -i "status|server"
; <<>> DiG 9.10.6 <<>> @dns-server.local.lan google.com
; (1 server found)
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46472
;; SERVER: 192.168.101.51#53(192.168.101.51)

Verify Automatic DNS from Client Side

I currently have two hosts on the network - alpine and MacBook-Pro. Issue both aping and nslookup from both machines:

## macOS
MacBook-Pro:~ kavish$ nslookup 192.168.101.52
Server:		192.168.101.51
Address:	192.168.101.51#53

52.101.168.192.in-addr.arpa	name = alpine.local.lan.

MacBook-Pro:~ kavish$
MacBook-Pro:~ kavish$ ping alpine -o
PING alpine.local.lan (192.168.101.52): 56 data bytes
64 bytes from 192.168.101.52: icmp_seq=0 ttl=64 time=1.015 ms

--- alpine.local.lan ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 1.015/1.015/1.015/0.000 ms
MacBook-Pro:~ kavish$

## alpine
alpine:~# nslookup 192.168.101.191
191.101.168.192.in-addr.arpa	name = MacBook-Pro.local.lan.

alpine:~#
alpine:~# ping Macbook-Pro -c 3
PING Macbook-Pro (192.168.101.191): 56 data bytes
64 bytes from 192.168.101.191: seq=0 ttl=64 time=1.877 ms
64 bytes from 192.168.101.191: seq=1 ttl=64 time=1.103 ms
64 bytes from 192.168.101.191: seq=2 ttl=64 time=1.169 ms

--- Macbook-Pro ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 1.103/1.383/1.877 ms
alpine:~#

No need to modify /etc/hosts on multiple machines.

“Never memorize something that you can look up.”Albert Einstein