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
+76
View File
@@ -0,0 +1,76 @@
// 节点配置文件:`-config node.ini` 启用;显式命令行 flag 优先于配置项,配置项优先于内置默认。
// 零依赖 ini:[节] 下 key = value,`#`/`;` 注释行忽略,等号两侧空白忽略。
package main
import (
"bufio"
"errors"
"os"
"strconv"
"strings"
"time"
)
// nodeConfig 配置文件映射(零值表示未配置,由调用方回退 flag/默认值)
type nodeConfig struct {
Addr string // [server] Addr 桥接监听地址
TunnelBind string // [server] TunnelBind 隧道监听绑定地址
JwtSecret string // [server] JwtSecret agent 登录 JWT 校验密钥
SweepSeconds int // [server] SweepSeconds TTL 清扫间隔(秒)
PortRange string // [tunnel] PortRange 隧道监听端口范围(如 20000-21000)
}
// loadConfigFile 解析 ini 文件;文件不存在返回错误(-config 显式指定了就应当存在)。
func loadConfigFile(path string) (*nodeConfig, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
cfg := &nodeConfig{}
section := ""
sc := bufio.NewScanner(f)
lineNo := 0
for sc.Scan() {
lineNo++
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
section = strings.ToLower(strings.TrimSpace(line[1 : len(line)-1]))
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue // 容错:无等号的行忽略
}
key = strings.ToLower(strings.TrimSpace(key))
value = strings.TrimSpace(value)
switch section + "." + key {
case "server.addr":
cfg.Addr = value
case "server.tunnelbind":
cfg.TunnelBind = value
case "server.jwtsecret":
cfg.JwtSecret = value
case "server.sweepseconds":
cfg.SweepSeconds, err = strconv.Atoi(value)
if err != nil || cfg.SweepSeconds <= 0 {
return nil, errors.New("server.sweepseconds 需为正整数, 行 " + strconv.Itoa(lineNo))
}
case "tunnel.portrange":
cfg.PortRange = value
}
}
return cfg, sc.Err()
}
// sweepDuration 秒数转 Duration;0 表示未配置
func (c *nodeConfig) sweepDuration() time.Duration {
if c.SweepSeconds <= 0 {
return 0
}
return time.Duration(c.SweepSeconds) * time.Second
}