Kea DHCPv4 + IPAM Hook 完整工作流程
基于代码分析,这是IPv4版本的完整执行流程:
阶段零:Hook 加载(Kea 启动时)
scss
Kea 启动 → 加载 libkea_spring_ipam_hookv4.so → load() 执行
↓
registerCallout("pkt4_receive", pkt4_receive) // 注册接收拦截点
registerCallout("lease4_select", lease4_select) // 注册租约选择拦截点
↓
等待 DHCPv4 请求
阶段一:请求接收(pkt4_receive)
vbnet
1. 客户端发送 DHCPDISCOVER 报文
↓
2. Kea 接收报文,触发 pkt4_receive Callout
↓
3. Hook 从报文中提取关键信息:
- MAC 地址(从 chaddr 字段提取,去除非十六进制字符)
- Hostname(Option 12)
- Transaction ID(用于日志追踪)
↓
4. 生成缓存键:cacheKey = MAC + ":" + subnetId(1)
↓
5. 检查内存缓存 g_assignedIpCache:
├─ 命中 → 直接使用 cached IP,跳过步骤 6-8
└─ 未命中 → 继续步骤 6
↓
6. 构建 JSON 请求体(buildPayload):
{
"request_id": "pkt4-{transid}",
"client_mac": "{MAC}",
"client_id": "",
"client_duid": "",
"hostname": "{hostname}",
"subnet_id": 1,
"relay_ip": "",
"vendor_class": "",
"ip_version": 4,
"timestamp": 0
}
↓
7. 通过 HttpClient::assignFromIpam 调用 IPAM API:
- URL: http://192.168.100.5:8080/api/v1/dhcp/assign
- Headers: Authorization: Bearer {token}, X-Internal-Token: {token}
- Timeout: 连接 500ms,总超时 1000ms
- 解析响应中的 assigned_ip、gateway、lease_seconds
↓
8. IPAM 响应处理:
├─ 成功 (HTTP 200) → 解析 assigned_ip (如 192.168.1.100)
├─ 失败 → 写入日志,return 0(Kea 使用本地池回退)
└─ 空 IP → 同上,回退本地池
↓
9. 将分配的 IP 写入内存缓存:
g_assignedIpCache[cacheKey] = assigned_ip
↓
10. 写入 MySQL 审计(upsertReservation):
- hosts 表:INSERT ... ON DUPLICATE KEY UPDATE
- 字段:dhcp_identifier(MAC HEX), dhcp_identifier_type(0),
dhcp4_subnet_id(1), ipv4_address(整型), hostname
- 注:使用 ON DUPLICATE KEY UPDATE 实现 upsert
↓
11. return 0 → Kea 继续处理 DISCOVER
阶段二:租约选择(lease4_select)
kotlin
1. Kea 准备从本地池中选择 IP 地址(这是关键的拦截点!)
↓
2. 触发 lease4_select Callout
- 此时 Kea 尚未最终决定分配哪个 IP
↓
3. 从缓存读取 IP:
cacheKey = MAC + ":" + subnetId(1)
├─ 无缓存 → 跳过,Kea 使用本地池分配
└─ 有缓存 → 继续步骤 4
↓
4. 【核心差异 - IPv4 vs IPv6】
⚠️ 重要发现:当前 IPv4 实现中,lease4_select 只做了日志记录,
没有实际注入 IP 地址!
IPv4 版本是"只读"的:
- 读取缓存中的 IP
- 写入日志
- 没有修改 lease 结构体
- 没有覆盖 Kea 的本地池分配
↓
5. return 0 → Kea 继续处理
- Kea 会从本地地址池中分配 IP(如 100-200 范围)
- 而不是使用 IPAM 返回的 IP
完整 DHCPv4 会话流程(当前实现)
bash
客户端 Kea + Hook IPAM
│ │ │
│ ① DHCPDISCOVER │ │
│─────────────────────────>│ │
│ │ │
│ ┌──────┴──────┐ │
│ │pkt4_receive │ │
│ │ 提取 MAC │ │
│ │ 查缓存: 无 │ │
│ └──────┬──────┘ │
│ │ │
│ │ ② POST /assign │
│ │─────────────────────────>│
│ │ │
│ │ ③ assigned_ip: 10.0.0.5 │
│ │<─────────────────────────│
│ │ │
│ ┌──────┴──────┐ │
│ │ 写入缓存 │ │
│ │ 写入DB审计 │ │
│ └──────┬──────┘ │
│ │ │
│ ④ DHCPOFFER │ │
│<─────────────────────────│ │
│ (IP 来自本地池, │ │
│ 非 IPAM 分配) │ │
│ │ │
│ ⑤ DHCPREQUEST │ │
│─────────────────────────>│ │
│ │ │
│ ┌──────┴──────┐ │
│ │lease4_select│ │
│ │ 查缓存记录 │ │
│ │ 但未修改IP │ │
│ └──────┬──────┘ │
│ │ │
│ ⑥ DHCPACK │ │
│<─────────────────────────│ │
│ (仍是本地池IP) │ │
│ │ │
│ 配置完成! │ │
│ 使用本地池地址 │ │
关键日志追踪点
ini
[kea-spring-ipam-hook] pkt4_receive entry
[kea-spring-ipam-hook] pkt4_receive transid=0x1234
[kea-spring-ipam-hook] pkt4_receive mac=44:a8:42:f6:6d:ee
[kea-spring-ipam-hook] pkt4_receive cacheKey=44a842f66dee:1
[kea-spring-ipam-hook] pkt4_receive payload={"client_mac":"44a842f66dee",...}
[kea-spring-ipam-hook] pkt4_receive assigned_ip=10.0.0.5
[kea-spring-ipam-hook] pkt4_receive reservation upsert ok
[kea-spring-ipam-hook] lease4_select entry
[kea-spring-ipam-hook] lease4_select found cached assigned_ip=10.0.0.5
[kea-spring-ipam-hook] lease4_select exit
Kea DHCPv6 + IPAM Hook 完整工作流程
阶段零:Hook 加载(Kea 启动时)
scss
Kea 启动 → 加载 libkea_spring_ipam_hookv6.so → load() 执行
↓
registerCallout("pkt6_receive", pkt6_receive) // 注册接收拦截点
registerCallout("pkt6_send", pkt6_send) // 注册发送拦截点
↓
等待 DHCPv6 请求
阶段一:请求接收(pkt6_receive)
rust
1. 客户端发送 SOLICIT 报文
↓
2. Kea 接收报文,触发 pkt6_receive Callout
↓
3. Hook 从报文中提取关键信息:
- DUID(客户端唯一标识)
- IAID(IA 标识符,从 Option 3/25/4 中提取)
- Hostname(Option 12)
- Transaction ID(用于日志追踪)
↓
4. 生成缓存键:cacheKey = "v6:" + DUID + ":" + subnetId(6)
↓
5. 检查内存缓存 g_assignedIpCache:
├─ 命中 → 直接使用 cached IP,跳过步骤 6-8
└─ 未命中 → 继续步骤 6
↓
6. 构建 JSON 请求体(buildPayloadV6):
{
"request_id": "pkt6-{transid}",
"client_id": "{DUID}",
"client_duid": "{DUID}",
"iaid": {IAID},
"hostname": "{hostname}",
"subnet_id": 6,
"ip_version": 6
}
↓
7. 通过 HttpClient::assignFromIpam 调用 IPAM API:
- URL: http://192.168.100.5:8080/api/v1/dhcp/assign
- Headers: Authorization: Bearer {token}, X-Internal-Token: {token}
- Timeout: 连接 500ms,总超时 1000ms
↓
8. IPAM 响应处理:
├─ 成功 (HTTP 200) → 解析 assigned_ip (如 ::69)
├─ 失败 → 清除缓存,return 0(Kea 使用本地池回退)
└─ 空 IP → 同上,回退本地池
↓
9. 将分配的 IP 写入内存缓存:
g_assignedIpCache[cacheKey] = assigned_ip
↓
10. 异步写入 MySQL 审计(upsertReservationV6):
- hosts 表:写入 DUID + subnet_id + hostname + iaid
- ipv6_reservations 表:写入 IPv6 地址 + IAID
- 注:即使写入失败,不影响 DHCP 响应
↓
11. return 0 → Kea 继续处理请求
阶段二:响应发送(pkt6_send)
markdown
1. Kea 已构建好响应报文(包含从本地池分配的临时地址)
↓
2. 触发 pkt6_send Callout
↓
3. 从缓存读取 IP:
cacheKey = "v6:" + DUID + ":" + subnetId(6)
├─ 无缓存 → 跳过修改,使用 Kea 原生响应
└─ 有缓存 → 继续步骤 4
↓
4. 【完全重建 IA_NA 选项】(核心逻辑):
a. 删除 Kea 原有的 IA_NA 选项:
response->delOption(3)
→ 清除 Kea 从本地池分配的地址
↓
b. 调用 addIA_NA_Option 创建全新的 IA_NA:
- 创建 Option6IA(3, iaid)
- 设置 T1 = 43200 秒(12小时)
- 设置 T2 = 75600 秒(21小时)
- 创建 Option6IAAddr(5, addr, 86400, 86400)
- 将 IAADDR 添加到 IA_NA
- 将 IA_NA 添加到响应包
↓
5. 响应包现在只包含 IPAM 分配的 IP(如 ::69)
↓
6. return 0 → Kea 发送修改后的响应
阶段三:完整 DHCPv6 会话流程
yaml
客户端 Kea + Hook IPAM
│ │ │
│ ① SOLICIT │ │
│─────────────────────────>│ │
│ │ │
│ ┌──────┴──────┐ │
│ │pkt6_receive │ │
│ │ 查缓存: 无 │ │
│ └──────┬──────┘ │
│ │ │
│ │ ② POST /assign │
│ │─────────────────────────>│
│ │ │
│ │ ③ assigned_ip: ::69 │
│ │<─────────────────────────│
│ │ │
│ ┌──────┴──────┐ │
│ │ 写入缓存 │ │
│ │ 写入DB审计 │ │
│ └──────┬──────┘ │
│ │ │
│ ④ ADVERTISE (::69) │ │
│<─────────────────────────│ │
│ │ │
│ ⑤ REQUEST (请求 ::69) │ │
│─────────────────────────>│ │
│ │ │
│ ┌──────┴──────┐ │
│ │ 查缓存: ::69 │ │
│ └──────┬──────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ pkt6_send │ │
│ │ 删除旧IA_NA │ │
│ │ 重建新IA_NA │ │
│ └──────┬──────┘ │
│ │ │
│ ⑥ REPLY (确认 ::69) │ │
│<─────────────────────────│ │
│ │ │
│ 配置成功! │ │
│ 2001:db8:100::69 │ │
关键技术决策
| 决策点 | 实现方式 | 原因 |
|---|---|---|
| IP 获取时机 | pkt6_receive 中调用 IPAM | 尽早获取 IP,为后续响应准备 |
| 响应修改位置 | pkt6_send 中重建 IA_NA | 在 Kea 构建完响应后覆盖,确保最终输出 |
| 缓存策略 | 内存 unordered_map + mutex | 避免同一会话重复调用 IPAM |
| 数据库操作 | pkt6_receive 中同步执行 | 审计需要,失败不影响 DHCP 主流程 |
| 地址注入方式 | 删除 IA_NA + 重建 | 彻底清除 Kea 本地池地址,避免多地址冲突 |
| 故障降级 | IPAM 失败时 return 0 | 让 Kea 使用配置的本地池(100-1ff) |
日志追踪关键点
ini
[kea-spring-ipam-hookv6] pkt6_receive entry
[kea-spring-ipam-hookv6] pkt6_receive transid=0x2b6318
[kea-spring-ipam-hookv6] pkt6_receive duid=00:01:...
[kea-spring-ipam-hookv6] pkt6_receive iaid=423929922
[kea-spring-ipam-hookv6] http request body={...}
[kea-spring-ipam-hookv6] http response code=200
[kea-spring-ipam-hookv6] http response body={"assigned_ip":"2001:db8:100::69"}
[kea-spring-ipam-hookv6] pkt6_receive IPAM assigned: 2001:db8:100::69
[kea-spring-ipam-hookv6] pkt6_send: IA_NA rebuilt with IPAM assigned IP