DHCP with dnsmasq, Docker, and arp-scan

Collin Dewey

10/03/2021

Last Modified: 09/20/2024

I personally would no longer suggest using this method. While I still like setting static IPs, it’s much easier managing it through a web GUI router software such as OPNsense or pfSense. I do unfortunately lose out on easily using arp-scan, which is unfortunate. If you’re hosting your own DHCP server because you’re forced to use some other router, this still absolutely works. I created a config conversion script, located on my GitHub. Adjust values as needed for your configuration.


Why static IP addresses?


I’m an avid user of everything technology, but also not an avid fan of walking between two computers in my house when I’m trying to do something between them. Or even just switching my keyboard, mouse and monitor cables between two computers. Copying files through a flash drive is slow, it would be much easier to just do it through the network. Methods of remotely controlling a computer makes EVERYTHING much more convenient. There is a problem with this however. Software like TeamViewer isn’t the best when you have a LAN connection with the computer you want to control. TeamViewer also doesn’t allow you to run on computers that lack a GUI, although they do have a Raspberry Pi version interestingly enough.

Remotely controlling a computer through SSH, VNC, and RDP, are much better methods than installing third party software. Plus, I can use all of these methods when I’m out of my house through my Wireguard VPN. There is one issue with this however, I’ve found that using hostnames are not the most reliable.

There’s also another reason as to why I would want static IP addresses, port forwarding. I don’t play games all that often but I do occasionally host a Minecraft server for me and my friends to play on. I find it a pain to go to my router page, logging in, just to change the IP on my port forwarding every few days. It would be easier to just never need to change the IP.

How do I go about setting a static IP address? Setting a static IP address is a pain to do on each device, but also impossible on some devices, such as smart plugs and lights.

The better way to do this is through the power of DHCP. Your home router probably already has a DHCP server, which assigns IPs to devices on your network. But in my case, I will be giving it out through a program called dnsmasq on a Raspberry Pi. Yes I know the name primarily has “DNS” in it, but dnsmasq supports more than just DNS, including being a DHCP server.


dnsmasq


At my house, I have dozens of devices to keep track of. I won’t bore you with a base dnsmasq configuration setup, but I will list my DHCP part for reference. You will need to go to your router and change the DHCP server to be the IP of the device running dnsmasq, in addition to setting a static IP on just that device.

dnsmasq.conf

dhcp-range=eth0,172.16.0.0,static
dhcp-range=eth0,172.16.0.40,172.16.0.99,45m
dhcp-option=eth0,option:router,172.16.0.1
dhcp-option=option:ntp-server,129.6.15.30,206.246.122.250
dhcp-option=252,"\n"
dhcp-option=vendor:MSFT,2,1i
dhcp-authoritative

# Host IP assignment
conf-file=/etc/dnsmasq.d/hosts.conf

In the excerpt of above configuration file, I specify the range of random IP assignments for those that I don’t manually specify, and the router. However, at this point I haven’t manually assigned any IPs. That’s where the last line comes in,

conf-file=/etc/dnsmasq.d/hosts.conf

This tells dnsmasq to look at an additional config file, which will be in the same format as dnsmasq.conf, just in another file. The actual IP assignment comes in with the usage of the dhcp-host option. Such as below… hosts.conf

dhcp-host=88:CE:46:95:0E:EC,EdgeRouter-Lite,172.16.0.1,12h
dhcp-host=8C:EB:32:DF:2D:C0,UniFi-AP,172.16.0.2,12h
dhcp-host=65:E3:62:1B:38:CF,PI,172.16.0.3,12h
dhcp-host=A9:F2:07:3A:13:9F,Network-Switch-1,172.16.0.4,12h
dhcp-host=85:B2:D3:5B:A6:34,Personal-Cloud,172.16.0.5,12h
dhcp-host=B2:B4:36:E1:43:4E,ObiHai,172.16.0.26,12h
dhcp-host=17:59:A0:DE:2A:73,GREEN,172.16.0.28,12h

arp-scan


However there’s another application I want to use that also needs IPs and names, that application being arp-scan. arp-scan sends ARP packets to all possible IPs within your specified range, and reports the responses. This unfortunately isn’t the most helpful when you don’t easily know which device is what.

Excerpt from arp-scan's man page

