init: Air780EPM 官方 LuatOS 项目代码基线

- 来源: 合宙 LuatOS 官方仓库 air780epm 模块完整代码
- 路径: luatos/air780epm/module/Air780EPM/demo 含官方 demo(含 mqtt/mqtts/socket/uart 等)
- 后续: 基于 demo 开发 UART<->MQTT 数据上报功能
This commit is contained in:
wangfq
2026-08-31 08:53:49 +08:00
commit 173dd6874f
813 changed files with 125484 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
# netdrv L2TPv2 客户端驱动设计文档
> 适用范围:在 LuatOS `netdrv` 框架下新增 L2TPv2RFC 2661)客户端驱动(LAC),
> 当前首期落地 PC 模拟器(`bsp/pc`),硬件 BSP 按需开启 `LUAT_USE_NETDRV_L2TP`。
---
## 1. 背景与目标
LuatOS 需要在内网/办公网场景下提供「二层隧道 + PPP 拨号」的能力,典型形态是
接入企业 LNSL2TP Network Server)获得内网 IP 并访问内网资源。
需求边界(与 lwip 自带 `pppol2tp.c` 的能力边界一致):
- ✅ L2TPv2 客户端(LAC),PPP over L2TPUDP 隧道)
- ✅ PPP 认证:PAP + CHAP(MD5);可选 L2TP 隧道共享密钥认证(`l2tp_secret`
- ❌ LNS(服务端)、L2TPv3、隧道切换、多隧道/多会话
- ❌ MSCHAPv2 / MPPE / EAP / CCP
核心约束:**外层 UDP 传输一律走 LuatOS network adapter**`network_ctrl_t` UDP
模式,与 OpenVPN 客户端一致),不直接创建 lwip `udp_pcb`。这样 VPN 虚拟网卡与
底层物理网卡(4G/WiFi/ETH/CH390H)完全解耦,底层可以是任意 netdrv 适配器。
---
## 2. 总体设计
```
Lua 脚本 (netdrv.setup / netdrv.ready / socket.*)
|
+------------------v------------------+
| luat_netdrv_l2tp.c (胶水层) |
| setup/ctrl(UPDOWN)/dhcp/debug |
| PPP 状态回调 -> net_lwip2 链路状态 + |
| IP_READY / IP_LOSE 事件 |
+------------------+------------------+
|
+------------------v------------------+
| components/network/l2tp/src (核心) |
| - l2tp_client.c: 生命周期/定时器/传输 |
| - l2tp_ctrl.c: 控制面 (SCCRQ..StopCCN,|
| ns/nr 窗口, 定时重传) |
| - l2tp_ppp.c: PPP 数据面封装 + 状态回调|
+-------+---------------+------------+
| |
network_ctrl_t (UDP) vendored lwIP PPP 栈
luat_network_adapter l2tp/src/ppp/*.c
| |
底层物理网卡(4G/WiFi/ETH) ppp_pcb / netif
```
### 2.1 分层
1. **胶水层**`luat_netdrv_l2tp.c`):把 L2TP 客户端包装成标准 `luat_netdrv_t`
驱动,负责配置拷贝、`LUAT_NETDRV_CTRL_UPDOWN` 控制、链路状态上报。
2. **客户端核心**`components/network/l2tp/src/`,按协议层拆为三个文件):
- `l2tp_client.c`:生命周期/状态机/定时器/网络适配器传输
`l2tp_udp_send``l2tp_timeout`、重连退避、`l2tp_client_*` API);
- `l2tp_ctrl.c`L2TPv2 控制面从 lwip22 `netif/ppp/pppol2tp.c` 移植,
仅替换 UDP 收发为 `network_tx/network_rx`,去掉 `udp_pcb` 与传输
netif 参数(线格式/AVP 构造解析、ns/nr 窗口、定时重传);
- `l2tp_ppp.c`PPP 会话运行在 vendor 的 lwIP PPP 栈上,
`struct link_callbacks``write/netif_output` 走 L2TP 数据封装,
并处理 `ppp_status_cb` 链路状态回调。
- 跨文件原型在 `components/network/l2tp/include/l2tp/``l2tp_ctrl.h` /
`l2tp_ppp.h`),公共客户端 API 在 `l2tp/l2tp_client.h`
3. **PPP 栈**`components/network/l2tp/src/ppp/`):lwIP 2.2.1
`ppp.c/lcp.c/ipcp.c/auth.c/fsm.c/upap.c/chap-new.c/chap-md5.c/magic.c/utils.c`
的 vendor 副本(保留 BSD 许可头,注明来源),由构建系统按需编译。
### 2.2 线程模型
- `create/connect/close``ppp_input` 的投递统一 marshal 到 lwip core 线程
`tcpip_callback_with_block`,模式同 WG 驱动的 `exec_netif_add`);
- network adapter 回调(运行在 adapter 任务/Lua 主线程)内**只做数据拷贝**,
通过 `tcpip_callback` 投递给 tcpip 线程,PPP 状态机绝不在 adapter 线程执行;
- `transport_err` 标志由 adapter 回调置位,由 500ms 周期定时器(tcpip 线程)
消费,避免跨线程直接操作 PPP 状态机。
### 2.3 就绪信号
PPP 的 `sifup()``np_up()` 把 phase 推进到 `PPP_PHASE_RUNNING` **之前**就会
回调 `link_status_cb(PPPERR_NONE)`,因此驱动的 ready 判定使用 `ppp->if4_up`
`sifup` 内部先置位再回调),而不是 phase==RUNNING,否则会出现一次误报
`IP_LOSE` 且不再上报 `IP_READY` 的竞态。
---
## 3. 方案选择与取舍
### 3.1 传输层:network adapter UDP 而非 lwip `udp_pcb`
| 方案 | 结论 |
|---|---|
| 直接创建 lwip `udp_pcb` 走底层 netif | 与 4G/WiFi/ETH 等 LuatOS 自有适配器耦合,需要额外的 netif 路由/ARP 处理,硬件差异大 |
| `network_ctrl_t` UDP(本方案) | 与 OpenVPN 客户端一致,底层网卡即插即用;天然支持「LNS 从随机源端口应答」(posix 适配器不 connect UDP socket |
代价:多一次 adapter 层数据拷贝,VPN 隧道吞吐受 adapter 缓冲限制
`network_tx/rx` 单包缓冲,L2TP 数据面当前使用 1600B 栈缓冲)。
### 3.2 PPP 栈:vendor lwip22 源码 vs 自己实现
lwip22 自带完整的 `ppp.c` 系列(LCP/IPCP/PAP/CHAP/FSM/魔法数),直接 vendor
`components/network/l2tp/src/ppp/` 并配套两个移植补丁:
1. **`ppp_pcb` 改用 `mem_malloc/mem_free`**:本仓库 lwip22 的 `memp_std.h`
裁剪掉了 PPP/PPPOL2TP 内存池,vendor 副本内自行 `LWIP_MEMPOOL_DECLARE`
可以编译,但为减少静态池开销并贴合本仓库约定,改为堆分配。
2. **MD5 走外部 mbedTLS**`LWIP_USE_EXTERNAL_MBEDTLS=1``lwip_md5_*`
映射到 LuatOS 已有 mbedtls`MBEDTLS_MD5_C` 在 PC 与硬件配置均已启用),
不再编译 lwip 内嵌 PolarSSL 副本。
### 3.3 编译选项覆盖:`luat_ppp_opts_override.h`
`bsp/pc/include/lwipopts.h` 写死 `PPP_SUPPORT 0`,且会**无条件重定义**,因此
per-file 命令行宏会被覆盖;同时 `ppp_opts.h``#if PPP_SUPPORT` 块在 override
生效之前求值(其中定义的 FSM/UPAP/CHAP/LCP 超时、`MAXNAMELEN/MAXSECRETLEN`
`PPP_MRU``PPP_IPV4_SUPPORT` 等全部缺失)。
解决办法:xmake 仅对 vendor PPP 文件与 L2TP 客户端文件附加
`LUAT_L2TP_PPP_BUILD=1`,并在 `ppp_opts.h` 之后强制 include
`luat_ppp_opts_override.h`,统一恢复特性集:
```
PPP_SUPPORT=1, PPPOL2TP/PPPOS/PPPOE=0, LWIP_PPP_API=0,
PAP_SUPPORT=1, CHAP_SUPPORT=1, MSCHAP/EAP/CCP/MPPE/VJ/LQR=0,
PPP_IPV4_SUPPORT=(LWIP_IPV4), PPP_IPV6_SUPPORT=0, PPP_SERVER=0,
LWIP_USE_EXTERNAL_MBEDTLS=1, MEMP_NUM_PPP_PCB=1
```
配套提供 `components/network/l2tp/include/ppp_settings.h``lwip22/include/arch/cc.h`
无条件定义 `PPP_INCLUDE_SETTINGS_HEADER`,但仓库从未提供该 port 头文件
(PPP 此前从未编译),本文件为空实现(选项全部走 override 头)。
> 注意:这些宏只作用于 vendor PPP 文件与 L2TP 客户端文件,**不是全局宏**。
> 全局开启 `PPP_SUPPORT` 会触发 `lwip22/core/init.c` 的
> `#error "PPP_SUPPORT needs at least one of PPPOS_SUPPORT..."` 编译检查。
### 3.4 L2TP 控制面:自实现(`PPPOL2TP_SUPPORT=0`
lwip 的 `pppol2tp.c``udp_pcb` 强耦合,且其头文件要求
`PPPOL2TP_SUPPORT=1`。本驱动把控制面逻辑平移到
`l2tp_ctrl.c`(常量、AVP 构造/解析、ns/nr 窗口、定时重传均照搬),
传输改为 adapter 调用,因此 vendor PPP 文件以 `PPPOL2TP_SUPPORT=0` 编译,
避免 `pppol2tp.c` 参与编译。
### 3.5 认证与隧道密钥
- `ppp_set_auth(ppp, PPPAUTHTYPE_ANY, user, passwd)`PAP/CHAP-MD5 均可;
- `l2tp_secret` 非空时按 RFC 2661 在 SCCRQ/SCCRP/SCCCN 中携带
Challenge / Challenge-Response AVPMD5(消息类型|secret|challenge))。
- 认证选项由**对端**在 LCP ConfReq 中提出:lwip 客户端的 LCP ConfReq 本身
不携带 auth 选项(`lcp_addci` 使用 gotoptions,客户端侧该位不置位),
因此 LNS 必须主动要求认证(真实 LNS 行为一致)。
### 3.6 断线检测与自动重连
- **PPP 层**LCP keepalive3s 间隔、3 次失败)检测静默断线(对端不响应);
对端 StopCCN 在 DATA 阶段不主动断开(沿用 pppol2tp 语义),由 keepalive 兜底。
- **传输层**`EV_NW_RESULT_CLOSE`/`Param1!=0``transport_err`500ms 周期
定时器消费并触发重连。
- 重连策略:指数退避(base 1s,max 60s,可配置),传输离线时按 base 轮询;
用户主动 `CTRL_UPDOWN=0` 不重连。
- 在线判定使用 setup 时捕获的 `transport_index`,而非运行时默认 adapter
netdrv 虚拟网卡注册后 `network_register_get_default()` 会变成 USERx 自身,
若用运行时默认值会导致「传输离线」误判。
### 3.7 数据面 MTU 与缓冲
PPP MRU 默认 1450`L2TP_DEFAULT_MTU`,可用 `l2tp_mtu` 配置);L2TP 数据头 6B
flags + tunnel id + session id)。发送侧把 pbuf 拼成扁平缓冲后走
`network_tx`(1600B 栈缓冲),接收侧同样扁平缓冲 -> pbuf -> `ppp_input`
---
## 4. 文件与构建接线
### 4.1 新增/修改文件
| 文件 | 说明 |
|---|---|
| `components/network/l2tp/include/l2tp/l2tp_client.h` | L2TP 客户端对外结构/API、协议常量 |
| `components/network/l2tp/include/l2tp/l2tp_ctrl.h` | 控制面跨文件原型(内部) |
| `components/network/l2tp/include/l2tp/l2tp_ppp.h` | PPP 胶水跨文件原型(内部) |
| `components/network/l2tp/src/l2tp_client.c` | 客户端核心(生命周期/定时器/传输/重连) |
| `components/network/l2tp/src/l2tp_ctrl.c` | 控制面(线格式/AVP/ns-nr 窗口/定时重传) |
| `components/network/l2tp/src/l2tp_ppp.c` | PPP 胶水(link_callbacks + 状态回调) |
| `components/network/netdrv/include/luat_netdrv_l2tp.h` | netdrv 胶水层头文件 |
| `components/network/l2tp/src/luat_netdrv_l2tp.c` | netdrv 胶水层(setup/ctrl/dhcp/debug/状态回调) |
| `components/network/l2tp/src/ppp/*.c` | vendored lwip 2.2.1 PPP 源码(10 个文件) |
| `components/network/l2tp/src/ppp/luat_ppp_opts_override.h` | PPP 编译选项覆盖 |
| `components/network/l2tp/include/ppp_settings.h` | lwip `PPP_INCLUDE_SETTINGS_HEADER` port 钩子 |
| `components/network/netdrv/include/luat_netdrv.h` | `luat_netdrv_l2tp_conf_t` + `conf.l2tp_conf` |
| `components/network/netdrv/include/luat_netdrv_drv.h` | `LUAT_NETDRV_IMPL_L2TP 6` + setup 声明 |
| `components/network/netdrv/src/luat_netdrv.c` | setup 分发新增 L2TP 分支 |
| `components/network/netdrv/binding/luat_lib_netdrv.c` | `l2tp_*` 配置解析 + `netdrv.L2TP` 常量 |
| `bsp/pc/include/luat_conf_bsp.h` | `LUAT_USE_NETDRV_L2TP 1` |
| `bsp/pc/include/lwipopts.h` | `MEMP_NUM_SYS_TIMEOUT` 17->24PPP/L2TP 定时器预算) |
| `bsp/pc/xmake.lua` | lwip22 PPP 源码剔除 + vendor PPP 编译 |
| `testcase/unit/net/netdrv_l2tp_basic/` | 测试套件 + Python mock LNS |
### 4.2 xmake 接线要点(`bsp/pc/xmake.lua`
```lua
-- 剔除 lwip22 自带 PPP(避免与 vendor 副本重复编译)
remove_files(lwip_path .. "netif/ppp/**.c")
-- netdrv 核心(VPN 子模块已拆出为独立目录)
add_files(luatos .. "components/network/netdrv/**.c")
-- L2TP 子模块:头文件路径 + vendor PPP + 客户端按文件附加编译宏
add_includedirs(luatos .. "components/network/l2tp/include")
add_includedirs(luatos .. "components/network/l2tp/src/ppp")
add_files(luatos .. "components/network/l2tp/src/ppp/*.c",
{defines = {"LUAT_L2TP_PPP_BUILD=1"}})
add_files(luatos .. "components/network/l2tp/src/l2tp_client.c",
luatos .. "components/network/l2tp/src/l2tp_ctrl.c",
luatos .. "components/network/l2tp/src/l2tp_ppp.c",
{defines = {"LUAT_L2TP_PPP_BUILD=1"}})
add_files(luatos .. "components/network/l2tp/src/luat_netdrv_l2tp.c")
```
### 4.3 Lua 用法
```lua
netdrv.setup(socket.LWIP_USER1, netdrv.L2TP, {
l2tp_remote_ip = "10.0.0.1", -- 必填,LNS IP(仅 IP 字面量,同 OpenVPN
l2tp_remote_port = 1701, -- 默认 1701
l2tp_username = "user", -- 可选
l2tp_password = "pass", -- 可选
l2tp_secret = nil, -- 可选,L2TP 隧道共享密钥
l2tp_mtu = 1450, -- 可选,默认 1450
l2tp_retry_enable = true, -- 可选,失败自动重连
l2tp_retry_base_ms = 1000,
l2tp_retry_max_ms = 60000,
})
-- 就绪判断沿用 netdrv.ready(id) 与 IP_READY/IP_LOSE 事件
-- netdrv.dhcp 对该网卡返回 -1IP 由 IPCP 下发)
```
---
## 5. 测试方案
PC 模拟器 + Python mock LNS`testcase/unit/net/netdrv_l2tp_basic/mock_lns.py`):
```bash
# pap 场景
python testcase/unit/net/netdrv_l2tp_basic/mock_lns.py --port 1701 --auth pap --log &
LUAT_L2TP_TEST=pap build/out/luatos-lua.exe \
../../testcase/common/scripts/ ../../testcase/unit/net/netdrv_l2tp_basic/scripts/
# chap / reject / reconnect 场景分别用:
# --port 1702 --auth chap (LUAT_L2TP_TEST=chap)
# --port 1703 --reject-auth (LUAT_L2TP_TEST=reject)
# --port 1701 --drop-after 8 (LUAT_L2TP_TEST=reconnect)
```
四个场景均已实测通过(`### OVERALL_PASS ###`):
| 场景 | 验证点 |
|---|---|
| pap | 隧道建立 + PAP 认证 + IPCP 下发 192.168.8.2 + 隧道内 TCP echo |
| chap | CHAP-MD5 认证(挑战/响应校验)+ 同样链路验证 |
| reject | 密码错误 -> 认证失败、一直 not ready |
| reconnect | 会话中途断开 -> `IP_LOSE` -> 自动重连成功 |
回归:`netdrv_evt_pkg`19/19)、`netdrv_lwip_intercept_basic`3/3)通过;
PC 增量编译 `Build completed successfully`,无新增告警。
### mock LNS 实现要点(实测中踩过的坑,写在这里供后续参考)
- 每端第一条控制消息 NS 从 **0** 开始(SCCRP 的 NS 必须为 0);
- ZLB 不消耗 NS 槽位:其 NS 与下一条真实消息相同,接收方不推进 `peer_ns`
- 数据包隧道/会话 ID 必须用**客户端分配**的 IDlwip 按 `remote_*_id` 校验);
- LCP/IPCP 是对称协议:服务端必须发送自己的 ConfReq,否则客户端 FSM 停在
ACKRCVD 并重传;
- LCP Echo-Reply 必须携带**服务端自己的 magic**lwip 会拒绝携带自身 magic
的 Echo-Reply(视为回环);
- CHAP 校验用的 secret 是 PPP 密码(客户端 `settings.passwd`),与 L2TP 隧道
`l2tp_secret` 无关;
- IPCP ConfNak 的 length 字段必须等于实际负载长度(含 DNS 选项时容易写错,
会导致客户端只解析到部分选项)。
---
## 6. 尚待完善之处
1. **硬件 BSP 使能**:首期只落地 PC。硬件 BSP 需要确认 lwip 导出
`pbuf/netif/sys/ip4_input` 等符号;若未导出 `ip4_input`,在
`LUAT_USE_NETDRV_LWIP_ARP` 下把 vendor `ppp.c` 的调用改走
`luat_netdrv_ip4_input`。硬件侧建议在正式发布前跑一遍 mock LNS 验证。
2. **IPCP DNS 下发**PC 当前 `LWIP_DNS=0``ppp.c``sdns()` 未编译,
IPCP 携带的 DNS 选项会被忽略。后续可评估启用 `LWIP_DNS` 或由驱动把
IPCP 协商出的 DNS 写入 `net_lwip2``set_dns_server`
3. **LNS 域名支持**`l2tp_remote_ip` 仅支持 IP 字面量;可通过传输 adapter
的 DNS 能力扩展域名解析(同 OpenVPN 现状)。
4. **隧道认证自动化测试**`l2tp_secret` 的 Challenge/Challenge-Response
流程已实现,mock LNS 也支持 `--secret`,但当前四场景未覆盖,待补测试。
5. **认证/加密能力扩展**MSCHAPv2、MPPECCP)、EAP 需要额外 vendor
`chap_ms.c/ccp.c/mppe.c/eap.c` 并调整编译选项,属后续需求。
6. **多实例**:当前按单会话设计(`MEMP_NUM_PPP_PCB=1`),多隧道/多会话、
隧道切换未支持。
7. **资源释放细节**:配置字符串(用户名/密码/密钥)在客户端生命周期内保留,
用户停止后不释放(便于再次启动);PPP 会话若处于 `TERMINATE` 进行中,
`ppp_free` 会等待状态归位,极端时序下可能保留 pcb 等待下一次 start 清理。
8. **定时器预算**`MEMP_NUM_SYS_TIMEOUT` 由 17 提到 24 以容纳
PPP/L2TP 的 LCP-FSM + LCP-Echo + 认证 + IPCP-FSM + L2TP 控制/周期定时器;
硬件侧若同时跑 OpenVPN/WG,需按实际固件核对预算。
9. **CI 集成**mock LNS 依赖本机 UDP 端口,目前是手工编排;后续可封装成
CI job(起 LNS -> 跑模拟器 -> 校验 OVERALL_PASS)。
10. **文档与 API 注释**`luat_lib_netdrv.c` 已补充 `netdrv.L2TP` 用法注释,
建议后续在 `script/` 侧补充完整 usage.md/示例。
---
## 7. 相关文档
- `components/network/lwip22/netif/ppp/pppol2tp.c`(控制面移植来源)
- `components/network/lwip22/include/netif/ppp/ppp_opts.h`(选项语义)
- RFC 2661Layer Two Tunneling Protocol "L2TP"
- `testcase/unit/net/netdrv_l2tp_basic/mock_lns.py`(测试用 LNS 参考实现)