WhyISC has retired isc-dhcp (end of life since 2022),
and kea is its official successor. We will use it to serve
DHCPv4 on a multi-subnet home gateway, with the host reservations kept in
a MariaDB database (instead of the configuration file) and a REST
interface for remote management.
- Website: https://www.isc.org/kea/
- Reference: Kea Administrator Reference Manual
- Website: Stork (web dashboard)
Build information
The MariaDB backend is a compile-time option, off by default (the official packages don’t include it), so ensure the following options:
[x] MYSQL MySQL database support [ ] PGSQL PostgreSQL database support
Installation
pkg install kea
keactrl starts every daemon enabled in
/usr/local/etc/kea/keactrl.conf;
as only DHCPv4 is wanted here, disable the others.
dhcp4=yes dhcp6=no dhcp_ddns=no netconf=no
kea_enable="YES" # IP address allocation (DHCPd)
Since Kea 3.x there is no separate
kea-ctrl-agent anymore: the REST interface is served
directly by the daemons (see below). Similarly the
database backends moved into hook libraries
(libdhcp_mysql.so), which must be loaded explicitly.
Configuration
The configuration
(/usr/local/etc/kea/kea-dhcp4.conf)
is JSON, extended with comments (#, //,
/* */) and file inclusion
(<?include "file.json"?>) — the latter is handy to keep
credentials out of a configuration file that is otherwise worth tracking
in a repository.
{
"Dhcp4": {
"interfaces-config": {
// net0 carries an IP alias: pin the listener to ONE address
"interfaces": [ "Interfaces to serve", "net1" ]
},
"lease-database": {
"type": "memfile", "persist": true,
"name": "/var/db/kea/kea-leases4.csv"
},
"valid-lifetime": 86400, // 1 day
"min-valid-lifetime": 600, // 10 minutes
"max-valid-lifetime": 604800, // 1 week
"authoritative": true,
"option-data": [
{ "name": "domain-name", "data": "Domain name" },
{ "name": "domain-name-servers", "data": "Name server" }
],
"subnet4": [
{
"id": 1, // stable id (kept mnemonic: third octet)
"subnet": "192.168.1.0/24",
"option-data": [
{ "name": "routers", "data": "192.168.1.254" }
],
"pools": [
// dynamic pool reserved to clients WITHOUT a host
// reservation (the isc-dhcp "deny unknown-clients"
// + pool "allow unknown-clients" pattern)
{ "pool": "192.168.1.200 - 192.168.1.249",
"client-classes": [ "UNKNOWN" ] }
]
}
],
"loggers": [
{
"name": "kea-dhcp4",
"output-options": [ { "output": "syslog:local7" } ],
"severity": "INFO"
}
]
}
}
If an interface carries several IPv4 addresses (alias), Kea opens
one broadcast socket per address and warns
(DHCPSRV_MULTIPLE_RAW_SOCKETS_PER_IFACE) that some responses may be
duplicated. Pin the listener with the
notation as
above.iface/address
There is no pool-level lease time (the isc-dhcp pool
max-lease-time): attach a lifetime to a client class
instead, here restricted to unknown clients on the LAN interface.
"client-classes": [
{
"name": "lan-guest",
"test": "not member('KNOWN') and pkt.iface == 'LAN interface'",
"valid-lifetime": 3600
}
],
The configuration can be checked without disturbing the running server, then applied with a reload:
kea-dhcp4 -t /usr/local/etc/kea/kea-dhcp4.conf keactrl reload
Host reservations in MariaDB
Reservations can be kept in MariaDB instead of
"reservations" entries in the configuration file: they are then
queried live (a change is effective immediately, no reload), can carry a
free-form comment, and are manageable remotely (SQL or
REST). Create the database and initialize the schema
with kea-admin:
CREATE DATABASE `kea-dhcp`;
CREATE USER 'kea'@'localhost' IDENTIFIED BY 'Password';
GRANT ALL ON `kea-dhcp`.* TO 'kea'@'localhost';
kea-admin db-init mysql -u root -p 'MariaDB root password' -n kea-dhcp -h localhost
When binary logging is enabled (replication), the schema’s
triggers require the SUPER privilege: run
kea-admin db-init with the MariaDB
root account, not the kea user (which only needs table
access at runtime).
Kea checks the schema version at startup and refuses to
run on mismatch, but never upgrades it itself: after a
kea package upgrade, run
kea-admin db-upgrade mysql -n kea-dhcp … before restarting.
The database now holds the only copy of the reservations, so add it to the
backup set.
Then declare the backend, loading the MySQL hook library (Kea 3.x);
the credentials are kept in a root-only file pulled in by
<?include?>:
"hooks-libraries": [
{ "library": "/usr/local/lib/kea/hooks/libdhcp_mysql.so" },
{ "library": "/usr/local/lib/kea/hooks/libdhcp_host_cmds.so" }
],
"hosts-database": <?include "/usr/local/etc/kea/db.json"?>,
{
"type": "mysql",
"name": "kea-dhcp",
"user": "kea",
"password": "Password",
"host": "localhost"
}
A reservation is a row in the hosts table; the
user_context column holds free-form JSON, conventionally
{"comment": …}. The dhcp4_subnet_id must match the
subnet id in the configuration.
INSERT INTO hosts (dhcp_identifier, dhcp_identifier_type,
dhcp4_subnet_id, ipv4_address, hostname, user_context)
VALUES (UNHEX('d83add38b6a0'), 0, -- 0 = hw-address
1, INET_ATON('192.168.1.4'),
'brain', '{"comment": "RPI-4B (home automation)"}');
REST interface
The daemon serves its management API itself over HTTP (Kea 3.x replaced
the former kea-ctrl-agent): declare an
http control socket next to the default unix one.
Kea refuses cleartext credentials in the configuration —
user and password must each come from a file.
"control-sockets": [
{ "socket-type": "unix",
"socket-name": "/var/run/kea/kea4-ctrl-socket" },
{ "socket-type": "http",
"socket-address": "Listen address",
"socket-port": 8000,
"authentication": <?include "/usr/local/etc/kea/http-auth.json"?> }
],
{
"type": "basic",
"realm": "kea-dhcp",
"directory": "/usr/local/etc/kea",
"clients": [ { "user-file": "api.user", "password-file": "api.pw" } ]
}
Commands are JSON over POST; for example querying a reservation
through the host_cmds hook:
curl -s -u User:Password -X POST -H 'Content-Type: application/json' \ -d '{"command": "reservation-get", "arguments": {"subnet-id": 1, "identifier-type": "hw-address", "identifier": "d8:3a:dd:38:b6:a0"}}' \ http://192.168.1.5:8000/
Remember the packet filter: with a default-deny ruleset the port stays unreachable from the network until a pass rule is added for the intended clients — which is a sane way to leave it until actually needed.
Migrating from isc-dhcp
The concepts map one to one; the main translations used here:
| isc-dhcp (dhcpd.conf) | Kea (kea-dhcp4.conf) |
|---|---|
host { hardware ethernet; fixed-address; } |
reservation (file or database row) |
deny unknown-clients + pool allow |
pool "client-classes": ["UNKNOWN"] |
pool max-lease-time |
client class with valid-lifetime |
class … match if substring(option vendor-class-identifier) |
"test": "substring(option[60].hex,0,9) == '…'" |
next-server / filename |
"next-server" / "boot-file-name" (subnet, class or reservation) |
log-facility local7; |
logger output "syslog:local7" |
Leases are not worth migrating on a small network: reserved devices keep
their addresses by definition, and with "authoritative": true
unknown renewals are NAKed so dynamic clients immediately re-discover a
fresh lease. The old server is simply stopped before the new one is
started (both installed side by side), which also keeps the rollback
trivial.