keaにどハマりした件
RHEL10の問題ではないし仕様と言えなくも無いけれど、問題を踏んでしまった当方からすると、甚だ疑問という感じなので書いておく。
事の発端
今朝6時過ぎに、iPhoneが「インターネットに接続されてないよ」と言い出した。「WiFiが死んだのか……。」と思い、ASUS Zen WiFiのアプリを立ち上げると、確かにインターネットに接続されてない。ここで、話の理解のために前提を少し説明する。
Zen WiFiは192.168.50/0を吹いている。メインのAPは、WAN側に192.168.1.7が割当たるように、192.168.1.4の自宅鯖のDHCP(kea-dhcp4)が設定されている。同じ192.168.1.4ではDNS(Unbound)を提供していて、192.168.50.0/24にも広報されている。
つまり、iPhoneには、192.168.50.0/24のアドレスが割り当てられ、DNSは192.168.1.4が設定される。
あちこち再起動
Zen WiFiを再起動しても直らない。いつもなら大体これで直るのに。となると、Zen WiFiの上流が怪しいので、コアとして使ってる10GスイッチとHGWも再起動。それでも直らない。
居室スイッチを再起動
まだ構築作業中なので自宅鯖は居室にあって、MacStudioと同じ10Gスイッチに接続してる。これかな?と思って再起動する。やっぱり直らない。この時、間違って近くに置いてあるミニPCのACアダプタのコネクタを抜いてしまって、再接続した(原因その1)。
自宅鯖を再起動
kea-dhcp4のステータスを見ると、active(実はこの時、あるエラーを見逃してる。原因その2)。念の為、自宅鯖を再起動するが、やはり直らない。
# systemctl status kea-dhcp4
● kea-dhcp4.service - Kea DHCPv4 Server
Loaded: loaded (/usr/lib/systemd/system/kea-dhcp4.service; enabled; preset: disabled)
Drop-In: /etc/systemd/system/kea-dhcp4.service.d
└─10-tuning-slice.conf
Active: active (running) since Tue 2026-04-28 08:07:54 JST; 1min 21s ago
Invocation: 4e0223faa9ee4c8b9c2f210be5a2eab6
Docs: man:kea-dhcp4(8)
Main PID: 1400 (kea-dhcp4)
Status: "Dispatching packets..."
IO: 5.1M read, 8K written
Tasks: 25 (limit: 815405)
Memory: 8.3M (peak: 9M)
CPU: 56ms
CGroup: /net.slice/kea-dhcp4.service
└─1400 /usr/sbin/kea-dhcp4 -c /etc/kea/kea-dhcp4.confMacStudioをWiFiだけにして確認
macOSなら色々ツールが使えるので、WiFiだけにして、pingやらdigやら……、ん?名前が引けてない。自宅鯖のUnboundか?
自宅鯖にsshでログインしようとすると、
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ここで、勘の良い人なら気付くw
まだ起きたばかりで頭が回ってなかった当方は気付かず(言い訳です)。
答え合わせ
原因1: スイッチと間違えてACアダプタのコネクタを接続し直したミニPCは、BIOS設定で復電後に自動的に起動する。固定IPアドレスが割り当ててあり、192.168.1.4になっている。つまり新たに構築した自宅鯖とIPアドレスが衝突する。
原因2: ミニPCに192.168.1.4が割当たるので、自宅鯖は再起動後に、192.168.1.4を割り当てられない。keaは、IPアドレスが割当たって無くても「ちゃんと」起動する。
すると、ssh 192.168.1.4はミニPCに向いてしまうので、REMOTE HOST IDENTIFICATION HAS CHANGEDになる。そして、以下の状態に陥る。
kea + systemdの問題
見落としたのは以下のエラー。
2026-04-28 08:07:54.366 WARN [kea-dhcp4.dhcpsrv] DHCPSRV_OPEN_SOCKET_FAIL failed to open socket: the interface enp2s0 is not runningソケットオープンに失敗してる。さて、これで何が起きるかというと。
- keaはrawで AF_PACKET socket を 1 度だけ open する
- インターフェイスが down / not ready なら open は失敗する
- socket open 失敗時に WARN を出して起動継続するが、リトライはしない(1の仕様)
このリトライしないことが仕様なのかどうか、という点についてはupsteamで問題を認識しながらも放置されているので、ある意味仕様。
そしてもう一つkeaには問題があって、コントロールソケット経由でstatistic-get-all 等は取れるけど、「リッスンしてるソケットが今いくつ開いているか」を返す API がない。可観測性が欠如してる。
次にsystemdを見ると、Type=notify になっている時、READYの判定は設定がロードされた、であって、DHCPサービスとして動作しているか、ではない。ソケットが0個でも、元気よく「active (running)です!」って報告する。
keaのこの「仕様」と、RHEL10のkeaのsystemdのユニットファイルが合わさって、「DHCPサーバとして動いてないし、リトライもしないけど、activeに見える」状態が完成する。
対策
まず、keaにユニット(/etc/systemd/system/kea-dhcp4.service.d/20-wait-iface.conf)を追加して、IPアドレスの付与を待つようにする。
[Unit]
Wants=lan-addresses-ready.service sys-subsystem-net-devices-enp2s0.device
After=lan-addresses-ready.service sys-subsystem-net-devices-enp2s0.device
[Service]
Restart=on-failure
RestartSec=5s付与を待つユニット(lan-addresses-ready.service)は、以下。
[Unit]
Description=Wait until required LAN addresses (IPv4 + IPv6 GUA non-tentative) are present on enp2s0
Documentation=file:///usr/local/sbin/wait-lan-addresses-ready.sh
DefaultDependencies=no
Wants=network-online.target
After=network-online.target
Before=kea-dhcp4.service kea-dhcp6.service
[Service]
Type=oneshot
Environment=IFACE=enp2s0
Environment="REQUIRE_V4=192.168.1.3 192.168.1.4"
Environment="REQUIRE_V6=240f:10a:da26:1::4"
Environment=POLL_INTERVAL_SEC=1
Environment=MAX_WAIT_SEC=110
ExecStart=/usr/local/sbin/wait-lan-addresses-ready.sh
RemainAfterExit=yes
TimeoutStartSec=120
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.targetヘルパースクリプト(/usr/local/sbin/wait-lan-addresses-ready.sh)は以下。
set -eu
IFACE="${IFACE:-enp2s0}"
REQUIRE_V4="${REQUIRE_V4:-192.168.1.3 192.168.1.4}"
REQUIRE_V6="${REQUIRE_V6:-240f:10a:da26:1::4}"
POLL_INTERVAL_SEC="${POLL_INTERVAL_SEC:-1}"
MAX_WAIT_SEC="${MAX_WAIT_SEC:-110}"
if ! command -v ip >/dev/null 2>&1; then
echo "wait-lan-addresses-ready: 'ip' (iproute2) is required" >&2
exit 2
fi
# v4_present <addr>
# exit 0 if <addr>/<prefix> is currently assigned to $IFACE.
v4_present() {
_addr=$1
ip -4 addr show dev "$IFACE" 2>/dev/null \
| grep -qE "[[:space:]]${_addr}/"
}
# v6_present_non_tentative <addr>
# exit 0 if <addr>/<prefix> is currently assigned to $IFACE AND
# the matching `inet6` line does NOT carry the `tentative` flag.
# The `tentative` token only appears on lines for addresses still
# undergoing Duplicate Address Detection. Linux refuses bind() on
# tentative addresses, so checking presence alone is not enough.
v6_present_non_tentative() {
_addr=$1
_line=$(ip -6 addr show dev "$IFACE" 2>/dev/null \
| grep -E "[[:space:]]${_addr}/" || true)
[ -n "$_line" ] || return 1
# `tentative` is reported by `ip -6 addr` immediately after the
# scope keyword for addresses that have not yet completed DAD.
case " $_line " in
*" tentative "*) return 1 ;;
*) return 0 ;;
esac
}
ready() {
for _v4 in $REQUIRE_V4; do
v4_present "$_v4" || return 1
done
for _v6 in $REQUIRE_V6; do
v6_present_non_tentative "$_v6" || return 1
done
return 0
}
# Single-shot fast path: most boots are already ready by the time
# every Before= consumer is being scheduled, so don't even sleep.
if ready; then
echo "wait-lan-addresses-ready: all required addresses present on ${IFACE}"
exit 0
fi
_waited=0
while [ "$_waited" -lt "$MAX_WAIT_SEC" ]; do
sleep "$POLL_INTERVAL_SEC"
_waited=$((_waited + POLL_INTERVAL_SEC))
if ready; then
echo "wait-lan-addresses-ready: all required addresses present on ${IFACE} after ${_waited}s"
exit 0
fi
done
# Diagnostic dump on timeout so the journal entry is self-contained.
echo "wait-lan-addresses-ready: TIMEOUT after ${MAX_WAIT_SEC}s on ${IFACE}" >&2
echo " required IPv4: ${REQUIRE_V4}" >&2
echo " required IPv6 (non-tentative): ${REQUIRE_V6}" >&2
echo " current state of ${IFACE}:" >&2
ip -4 addr show dev "$IFACE" >&2 || true
ip -6 addr show dev "$IFACE" >&2 || true
exit 1それでも謎は残る
早朝にこの問題を起こしたトリガーは分からない。その時にはミニPCの電源は入ってない。なので、昨日自宅鯖を再起動した時に、192.168.1.4がまだ付与されてない段階でkeaは起動してしまい、他のサービスは起動していた(sshでログインは出来ていたし)、というのが一番怪しいシナリオ。

