Skip to content

RTFM · Networking

DHCP and address assignment: from empty interface to configured host

Address assignment is where several systems people treat as 'one thing' actually meet: DHCPv4, DHCPv6, Router Advertisements, SLAAC, prefix delegation, DNS, VLANs, relays and firewalling. This chapter follows a client from an empty interface to a fully configured host and shows who supplied every single piece; with working Kea configurations verified against the installed Kea 3.2 manuals and example set.

Saphira Linux dragon mascot

The address-assignment toolbox

A host may obtain addresses and configuration through several independent mechanisms. They coexist, and they fail independently, which is exactly why 'the machine has an IPv6 address' proves nothing about routes or DNS.

The mechanisms and where they live
IPv4
|-- static address (configured by hand)
|-- DHCPv4 (lease from a server)
'-- IPv4 link-local 169.254.0.0/16 when normal configuration fails

IPv6
|-- link-local fe80::/10 (always present, never routed)
|-- SLAAC (address built from a Router-Advertised prefix)
|-- stateful DHCPv6 (IA_NA: server assigns the address)
|-- stateless DHCPv6 options (DNS, search lists via O flag)
|-- static/global address (infrastructure interfaces)
|-- temporary/privacy addresses (rotating outbound identity)
'-- DHCPv6 Prefix Delegation (IA_PD: prefix routed to another router)

On a working dual-stack LAN a typical client ends up with one IPv4 lease, one stable SLAAC address, one rotating privacy address, and a link-local on each interface; all simultaneously, each with its own lifetime and owner. Troubleshooting starts by identifying which mechanism supplied which address.

DHCPv4: DORA, ports and the broadcast dance

A new client knows nothing, not even its own address, so the first exchange happens by broadcast on UDP. The server listens on port 67, clients listen on port 68. The acquisition sequence is named DORA:

Discover, Offer, Request, Acknowledge
Client                              Server
  |                                   |
  | DHCPDISCOVER  (broadcast)         |
  |---------------------------------->|
  |                                   |
  | DHCPOFFER     (proposed address)  |
  |<----------------------------------|
  |                                   |
  | DHCPREQUEST   (broadcast, names   |
  |  the offer it accepts)            |
  |---------------------------------->|
  |                                   |
  | DHCPACK       (lease granted,     |
  |  options attached)                |
  |<----------------------------------|
  • DISCOVER and REQUEST are broadcast because the client has no address yet; every DHCP server on the segment hears them. This is why one rogue server can poison a whole LAN.
  • REQUEST names the chosen server by its server-identifier option, so losing servers also learn they lost the race.
  • Ports in motion: client 68 -> server 67 during acquisition; renewal is unicast client 68 -> server 67.
  • The initial exchange uses 0.0.0.0 as source and 255.255.255.255 as destination; remember this when writing firewall rules.
  • Clients identify themselves by client-identifier option (or MAC hardware address when absent); servers key leases and reservations on it.

Lease lifetime: renew, rebind, expire

A lease is a contract with timers. At half the lifetime (T1, the renew timer) the client unicasts a renewal REQUEST to the original server. If the server is silent, the client keeps trying until T2 (the rebind timer, nominally 7/8 of the lifetime), then broadcasts to ANY server willing to extend. At expiry the address is surrendered; the client must stop using it and restart discovery.

The three deadlines
lease granted ------------------------- expiry
        |            |               |
      T1 renew    T2 rebind       hard stop
      (unicast    (broadcast,     (address
       to owner)   any server)     released)

In Kea, valid-lifetime sets the total; renew-timer and rebind-timer are optional; when omitted, the client picks its own per RFC 2131 and Kea simply does not send options 58/59. Set them explicitly when you want predictable renewal windows.

Capture with annotation
# Watch the whole dance on the server-side interface.
tcpdump -eni servergw0 'port 67 or port 68'

# What you will see and what it means:
#  0.0.0.0.68 > 255.255.255.255.67:  DHCPDISCOVER  <- new client, no address
#  192.168.20.1.67 > 255.255.255.255.68: DHCPOFFER  <- server proposal
#  0.0.0.0.68 > 255.255.255.255.67:  DHCPREQUEST   <- client names its winner
#  192.168.20.1.67 > 255.255.255.255.68: DHCPACK    <- lease + options granted

A complete Kea DHCPv4 configuration, verified

Kea 3.2 reads JSON. This working configuration serves one subnet on a named Saphira gateway interface; every directive below cross-checked against the installed example set (/usr/share/doc/kea/examples/kea4/) and the Kea Administrator Reference Manual shipped with the package.

