0
0
0

架构与控制面/数据面划分

文章摘要
|

来源:intranet-tunnel/README.md## 一、架构设计(原文 2353 字符)

一、架构设计

1.1 双层连接模型

系统严格分离控制平面与数据平面:

连接类型用途数量生命周期传输内容
控制连接鉴权、心跳、隧道注册、规则下发每客户端 1 条持久,断开自动重连控制协议消息(JSON 帧)
工作连接实际数据转发按需创建,可池化临时,用后回收(空闲 60s)原始 TCP 字节流

控制连接与工作连接复用同一个监听端口(47800),靠首帧消息类型区分(Login vs NewWorkConn),因此只需对外暴露一个端口。

1.2 数据流时序

公网用户                 Nginx                 Server                    Client              内网服务
   │                      │                      │                         │                    │
   │── HTTPS 请求 ───────>│                      │                         │                    │
   │                      │  SSL 终结            │                         │                    │
   │                      │── 明文 HTTP ────────>│                         │                    │
   │                      │   (Host: a.b.com)    │  按 Host 匹配路由表      │                    │
   │                      │                      │── ReqWorkConn ─────────>│                    │
   │                      │                      │<─ 新建 TCP 工作连接 ─────│                    │
   │                      │                      │── NewWorkConn(指派) ───>│                    │
   │                      │                      │                         │── net.Dial ───────>│
   │                      │                      │<═══ 双向 io.Copy ══════>│<══ 双向 io.Copy ══>│
   │<────── 响应 ─────────│<─────────────────────│                         │                    │

关键点:

  1. 服务端不处理 SSL 证书,HTTPS 完全由 Nginx 卸载,服务端只按 Host 头路由明文 HTTP;
  2. 工作连接由客户端主动发起,因此内网侧无需任何入站端口开放;
  3. 启用 yamux 多路复用后,工作连接不再新建 TCP,而是直接在既有连接上开一条逻辑流,省掉握手与 TLS 开销。

1.3 关键设计决策

决策原因
控制消息采用 [4 字节长度][JSON] 自定义帧变长消息必须有长度前缀才能可靠分帧;上限 1 MiB 防御超长帧耗尽内存
工作连接复用一个监听端口、靠首帧类型区分减少对外暴露端口;TLS 握手只需一种配置
数据面自解析 HTTP 请求行与 Host 头,而非 net/http.Server需要把原始 TCP 连接整体交给后端(keep-alive 复用、WebSocket 升级),http.Server 的缓冲与 Hijack 语义存在字节丢失风险
隧道标识统一使用数据库自增主键的十进制字符串协议层与存储层一一对应,避免额外 ID 生成器
统计在内存聚合、周期性落库数据面按字节写库不可行;30 秒窗口写入兼顾实时性与数据库压力
时间序列分桶在 Go 侧完成date_trunc / DATE_FORMAT / DATEPART 在四种目标数据库间不通用
custom_domain / remote_port 使用可空类型让「未绑定」在 PostgreSQL / MySQL / SQLite / SQL Server 的唯一索引下语义一致
SQLite 使用纯 Go 驱动(glebarez/sqlite保证 CGO_ENABLED=0 静态编译与 alpine 运行镜像可用


来源:intranet-tunnel/README.md## 三、端口规划(原文 580 字符)

三、端口规划

全部使用高位端口,避免与宿主机常规服务冲突。

端口用途建议暴露范围
47800控制连接监听(客户端接入 + 工作连接)对外(客户端需从公网接入)
47801Web 管理 API + 管理后台前端仅 127.0.0.1,由 Nginx 代理
48080公网 HTTP/HTTPS 流量入口仅 127.0.0.1,由 Nginx 代理
48081-48100TCP 隧道端口池对外(或用 Nginx stream 解耦)
48443内置 HTTPS 入口(证书自动化启用后)对外或用 Nginx 终止 TLS
49000-49100TCP/UDP 端口转发范围(FORWARD_PORT_RANGE按需对外(含 UDP)

若希望服务端控制端口也不直接对外,可把 docker-compose.yml 中 47800 的映射改为 127.0.0.1:47800:47800,并启用 deploy/nginx/conf.d/stream/tunnel-stream.conf 中注释掉的「控制连接转发」段落。



来源:intranet-tunnel/README.md## 九、控制协议(原文 1509 字符)

九、控制协议

9.1 帧格式

+----------------+----------------------------+
| 4 字节大端长度  |  JSON 消息体(≤ 1 MiB)     |
+----------------+----------------------------+
{
  "type": "Login",
  "transaction_id": "可选,用于请求/响应对应",
  "payload": { }
}

9.2 消息类型

Type方向说明
LoginC → S携带 client_id、token、版本、主机信息、是否启用多路复用、本地静态隧道
LoginRespS → C返回 run_id、心跳间隔、是否启用多路复用,以及该客户端的完整隧道列表
NewTunnelS → C下发/更新隧道配置(服务端启动或后台新增时)
NewTunnelRespC → S隧道配置处理结果
CloseTunnelS → C通知客户端注销隧道
ReqWorkConnS → C请求客户端新建一条工作连接
NewWorkConn双向工作连接握手:客户端侧携带 run_id 认证;服务端侧携带 tunnel_id 指派隧道
Heartbeat / HeartbeatRespC ↔ S心跳与响应
NotifyS → C主动通知(re_login=true 时客户端立即重连)

9.3 关键流程

登录与多路复用协商

  1. 客户端在原始 TCP(或 TLS)连接上发送 Login
  2. 服务端校验 Token(HMAC 摘要常数时间比对)后回复 LoginResp(此时仍在原始连接上);
  3. 若双方都支持多路复用,服务端在该连接上建立 yamux Server 会话,客户端建立 Client 会话;
  4. 客户端 Open() 第一条流作为控制流,服务端 Accept() 该流,此后所有控制消息走此流;
  5. 后续服务端需要数据通道时直接 Open() 新流,客户端 Accept() —— 无需任何 TCP/TLS 握手。

工作连接获取(服务端视角)

公网连接到达
   ├─ 多路复用可用? → 直接在 yamux 会话上 Open 一条流 → 写 NewWorkConn{TunnelID} 指派
   └─ 否则
        ├─ 池中有空闲连接? → 取出 → 写 NewWorkConn{TunnelID} 指派
        └─ 池空 → 每 2 秒发送 ReqWorkConn(最多到超时)
                    → 客户端拨号并发送 NewWorkConn{RunID} 认证
                    → 服务端校验 RunID 后放入连接池 → 唤醒等待者

在线判定:服务端对控制连接设置读超时(默认 90 秒 = 3 × 心跳间隔),同时心跳巡检协程每 15 秒兜底检查,超时即关闭会话、释放工作连接、更新数据库状态为 offline。


支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论