OPNsense: Migrate from ISC DHCP to Dnsmasq
AI-generated guide
This guide and the lease conversion script were written by an AI. I did not write the text myself. I did, however, carry out the procedure exactly as described on my production OPNsense 26.7 with five VLANs, 21 static mappings and 76 active leases, and verified the result afterwards.
ISC DHCP has been end-of-life since 2022. OPNsense still ships it (marked as [legacy] in the menu), but new installations use Dnsmasq since 25.7 and ISC will eventually move out of the core.
This guide migrates an existing multi-VLAN setup from ISC DHCPv4 to Dnsmasq. It also covers something most guides skip: taking over the active leases, so Dnsmasq knows which addresses are in use from the first second instead of waiting until every client has renewed.
Why Dnsmasq and not Kea
OPNsense offers both. For a home or small office network Dnsmasq is probably the better fit:
| Dnsmasq | Kea | |
|---|---|---|
| OPNsense default for new installs | yes (since 25.7) | no |
| PXE / network boot options in the GUI | yes | no |
| Registers dynamic client names in DNS | yes | static reservations only |
| Dynamic IPv6 prefix tracking | yes | no |
| High availability / failover | no | yes |
Unless you need DHCP failover, pick Dnsmasq.
Before you start
Rollback is always possible
Disable Dnsmasq, re-enable ISC on all interfaces. Clients keep their leases until they expire, so there is no hard outage at any point of this migration.
- Backup: System → Configuration → Backups → Download configuration
- Work from a device with a static IP or a fixed DHCP assignment so you can always reach the GUI
- If you have IPv6 via SLAAC (Router Advertisements) and ISC DHCPv6 is disabled, IPv6 is not affected at all. Do not create IPv6 ranges in Dnsmasq.
1. Collect the ISC values
For every interface under Services → ISC DHCPv4 → [Interface], note:
| ISC field | Goes to |
|---|---|
| Range from / to | Dnsmasq → DHCP ranges |
| Additional Pools | one extra range each |
| DNS servers (if filled) | Dnsmasq → DHCP options, option 6 |
| DNS servers (empty) | means "system DNS", you have to set option 6 explicitly in Dnsmasq |
| Gateway (empty) | nothing, Dnsmasq defaults to the interface IP as well |
| Default lease time | Dnsmasq range → Lease time (empty = 86400 s) |
| NTP servers | option 42 |
| Static mappings | count them for the import check |
Interfaces without ISC config
The Dnsmasq interface dropdown lists every interface. Only select the ones that actually had an ISC instance.
2. Export the static mappings
Services → ISC DHCPv4 → [Interface] → download icon below the static mappings table.
The export is one CSV per interface with the columns ip_address,hw_address,hostname,description. Merge them into one file and check for:
- Duplicate hostnames: Dnsmasq registers names in DNS, give each host a unique name
- Same MAC in several subnets (e.g. a test VM): fine, Dnsmasq picks the entry matching the subnet, and the GUI import accepts it
3. Dnsmasq → General
Services → Dnsmasq DNS & DHCP → General, enable advanced mode.
| Setting | Value |
|---|---|
| Enable | leave off until step 7. Dnsmasq cannot bind port 67 while ISC is running |
| Interface | every interface that had ISC enabled |
| DNS → Listen port | 0 if another resolver (Pi-hole, Unbound, AdGuard) stays in charge. This disables the Dnsmasq DNS part completely |
| DHCP → DHCP FQDN | on |
| DHCP → DHCP default domain | shows your system domain, cannot be empty |
| DHCP → DHCP local domain | on |
| DHCP → DHCP authoritative | on (Dnsmasq is the only DHCP server) |
| DHCP → DHCP register firewall rules | on |
| DHCP → Router advertisements | off if radvd handles IPv6 |
| ISC / KEA DHCP (legacy) | all off |
Apply.
Authoritative and network boot
Authoritative means Dnsmasq answers requests for unknown leases instead of ignoring them. A TFTP server or a ProxyDHCP for PXE boot is not a second DHCP server, so this setting is correct even with network boot in place.
4. DHCP ranges
Tab DHCP ranges, one entry per interface. Leave Subnet mask on automatic, Constructor on None, Tag and Domain empty.
| Field | Value |
|---|---|
| Interface | the interface |
| Start / End address | the ISC range |
| Lease time | empty for the default, or your ISC value |
| Description | e.g. LAN IPv4 |
Example for a typical setup:
| Interface | Start | End | Lease time |
|---|---|---|---|
| LAN | 192.168.1.150 | 192.168.1.254 | 86400 |
| IoT | 192.168.5.50 | 192.168.5.254 | 31536000 |
| Guest | 192.168.10.50 | 192.168.10.254 | 86400 |
Long lease times are no longer needed for stable IoT addresses
Under ISC a one-year lease is a common trick to keep IoT devices on the same address without creating a static mapping for every single one. Dnsmasq derives the offered address from a hash of the MAC address, so a device gets its old address again even after the lease expired and was forgotten. You can keep the long lease, but it now only controls how long a replaced device stays visible in the lease list.
5. Import the static mappings
Tab Hosts → upload icon (bottom right) → your merged CSV. Compare the row count, open two or three entries and check MAC, IP and hostname. Apply.
Reservations outside the dynamic range keep working as long as a range exists in the same subnet. You do not have to renumber anything.
6. DHCP options
Tab DHCP options, section Options. Type Set, Tag empty, Force off.
DNS (option 6)
ISC used the system DNS servers when the field was empty. Dnsmasq does not do this reliably, so set it explicitly. If some networks use different resolvers (e.g. a dedicated Pi-hole for IoT), create one entry per interface:
| Interface | Option | Value |
|---|---|---|
| LAN | dns-server [6] | 192.168.1.3,192.168.1.4 |
| Guest | dns-server [6] | 192.168.1.3,192.168.1.4 |
| IoT | dns-server [6] | 192.168.1.6,192.168.1.7 |
NTP (option 42)
One entry covers all networks:
| Interface | Option | Value |
|---|---|---|
| (empty = all) | ntp-server [42] | 0.0.0.0 |
0.0.0.0 means 'this interface'
Dnsmasq replaces 0.0.0.0 with the address of the interface the request came in on. Works for any option that takes an IP address. Under ISC you had to enter the interface IP on every single interface.
Apply.
7. Switch over and take over the active leases
Dnsmasq starts with an empty lease table. Without a takeover it would not know which addresses ISC has handed out until every client renews. With day-long leases that is fine, with year-long IoT leases it means months of blind spots in which Dnsmasq might offer an address that is actually in use (it pings first, but sleeping devices do not answer).
The script below converts the active ISC leases into the Dnsmasq lease file format. Nothing runs on the firewall itself, you only copy two files.
| # | Where | Action |
|---|---|---|
| 1 | GUI | Services → ISC DHCPv4 → [each interface]: untick Enable → Save |
| 2 | PC | scp root@<opnsense>:/var/dhcpd/var/db/dhcpd.leases ./dhcpd.leases |
| 3 | PC | python3 isc2dnsmasq_leases.py --isc ./dhcpd.leases --out ./dnsmasq.leases --force |
| 4 | PC | scp ./dnsmasq.leases root@<opnsense>:/var/db/dnsmasq.leases |
| 5 | GUI | Services → Dnsmasq DNS & DHCP → General → Enable → Save → Apply |
| 6 | GUI | Tab Leases: the imported leases show up immediately |
Do steps 1 to 5 in one go. Clients keep working on their existing leases in the meantime.
--force only skips the check for running dnsmasq/dhcpd processes, which is meant for running the script on the firewall itself. On your PC it would otherwise trip over a local dnsmasq from libvirt or NetworkManager.
Lease file path
OPNsense does not set dhcp-leasefile, so Dnsmasq uses the FreeBSD default /var/db/dnsmasq.leases. ISC runs chrooted, its lease file is /var/dhcpd/var/db/dhcpd.leases.
isc2dnsmasq_leases.py
What it does: keeps the last block per IP (ISC appends new states), takes only binding state active, skips leases expiring within 10 minutes, turns ends never into expiry 0 (infinite), strips domain parts from hostnames and writes * as client ID. Dnsmasq matches such leases by MAC and fills in the client ID on the first renewal. An existing output file is backed up first.
| isc2dnsmasq_leases.py | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
8. Verify
- Renew a lease on a client:
- Check address, gateway and DNS:
- A client with a fixed assignment shows up in the Leases tab with Lease Type
static. - Over the next days, confirm that IoT devices keep their addresses after a reboot.
Side effect you will probably notice: reconnects are a lot faster. ISC syncs its lease file on every transaction and hooks into other services, Dnsmasq answers known clients straight from memory.
Rollback
- Dnsmasq → General → Enable off → Apply
- ISC DHCPv4 → Enable on every interface → Save
- Read Services → Dnsmasq DNS & DHCP → Log File, fix the cause, try again from step 7
Afterwards
- ISC stays installed: as of 26.7 it is still part of the OPNsense core and cannot be uninstalled. Disabled on all interfaces,
dhcpdsimply does not run. When a future release moves it to a plugin, do not install that plugin. - Local hostname resolution: with the Dnsmasq DNS part disabled, your resolver does not know the DHCP client names. If you want that, enable Dnsmasq DNS on a side port (e.g.
53053, not5353which is mDNS) and forward your local domain from Pi-hole/Unbound to127.0.0.1:53053. - Network boot: Dnsmasq has the PXE options in the GUI (DHCP boot, DHCP tags). A TFTP server next to it is enough, no second DHCP server needed.
Sources
- OPNsense docs: Dnsmasq DNS & DHCP
- opnsense/core #9155: Move ISC DHCP to plugins
- Home Network Guy: Migrate from ISC DHCP to dnsmasq or Kea
- dnsmasq-discuss: why addresses are allocated by MAC hash