Working kea-dhcp4.conf
// /etc/kea/kea-dhcp4.conf
{
  "Dhcp4": {

    // Listen ONLY on the intended gateway interface.
    "interfaces-config": {
      "interfaces": [ "servergw0" ]
    },

    // Memfile: leases survive restart, file cleaned hourly.
    "lease-database": {
      "type": "memfile",
      "persist": true,
      "name": "kea-leases4.csv",
      "lfc-interval": 3600
    },

    // Lease contract. Omitting renew/rebind lets clients pick
    // their own timers; set them to emit options 58/59.
    "valid-lifetime": 86400,
    "renew-timer": 43200,
    "rebind-timer": 75600,

    "subnet4": [
      {
        "id": 1,
        "subnet": "192.168.20.0/24",
        "interface": "servergw0",
        "pools": [ { "pool": "192.168.20.100 - 192.168.20.199" } ],
        "option-data": [
          { "name": "routers",              "data": "192.168.20.1" },
          { "name": "domain-name-servers",  "data": "192.168.20.53" },
          { "name": "domain-name",          "data": "example.test" }
        ]
      }
    ],

    "loggers": [ {
      "name": "kea-dhcp4",
      "output-options": [ { "output": "stdout" } ],
      "severity": "INFO"
    } ]
  }
}
What each block does
BlockPurpose
interfaces-configBinds the daemon to named interfaces; anything unlisted hears nothing. The primary anti-rogue control you own.
lease-database / memfileWhere leases live. persist=true writes them to disk (production default); lfc-interval schedules Lease File Cleanup so the CSV does not grow forever.
valid-lifetimeThe lease contract length. Short = fast renumbering, more chatter; long = stable, slow failover. 24h is a calm default.
renew-timer / rebind-timerT1 and T2. Explicit values make the renewal window predictable and testable.
subnet4 / subnetThe link the pool belongs to. Kea selects it by receiving interface or relay giaddr; a mismatch here is the classic 'Kea runs but nobody gets leases' cause.
idStable numeric subnet identity; referenced by relay/shared-network/pool maths.
poolsThe dynamic range actually handed out. Keep reservations outside it (or read the reservation notes below).
option-data / routersOption 3: the default gateway the client will install as its IPv4 route.
option-data / domain-name-serversOption 6: recursive resolvers the client installs.
option-data / domain-nameSearch domain appended to unqualified hostnames.
loggersstdout + INFO is right for learning; switch to syslog:/var/log once proven.

Reservations: pinning addresses the server's way

A reservation asks the DHCP server to always answer a particular client with a particular address. This beats hand-editing a static address onto an ordinary LAN client: the central server keeps one address plan, the client still behaves like a DHCP client (roaming, renewing, reporting), and changing the plan means editing one file instead of walking desk to desk.

Three reservation identifier styles, all verified
// Inside "subnet4": [ { ... "reservations": [ ... ] } ]

// 1. By hardware (MAC) address - the everyday workhorse.
{
  "hw-address": "00:11:22:33:44:55",
  "ip-address": "192.168.20.20",
  "hostname": "printer01"
}

// 2. By client identifier - survives NIC replacement, common for VMs.
{
  "client-id": "01:11:22:33:44:55:66",
  "ip-address": "192.168.20.21",
  "hostname": "libvirt-lamp"
}

// 3. By DUID - the IPv6-native identity, with per-host options.
{
  "duid": "01:02:03:04:05",
  "ip-address": "192.168.20.22",
  "option-data": [ {
    "name": "domain-name-servers",
    "data": "192.168.20.53,192.168.20.54"
  } ]
}
  • Kea can match reservations by hw-address, client-id, duid, circuit-id (from relays) and flex-id; declare in host-reservation-identifiers ONLY the types you use; each extra type costs a lookup on every lease.
  • reservations-in-subnet (default true) lets reserved addresses live inside the dynamic pool; Kea excludes them from allocation. reservations-out-of-pool=true skips that check for speed; then keep reservations outside the pool range yourself.
  • printer01 with MAC 00:11:22:33:44:55 and reserved 192.168.20.20 will always answer at the same address while remaining a normal DHCP client.

True static configuration remains correct for infrastructure: the Kea server itself, router and OVS gateway interfaces, authoritative DNS servers; anything the DHCP service depends on. If the addresser depends on the addressee, do not create a circular dependency.

DHCPv6: the same dance, different music

DHCPv6 acquisition (SARR: Solicit, Advertise, Request, Reply) parallels DORA but travels over IPv6 multicast on UDP; clients on 546, servers on 547, all-servers address ff02::1:2.

Solicit, Advertise, Request, Reply
Client                              Server
  |                                   |
  | SOLICIT   -> ff02::1:2            |
  |---------------------------------->|
  |                                   |
  | ADVERTISE                         |
  |<----------------------------------|
  |                                   |
  | REQUEST                           |
  |---------------------------------->|
  |                                   |
  | REPLY (addresses/options granted) |
  |<----------------------------------|

