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+范围即可
This commit is contained in:
2026-09-21 18:28:26 +08:00
parent 984041f1a0
commit 4dad7fbbbe
5 changed files with 324 additions and 18 deletions
+21 -4
View File
@@ -24,9 +24,10 @@ const loginTimeout = 10 * time.Second
// Node 穿透节点
type Node struct {
JwtSecret string // agent 登录 JWT 校验密钥(HS256 系)
BindTunnel string // 隧道监听绑定地址(默认 0.0.0.0)
SweepInterval time.Duration // TTL 清扫间隔(默认 30s)
JwtSecret string // agent 登录 JWT 校验密钥(HS256 系)
BindTunnel string // 隧道监听绑定地址(默认 0.0.0.0)
SweepInterval time.Duration // TTL 清扫间隔(默认 30s)
TunnelPortRange PortRange // 隧道监听端口范围(未配置时 ListenPort=0 走系统随机分配)
listener net.Listener
@@ -54,6 +55,9 @@ func (n *Node) Start(addr string) error {
}
n.listener = l
slog.Info("节点 桥接监听", "addr", l.Addr().String())
if n.TunnelPortRange.Valid() {
slog.Info("节点 隧道端口范围", "range", n.TunnelPortRange.String())
}
go n.acceptLoop()
go n.sweepLoop()
return nil
@@ -236,7 +240,7 @@ func (n *Node) registerTunnel(s *Session, t agent.Tunnel) (agent.Tunnel, error)
ts := &TunnelServer{tunnel: t, session: s}
ts.touch()
l, err := net.Listen("tcp", net.JoinHostPort(n.BindTunnel, strconv.Itoa(t.ListenPort)))
l, err := n.listenTunnel(t.ListenPort)
if err != nil {
return t, err
}
@@ -251,6 +255,19 @@ func (n *Node) registerTunnel(s *Session, t agent.Tunnel) (agent.Tunnel, error)
return ts.tunnel, nil
}
// listenTunnel 建立隧道公网监听:指定端口直接监听;端口 0 时若配置了范围则在范围内取空闲端口,
// 未配置范围回退系统随机分配。
func (n *Node) listenTunnel(listenPort int) (net.Listener, error) {
if listenPort == 0 && n.TunnelPortRange.Valid() {
l, err := n.TunnelPortRange.listen(n.BindTunnel)
if err != nil {
return nil, fmt.Errorf("分配隧道端口失败: %w", err)
}
return l, nil
}
return net.Listen("tcp", net.JoinHostPort(n.BindTunnel, strconv.Itoa(listenPort)))
}
// unregisterTunnel 删除隧道(关闭公网监听)
func (n *Node) unregisterTunnel(id string) bool {
n.mu.Lock()