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.conf

MacStudioを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

ソケットオープンに失敗してる。さて、これで何が起きるかというと。

  1. keaはrawで AF_PACKET socket を 1 度だけ open する
  2. インターフェイスが down / not ready なら open は失敗する
  3. 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でログインは出来ていたし)、というのが一番怪しいシナリオ。