Two message families must never be blurred:

IA_NA versus IA_PD
Identity AssociationAsks forTypical asker
IA_NAAn address for THIS interfaceAny host
IA_PDA PREFIX to route and re-delegate furtherAnother router
Working kea-dhcp6.conf
// /etc/kea/kea-dhcp6.conf - verified against the installed kea6 examples
{
  "Dhcp6": {

    "interfaces-config": {
      "interfaces": [ "servergw0" ]
    },

    "lease-database": {
      "type": "memfile",
      "persist": true,
      "name": "kea-leases6.csv",
      "lfc-interval": 3600
    },

    // IPv6 leases carry TWO lifetimes. Preferred < valid:
    // addresses leave DNS/preference before they die.
    "preferred-lifetime": 3750,
    "valid-lifetime": 7200,
    "renew-timer": 1800,
    "rebind-timer": 3375,

    "subnet6": [
      {
        "id": 1,
        "subnet": "2a02:8012:bc57:fead::/64",
        "interface": "servergw0",
        // Pool may be a sub-range of the /64; a /80 pool is idiomatic.
        "pools": [ { "pool": "2a02:8012:bc57:fead::1000 - 2a02:8012:bc57:fead::1fff" } ],
        "option-data": [
          { "name": "dns-servers", "data": "2a02:8012:bc57:53a::53" }
        ]
      }
    ],

    "loggers": [ {
      "name": "kea-dhcp6",
      "output-options": [ { "output": "stdout" } ],
      "severity": "INFO"
    } ]
  }
}

DHCPv6 is not your IPv6 router

The single most common IPv6 misunderstanding, drawn once so it never bites again:

Who supplies what
                 IPv6 client
                     |
          +----------+----------+
          |                     |
         RA                  DHCPv6
          |                     |
   default router         address/options
   on-link prefix         DNS (option 23)
   SLAAC flags            search list (option 24)
   (M and O flags)        stateful IA_NA addresses
  • The IPv6 default route normally comes from Router Advertisements. DHCPv6 does not hand the client a default gateway in the DHCPv4 sense.
  • Therefore: IPv6 address present + no default route = investigate RA first, not DHCP.
  • A stateful DHCPv6 address alone does not prove working IPv6; the client may hold an IA_NA address it cannot route through.
Client-side proof
ip -6 addr          # which addresses exist, and which are 'mngtmpaddr'
ip -6 route         # SUCCESS looks like:
#   2a02:8012:bc57:fead::/64 dev eth0 proto kernel metric 256
#   default via fe80::XXXX dev eth0 proto ra metric 1024
#                                     ^^^ 'proto ra' means RA supplied it

Prove it works: Route ownership identified

the client's default route shows proto ra, and you can name which device sent the advertisement (tcpdump -eni <if> icmp6 and look for router advertisement) before blaming the DHCP server.

RA flags and the three deployment models

Router Advertisements carry two flags that decide how clients behave. Verified directive names from radvd.conf(5):

The flags
Flag / directiveMeaningClient behaviour when set
M flag / AdvManagedFlag onManaged address configurationClients use stateful DHCPv6 for ADDRESSES (IA_NA)
O flag / AdvOtherConfigFlag onOther configuration availableClients use stateless DHCPv6 for OPTIONS (DNS, search list)
AdvAutonomous on (per prefix)Prefix usable for SLAACClient self-builds an address in the advertised /64
AdvOnLink on (per prefix)Prefix is on this linkClients may address neighbours directly in it

The three models, as complete radvd fragments:

Support boundary, verified against the live Saphira repository: radvd is not yet packaged; these fragments are verified syntax from the radvd manual, ready for the day it ships (or for a self-provided build). The RA/SLAAC mechanics they teach are protocol, not packaging.

SLAAC only
# Model 1: SLAAC only (M=0, O=0) - addresses AND options from RA.
interface servergw0
{
  AdvSendAdvert on;
  prefix 2a02:8012:bc57:fead::/64
  {
    AdvOnLink on;
    AdvAutonomous on;
  };
  RDNSS 2a02:8012:bc57:53a::53 { };
};
SLAAC plus stateless DHCPv6
# Model 2: SLAAC + DHCPv6 for options (M=0, O=1).
interface servergw0
{
  AdvSendAdvert on;
  AdvOtherConfigFlag on;     # options come from DHCPv6
  prefix 2a02:8012:bc57:fead::/64
  {
    AdvOnLink on;
    AdvAutonomous on;
  };
};
Stateful DHCPv6
# Model 3: stateful DHCPv6 (M=1) - Kea assigns addresses.
interface servergw0
{
  AdvSendAdvert on;
  AdvManagedFlag on;
  AdvOtherConfigFlag on;
  # No AdvAutonomous prefix unless you ALSO want SLAAC addresses.
  prefix 2a02:8012:bc57:fead::/64
  {
    AdvOnLink on;
    AdvAutonomous off;
  };
};

