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:
@@ -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
|
||||
}
|
||||
+80
-14
@@ -2,6 +2,8 @@
|
||||
// 无管理 API、无数据库;隧道由 agent 经桥接控制流注册。
|
||||
// agent 登录用 JWT 校验(HS256);token 正式部署由 cloud 签发,
|
||||
// 手工/测试场景可用 -print-token 现场签发。
|
||||
//
|
||||
// 配置来源优先级:显式命令行 flag > -config 指定的配置文件 > 内置默认。
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -17,14 +19,18 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", ":5212", "桥接监听地址")
|
||||
tunnelBind := flag.String("tunnel-bind", "0.0.0.0", "隧道监听绑定地址")
|
||||
jwtSecret := flag.String("jwt-secret", "", "agent 登录 JWT 校验密钥(HS256)")
|
||||
sweep := flag.Duration("sweep", 30*time.Second, "TTL 清扫间隔")
|
||||
// 便捷签发:node -jwt-secret s3cret -print-token -agent a1 -ttl 24h
|
||||
printToken := flag.Bool("print-token", false, "用 -jwt-secret 签发一张 agent 登录 token 后退出")
|
||||
tokenAgent := flag.String("agent", "", "print-token 时写入 agentId claim")
|
||||
tokenTTL := flag.Duration("ttl", 24*time.Hour, "print-token 签发的有效期")
|
||||
var (
|
||||
config = flag.String("config", "", "配置文件路径(node.ini); 显式 flag 可覆盖配置项")
|
||||
addr = flag.String("addr", ":5212", "桥接监听地址")
|
||||
tunnelBind = flag.String("tunnel-bind", "0.0.0.0", "隧道监听绑定地址")
|
||||
jwtSecret = flag.String("jwt-secret", "", "agent 登录 JWT 校验密钥(HS256)")
|
||||
sweep = flag.Duration("sweep", 30*time.Second, "TTL 清扫间隔")
|
||||
portRange = flag.String("port-range", "", "隧道监听端口范围(如 20000-21000); 未指定端口的隧道在范围内取空闲端口, 空=系统随机")
|
||||
// 便捷签发:node -jwt-secret s3cret -print-token -agent a1 -ttl 24h
|
||||
printToken = flag.Bool("print-token", false, "用 -jwt-secret 签发一张 agent 登录 token 后退出")
|
||||
tokenAgent = flag.String("agent", "", "print-token 时写入 agentId claim")
|
||||
tokenTTL = flag.Duration("ttl", 24*time.Hour, "print-token 签发的有效期")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *printToken {
|
||||
@@ -41,16 +47,76 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if *jwtSecret == "" {
|
||||
slog.Error("缺少 -jwt-secret(agent 登录 JWT 校验密钥)")
|
||||
// 终值 = 内置默认 ← 配置文件(非零项) ← 显式 flag
|
||||
explicit := map[string]bool{}
|
||||
flag.Visit(func(f *flag.Flag) { explicit[f.Name] = true })
|
||||
|
||||
var (
|
||||
finalAddr = *addr
|
||||
finalTunnelBind = *tunnelBind
|
||||
finalSecret = *jwtSecret
|
||||
finalSweep = *sweep
|
||||
finalRange node.PortRange
|
||||
)
|
||||
if *config != "" {
|
||||
cfg, err := loadConfigFile(*config)
|
||||
if err != nil {
|
||||
slog.Error("读取配置文件失败", "path", *config, "err", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if cfg.Addr != "" {
|
||||
finalAddr = cfg.Addr
|
||||
}
|
||||
if cfg.TunnelBind != "" {
|
||||
finalTunnelBind = cfg.TunnelBind
|
||||
}
|
||||
if cfg.JwtSecret != "" {
|
||||
finalSecret = cfg.JwtSecret
|
||||
}
|
||||
if d := cfg.sweepDuration(); d > 0 {
|
||||
finalSweep = d
|
||||
}
|
||||
if cfg.PortRange != "" {
|
||||
r, err := node.ParsePortRange(cfg.PortRange)
|
||||
if err != nil {
|
||||
slog.Error("配置文件端口范围无效", "err", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
finalRange = r
|
||||
}
|
||||
}
|
||||
if explicit["addr"] {
|
||||
finalAddr = *addr
|
||||
}
|
||||
if explicit["tunnel-bind"] {
|
||||
finalTunnelBind = *tunnelBind
|
||||
}
|
||||
if explicit["jwt-secret"] {
|
||||
finalSecret = *jwtSecret
|
||||
}
|
||||
if explicit["sweep"] {
|
||||
finalSweep = *sweep
|
||||
}
|
||||
if explicit["port-range"] {
|
||||
r, err := node.ParsePortRange(*portRange)
|
||||
if err != nil {
|
||||
slog.Error("端口范围无效", "err", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
finalRange = r
|
||||
}
|
||||
|
||||
if finalSecret == "" {
|
||||
slog.Error("缺少 jwt-secret(agent 登录 JWT 校验密钥; -jwt-secret 或配置文件 server.jwt-secret)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
n := node.New()
|
||||
n.JwtSecret = *jwtSecret
|
||||
n.BindTunnel = *tunnelBind
|
||||
n.SweepInterval = *sweep
|
||||
if err := n.Start(*addr); err != nil {
|
||||
n.JwtSecret = finalSecret
|
||||
n.BindTunnel = finalTunnelBind
|
||||
n.SweepInterval = finalSweep
|
||||
n.TunnelPortRange = finalRange
|
||||
if err := n.Start(finalAddr); err != nil {
|
||||
slog.Error("节点启动失败", "err", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user