- 来源: 合宙 LuatOS 官方仓库 air780epm 模块完整代码 - 路径: luatos/air780epm/module/Air780EPM/demo 含官方 demo(含 mqtt/mqtts/socket/uart 等) - 后续: 基于 demo 开发 UART<->MQTT 数据上报功能
17 KiB
netdrv L2TPv2 客户端驱动设计文档
适用范围:在 LuatOS
netdrv框架下新增 L2TPv2(RFC 2661)客户端驱动(LAC), 当前首期落地 PC 模拟器(bsp/pc),硬件 BSP 按需开启LUAT_USE_NETDRV_L2TP。
1. 背景与目标
LuatOS 需要在内网/办公网场景下提供「二层隧道 + PPP 拨号」的能力,典型形态是 接入企业 LNS(L2TP Network Server)获得内网 IP 并访问内网资源。
需求边界(与 lwip 自带 pppol2tp.c 的能力边界一致):
- ✅ L2TPv2 客户端(LAC),PPP over L2TP(UDP 隧道)
- ✅ 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 分层
- 胶水层(
luat_netdrv_l2tp.c):把 L2TP 客户端包装成标准luat_netdrv_t驱动,负责配置拷贝、LUAT_NETDRV_CTRL_UPDOWN控制、链路状态上报。 - 客户端核心(
components/network/l2tp/src/,按协议层拆为三个文件):l2tp_client.c:生命周期/状态机/定时器/网络适配器传输 (l2tp_udp_send、l2tp_timeout、重连退避、l2tp_client_*API);l2tp_ctrl.c:L2TPv2 控制面从 lwip22netif/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。
- PPP 栈(
components/network/l2tp/src/ppp/):lwIP 2.2.1ppp.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/ 并配套两个移植补丁:
ppp_pcb改用mem_malloc/mem_free:本仓库 lwip22 的memp_std.h裁剪掉了 PPP/PPPOL2TP 内存池,vendor 副本内自行LWIP_MEMPOOL_DECLARE可以编译,但为减少静态池开销并贴合本仓库约定,改为堆分配。- 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 AVP(MD5(消息类型|secret|challenge))。- 认证选项由对端在 LCP ConfReq 中提出:lwip 客户端的 LCP ConfReq 本身
不携带 auth 选项(
lcp_addci使用 gotoptions,客户端侧该位不置位), 因此 LNS 必须主动要求认证(真实 LNS 行为一致)。
3.6 断线检测与自动重连
- PPP 层:LCP keepalive(3s 间隔、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->24(PPP/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)
-- 剔除 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 用法
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 对该网卡返回 -1(IP 由 IPCP 下发)
5. 测试方案
PC 模拟器 + Python mock LNS(testcase/unit/net/netdrv_l2tp_basic/mock_lns.py):
# 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 必须用客户端分配的 ID(lwip 按
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. 尚待完善之处
- 硬件 BSP 使能:首期只落地 PC。硬件 BSP 需要确认 lwip 导出
pbuf/netif/sys/ip4_input等符号;若未导出ip4_input,在LUAT_USE_NETDRV_LWIP_ARP下把 vendorppp.c的调用改走luat_netdrv_ip4_input。硬件侧建议在正式发布前跑一遍 mock LNS 验证。 - IPCP DNS 下发:PC 当前
LWIP_DNS=0,ppp.c的sdns()未编译, IPCP 携带的 DNS 选项会被忽略。后续可评估启用LWIP_DNS或由驱动把 IPCP 协商出的 DNS 写入net_lwip2的set_dns_server。 - LNS 域名支持:
l2tp_remote_ip仅支持 IP 字面量;可通过传输 adapter 的 DNS 能力扩展域名解析(同 OpenVPN 现状)。 - 隧道认证自动化测试:
l2tp_secret的 Challenge/Challenge-Response 流程已实现,mock LNS 也支持--secret,但当前四场景未覆盖,待补测试。 - 认证/加密能力扩展:MSCHAPv2、MPPE(CCP)、EAP 需要额外 vendor
chap_ms.c/ccp.c/mppe.c/eap.c并调整编译选项,属后续需求。 - 多实例:当前按单会话设计(
MEMP_NUM_PPP_PCB=1),多隧道/多会话、 隧道切换未支持。 - 资源释放细节:配置字符串(用户名/密码/密钥)在客户端生命周期内保留,
用户停止后不释放(便于再次启动);PPP 会话若处于
TERMINATE进行中,ppp_free会等待状态归位,极端时序下可能保留 pcb 等待下一次 start 清理。 - 定时器预算:
MEMP_NUM_SYS_TIMEOUT由 17 提到 24 以容纳 PPP/L2TP 的 LCP-FSM + LCP-Echo + 认证 + IPCP-FSM + L2TP 控制/周期定时器; 硬件侧若同时跑 OpenVPN/WG,需按实际固件核对预算。 - CI 集成:mock LNS 依赖本机 UDP 端口,目前是手工编排;后续可封装成 CI job(起 LNS -> 跑模拟器 -> 校验 OVERALL_PASS)。
- 文档与 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 2661(Layer Two Tunneling Protocol "L2TP")
testcase/unit/net/netdrv_l2tp_basic/mock_lns.py(测试用 LNS 参考实现)