Files
wangfq 173dd6874f init: Air780EPM 官方 LuatOS 项目代码基线
- 来源: 合宙 LuatOS 官方仓库 air780epm 模块完整代码
- 路径: luatos/air780epm/module/Air780EPM/demo 含官方 demo(含 mqtt/mqtts/socket/uart 等)
- 后续: 基于 demo 开发 UART<->MQTT 数据上报功能
2026-08-31 08:53:49 +08:00

225 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-TRFC 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/21ECP-256/384/521RFC 4753),
ESP AES-GCM-128/256RFC 4106 AEADSA 内无 INTEG transform);
- MOBIKERFC 4555opt-in,默认关闭):本地地址变化时发送
INFORMATIONAL(UPDATE_SA_ADDRESSES + NAT-D),处理对端发起的地址更新,
以及 established 状态下的对端地址学习;不做 ADDITIONAL_IP4_ADDRESS
通告与 COOKIE2
- CPConfiguration Payload)下发虚拟 IPv4 + DNS
- ESP 隧道模式(RFC 4303AES-CBC-128/256 + HMAC-SHA1-96 /
HMAC-SHA2-256-128 或 AES-GCM-128/25632 包反重放窗口,
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` | MD4Apache-2.0, 从 mbedTLS 2.x vendor, 自包含) |
| `include/ipsec/ipsec_vendor_chap_ms.h` + `src/ipsec_vendor_chap_ms.c` | MS-CHAPv2BSD, 移植自 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 7`
- `luat_netdrv.h``luat_netdrv_ipsec_conf_t` + `conf->ipsec_conf`
- `luat_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 1`
- `bsp/pc/include/lwipopts.h``MEMP_NUM_SYS_TIMEOUT 30`
- `bsp/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_INITNAT 探测后切换到本地/远端 4500RFC 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);
ESP `aes256-sha256, aes128-sha1, aes256gcm16, aes128gcm16!`
AEAD 提案无 INTEG transformtransform id 20 + KEY_LENGTH 属性)。
- CHILD_SA 重协商默认带 PFS:请求载荷顺序 `SA | Ni | KE | TSi | TSr`
使用 IKE_SA_INIT 协商出的 DH 组生成新的临时密钥对;响应解析新 Nr 与
KEKEYMAT = 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 AEADRFC 4106):8 字节显式 IVGCM IV = salt(4) | IV(8)
AAD = SPI | SEQICV 16 字节,padding 按 4 字节对齐;KEYMAT 每方向为
`enc_key(16/32) + salt(4)`RFC 7296 §3.3.2),无 INTEG 密钥。
- AUTHEAP 模式按 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` 时按该信任锚做链校验并校验
SAN `ipsec.air32.cn`;未配置时默认 **fail-closed**(固件不再内置任何
信任锚),仅当显式设置 `ipsec_insecure_cert_ok=true` 才接受服务器证书,
且仍强制 SAN 匹配。mbedTLS 校验需传 `mbedtls_x509_crt_profile_default`
PEM 缓冲需 NUL 结尾(mbedTLS 3.x 的 PEM 识别条件)。
## 5. 配置参考(Lua
```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` 时回退重连。
- 硬件 BSP:需确认目标板 lwip 导出符号(`pbuf/netif/sys/ip4_input`
与 adapter 行为,并按验收清单在目标板重跑;
- 服务器 FORWARD 链的 IPsec 池 ACCEPT 规则与 `rightsourceip`
段需按实际网络调整(当前云主机 iptables 规则与池配置不完全一致,
隧道到网关本机可用,跨子网转发需核对);
- `netdrv.debug` 会输出 IKE/ESP 帧日志(含 SK 明文转储仅调试用)。
## 9. 相关文档
- RFC 7296IKEv2)、RFC 3948NAT-T)、RFC 4303ESP)、
RFC 2759/3079MS-CHAPv2/MSK
- `docs/l2tp-design.md`(线程模型/胶水层/构建接线参考)
- `components/network/l2tp/src/l2tp_client.c` /
`components/network/openvpn/src/ovpn_client.c`(传输与 netif 参考)