$ arp-scan --interface=eth0 192.168.0.0/24
Interface: eth0, datalink type: EN10MB (Ethernet)
Starting arp-scan 1.4 with 256 hosts (http://www.nta-monitor.com/tools-resources/security-tools/arp-scan/)
192.168.0.1     00:c0:9f:09:b8:db       QUANTA COMPUTER, INC.
192.168.0.3     00:02:b3:bb:66:98       Intel Corporation
192.168.0.5     00:02:a5:90:c3:e6       Compaq Computer Corporation
192.168.0.87    00:0b:db:b2:fa:60       Dell ESG PCBA Test
192.168.0.90    00:02:b3:06:d7:9b       Intel Corporation
192.168.0.153   00:10:db:26:4d:52       Juniper Networks, Inc.
192.168.0.191   00:01:e6:57:8b:68       Hewlett-Packard Company
192.168.0.251   00:04:27:6a:5d:a1       Cisco Systems, Inc.
192.168.0.196   00:30:c1:5e:58:7d       HEWLETT-PACKARD

arp-scan allows you to specify a file that relates the MAC addresses to names with the --macfile file command line argument. Unfortunately for us, arp-scan doesn’t want to see dhcp-host, rather we have to use the format that it wants. Which looks like this…

macfile.txt

88CE46950EEC    EdgeRouter-Lite
8CEB32DF2DC0    UniFi-AP
65E3621B38CF    PI
A9F2073A139F    Network-Switch-1
85B2D35BA634    Personal-Cloud
B2B436E1434E    ObiHai
1759A0DE2A73    GREEN

Now when I specify the macfile, I can see a much nicer output.

$ sudo arp-scan --localnet --interface eth0 --macfile macfile.txt
Interface: eth0, type: EN10MB, MAC: 65:e3:62:1b:38:cf, IPv4: 172.16.0.3
Starting arp-scan 1.9.7 with 256 hosts (https://github.com/royhills/arp-scan)
172.16.0.1      88:ce:46:95:0e:ec       EdgeRouter-Lite
172.16.0.2      8c:eb:32:df:2d:c0       UniFi-AP
172.16.0.5      85:b2:d3:5b:a6:34       Personal-Cloud
172.16.0.3      65:e3:62:1b:38:cf       PI
172.16.0.28     17:59:a0:de:2a:73       GREEN
172.16.0.4      a9:f2:07:3a:13:9f       Network-Switch-1
172.16.0.28     17:59:a0:de:2a:73       GREEN (DUP: 2)
172.16.0.26     b2:b4:36:e1:43:4e       ObiHai
9 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.9.7: 256 hosts scanned in 2.157 seconds (118.68 hosts/sec). 8 responded

However, the result is unsorted and contains duplicates sometimes. changing the command a bit results in a clean looking, sorted, output.

$ sudo arp-scan --macfile arp-scan.txt --localnet --interface eth0 --ignoredups --plain | sort -t . -k 3,3n -k 4,4n
172.16.0.1      88:ce:46:95:0e:ec       EdgeRouter-Lite
172.16.0.2      8c:eb:32:df:2d:c0       UniFi-AP
172.16.0.3      65:e3:62:1b:38:cf       PI
172.16.0.4      a9:f2:07:3a:13:9f       Network-Switch-1
172.16.0.5      85:b2:d3:5b:a6:34       Personal-Cloud
172.16.0.26     b2:b4:36:e1:43:4e       ObiHai
172.16.0.28     17:59:a0:de:2a:73       GREEN

Automation


Sure I could maintain a separate list to keep between arp-scan and dnsmasq, but that’s no fun. Instead I could do some simple bash scripting to generate multiple list formats from a format of my own. There’s four things I need to know. Requested IP, MAC address, name, and DHCP lease time. I decided on a simple tsv format with tab delimiting the sections, in addition to making comments start with # and ignoring empty lines.

dhcp.tsv

#Networking     eth0 (1-19)
88CE46950EEC    EdgeRouter-Lite 172.16.0.1      12h
8CEB32DF2DC0    UniFi-AP        172.16.0.2      12h
65E3621B38CF    PI      172.16.0.3      12h
A9F2073A139F    Network-Switch-1        172.16.0.4      12h
85B2D35BA634    Personal-Cloud  172.16.0.5      12h
B2B436E1434E    ObiHai  172.16.0.26      12h

#Misc           eth0 (20-39)
1759A0DE2A73    GREEN    172.16.0.28     12h

generate.sh

#!/usr/bin/env bash

cat dhcp.tsv | while IFS="	" read -r col1 col2 col3 col4
do
    if [[ $col1 != *"#"* ]] && [[ $col1 != "" ]]; then
        echo "$col1 $col2"
    fi
done > arp-scan.txt

cat dhcp.tsv | while IFS="	" read -r col1 col2 col3 col4
do
    if [[ $col1 != *"#"* ]] && [[ $col1 != "" ]]; then
        echo "dhcp-host=${col1:0:2}:${col1:2:2}:${col1:4:2}:${col1:6:2}:${col1:8:2}:${col1:10:2},$col2,$col3,$col4"
    fi
done > /etc/dnsmasq.d/hosts.conf

systemctl restart dnsmasq.service

Docker


Docker is containerization software, meaning each container has access to its own files, but still uses the host system’s kernel. I’d recommend reading more about Docker if you don’t know anything about it. They have a nice overview on docker and how it works over on the Docker Docs Website.

It’s fairly easy to install dnsmasq on any Linux distribution. However, in my efforts to make my “homelab” as modular as possible, I decided, why not put it in a docker container. So I did. I used alpine, which is a very light weight base docker image, installed dnsmasq and bash, modified my script above a little to start dnsmasq instead of treating it like a service, profit, dnsmasq in a docker container providing both DHCP and DNS for my home network.

Dockerfile

FROM alpine:latest
RUN apk --no-cache add dnsmasq bash
COPY ./startup.sh /startup.sh
ENTRYPOINT ["/bin/bash", "startup.sh"]

At the time of writing, I manage my docker containers across multiple docker-compose files, so I quickly made a docker-compose file for my docker image.

docker-compose.yml

version: "3.5"

services:
  dnsmasq:
    build: ./build
    container_name: dnsmasq
    volumes:
      - ./dnsmasq.conf:/etc/dnsmasq.conf
      - ./dhcp.tsv:/dhcp.tsv
      - ./arp-scan.txt:/arp-scan.txt
    cap_add:
      - NET_ADMIN
    network_mode: host
    restart: unless-stopped

After running docker-compose up -d, I was left with functional dnsmasq in a docker image.


You can see the files mentioned in the article on my GitHub


Edit History:

10/03/21 - Initial Release

04/11/22 - Minor Example Revision

09/07/23 - Replace /bin/bash with /usr/bin/env

09/20/24 - Add notice

>> Home