Renumbering subtlety, verified from radvd.conf(5): clients ignore AdvValidLifetime below two hours for an EXISTING prefix (RFC 4862 §5.5.3). Do not plan a fast renumber by advertising a short valid lifetime; the floor is two hours.

Prefix Delegation: obtaining space is a separate job from assigning it

The Saphira reference network, end to end. Each stage is a different mechanism with a different owner; this separation is the teaching point:

One /48, four mechanisms
ISP
 |
PPPoE
 |
ppp0
 |
dhcpcd , DHCPv6-PD (IA_PD) requests a /48
 |      , receives 2a02:8012:bc57::/48
 |
2a02:8012:bc57::/48
 |
+-- :fead::/64  servers   (static -> servergw0, radvd advertises)
+-- :db01::/64  data      (static -> datagw0)
+-- :53a::/64   nameservers (static -> ns1gw0)
'-- further /64s reserved for VPNs, guests, labs

Keep the four jobs apart in your head and your config:

  • DHCPv6-PD obtains the delegated prefix from the ISP. It assigns nothing.
  • Static/interface configuration assigns chosen /64s to router interfaces. It is a local decision.
  • Routing makes those prefixes reachable; ip -6 route, forwarding, firewall sets.
  • radvd tells downstream hosts about each /64. It advertises; it does not create.
The ia_pd pattern
# The WAN client on the reference router (production file, verbatim).
interface ppp0
        noipv4
        ipv6only
        ipv6rs

        # Request the delegated /48, however do not assign it to
        # LAN interfaces. LAN /64s are statically configured elsewhere.
        ia_pd 2/::48 -

The trailing dash is the whole philosophy: request the /48 with IAID 2, assign nothing automatically. The router, not dhcpcd, decides which /64 lands where. Support boundary, stated honestly: this production router currently runs its PPP layer from our published articles; Saphira packaging of pppd/rp-pppoe is planned, and DHCPv6-PD as a role remains dhcpcd/Kea territory until then.

Multiple VLANs in one Kea server

One Kea instance serves many subnets; each keeps its own pool, gateway and DNS options. Deliberately different pools per VLAN beat one enormous shared pool: smaller blast radius, per-zone options, and lease maths that stay legible.

Four VLANs, four policies
// subnet4 entries (excerpt) - one per VLAN
{ "id": 10, "subnet": "192.168.10.0/24", "interface": "usergw0",
  "pools": [ { "pool": "192.168.10.100 - 192.168.10.199" } ],
  "option-data": [
    { "name": "routers",             "data": "192.168.10.1" },
    { "name": "domain-name-servers", "data": "192.168.20.53" }
  ]
},
{ "id": 20, "subnet": "192.168.20.0/24", "interface": "servergw0",
  "pools": [ { "pool": "192.168.20.100 - 192.168.20.149" } ],
  "option-data": [
    { "name": "routers",             "data": "192.168.20.1" },
    { "name": "domain-name-servers", "data": "192.168.20.53" }
  ]
},
{ "id": 30, "subnet": "192.168.30.0/24", "interface": "mgmtgw0",
  "pools": [ { "pool": "192.168.30.50 - 192.168.30.99" } ],
  "option-data": [
    { "name": "routers",             "data": "192.168.30.1" },
    { "name": "domain-name-servers", "data": "192.168.30.53" }
  ]
},
{ "id": 40, "subnet": "192.168.40.0/24", "interface": "guestgw0",
  "pools": [ { "pool": "192.168.40.100 - 192.168.40.199" } ],
  "option-data": [
    { "name": "routers",             "data": "192.168.40.1" },
    // Guests get an external resolver pair: they are not our DNS clients.
    { "name": "domain-name-servers", "data": "9.9.9.9,149.112.112.112" }
  ]
}

Lease storage, inspection and survival

Our configuration uses the memfile backend. Verified behaviour from the shipped Administrator Reference Manual:

  • Lease file: [kea-install-dir]/var/lib/kea/kea-leases4.csv (and kea-leases6.csv). As of Kea 2.7.9 the name may be given without a path; the data directory is compile-time, overridable only via KEA_DHCP_DATA_DIR.
  • persist=true (the default and the production setting) writes every lease to disk; false is for throwaway labs only, because everything is lost on restart.
  • lfc-interval (default 3600) schedules Lease File Cleanup: kea-lfc rewrites the CSV without historical rows. Kea runs it itself; the standalone kea-lfc binary exists for manual repair: kea-lfc -4 -p <pid> -x <previous> -i <copy> -o <output> -f <finish> -c <config>.
  • Leases survive service restart: memfile reloads the CSV at startup. Damaged rows are logged and skipped up to max-row-errors before the server refuses to continue; a corrupted file degrades, it does not silently empty.
  • Backup the CSVs with the configuration. Copy both while the service is stopped (or use LFC boundaries); a live CSV copied mid-write can contain a torn row; exactly what max-row-errors tolerates.

