纯 Dart 从零实现 SSTP/PPP VPN 客户端:把 Flutter 用到系统级
大多数开发者用 Dart 和 Flutter 只做 UI。sstp_client 项目想试试把 Dart 的能力推到底层系统编程:纯 Dart 实现微软的 Secure Socket Tunneling Protocol(MS-SSTP)、Point-to-Point Protocol(PPP)引擎和跨平台 TUN 设备路由,覆盖 Linux、Windows、macOS。文章记录了完整架构、二进制协议的原生处理,以及调试 Apple Silicon 内存绑定时发现的一个有趣怪癖。
架构分层
构建 VPN 客户端要从标准 HTTPS 握手一路处理到单个 IP 包。项目把网络栈拆成分层解耦结构:
- TunnelBackend(LinuxTunBackend / WindowsTunBackend / MacosUtun):最底层,负责创建虚拟网络接口、收发原始 IP 数据报
- SstpSession(PPP 引擎:LCP → MSCHAPv2 认证 → IPCP):协商连接
- SstpFramer(重组缓冲区与 SSTP 封装):切分字节流为控制帧与数据负载
- TlsTransport(dart:io SecureSocket + HTTP Bootstrap):最外层加密通道
三层实现要点
TlsTransport 与 SstpFramer。SSTP 本质是通过 443 端口的加密 HTTPS 通道隧道 PPP 流量。TlsTransport 先拉起标准 dart:io SecureSocket 并执行 HTTP SSTP_DUPLEX_POST 请求建立隧道;连接建立后 SstpFramer 接管,用有状态重组缓冲区把传入字节切分为 SSTP 控制帧或原始数据负载。
SstpSession 状态机。SSTP 隧道打开后要用 PPP 协商连接,握手状态机完全用 Dart 原生实现:LCP(链接控制协议)协商 MRU 等配置项;MSCHAPv2 用 RFC 2759 向量做密码学认证;IPCP 协商网络层配置,让远端服务器分配虚拟 IP。
TunnelBackend 硬件面。拿到 IP 还不够,得拦截系统网络流量。项目用 Dart 的 FFI 建了统一平台无关接口和三个系统级后端:Linux 打开 /dev/net/tun 并触发 ioctl(TUNSETIFF) 系统调用创建虚拟网卡,用原生 ip route 命令改路由表;Windows 没有原生用户态 TUN 驱动,后端对接 Wintun(WireGuard 用的高效驱动),动态加载 wintun.dll 并用 netsh 更新路由;macOS 用内核内置 utun 框架,拉起 PF_SYSTEM 控制套接字——utun 会给每个包加 4 字节地址族头(Linux 没有),Dart 后端入站剥离、出站前补上。
突破异步瓶颈
Dart 以单线程事件循环著称。从内核 TUN 接口或 Windows 驱动环形缓冲区读取是阻塞操作,如果主线程等包,网络栈直接卡死。解法是把读循环完全解耦:每个平台后端派生一个独立隔离工作线程(Dart Isolate),在 C 侧跑紧凑的连续阻塞读循环,通过异步 SendPort 把原始包缓冲传回主 Dart 事件循环,不卡顿、不产生延迟尖峰。
Apple Silicon 的可变参数 ABI bug
最值得的部分是跨架构底层 bug 的调试。CI 在 Apple Silicon(ARM64)runner 上测试时,一段在 x86-64 Linux 编译完全正常的代码突然在内核里抛垃圾指针异常。
元凶是 ioctl 系统调用签名。x86-64 架构上,标准参数和可变参数常通过 CPU 寄存器传递;但 macOS ARM64 平台 ABI 规定可变参数(...)必须压栈而不是放寄存器。标准 Dart FFI 绑定不区分这两者,内核就在错误的位置找参数。修复是显式告诉 Dart 的 FFI 分配器:仅对 Darwin 目标使用 VarArgs 声明——
typedef NativeIoctl = ffi.Int32 Function(ffi.Int32, ffi.UnsignedLong, ffi.VarArgs);
现状与验证
客户端当前支持 IPv4 的全隧道和分流路由,断开时干净清理、瞬间恢复系统原始路由。核心栈全部用离线的 mock 加密向量(RFC 2759)和合成管道做了单测——不需要 root 权限或真实服务器就能验证代码完整性。想探索 Dart 底层网络、字节流处理或系统 FFI 实现,可以直接读源码(项目仓库见原文链接)。
来源:How I Built a Pure-Dart SSTP & PPP VPN Client From Scratch (Linux, Windows, macOS) - DEV Community