4566704 4dad7fbbbe feat(node): 配置文件启动与隧道端口范围——云主机防火墙可按范围放行
改了什么:
- cmd/node/config.go: 零依赖 ini 配置加载([server] Addr/TunnelBind/JwtSecret/SweepSeconds + [tunnel] PortRange); -config node.ini 启用
- main.go: -config 与 -port-range flag; 配置来源优先级 显式 flag > 配置文件(非零项) > 内置默认(flag.Visit 判显式)
- internal/node/portrange.go: PortRange 解析(20000-21000/单端口/容错) + listen(随机起点顺延探测, 占用即生效避免 TOCTOU)
- node.go: TunnelPortRange 字段; listenTunnel 统一隧道建听——端口 0 且配置范围时范围内取空闲端口, 否则原路径(指定端口直监听/系统随机)
- Start 日志打出端口范围; portrange_test.go 覆盖解析与占满/释放取端口

为什么: agent 注册隧道从不指定端口(ListenPort=0), 节点 net.Listen 落系统随机高位段, 云主机防火墙无法预开; 配置范围后临时/永久隧道都落在固定段内, 防火墙放行 5212+范围即可
2026-09-21 18:28:26 +08:00

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

宿主代码:

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。

方式二:整目录拷贝(备用)

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 重新拷贝整目录, 不在副本里改协议。

节点部署(多台)

# 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

验证

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 ./...

手动验证:

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 重新拷贝整目录, 不在副本里改协议逻辑(与仓库"拷贝下沉"惯例一致,取舍已明确)。
S
Description
零一轻量内网穿透
Readme
99 KiB
Languages
Go 100%