Skip to main content
RunBook Academy

← All labs in OPNsense

Lab · intermediate · ~60 min

Lab: Configure a DHCPv4 scope and a DHCP relay across VLANs

B · Nested virtualisationC · Simulation

Objectives

  • Configure a DHCPv4 scope for the LAN interface with the correct pool, gateway, DNS and lease time
  • Add static reservations by MAC address
  • Advertise option 66 and option 150 for VoIP provisioning
  • Build a DHCP relay on a second VLAN and confirm a client on that VLAN receives an address from the LAN's scope
  • Read the DHCP lease file to verify the four-tuple (IP, MAC, hostname, lease expiry)

Prerequisites

This lab builds a complete DHCPv4 service on OPNsense: a scope on the LAN interface with reservations, the VoIP provisioning options, and a relay for a second VLAN whose traffic cannot reach the server directly. By the end you will have walked a lease through three layers — server, relay, client — and read the result back out of OPNsense’s lease database.

The lab uses a single-tenant design that is small enough to fit on one nested host: two VLANs on a trunk, one DHCP server, one relay, two clients. Scaling this up to a real estate is a matter of repeating the same scope, not rewriting it.

Objective

By the end of this lab, you can:

  • Build a DHCPv4 scope with the right pool, gateway, DNS and lease time for a subnet.
  • Add static reservations by MAC address without breaking dynamic allocation for everyone else.
  • Advertise DHCP option 66 (TFTP server) and option 150 (TFTP server IP for VoIP) without contaminating option 1 (subnet mask) or option 3 (router).
  • Configure a DHCP relay on a VLAN whose broadcast domain does not reach the server.
  • Read the lease database to verify the four-tuple a working lease produces.

Requirements

You need:

  • An OPNsense instance with at least three interfaces: WAN, LAN (the DHCP server’s home), and a VLAN-capable parent interface for VLAN 20 (the relayed segment).
  • Two DHCP clients — physical hosts, VMs, or containers — one on each VLAN.
  • Shell access on OPNsense (ssh root@<lan-ip> or the console).
  • Optional but recommended: tcpdump on a Linux client so you can watch the four-packet DORA exchange.

If you do not have a switch that can carry 802.1Q, you can run the whole topology on one nested host using VLAN sub-interfaces on Linux or two OPNsense VLANs.

Tasks

Task 1: Lay down the address plan

Before touching the GUI, write the address plan. This is the part most operators skip, and it is the part most operators get wrong. A DHCP scope is just the dynamic half of an address plan; the reservations and the static hosts must fit in the static half.

VLAN 10 (LAN)         10.10.10.0/24
  gateway             10.10.10.1   (OPNsense LAN)
  DNS                 10.10.10.1   (Unbound on OPNsense)
  DHCP pool           10.10.10.100 - 10.10.10.200
  reserved (static)   10.10.10.10 - 10.10.10.99
  reservations        .50 printer, .51 AP, .52 NAS, .53 camera NVR
  lease time          12h
  option 66           10.10.10.20   (TFTP, lab only)
  option 150          10.10.10.21   (VoIP provisioning)

VLAN 20 (relayed)     10.10.20.0/24
  gateway             10.10.20.1   (OPNsense VLAN 20)
  relay points at     10.10.10.1   (the DHCP server on LAN)
  DHCP pool           10.10.20.100 - 10.10.20.200
  lease time          6h

The pool is the top half of the subnet; the bottom half is for static hosts. If you do not enforce that split on paper, the dynamic pool will silently overlap a static reservation one day and two devices will fight over the address.

Task 2: Configure the LAN scope

From the OPNsense GUI, Services → DHCPv4 → [LAN]:

FieldValue
Enableon
Range from10.10.10.100
Range to10.10.10.200
Gateway10.10.10.1 (default is fine)
DNS servers10.10.10.1 (Unbound)
Domain namelab.local
Default lease time7200 (2h, in seconds)
Maximum lease time43200 (12h)
Static ARP entriesoff

Click Save, then Apply changes at the top of the page.

Confirm the daemon actually started before doing anything else:

# On OPNsense
service dhcpd status
sockstat -l -p 67

sockstat should show dhcpd listening on *:67 on the LAN interface. If it is not listening, the GUI said “applied” but the service did not restart — check System → Log → General for the reason. Common causes: the LAN interface address is not in the range you configured the pool for, or you have another DHCP server on the same broadcast domain.

Task 3: Add static reservations

Services → DHCPv4 → [LAN] → scroll to Static ARP entries below the pool. Add one entry per device that needs a stable IP:

printer  10.10.10.50  aa:bb:cc:dd:ee:01  printer.lab.local
ap       10.10.10.51  aa:bb:cc:dd:ee:02  ap.lab.local
nas      10.10.10.52  aa:bb:cc:dd:ee:03  nas.lab.local
nvr      10.10.10.53  aa:bb:cc:dd:ee:04  nvr.lab.local

Save and apply. The GUI writes the entries to /etc/dhcpd.conf (or /var/dhcpd/etc/dhcpd.conf on some builds) and restarts the daemon. Verify:

grep -A4 'host printer' /etc/dhcpd.conf

Task 4: Advertise option 66 and option 150

Option 66 is a string (a hostname, conventionally); option 150 is a list of IPv4 addresses (used by Cisco VoIP handsets and similar gear that will not accept hostnames). Both are configured under the per-interface scope, in the Additional BOOTP/DHCP options box.

option 66 tftp.lab.local
option 150 10.10.10.21

OPNsense takes the option number and the value and emits the right DHCP option wire format. Save and apply.

Verify the resulting config file:

grep -E 'option (tftp-server|tftp-server-name)' /etc/dhcpd.conf

You should see both option tftp-server-name "tftp.lab.local"; and option tftp-server 10.10.10.21;. The names differ because the underlying ISC DHCP daemon calls them that, but both option codes 66 and 150 are present.

If a VoIP phone in this lab refuses to provision, the first thing to check is whether the phone received option 150 at all. Many phones log this on boot; you can also confirm by capturing on the LAN side:

sudo tcpdump -i eth0 -vvv -s 0 -n port 67 and port 68 2>&1 \
  | grep -E 'option 66|option 150'

Task 5: Get a lease on the LAN

From a client on the LAN, request a fresh lease. The exact command depends on the OS:

# Linux (NetworkManager)
sudo nmcli connection down "Wired connection 1" && \
sudo nmcli connection up   "Wired connection 1"

# Linux (dhclient)
sudo dhclient -r eth0
sudo dhclient -v eth0

# Windows
ipconfig /release && ipconfig /renew

# macOS
sudo ipconfig set en0 DHCP && sudo ipconfig getpacket en0

Confirm the client received the values you configured:

ip -4 addr show dev eth0 | grep inet
ip -4 route show default
resolvectl status | grep -A2 eth0

The IP should be inside 10.10.10.100 - 10.10.10.200, the gateway should be 10.10.10.1, and the DNS server should be 10.10.10.1.

Task 6: Read the lease database

The DHCP server’s record of the lease is the authoritative answer to “did this work?”, because the GUI’s status bar lies — it counts handshakes, not leases handed out. Read the file directly:

cat /var/dhcpd/var/db/dhcpd.leases | tail -40

A successful entry looks like:

lease 10.10.10.105 {
  starts 4 2026/08/14 12:00:00;
  ends 4 2026/08/14 14:00:00;
  cltt 4 2026/08/14 12:00:00;
  binding state active;
  next binding state free;
  hardware ethernet aa:bb:cc:dd:ee:10;
  client-hostname "laptop01";
}

The four-tuple to read off is IP, MAC, hostname, expiry. If any of those is wrong, the client did not get what you intended — even if the GUI said it did.

Task 7: Build the relay on VLAN 20

Services → DHCPv4 → Relay. Add an entry:

FieldValue
Enabledon
InterfaceVLAN20
Agent10.10.10.1 (the DHCP server’s IP)
Source address10.10.20.1 (the relay’s VLAN 20 IP)

Save and apply. Confirm the relay daemon is up:

sockstat -l -p 67
ps -axo pid,command | grep -E 'dhcrelay|dhcpd'

You should see both dhcpd (on *:67 for LAN) and dhcrelay (on *:67 for VLAN 20, plus a unicast socket to 10.10.10.1).

Task 8: Get a lease on the relayed VLAN

From a client on VLAN 20, request a lease (same commands as Task 5, just on a different interface). Confirm:

ip -4 addr show dev eth0.20 | grep inet

The IP must be inside 10.10.20.100 - 10.10.20.200. That is the proof the relay worked: the client is on VLAN 20, but the address it got is from the pool the LAN’s DHCP server is serving for the 10.10.20.0/24 range.

Task 9: Capture the DORA on the relay

This is the diagnostic every operator should be able to produce from memory. Capture on the relayed VLAN side, watch the four packets:

sudo tcpdump -i eth0.20 -nn -vvv -s 0 -c 8 port 67 or port 68

From the VLAN 20 client, release and renew. The capture shows:

12:00:00.000 client → ff:ff:ff:ff:ff:ff DHCP Discover
12:00:00.010 server → client              DHCP Offer
12:00:00.020 client → ff:ff:ff:ff:ff:ff DHCP Request
12:00:00.030 server → client              DHCP ACK

If the relay is broken, you will see the Discover go out and nothing come back. If the relay is up but the server is missing the VLAN 20 scope, you will see the Discover go out, the server log a DHCPOFFER on 10.10.10.1, and the client get nothing.

Validation

  • A client on LAN leases an address in 10.10.10.100 - 10.10.10.200, with gateway 10.10.10.1 and DNS 10.10.10.1.
  • A client on VLAN 20 leases an address in 10.10.20.100 - 10.10.20.200.
  • /etc/dhcpd.conf shows both option 66 (string) and option 150 (IPv4 address).
  • A reservation for aa:bb:cc:dd:ee:01 maps to 10.10.10.50, confirmed in the lease file by IP, MAC, hostname and expiry.
  • sockstat -l -p 67 shows both dhcpd and dhcrelay listening.
  • A tcpdump on VLAN 20 shows the four-packet DORA exchange between the relayed client and the LAN’s server.

Expected Result

A working DHCPv4 service that serves both the directly-connected LAN and a second VLAN through a relay, with reservations for the static half of the subnet and option 66/150 advertised to all dynamic clients. The lease database contains entries for both VLANs and the relay daemon is logged as handing requests to the server.

Troubleshooting

The client gets no offer. Check, in order: (1) sockstat -l -p 67 on OPNsense shows dhcpd on the right interface, (2) the LAN/VLAN 20 interface address is inside the pool’s parent subnet, (3) no firewall rule on the client’s VLAN blocks UDP/67 and UDP/68 between the broadcast domain and OPNsense, (4) System → Log → General does not contain dhcpd: ... not authoritative.

The relay works but the wrong scope answers. OPNsense shows the active scope per interface. A relayed request arrives at the server with giaddr set to the relay’s source IP. The server matches the scope whose subnet contains giaddr, not the interface the request came in on. If the relay’s giaddr is on 10.10.20.1, you need a scope for 10.10.20.0/24 enabled on the OPNsense interface that owns that address (VLAN 20).

Reservations silently disappear after a GUI change. Static mappings are stored in the GUI’s config.xml, not in /etc/dhcpd.conf. A XML export/import round-trip preserves them; a hand-edited dhcpd.conf does not. Always edit reservations through the GUI.

Option 150 arrives empty. The DHCP daemon parses option 150 as a comma-separated list of IPv4 addresses, not as a string. Use 10.10.10.21 and not tftp.lab.local for option 150; that is option 66’s job.

Cleanup

Reset OPNsense to a clean state and undo the client changes.

# On OPNsense: disable the DHCP scope and the relay through the GUI.
# Services → DHCPv4 → [LAN]  → uncheck Enable → Apply changes
# Services → DHCPv4 → [VLAN20] → uncheck Enable (only if you created one)
# Services → DHCPv4 → Relay    → uncheck Enabled → Apply changes

# Confirm both daemons are stopped
sockstat -l -p 67 | grep -E 'dhcpd|dhcrelay' || echo "no DHCP daemons listening"

# On each client, release the lease
sudo dhclient -r  # Linux
ipconfig /release # Windows

If you created a test-only scope on VLAN 20 for this lab, delete it now. The GUI keeps deleted scopes in /var/dhcpd/etc/ for reference but they will re-appear in the GUI on every firmware upgrade until you remove them through Services → DHCPv4 → [LAN] → Delete.

What you learned

  • A DHCP scope is the dynamic half of an address plan; the static half has to be reserved by the operator, never by accident.
  • Options 66 (string) and 150 (IPv4 list) are different wire types even though they look similar in the GUI. Mixing them up is the most common VoIP-provisioning failure.
  • A relay does not run a DHCP server. It only repackages broadcasts as unicasts. The scope the server hands out must exist on the interface that owns the relay’s giaddr subnet.
  • The lease file is the authoritative answer. The GUI’s status page counts handshakes; the lease file counts leases that completed and were not yet released.

Deliverables

  • · A working DHCPv4 scope on LAN with pool, gateway, DNS, lease time, and at least two reservations
  • · Option 66 and option 150 advertised to clients
  • · A DHCP relay on VLAN 20 handing out addresses from the LAN scope
  • · Lease file dump showing a successful lease on the relayed VLAN

Verification status

Last reviewed
2026-08-14
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.