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

17 KiB
Raw Permalink Blame History

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 adapternetwork_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_sendl2tp_timeout、重连退避、l2tp_client_* API);
    • l2tp_ctrl.cL2TPv2 控制面从 lwip22 netif/ppp/pppol2tp.c 移植, 仅替换 UDP 收发为 network_tx/network_rx,去掉 udp_pcb 与传输 netif 参数(线格式/AVP 构造解析、ns/nr 窗口、定时重传);
    • l2tp_ppp.cPPP 会话运行在 vendor 的 lwIP PPP 栈上, struct link_callbackswrite/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/closeppp_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 走外部 mbedTLSLWIP_USE_EXTERNAL_MBEDTLS=1lwip_md5_* 映射到 LuatOS 已有 mbedtlsMBEDTLS_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/MAXSECRETLENPPP_MRUPPP_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.hlwip22/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.cudp_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 keepalive(3s 间隔、3 次失败)检测静默断线(对端不响应); 对端 StopCCN 在 DATA 阶段不主动断开(沿用 pppol2tp 语义),由 keepalive 兜底。
  • 传输层EV_NW_RESULT_CLOSE/Param1!=0transport_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 默认 1450L2TP_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

-- 剔除 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 对该网卡返回 -1IP 由 IPCP 下发)

5. 测试方案

PC 模拟器 + Python mock LNStestcase/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_pkg19/19)、netdrv_lwip_intercept_basic3/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 必须携带服务端自己的 magiclwip 会拒绝携带自身 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=0ppp.csdns() 未编译, IPCP 携带的 DNS 选项会被忽略。后续可评估启用 LWIP_DNS 或由驱动把 IPCP 协商出的 DNS 写入 net_lwip2set_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 参考实现)