For live inspection, the control channel is the modern route. kea-shell speaks to the Control Agent; the lease commands come from the libdhcp_lease_cmds.so hook, which is installed:

Two ways to read lease state
# Single lease by address / all leases (control channel over REST)
kea-shell --host 127.0.0.1 --port 8000 lease4-get \
+  --parameters '{ "ip-address": "192.168.20.101" }'
kea-shell --port 8000 lease4-get-all

# Raw CSV - perfectly readable on a small LAN
columns: address, hwaddr, client_id, valid_lft, ... 
tail -5 /var/lib/kea/kea-leases4.csv

Present tooling, honestly: kea-dhcp4, kea-dhcp6, kea-dhcp-ddns, keactrl (start|stop|reload|status), kea-shell, kea-lfc, kea-admin and the hook set including libdhcp_lease_cmds.so, libdhcp_ha.so and libdhcp_lease_query.so are installed. Tools that are NOT installed are not documented here.

Validate, run, and prove it is listening

Kea validates before it risks. The installed manual defines -t exactly: 'Checks the configuration file and reports the first error, if any. Note that not all parameters are completely checked; in particular, service and control channel sockets are not opened, and hook libraries are not loaded.' (-T goes further: it also establishes database connections.)

Validation and its two outcomes
kea-dhcp4 -t /etc/kea/kea-dhcp4.conf
kea-dhcp6 -t /etc/kea/kea-dhcp6.conf

# A syntax error announces itself, e.g.:
#   /etc/kea/kea-dhcp4.conf:12.3: syntax error, unexpected '\'",
#   expecting ...   <- JSON broke; fix the file, nothing else changed
#
# Success looks like:
#   DHCPv4 server has completed configuration review: 1 subnet(s),
#   0 host reservations found, ... (and exit status 0)

Passing -t proves JSON structure and value sanity; nothing about the network design. It will happily bless a pool on a subnet no interface or relay ever touches. Design correctness is proven with packets, next.

Service checks
# OpenRC (Saphira's init)
rc-service kea-dhcp4 status
rc-service kea-dhcp4 start
rc-update add kea-dhcp4 default

rc-service kea-dhcp6 status
rc-service kea-dhcp6 start

# Or the bundled multi-daemon controller:
keactrl status
keactrl start
keactrl reload
Listening sockets
# Which socket belongs to which protocol:
ss -lunp | grep -E ':(67|68|546|547)\b'
#  :67/udp  kea-dhcp4  <- DHCPv4 server
#  :547/udp kea-dhcp6  <- DHCPv6 server
#  (68 and 546 are CLIENT ports - seen on clients, not servers)

# And confirm it bound the interface you intended:
ss -lunp | grep kea

Firewalling DHCP and its IPv6 cousins

DHCPv4's initial exchange breaks normal firewall assumptions: the client sends from 0.0.0.0:68 to 255.255.255.255:67 before it owns any address. Rules must accept that explicitly:

DHCPv4 rules
nft add rule inet filter input \
+  iifname "servergw0" udp dport 67 accept \
+  comment "DHCPv4: accept 0.0.0.0:68 -> 255.255.255.255:67 from clients"

# Relayed server side: relays send from their own address to :67
nft add rule inet filter input \
+  ip saddr 192.168.10.0/24 udp dport 67 accept

Never casually block ICMPv6 on a DHCPv6/RA network. Router Advertisements, Neighbour Discovery and Path MTU are protocol functions, not optional ping traffic; filter them by type and interface, and DHCPv6/RA designs fail mysteriously without them.

DHCPv6 and ICMPv6 rules
# DHCPv6: clients -> ff02::1:2 port 547
nft add rule inet filter input \
+  iifname "servergw0" ip6 daddr ff02::1:2 udp dport 547 accept

# RA and NDP stay reachable on LAN interfaces:
nft add rule inet filter input \
+  iifname "servergw0" icmpv6 type { router-advertisement, \
+    neighbour-solicit, neighbour-advert, packet-too-big } accept

Rogue DHCP: the wrong gateway from nowhere

Because DISCOVER/REQUEST are broadcast, ANY server on the segment may answer. The classic accident: an old consumer router plugged into the LAN, WAN port dangling, DHCP enabled; suddenly dozens of machines receive:

