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

323 lines
17 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 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 参考实现)