TunnelMesh 架构概览
TunnelMesh 架构概览
Server 负责控制面、管理 API、Web 管理后台、HTTP/WSS 公网入口和 relay;Agent 通过 TLS WebSocket 主动连接 Server 并访问其宿主机/内网服务;Client 通过 TLS WebSocket 发起用户侧 forward、publish 和 SSH/stdin 代理。
tp-* HTTP 代理入口的链路(复用既有 443,不新增公网端口;唯一策略执行点是 Server):
浏览器 / curl / 系统代理(HTTPS 代理方案,只能填 https://)
└─ TLS + SNI: tp-<name>.<domain_suffix> 复用既有 443,不新增公网端口
└─ OpenResty tp-* server 块:server 级 access_by_lua_file
· 打过 proxy_connect 补丁的内核对 CONNECT 跳过 location 匹配
· 只搬字节,不含任何策略判断
· 注入可信头 X-TunnelMesh-Route(来自 SNI)/ -Client-IP / -Client-Port
└─ 明文回环 127.0.0.1:8089(server.proxy_entry.listen)
└─ tunnelmesh-server:ProxyEntryListener
· trusted_proxies 前置校验,不在白名单则读请求前直接关闭
· proxyentry 策略链:身份解析 → 源 IP ACL → Basic 认证 → 目标校验 → 并发限额
└─ relay.NodeTransport.OpenStream(集群模式下自动跨节点)
└─ Agent 出口:目标侧二次 SSRF / 私网 / 端口校验后建立真实连接
请求链路遵循 Handler → Service → Repository。管理数据以 SQLite 或 MySQL 为权威来源,租约/注册发现默认使用 MySQL,可切换 etcd。运行时连接和流状态不写入 Prometheus 高基数标签。
公网模式的既有入口是 HTTP/HTTPS/WSS;启用内嵌 VPN 网关后 Server 额外持有一个公网 UDP 端口(WireGuard 端点,见 ADR 0002,数据面在 -tags vpn 构建里)。tp-* 代理入口同样不新增公网监听端口,与既有 443 由 SNI 分流;OpenResty 与 Lua 只搬字节,不含任何授权逻辑,唯一策略执行点是 Server。Nginx 配置见 Nginx 推荐配置,代理入口的模板渲染与上线步骤见 OpenResty tp-* 代理入口。
内嵌 VPN 网关
用户用原生 WireGuard 客户端导入后台签发的 peer 配置,无需安装 tunnelmesh-client。
决策背景与被解除的旧约束见 ADR 0002,
设计定义见设计规格,
技术前提由 Task 0 可行性报告实测(Task 1-4 全 PASS)。
数据面在 //go:build vpn 后面:不带该 tag 的二进制只有管理面,且在该 tag 缺失而
server.vpn.enabled: true 时启动失败,不会静默不工作。部署变体与体积实测见
VPN 网关部署,容量、租约与 error_class 对照见
VPN 网关运维。
原生 WireGuard 客户端(用户机器,无需安装 tunnelmesh-client)
└─ 加密 UDP → Server 公网 IP:server.vpn.listen(独立端口,不经 Nginx/OpenResty)
└─ tunnelmesh-server 进程内 VPN 网关
· vpnBind:只绑 listen 写明的 host(上游默认绑 ":"+port,即所有接口),仅 IPv4
· wireguard-go device:Noise 握手、密钥、重放保护,按 peer 的 AllowedIPs 过滤源地址
· 内存态 tun.Device ←→ gVisor netstack(无 /dev/net/tun,无 CAP_NET_ADMIN)
· handlePacket 逐包决策,顺序固定:
1. 解析 IPv4 与传输头,失败即计数丢弃
2. 非首片分片 → fragment_dropped(不重组;先于 peer 解析,因为非首片没有传输头)
3. 按源 IP 反查 peer → peer_unknown / peer_revoked / peer_expired
4. 令牌桶限速 → rate_limited
5. 目标是网关自己的 VPN 地址 → target_denied
6. entry.Policy.Allow(每包执行,不缓存到流上)→ target_denied / metadata_denied / port_denied
7. 按 IP 协议分派,不在 TCP/UDP/ICMP-echo 白名单内 → protocol_unsupported
├─ TCP:流表按四元组去重 → 同步注册监听器(AddProtocolAddress + gonet.ListenTCP + accept 泵)
│ → 然后才 InjectInbound,所以首个 SYN 必然命中 demux,用户不会白等一次客户端 RTO
│ → netstack 终结 → relay.NodeTransport.OpenStream(protocol="tcp")
│ → Agent Dialer.DialTCP → 内网目标
├─ UDP:流表按 (peer, 源, 目标) 建 association → OpenStream(protocol="udp") → Agent Dialer.DialUDP
│ 回程数据报的 IPv4 头与 UDP 校验和由网关自己组装,经 device.emit 直接上线,不经 netstack 路由
└─ ICMP echo:仅 echo request 被中继(网关承诺绝不发出不是自己构造的 ICMP)
→ 流表按 (identifier, sequence) 去重,占用 icmp_max_concurrent 名额
→ OpenStream(protocol="icmp-echo") → Agent 非特权 ping socket → 内网目标
→ 回程 reply 由网关还原 peer 的 identifier/sequence 并重算校验和
· 拒绝路径:error_class 计数 + 按 (peer, reason, 目标 /24) 聚合,窗口过后落 vpn_packet_denied 审计;
一律静默,不回 RST、不回 ICMP 错误——回一个不可达就等于承认该地址存在
· 后台循环:idle reaper(idle_timeout)、子网租约续租(TTL 1 分钟,间隔 TTL/3,epoch fencing)
· 退出:shutdown_timeout 内 drain,超时强制关闭在途流并报告 in_flight_at_timeout 与 flows_forced
三条腿都经 relay.NodeTransport.OpenStream,所以集群模式下出口 Agent 挂在别的 Server 节点上时
自动走既有 relay,不需要额外接线。目标访问控制逐包在 Server 侧执行并包装既有 routing.Policy,
Agent 不新增特权、不维护防火墙规则、不改内网路由,因此内网侧看到的源地址是 Agent 宿主机地址
(能力边界见规格第 4.3 节与 VPN 使用者帮助)。