DHCP Relay and DHCP Server lab
This lab shows an IPv4 DHCP server and DHCP relay example, both running dnsmasq: one daemon covers the two roles, so the whole lab uses a single configuration syntax.
The client is on a subnet where no DHCP server lives. Its broadcast requests are picked up by the router in the middle, which relays them as unicast to the server on the other subnet.
Overview¶
Network diagram¶
Here is the logical and physical view:
flowchart TD
VM1["VM1 (DHCP client)<br/>vtnet0: 10.0.12.123<br/>(leased)"]
VM2["VM2 (router, DHCP relay)<br/>vtnet0: 10.0.12.2<br/>vtnet1: 10.0.23.2"]
VM3["VM3 (DHCP server)<br/>vtnet1: 10.0.23.3<br/>pool: 10.0.12.100-200"]
VM1 ---|"10.0.12.0/24<br/>broadcast requests"| VM2
VM2 ---|"10.0.23.0/24<br/>relayed as unicast"| VM3
The server hands out addresses for 10.0.12.0/24, a subnet it has no interface on: it only ever sees those requests through the relay.
Setting up the lab¶
Downloading BSD Router Project images¶
Download a BSDRP serial image (to avoid needing an X display) from SourceForge.
Downloading BSDRP lab scripts and starting the lab¶
More information on the BSDRP lab scripts is available in How to build a BSDRP router lab.
Start a lab with 3 full-meshed routers (no common LAN). The -r dhcp flag
makes the lab script build per-VM cloud-init disks that apply the configuration
below automatically on first boot (through the labconfig script shipped in
the image):
# tools/BSDRP-lab-bhyve.sh -i BSDRP-2.3-full-amd64.img.xz -n 3 -r dhcp
BSD Router Project (https://bsdrp.net) - bhyve full-meshed lab script
Setting-up a virtual lab with 3 VM(s):
- Working directory: /root/BSDRP-VMs
- Each VM has a total of 1 (1 cores and 1 threads) and 1G RAM
- Emulated NIC: virtio-net
- Boot mode: UEFI
- Switch mode: bridge + tap
- 0 LAN(s) between all VM
- Full mesh Ethernet links between each VM
- Regression test lab: dhcp
VM 1 has the following NIC:
- vtnet0 connected to VM 2
- vtnet1 connected to VM 3
VM 2 has the following NIC:
- vtnet0 connected to VM 1
- vtnet1 connected to VM 3
VM 3 has the following NIC:
- vtnet0 connected to VM 1
- vtnet1 connected to VM 2
To connect VM'serial console, you can use:
- VM 1 : sudo cu -l /dev/nmdm-BSDRP.1B
- VM 2 : sudo cu -l /dev/nmdm-BSDRP.2B
- VM 3 : sudo cu -l /dev/nmdm-BSDRP.3B
Without -r dhcp, the 3 VMs boot unconfigured and you can enter the
configuration of each router by hand, as described below.
Router configuration¶
VM3 (DHCP server)¶
port=0 disables the DNS side of dnsmasq, leaving only the DHCP server.
sysrc hostname=VM3 \
gateway_enable=NO \
ipv6_gateway_enable=NO \
ifconfig_vtnet1="inet 10.0.23.3/24" \
defaultrouter=10.0.23.2 \
dnsmasq_enable=YES
cat > /usr/local/etc/dnsmasq.conf <<EOF
port=0
log-dhcp
dhcp-range=10.0.12.100,10.0.12.200,255.255.255.0,12h
dhcp-option=3,10.0.12.2
EOF
service hostname restart
service netif restart
service routing restart
service dnsmasq start
config save
A relayed pool needs an explicit netmask
For a directly connected subnet, dnsmasq reads the netmask off its own
interface. Here 10.0.12.0/24 is only reachable through the relay, so the
netmask has to be the third field of dhcp-range. Without it, dnsmasq
refuses to serve the relayed requests.
dhcp-option=3,10.0.12.2 is the default router sent to the clients: the
relay side of VM2, not the server.
VM2 (router and DHCP relay)¶
The relay needs an address on the client subnet, given as the first field of
dhcp-relay; the second field is the server to unicast to.
sysrc hostname=VM2 \
gateway_enable=YES \
ipv6_gateway_enable=YES \
ifconfig_vtnet0="inet 10.0.12.2/24" \
ifconfig_vtnet1="inet 10.0.23.2/24" \
dnsmasq_enable=YES
cat > /usr/local/etc/dnsmasq.conf <<EOF
port=0
log-dhcp
dhcp-relay=10.0.12.2,10.0.23.3
EOF
service hostname restart
service netif restart
service routing restart
service dnsmasq start
config save
VM1 (DHCP client)¶
sysrc hostname=VM1 \
gateway_enable=NO \
ipv6_gateway_enable=NO \
ifconfig_vtnet0=SYNCDHCP
service hostname restart
service netif restart
service routing restart
config save
Final testing¶
The examples below were captured with dnsmasq 2.93 on BSDRP 2.3.
Check the address received by the client¶
[root@VM1]~# ifconfig vtnet0
vtnet0: flags=1008843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
options=880028<VLAN_MTU,JUMBO_MTU,LINKSTATE,HWSTATS>
ether 58:9c:fc:01:02:01
inet 10.0.12.123 netmask 0xffffff00 broadcast 10.0.12.255
inet6 fe80::5a9c:fcff:fe01:201%vtnet0 prefixlen 64 scopeid 0x1
media: Ethernet autoselect (10Gbase-T <full-duplex>)
The lease shows where the answer came from: the address is in the 10.0.12.0/24 pool, but the server identifier is VM3, on the other subnet.
[root@VM1]~# cat /var/db/dhclient.leases.vtnet0
lease {
interface "vtnet0";
fixed-address 10.0.12.123;
next-server 10.0.23.3;
option subnet-mask 255.255.255.0;
option routers 10.0.12.2;
option host-name "VM1";
option broadcast-address 10.0.12.255;
option dhcp-lease-time 43200;
option dhcp-message-type 5;
option dhcp-server-identifier 10.0.23.3;
option dhcp-renewal-time 21600;
option dhcp-rebinding-time 37800;
renew 1 2026/9/21 14:02:54;
rebind 1 2026/9/21 18:32:54;
expire 1 2026/9/21 20:02:54;
}
Check the relay¶
log-dhcp makes dnsmasq report the relay it set up, and the relaying itself:
[root@VM2]~# grep dnsmasq /var/log/daemon.log | grep -v 'compile time'
Sep 21 08:02:53 VM2 dnsmasq[91068]: started, version 2.93 DNS disabled
Sep 21 08:02:53 VM2 dnsmasq-dhcp[91068]: DHCP relay from 10.0.12.2 to 10.0.23.3
Sep 21 08:02:54 VM2 dnsmasq-dhcp[91068]: DHCP relay at 10.0.12.2 -> 10.0.23.3
On the wire, between the relay and the server, the broadcast requests of the client have become a unicast conversation between the two routers:
[root@VM2]~# tcpdump -pni vtnet1 -c 4 port 67
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on vtnet1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
08:02:26.361695 IP 10.0.12.2.67 > 10.0.23.3.67: BOOTP/DHCP, Request from 58:9c:fc:01:02:01, length 300
08:02:26.362376 IP 10.0.23.3.67 > 10.0.12.2.67: BOOTP/DHCP, Reply, length 300
08:02:26.634279 IP 10.0.12.2.67 > 10.0.23.3.67: BOOTP/DHCP, Request from 58:9c:fc:01:02:01, length 300
08:02:26.635027 IP 10.0.23.3.67 > 10.0.12.2.67: BOOTP/DHCP, Reply, length 300
4 packets captured
4 packets received by filter
0 packets dropped by kernel
Check the server¶
The server logs the full DISCOVER / OFFER / REQUEST / ACK exchange, and shows which pool it picked the address from:
[root@VM3]~# grep dnsmasq-dhcp /var/log/daemon.log
Sep 21 08:02:54 VM3 dnsmasq-dhcp[69781]: 1963122818 available DHCP range: 10.0.12.100 -- 10.0.12.200
Sep 21 08:02:54 VM3 dnsmasq-dhcp[69781]: 1963122818 DHCPDISCOVER(vtnet1) 58:9c:fc:01:02:01
Sep 21 08:02:54 VM3 dnsmasq-dhcp[69781]: 1963122818 DHCPOFFER(vtnet1) 10.0.12.123 58:9c:fc:01:02:01
Sep 21 08:02:54 VM3 dnsmasq-dhcp[69781]: 1963122818 DHCPREQUEST(vtnet1) 10.0.12.123 58:9c:fc:01:02:01
Sep 21 08:02:54 VM3 dnsmasq-dhcp[69781]: 1963122818 DHCPACK(vtnet1) 10.0.12.123 58:9c:fc:01:02:01 VM1
Note the interface in those lines: vtnet1, the link toward the relay, not a link on the client subnet.
Granted leases are kept in a single file, with their expiry date as a Unix timestamp, the client MAC, the address, and the hostname it announced:
[root@VM3]~# cat /var/db/dnsmasq.leases
1790020974 58:9c:fc:01:02:01 10.0.12.123 VM1 01:58:9c:fc:01:02:01