Files
zonat/README.md
T

177 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# zonat — 内网穿透轻量库
从完整版内网穿透项目(client + server + cloud 三端约 1.4 万行)裁剪出的轻量实现,
只服务远程桌面场景(VNC / RDP / webshell / 文件)。背景与裁剪依据见
capricorn 仓库 `docs/remote-desktop-nat-analysis.md` 第八节。
**在 zomaintain 中的定位**:`backend/zonat` 是 backend/ 下的**共享库 module**
(module 名 `zonat`,非业务端)——被控场所侧宿主(网吧服务器 BS,即 `backend/server`)
内嵌 `agent/` 包建立穿透隧道;穿透节点用 `cmd/node` 独立部署在公网/机房。
业务端之间仍是「零 import」,zonat 是唯一例外:桥接协议必须单一来源,防止两端漂移。
**打包约定**:
- **单 Go module**(node + agent + wire 全在一起,单一来源,方便维护):端到端测试可
进程内直接跑通;后续如需按端拆分再拆。
- **宿主用 import 引用 agent(推荐)**:`agent/` 是公开包且自包含(5 个文件,仅依赖标准库 +
smux),宿主 go.mod `require zonat` + `replace zonat => ../zonat` 即可;构建加 `-trimpath`
后,agent 日志路径稳定为 `zonat@版本/agent/agent.go` 模块形式(已实测,见下)。
- **整目录拷贝为备用方案**:不依赖模块引用的场景(如临时放进某个项目调试)直接拷 `agent/`
目录,日志走宿主项目内路径,同样零修改。
- **日志注入**:agent 包不依赖任何具体日志实现,宿主注入 4 方法 `Logger` 接口
(logrus 原生满足,slog 写几行适配器),agent 日志即归入宿主的格式/级别/输出。
唯一例外 `DefaultLogger` 是仅标准库的兜底(stderr,Info 级)。
## 架构
```
控制面:cloud ──WS──► 场所服务器(内嵌 agent 库)──桥接帧命令注册隧道──► 节点(纯内存,无DB/无管理API)
数据面:浏览器 ──► 节点:隧道端口 ──smux(TCP桥接)──► agent ──LAN TCP──► VNC/RDP/3000
```
**多节点模型**:每条 Tunnel 自带 `NodeAddr` + `Token`,由独立连接完成
"登录 → smux → 控制流注册自身 → 数据流服务";Agent 只是隧道注册表,不持有网络连接。
因此一个 agent 的多条隧道可以分布在多个不同节点,各隧道的连接/重连/心跳互不影响。
相比完整版裁掉:KCP 桥接、UDP 隧道、http(s) 反向代理模式、协议嗅探与兜底站、
管理 REST、数据库、P2P、限流统计、zonat cloud 整端。
保留:帧协议(wire format 与完整版一致)、smux 多路复用(恒开,不再有开关)、
TCP 桥接、心跳重连、TTL 空闲自动删除(对应完整版 AutoDelete)。
## 协议
- 帧格式:`ver(1) + cmd(1) + length(2,LE) + sid(4,LE) + data(JSON)`,与完整版桥接帧一致。
- 命令字与完整版对齐:`CmdLogin=0`、`CmdPing=3`、`CmdTarget=4`(同 CmdTunnelLogin/Ping/Target),
新增 `CmdRegisterTunnel=10`、`CmdUnregisterTunnel=11`、`CmdTunnelClosed=12`(推送)。
- **一条隧道一条桥接连接**(TCP + smux):连接建立时先用该隧道的 Token 登录,再经控制流
注册自身。数据流由节点主动 OpenStream,首帧 CmdTarget 告知目标,agent 拨号本地 TCP
目标后回执,之后纯字节管道(**无协议嗅探**)。
- 隧道生命周期由 agent 注册:`port=0` 节点随机分配;`ttl>0` 空闲超时由节点清扫并推送
CmdTunnelClosed;永久隧道(ttl=0)断线后各自自动重连重注册。
- 同一 agentId 在一个节点上可有多个并发会话(每隧道一个);同 ID 隧道重复注册按
"隧道 ID + agentId" 粒度顶替(同 agent 更新配置,跨 agent 活隧道拒绝)。
- 鉴权:登录帧带 **JWT**(HS256 系,节点用 `-jwt-secret` 密钥校验)。token 必须带 `exp`;
若带 `agentId`/`sub` claim 则必须与登录 AgentId 一致。正式部署由 cloud 签发下发,
每条隧道各自的 token 可不同(含不同有效期/不同节点密钥)。
## 目录
```
zonat/
├── agent/ 可拷贝分发:agent.go + tunnel.go + forward.go + frame.go + protocol.go(自包含,仅依赖标准库+smux)
├── cmd/node/ 节点入口(独立部署二进制;含 -print-token 手工签发)
├── cmd/agent/ 被控端独立二进制(测试/独立跑用;slog 适配器示例)
├── cmd/tcp-echo/ 测试目标:TCP 回显(模拟 VNC/RDP/3000)
├── cmd/http-demo/ 测试目标:HTTP 服务(webshell/文件场景)
├── internal/node/ 节点:JWT 登录 / 桥接会话 / 隧道注册表 / 纯 TCP 隧道监听 / TTL 清扫
└── e2e/ 端到端测试(进程内,不依赖外部环境)
```
node 与 agent 共用同一份 wire 实现:节点侧 `import "git.zeroonesoft.cn/golib/zonat/agent"` 复用
Frame/Tunnel/命令字(agent 包是协议的唯一来源,节点不另留副本,防止两端漂移)。
## 宿主接入 agent(如 backend/server)
### 方式一:import 引用(推荐)
宿主 go.mod(宿主在 `backend/<端>` 下,与本库平级,用相对路径 replace):
```
require zonat v0.0.0
replace zonat => ../zonat // 例:backend/server、backend/user
```
宿主代码:
```go
import "git.zeroonesoft.cn/golib/zonat/agent"
ag := agent.New(agentId) // 节点地址与令牌随各隧道下发
ag.Logger = logrus.StandardLogger() // logrus 四个方法签名天然满足接口;不注入也有兜底
ag.OnTunnelPort = func(id string, port int) { /* 保存/上报分配到的端口 */ }
go ag.Run()
// 每条隧道自带节点与 JWT,可指向不同节点
port, _ := ag.RegisterTunnel(agent.Tunnel{
Id: "vnc-1", NodeAddr: "node1.example.com:5212", Token: jwtFromCloud,
TargetIp: "127.0.0.1", TargetPort: 5900, TTLSec: 300,
})
port2, _ := ag.RegisterTunnel(agent.Tunnel{
Id: "web-1", NodeAddr: "node2.example.com:5212", Token: anotherJwt,
TargetIp: "127.0.0.1", TargetPort: 3000,
})
```
`RegisterTunnel` 同步等待首次"连接+登录+注册"完成并返回节点分配的端口;之后由后台
worker 维持:永久隧道(TTLSec=0)断线自动重连重注册,临时隧道(TTLSec>0)一次性。
**宿主构建必须加 `-trimpath`**,否则 agent 日志记的是构建机全路径
(`D:/golib/zonat/agent/agent.go`)。实测对比(模拟宿主 Logger 记录调用点):
```
不加 -trimpath: WARN D:/golib/zonat/agent/agent.go:119
加 -trimpath: WARN zonat@v0.0.0/agent/agent.go:119 ← 模块名+包相对路径,跨机器稳定
```
`@v0.0.0` 来自宿主 require 的版本号(tag 后即 `@v1.0.0`),Logger 里可顺手替换掉。
**注意**:加了 `-trimpath` 后宿主自身日志路径也会变成 `server/internal/...` 模块形式,
且这两类路径都不含宿主 rootDir——宿主 Logger 若做过"削 rootDir、兜底取文件名"式处理,
需同步升级为**保留模块形式路径**(else 分支一行改动),否则退化成 `agent.go:119`。
### 方式二:整目录拷贝(备用)
```bash
cp -r /d/zomaintain/backend/zonat/agent /d/zomaintain/backend/server/pkg/agent
cd /d/zomaintain/backend/server && go mod tidy # 只为拉一次 xtaci/smux
```
import 改为 `server/pkg/agent`。源码在宿主项目内,`runtime.Caller` 记录宿主内路径,
宿主现有 rootDir 削前缀**原样生效**(得到 `pkg/agent/agent.go:88`),无需 `-trimpath`。
已用临时 server 模块验证零修改编译。升级纪律:从 `backend/zonat` 重新拷贝整目录,
不在副本里改协议。
## 节点部署(多台)
```bash
# Windows 交叉编译 Linux 节点
GOOS=linux GOARCH=amd64 go build -o bin/node-linux-amd64 ./cmd/node
# 本机
go build -o bin/node.exe ./cmd/node
# 各节点:./node-linux-amd64 -addr :5212 -jwt-secret <各节点不同密钥> -tunnel-bind 0.0.0.0
# 手工签发一张登录 token(正式部署由 cloud 签发):
./node-linux-amd64 -jwt-secret <密钥> -print-token -agent <agentId> -ttl 24h
```
## 验证
```bash
cd /d/zomaintain/backend/zonat
go test ./e2e/ -v # 13 个用例:TCP 回显(并发/大包/二进制安全)、HTTP GET/POST/keep-alive、
# 单agent双隧道连两节点、注销、TTL 过期、ID 冲突(同agent顶替/跨agent拒绝)、
# JWT 拒绝(错误签名/过期/agentId不一致)、踢会话重连重注册、日志注入
go build ./...
```
手动验证:
```bash
go build -o bin/ ./cmd/...
./bin/tcp-echo -addr 127.0.0.1:9000 &
./bin/node -addr :5212 -jwt-secret s3cret -tunnel-bind 127.0.0.1 &
TOK=$(./bin/node -jwt-secret s3cret -print-token -agent a1 -ttl 1h)
./bin/agent -node 127.0.0.1:5212 -token "$TOK" -id a1 -tunnel "id=demo,target=127.0.0.1:9000"
# agent 日志输出 "隧道端口 id=demo listen=<port>" 后:
nc 127.0.0.1 <port> # 输入任意内容,回显即穿透成功
```
## 已知简化(P0 范围)
- 无 TLS:节点 ↔ agent 明文(内网/机房部署假设);JWT 解决的是"静态共享秘密 + 无过期",
不解决信道窃听——明文信道上 token 在 exp 前仍可被截获重放。如需加密后续加 smux 前置 TLS。
- 每隧道一条连接:同一节点上的多条隧道不共享桥接连接(简单优先;如需收敛连接数,
后续可在 agent 内按 NodeAddr 池化)。
- 无限流/流量统计;连接数仅内存计数。
- 临时隧道 TTL 过期由节点清扫推送,agent 不重试(与完整版临时通道语义一致:由上层重新申请)。
- HTTP 隧道场景(webshell)验证走纯 tcp 模式;完整版的 http 反代模式不再实现。
- 拷贝式分发下各宿主副本可能随时间漂移:纪律是升级一律从 `backend/zonat` 重新拷贝整目录,
不在副本里改协议逻辑(与仓库"拷贝下沉"惯例一致,取舍已明确)。