The symptom
Client receives:  gateway 192.168.1.254   (the dusty router)
Expected:         gateway 192.168.1.1     (your Kea server)
Capture and identify
# Catch every offer on the wire; the server MAC of each OFFER
# is visible in the link header:
tcpdump -eni servergw0 'port 67 or port 68'

# Two different source MACs answering = two DHCP servers.
# The DHCP server-identifier option inside each offer names them,
# and ip -6 equivalent for rogue RAs:
tcpdump -eni servergw0 icmp6 and 'ip6[40] == 134'
  • Symptoms downstream of rogue DHCP look like flaky networking: intermittent 'wrong gateway', DNS pointing somewhere strange, addresses from an alien pool.
  • The IPv6 twin: a rogue RA announces a default route to every SLAAC client instantly; same capture discipline, port-free ICMPv6.
  • Defences you own: interfaces-config binding (Kea hears only intended links), switch DHCP-snooping where available, and never plugging WAN ports into LANs.

DNS interactions: who told the client THAT resolver?

Clients may learn DNS from five independent sources: DHCPv4 (option 6), DHCPv6 (option 23), RA RDNSS, VPN software, and static resolver configuration. Multiple sources produce layered resolver lists whose order surprises people.

Determine the resolver actually installed
# What does the machine ACTUALLY use?
resolvectl status            # per-link resolvers + their source
# or, minimal systems:
cat /etc/resolv.conf

# On the wire, who is advertising?
tcpdump -eni servergw0 'port 547 or (icmp6 and ip6[40] == 134)'

When resolvers disagree: VPN pushing 10.8.0.1, DHCP saying 192.168.20.53, RA saying the router; behaviour depends on resolver priority rules per OS. Design one authority per LAN segment and verify on a real client, not in the config file.

Address conflicts: the usual suspects

  • Static address inside the DHCP pool: Kea offers the address again to someone else; two hosts, one address. Keep static out of pools (or reserve it).
  • Duplicate reservation: two reservations claiming one address, or two servers reserving differently. Kea logs reservation conflicts; read the log.
  • Cloned VM with duplicated identity; same MAC and/or client-id as its template; both hosts claim one lease. Regenerate machine-id and MAC on clones.
  • Changed NIC/MAC: a reservation keyed to the old hardware address no longer matches; the host drifts into the dynamic pool.
  • Stale lease: a long valid-lifetime holds a dead client's address after replacement. Shorten lifetimes or clear the lease via the control channel.
  • IPv6 Duplicate Address Detection; the kernel marks a colliding address 'dadfailed'; it exists, find it with ip -6 addr and investigate the twin, not just this host.
  • Privacy/temporary addresses; the rotating address is normal, not a conflict; services bound to a specific dynamic IPv6 address break when it rotates. Bind services to a static address.
  • Service bound to an expired/renumbered address; the address died with the lease; the service still listens on a ghost. Check ss -lntp against current addresses.

Renumbering: the delegated prefix will change someday

IPv4 taught a generation to believe addresses are furniture. A delegated IPv6 prefix is more like a lease on the building. Plan for:

The day it happens
old delegated prefix 2a02:8012:bc57::/48
        |
   ISP renumber / provider change
        |
new delegated prefix 2a02:XXXX:YYYY::/48
  • Interface addresses: SLAAC re-addresses itself via RA lifetimes (observe the two-hour floor above); static /64 assignments must be regenerated.
  • RA configurations: prefix stanzas rewritten, or templated from the delegation so this becomes sed, not surgery.
  • DNS AAAA records: with the new prefix live, update zones, or better, keep public names on a DNS provider and script the updates.
  • Firewall rules and nftables sets written as specific addresses become stale; generate rules from the prefix.
  • Services bound to specific addresses (ss -lntp) need rebinding; prefer binding to interfaces or wildcard where safe.
  • DHCPv6 pools and Kea subnet6 declarations: rewritten with the prefix.
  • Reverse DNS: the old prefix's PTR zone goes away with it; the new zone must be delegated/managed (see the reverse-DNS chapter).

Preferred versus valid lifetimes are the mechanism that makes this survivable: clients hold the old address (valid) while preferring the new one (preferred), until validity expires. Kea's separate preferred-lifetime/valid-lifetime and radvd's AdvPreferredLifetime/AdvValidLifetime express the same contract to different halves of the system.

High availability: two servers, one pool, the honest story

Two independent DHCP servers answering from the same pool is not redundancy; it is a collision generator:

The problem
server A thinks 192.168.20.150 is free
server B thinks 192.168.20.150 is free
      -> two clients, one address, intermittent doom

