- 来源: 合宙 LuatOS 官方仓库 air780epm 模块完整代码 - 路径: luatos/air780epm/module/Air780EPM/demo 含官方 demo(含 mqtt/mqtts/socket/uart 等) - 后续: 基于 demo 开发 UART<->MQTT 数据上报功能
14 KiB
netdrv IKEv2/IPsec 客户端设计文档
适用范围:LuatOS
netdrv框架下的 IKEv2/IPsec 隧道模式客户端 (ESP tunnel mode)。首期已在 PC 模拟器上对接真实 strongSwan 网关ipsec.air32.cn(strongSwan 5.9.13, Ubuntu 24.04 云主机)联调通过。
1. 目标与范围
在 netdrv 框架内新增 netdrv.IPSEC 虚拟网卡,实现:
- IKEv2 发起端(RFC 7296),服务器证书链校验 + 强制 EAP-MSCHAPv2;
- NAT-T(RFC 3948):UDP 500 启动,探测到 NAT 后切换 4500;
- DPD 保活、CHILD_SA 到期重协商(CREATE_CHILD_SA,默认带 PFS, KE + 新 Ni/Nr 按 RFC 7296 §2.17 派生 KEYMAT;网关不支持 PFS 时自动 无 KE 重试一次并保持旧 SA 在线)、IKE SA 到期全量重建;
- 套件扩展:IKE DH 组 19/20/21(ECP-256/384/521,RFC 4753), ESP AES-GCM-128/256(RFC 4106 AEAD,SA 内无 INTEG transform);
- MOBIKE(RFC 4555,opt-in,默认关闭):本地地址变化时发送 INFORMATIONAL(UPDATE_SA_ADDRESSES + NAT-D),处理对端发起的地址更新, 以及 established 状态下的对端地址学习;不做 ADDITIONAL_IP4_ADDRESS 通告与 COOKIE2;
- CP(Configuration Payload)下发虚拟 IPv4 + DNS;
- ESP 隧道模式(RFC 4303)AES-CBC-128/256 + HMAC-SHA1-96 / HMAC-SHA2-256-128 或 AES-GCM-128/256,32 包反重放窗口, ESP-in-UDP(4500)。
明确不做:IKEv1、L2TP/IPsec、ESP 传输模式、客户端证书认证、
IPv6 隧道、MOBIKE 的 ADDITIONAL_IP4_ADDRESS 通告与 COOKIE2。
MOBIKE 默认关闭(ipsec_mobike_enable 未配置时行为与旧版一致,
地址变化仍走断线重连)。
2. 文件布局
代码按用户要求拆分到独立子模块 components/network/ipsec/(内部头在
include/ipsec/,源文件在 src/):
| 文件 | 说明 |
|---|---|
include/ipsec/ipsec_crypto.h + src/ipsec_crypto.c |
PRF+/HMAC、DH modp2048、IKE/CHILD 密钥派生、AUTH 计算/验签、X.509 链+SAN 校验(信任锚由 ipsec_ca_cert_pem 提供;不提供时默认 fail-closed,仅当显式开启 ipsec_insecure_cert_ok 才接受证书且仍校验 SAN) |
include/ipsec/ipsec_esp.h + src/ipsec_esp.c |
ESP 隧道封装/解封(AES-CBC + HMAC 或 AES-GCM AEAD)、SPI 方向、32 包反重放 |
include/ipsec/ipsec_ike.h + src/ipsec_ike.c |
IKEv2 状态机:SA_INIT/AUTH/EAP/CREATE_CHILD_SA/INFORMATIONAL、payload 编解码、SK 加密、NAT-T 切换、DPD、重协商、虚拟 netif 与 adapter 收发 |
include/ipsec/ipsec_vendor_md4.h + src/ipsec_vendor_md4.c |
MD4(Apache-2.0, 从 mbedTLS 2.x vendor, 自包含) |
include/ipsec/ipsec_vendor_chap_ms.h + src/ipsec_vendor_chap_ms.c |
MS-CHAPv2(BSD, 移植自 lwIP2.2 chap_ms.c),含 strongSwan 线格式与 RFC 3079 MSK 派生、RFC 2759 测试向量自检 |
src/luat_netdrv_ipsec.c |
netdrv 胶水层:setup/ctrl(UPDOWN)/dhcp(-1)/debug + 链路状态回调 |
components/network/netdrv/include/luat_netdrv_ipsec.h |
胶水层头文件(保持在 netdrv/include) |
接入点:
luat_netdrv_drv.h:LUAT_NETDRV_IMPL_IPSEC 7luat_netdrv.h:luat_netdrv_ipsec_conf_t+conf->ipsec_confluat_netdrv.c/luat_lib_netdrv.c:setup 分发与ipsec_*参数解析bsp/pc/xmake.lua:ipsec 子模块头文件搜索路径(components/network/ipsec/include)bsp/pc/include/luat_conf_bsp.h:LUAT_USE_NETDRV_IPSEC 1bsp/pc/include/lwipopts.h:MEMP_NUM_SYS_TIMEOUT 30bsp/pc/port/luat_crypto_mini.c:PC 模拟器 TRNG 每次调用重新播种
3. 架构与数据流
Lua (netdrv.setup / socket.*)
|
v
luat_netdrv_ipsec.c (setup/ctrl/dhcp/debug, IP_READY/IP_LOSE 事件)
|
v
ipsec_ike.c (IKEv2 状态机, tcpip 线程)
| netif (虚拟网卡, 全流量默认路由)
| | netif output -> ESP 封装 -> adapter UDP TX(4500)
| | adapter RX(4500) -> IKE(4字节零前缀) 或 ESP -> 内层 IP 注入
|
+-- network_ctrl_t (luat_network_adapter, 单 socket: 500 -> 4500)
- IKE 与 ESP 共用同一个 adapter UDP socket:先在 500 上完成 IKE_SA_INIT,NAT 探测后切换到本地/远端 4500(RFC 3948 非 ESP marker 区分 IKE 与 ESP)。这是标准 NAT-T 形态,避免了双 socket 同端口绑定的问题。
- 所有状态机工作经
tcpip_callback_with_block汇聚到 tcpip 线程; adapter 回调只拷贝数据并投递,与 L2TP/OpenVPN 客户端一致。 - CP 下发虚拟 IP 后
netif_set_addr+netif_set_default,DNS 写入network_set_dns_server。
4. 协议要点与互操作细节
- 套件:IKE
{aes256-sha256, aes128-sha1} × {modp2048, ecp256, ecp384, ecp521}!(8 个提案,DH14 优先;响应者选中其它组时按 RFC 7296 §2.7 的 INVALID_KE 通知重试,IKE_SA_INIT 默认先发 DH14 的 KE); ESPaes256-sha256, aes128-sha1, aes256gcm16, aes128gcm16!(AEAD 提案无 INTEG transform,transform id 20 + KEY_LENGTH 属性)。 - CHILD_SA 重协商默认带 PFS:请求载荷顺序
SA | Ni | KE | TSi | TSr, 使用 IKE_SA_INIT 协商出的 DH 组生成新的临时密钥对;响应解析新 Nr 与 KE,KEYMAT = prf+(SK_d, g^ir | Ni | Nr)。响应缺少 KE 或带 NO_PROPOSAL_CHOSEN/TS_UNACCEPTABLE 时,自动无 KE 重试一次并保持旧 SA 在线(与旧版行为一致)。 - MOBIKE 更新载荷顺序:
N(UPDATE_SA_ADDRESSES) | N(NAT_DETECTION_SOURCE_IP) | N(NAT_DETECTION_DESTINATION_IP)(RFC 4555 §2.2),NAT-D 复用 IKE_SA_INIT 的 SHA-1 哈希方式;对端发起的更新用当前本地 IP 校验 目标 NAT-D,不匹配则忽略;established 状态下的地址学习只发生在 密码学验证通过之后:ESP 报文在 ICV/解密成功后、对端 UPDATE_SA_ADDRESSES 在 SK 解密 + NAT-D 校验通过后、已认证的 INFORMATIONAL 响应在 ICV 校验通过后(RFC 4555 §3, 未认证报文一律不得改写对端地址)。 - IKE 请求单在途槽位:
pending_msgid/last_tx全 client 一份, DPD/MOBIKE 更新/CREATE_CHILD_SA 任一在途时其余请求让路(下一秒 tick 重试);INFORMATIONAL 响应(DPD/MOBIKE ack)经 SK 解密 + ICV 校验后才采信;msgid 全链路 32 位(RFC 7296 单调不回绕)。 - IKE SK 载荷:AES-CBC + HMAC;ICV 只覆盖 IKE 报文本身(strongSwan 收到 4500 报文会先剥离 4 字节非 ESP marker 再校验)。
- ESP AEAD(RFC 4106):8 字节显式 IV,GCM IV = salt(4) | IV(8),
AAD = SPI | SEQ,ICV 16 字节,padding 按 4 字节对齐;KEYMAT 每方向为
enc_key(16/32) + salt(4)(RFC 7296 §3.3.2),无 INTEG 密钥。 - AUTH:EAP 模式按 RFC 7296 §2.16 用 MSK 作为共享密钥;
"Key Pad for IKEv2"为 17 字节(不含 NUL)。 - MSK:按 strongSwan 的 RFC 3079 实现——
master=SHA1(HH|NT-Response|Magic1),recv/send=SHA1(master[0:16]|0x00*40|Magic2/3|0xF2*40),MSK=recv[0:16]|send[0:16]|0x00*32。 - MS-CHAPv2 线格式采用 strongSwan/Windows 客户端格式:
opcode|id|ms_length|value_size|...(区别于 RFC 2759 排版)。 - ESP SPI 方向:发起端发送用响应者分配的 SPI(SAr2),接收 用自己提议的 SPI(SAi2)。
- 证书链:配置了
ipsec_ca_cert_pem时按该信任锚做链校验并校验 SANipsec.air32.cn;未配置时默认 fail-closed(固件不再内置任何 信任锚),仅当显式设置ipsec_insecure_cert_ok=true才接受服务器证书, 且仍强制 SAN 匹配。mbedTLS 校验需传mbedtls_x509_crt_profile_default, PEM 缓冲需 NUL 结尾(mbedTLS 3.x 的 PEM 识别条件)。
5. 配置参考(Lua)
netdrv.setup(socket.LWIP_USER1, netdrv.IPSEC, {
ipsec_remote_ip = "154.8.159.79", -- 网关 IP(仅字面量)
ipsec_remote_port = 500, -- IKE 端口, 默认 500
ipsec_username = "vpnuser",
ipsec_password = "xxxx",
ipsec_san = "ipsec.air32.cn", -- 服务器 SAN 校验
-- ipsec_ca_cert_pem = "-----BEGIN CERTIFICATE-----...", -- 建议配置信任锚;
-- -- 未配置且未开启 insecure 时无法连接 (fail-closed)
-- ipsec_insecure_cert_ok = false, -- 默认 false; true 表示无 CA 时仅校验 SAN 即接受
ipsec_mtu = 1400,
ipsec_retry_enable = true,
ipsec_retry_base_ms = 1000,
ipsec_retry_max_ms = 60000,
-- ipsec_mobike_enable = true, -- MOBIKE 双向地址更新 (默认关闭)
})
ipsec_mobike_enable:布尔,默认 false。开启后客户端在 1s tick 轮询
本地地址,变化时发送 UPDATE_SA_ADDRESSES;底层 socket 关闭但传输仍在线
时重开同端口 socket 并走更新流程而非全量重建;同时支持对端发起的地址
更新与 established 状态下的地址学习。仅 utest 构建提供测试钩子
netdrv.ipsec_sim_addr_change(id) 模拟一次本地地址变更。
6. 联调记录(2026-08-12, PC 模拟器 ↔ ipsec.air32.cn)
联调期间发现并修复的问题:
| # | 现象 | 根因 | 修复 |
|---|---|---|---|
| 1 | 网关对 IKE_SA_INIT 无响应 | Ni/NAT-D 载荷的 Next Payload 误用 notify 值(16388)而非载荷类型(41) | 改为 IPSEC_PAYLOAD_NOTIFY |
| 2 | IKE_AUTH 无响应, 网关日志 message ID 16777216, expected 1 |
msgid 以单字节写入 (01 00 00 00) 而非 4 字节大端 |
ike_put32(msg+20, msgid) |
| 3 | 网关日志 MAC verification failed |
mbedTLS 3 aes_crypt_cbc 会把最后一组密文写回 IV 缓冲, 覆盖了包内 IV |
用独立 IV 缓冲 |
| 4 | 网关日志 MAC verification failed(修复 3 后仍失败) |
4500 上 ICV 把 4 字节零前缀算进去了, 而 strongSwan 收到会先剥离 | ICV 只覆盖 IKE 报文 |
| 5 | trust anchor parse failed -0x2180 |
mbedTLS 3.x 仅当缓冲区 NUL 结尾才识别 PEM | 内嵌/用户 PEM 均保留 NUL 结尾 |
| 6 | cert chain verify failed flags=0xFFFFFFFF |
mbedtls_x509_crt_verify_with_profile 的 profile 传 NULL 返回 BAD_INPUT_DATA |
传 mbedtls_x509_crt_profile_default |
| 7 | 服务器只发叶子证书, 链校验失败 | strongSwan leftsendcert=always 只发叶子;Let's Encrypt 2026 新链 |
内置 YE2/ISRG Root YE/ISRG Root X2 中间 CA |
| 8 | server AUTH signature verification failed |
裸 r|s 转 DER 时长度公式少算 2 字节 | 修正 total=6+r+s+pad |
| 9 | 服务器 INVALID_SYNTAX 拒绝 EAP Identity 响应 |
EAP 载荷构建后未 p += plen, 内层为空 |
修复三处 EAP 载荷 |
| 10 | unhandled MS-CHAPv2 request opcode 1 |
用 RFC 2759 排版解析 strongSwan 的 Challenge | 按 strongSwan 线格式解析 |
| 11 | 最终 AUTH 被拒 | 内层 "Key Pad for IKEv2" 用 sizeof(含 NUL 18 字节) |
用 17 字节 |
| 12 | 最终 AUTH 被拒(修复 11 后仍失败) | MSK 的 master 用 sizeof(inner)=79(实际 67);且 0x36/0x5C 填充与 strongSwan 的 0x00/0xF2 不符;32 字节 key 从 20 字节 digest 越界拷贝 |
按 strongSwan 实现重写 MSK 派生 |
| 13 | ESP 无回包, 网关 XfrmInNoStates |
ESP SPI 方向反了(发送用了自己提议的 SPI) | 发送用 SAr2 的 SPI |
| 14 | 隧道 IP 与策略不匹配 | CP 的 IPv4 字节序(ip4_addr_set_u32 需先 lwip_htonl) |
恢复 lwip_htonl(ike_get32()) |
| 15 | 证书校验 NOT_TRUSTED(flags=0x8) |
网关改用私建 CA(CN=IKEv2 VPN CA,自签 10 年),strongSwan 只发叶子,内置 ISRG Root X1 / LE 中间链不再适用 |
测试脚本改从 scripts/ikev2-ca.crt 读取 CA 作为 ipsec_ca_cert_pem 传入;同时按需求移除固件内置信任锚,未配 CA 的默认行为在后续提交中收紧为 fail-closed(需显式 ipsec_insecure_cert_ok=true 才接受证书,且仍校验 SAN) |
服务器侧配合项(已处理,用户授权调试):
- 云主机
dirtyfrag.conf禁用esp4内核模块(install esp4 /bin/false), 导致 XFRM SAD 安装失败(netlink error: Requested type not found (93)): 已注释该规则并加载esp4/xfrm_user/xfrm4_tunnel; ipsec.secrets私钥声明为: RSA但实际是 ECDSA P-384: 已改为: ECDSA;- 联调后
charondebug已恢复ike 2,esp=aes256-sha256, aes128-sha1!已恢复原样。
7. 测试结果(PC 模拟器, testcase/unit/net/netdrv_ipsec_basic)
connect:IKE_SA_INIT → 证书链+SAN 校验 → 服务器 AUTH 验签 → EAP-MSCHAPv2(含服务器 Authenticator Response 校验)→ 双端 AUTH → CP 下发虚拟 IP/DNS → 隧道上线 → 隧道内 TCP 到网关 SSH 收到 banner, 3 passed / 0 failed;badpass:错误密码 → EAP 失败 → 不 ready,通过;sanit:错误 SAN → 证书校验拒绝 → 不 ready,通过。
前置:
testcase/unit/net/netdrv_ipsec_basic/scripts/ikev2-ca.crt(网关私建 CA)必须存在,测试会把它作为ipsec_ca_cert_pem传入; 测试账号凭据通过环境变量LUAT_IPSEC_USERNAME/LUAT_IPSEC_PASSWORD注入(不随源码分发),未设置时测试跳过。 固件默认 fail-closed:未传ipsec_ca_cert_pem且未显式开启ipsec_insecure_cert_ok时拒绝连接(固件不再内置任何信任锚)。
8. 遗留与后续
- 集成联调(需网关侧重配,未完成项见交付说明):
- PFS:网关
pfs=yes+ 缩短 lifetime,验证重协商日志、DPD、隧道不断流; - ECP/GCM:网关
ike=aes256-sha256-ecp256!/esp=aes256gcm16!建连; - MOBIKE:
netdrv.ipsec_sim_addr_change触发 UPDATE_SA_ADDRESSES, 网关mobike=no时回退重连。
- PFS:网关
- 硬件 BSP:需确认目标板 lwip 导出符号(
pbuf/netif/sys/ip4_input) 与 adapter 行为,并按验收清单在目标板重跑; - 服务器 FORWARD 链的 IPsec 池 ACCEPT 规则与
rightsourceip段需按实际网络调整(当前云主机 iptables 规则与池配置不完全一致, 隧道到网关本机可用,跨子网转发需核对); netdrv.debug会输出 IKE/ESP 帧日志(含 SK 明文转储仅调试用)。
9. 相关文档
- RFC 7296(IKEv2)、RFC 3948(NAT-T)、RFC 4303(ESP)、 RFC 2759/3079(MS-CHAPv2/MSK)
docs/l2tp-design.md(线程模型/胶水层/构建接线参考)components/network/l2tp/src/l2tp_client.c/components/network/openvpn/src/ovpn_client.c(传输与 netif 参考)