The installed Kea ships the HA hook (libdhcp_ha.so, verified in /usr/lib/kea/hooks/) which solves this properly: partners share lease state, detect each other's failure via heartbeats, and either split traffic or run hot-standby. The lease-cmds hook must be loaded too: HA uses it to synchronise leases.

Kea HA, minimal verified shape
// Both servers, identical except this-server-name (verified
// against the shipped HA examples):
"hooks-libraries": [ {
  "library": "libdhcp_lease_cmds.so",
  "parameters": { }
},
{
  "library": "libdhcp_ha.so",
  "parameters": {
    "high-availability": [ {
      "this-server-name": "dhcp1",
      "mode": "hot-standby",        // or "load-balancing"
      "heartbeat-delay": 10000,
      "max-response-delay": 60000,
      "max-ack-delay": 5000,
      "max-unacked-clients": 5,
      "sync-timeout": 60000
    } ]
  }
} ]

Remember the failure-domain lesson from the deployment-models chapter: an HA pair on one switch, one power feed, or one hypervisor shares a fate that defeats the point. Standby servers belong in a different failure domain or they are decoration.

Security notes worth repeating

  • DHCP is generally unauthenticated on ordinary LANs; it is convenience, not access control. 802.1X/RADIUS (see network-services) is the actual authentication layer.
  • Bind Kea to intended interfaces only (interfaces-config) and firewall the ports per interface; do not expose Kea on transit or management links.
  • Rogue servers are a broadcast problem; monitor for multiple offerers as routine.
  • A reservation is not authentication: it pins an address to an identifier, which is trivially spoofed. It is bookkeeping, not identity proof.
  • Management and control surfaces (Control Agent REST, kea-shell, stats sockets) belong on the management network or localhost; never public.
  • Lease data reveals hostnames and device identities; treat /var/lib/kea/kea-leases*.csv as private, and include them in backup-access policy accordingly.

Worked example 1: home LAN, complete build

Everything on one Saphira box: it IS the gateway. Pool 192.168.1.100-199, DNS pointing back at the router's own resolver.

Home build
// /etc/kea/kea-dhcp4.conf - home LAN
{
  "Dhcp4": {
    "interfaces-config": { "interfaces": [ "lan0" ] },
    "lease-database": { "type": "memfile", "persist": true, "lfc-interval": 3600 },
    "valid-lifetime": 86400,
    "subnet4": [ {
      "id": 1, "subnet": "192.168.1.0/24", "interface": "lan0",
      "pools": [ { "pool": "192.168.1.100 - 192.168.1.199" } ],
      "option-data": [
        { "name": "routers",             "data": "192.168.1.1" },
        { "name": "domain-name-servers", "data": "192.168.1.1" },
        { "name": "domain-name",         "data": "home.example.test" }
      ]
    } ]
  }
}
Start and prove
# Build, validate, start, prove
kea-dhcp4 -t /etc/kea/kea-dhcp4.conf
rc-service kea-dhcp4 start
ss -lunp | grep ':67 '

# Client side: renew and watch the lease land
#   dhcpcd -k lan0 && dhcpcd lan0        (or your client's renew)
ip -4 addr show lan0; ip route

# Server side: watch DORA live
tcpdump -eni lan0 'port 67 or port 68'

# A client that renewed (RENEW unicast, not broadcast) proves T1 works:

Prove it works: A home client is completely configured

the client holds a 192.168.1.1xx lease, its route says via 192.168.1.1, its resolver says 192.168.1.1, and the capture shows the full DORA; every piece of config accounted for.

Worked example 2: dual-stack business VLAN (who supplies what)

VLAN 20 servers: IPv4 192.168.20.0/24 and IPv6 2a02:8012:bc57:fead::/64. Three daemons cooperate; the table says exactly which supplies each bit.

Every piece of client configuration, and its supplier
Client receivesSupplied byMechanism
IPv4 addresskea-dhcp4DORA lease
IPv4 gateway (192.168.20.1)kea-dhcp4option 3 routers
IPv4 DNS (192.168.20.53)kea-dhcp4option 6
IPv6 global addressSLAAC from RAAdvAutonomous prefix
IPv6 default routeRArouter advertisement, proto ra
IPv6 DNS (2a02:8012:bc57:53a::53)radvd RDNSS (or Kea opt. 23 if O=1)RDNSS
Search domainkea-dhcp4 option 15 / stateless DHCPv6domain-name / domain-list
Kea, both families
// Kea DHCPv4 for the VLAN (inside subnet4)
{ "id": 20, "subnet": "192.168.20.0/24", "interface": "servergw0",
  "pools": [ { "pool": "192.168.20.100 - 192.168.20.149" } ],
  "option-data": [
    { "name": "routers",             "data": "192.168.20.1" },
    { "name": "domain-name-servers", "data": "192.168.20.53" }
  ]
}

// Kea DHCPv6: stateful model - RA (below) says M=1, O=1, so Kea
// supplies addresses AND options; no AdvAutonomous on the prefix.
// See the kea-dhcp6.conf in this chapter; subnet entry:
{ "id": 20, "subnet": "2a02:8012:bc57:fead::/64", "interface": "servergw0",
  "pools": [ { "pool": "2a02:8012:bc57:fead::1000 - 2a02:8012:bc57:fead::1fff" } ],
  "option-data": [
    { "name": "dns-servers", "data": "2a02:8012:bc57:53a::53" }
  ]
}
radvd, stateful
// radvd.conf - stateful model matching the Kea choices above
interface servergw0
{
  AdvSendAdvert on;
  AdvManagedFlag on;         # M=1: addresses via DHCPv6
  AdvOtherConfigFlag on;     # O=1: options via DHCPv6
  prefix 2a02:8012:bc57:fead::/64
  {
    AdvOnLink on;
    AdvAutonomous off;       # no SLAAC: Kea owns addresses
  };
};

Prove it works: Dual-stack VLAN fully accounted

a client holds an IPv4 lease AND a DHCPv6 IA_NA address, shows proto ra default route, uses the named resolvers, and you can attribute every received value to its supplying daemon.

Worked example 3: routed multi-VLAN with relay (design-first)

One Kea server on the server VLAN; users and guest VLANs are routed, so their broadcasts never reach Kea unaided. The relay architecture from this chapter applies:

One server, many links
            Kea 192.168.20.53
             |
          server VLAN 192.168.20.0/24
             |
       Saphira router
      /      |        \
  usergw0  servergw0  guestgw0
  192.168.10.1          192.168.40.1
      |                   |
  relay here          relay here
  (giaddr 192.168.10.1) (giaddr 192.168.40.1)
      |                   |
  users VLAN         guest VLAN
  • Kea already holds subnet4 entries for 192.168.10.0/24 and 192.168.40.0/24 (see the multi-VLAN section); that is the server half.
  • The relay half needs a relay agent on the router. Verified boundary: Saphira does not currently package one; options are a self-provided relay, or collapsing the VLANs onto one bridge where isolation policy allows. When a relay ships in the repository, this page documents it; with packets, not assumptions.
  • Verification is two-sided: capture port 67/68 on the CLIENT VLAN (broadcast visible, unicast reply returning via relay) and on the SERVER VLAN (giaddr-stamped unicast arriving).

Prove it works: Relayed VLAN behaves like a local one

a users-VLAN client completes DORA across the relay, receives a 192.168.10.x lease with the users gateway and DNS, and the capture shows the reply arriving on the client VLAN.

Troubleshooting matrix and the diagnostic ladder

Symptom, layer, first evidence
SymptomLikely layerFirst evidence
No IPv4 addressDHCPv4 / service / VLANtcpdump ports 67/68 on both ends
IPv4 address, no Internetgateway option / routing / firewallip route: ping the gateway
IPv6 link-local onlyRA / DHCPv6 / prefixip -6 addr: RA capture
Global IPv6, no default routeRA missingip -6 route (no proto ra line)
IPv6 route but no DNSDHCPv6 / RDNSS / resolverresolvectl status
Wrong gateway intermittentlyrogue DHCPcapture multiple OFFERs
Same address on two hostspool / static overlapleases + ARP/neighbour table
One VLAN works, another does notrelay / interface binding / firewallcapture both sides of the relay
Kea starts but no leasesinterface binding / subnet mismatchservice logs + packet capture
Lease works once then failsrenewal / lease DB / firewalllease timestamps + T1 capture
The ladder: reasoning, not command roulette
1.  Does the interface exist?          ip link
2.  Is it up?                           ip addr / carrier
3.  Is the service listening there?     ss -lunp + interfaces-config
4.  Does the client request leave?      capture on client side
5.  Does the server receive it?         capture on server side
6.  Does the server answer?             capture OFFER/ADVERTISE
7.  Does the answer return?             capture on client again
8.  What address did the client install?   ip addr
9.  What route did it install?          ip route / ip -6 route
10. What resolver did it install?       resolvectl status
11. Can it reach the gateway?           ping the gateway
12. Can it reach an external IP?        ping 1.1.1.1 / 2606:4700::1111
13. Can it resolve a name?              dig A + AAAA a public name

The ladder is ordered by causality: each step failing explains every symptom above it. Walk it from the top every time; skipping to step 13 is how an afternoon disappears.

Where the rest of the story lives

This chapter stays self-sufficient for a working Kea deployment, and deliberately does not duplicate the deeper material: