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:
+21
-4
@@ -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()
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// 端口范围:临时/永久隧道注册时未指定监听端口(ListenPort=0)的取值范围。
|
||||
// 云主机防火墙按范围放行,避免隧道端口落在系统随机高位段而无法预开。
|
||||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PortRange 端口范围(含两端);Min==0 表示未配置(回退系统随机分配)。
|
||||
type PortRange struct {
|
||||
Min, Max int
|
||||
}
|
||||
|
||||
// Valid 是否已配置有效范围
|
||||
func (r PortRange) Valid() bool { return r.Min > 0 && r.Max >= r.Min }
|
||||
|
||||
// ParsePortRange 解析 "20000-21000"(单端口 "20000" 视为 min=max)。
|
||||
func ParsePortRange(s string) (PortRange, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return PortRange{}, nil
|
||||
}
|
||||
minStr, maxStr, ok := strings.Cut(s, "-")
|
||||
if !ok {
|
||||
maxStr = minStr
|
||||
}
|
||||
minP, err := strconv.Atoi(strings.TrimSpace(minStr))
|
||||
if err != nil {
|
||||
return PortRange{}, fmt.Errorf("端口范围格式错误:%s", s)
|
||||
}
|
||||
maxP, err := strconv.Atoi(strings.TrimSpace(maxStr))
|
||||
if err != nil {
|
||||
return PortRange{}, fmt.Errorf("端口范围格式错误:%s", s)
|
||||
}
|
||||
r := PortRange{Min: minP, Max: maxP}
|
||||
if !r.Valid() || r.Max > 65535 {
|
||||
return PortRange{}, fmt.Errorf("端口范围无效:%s", s)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// String 还原配置形式(便于日志)
|
||||
func (r PortRange) String() string {
|
||||
if !r.Valid() {
|
||||
return ""
|
||||
}
|
||||
if r.Min == r.Max {
|
||||
return strconv.Itoa(r.Min)
|
||||
}
|
||||
return fmt.Sprintf("%d-%d", r.Min, r.Max)
|
||||
}
|
||||
|
||||
// listen 从范围内随机起点顺延探测,返回第一个监听成功的 listener(占用即生效,避免探测与使用竞态)。
|
||||
// 全部被占返回错误。bind 为隧道监听绑定地址。
|
||||
func (r PortRange) listen(bind string) (net.Listener, error) {
|
||||
if !r.Valid() {
|
||||
return nil, fmt.Errorf("端口范围未配置")
|
||||
}
|
||||
start := r.Min + rand.Intn(r.Max-r.Min+1)
|
||||
for i := 0; i <= r.Max-r.Min; i++ {
|
||||
port := r.Min + (start-r.Min+i)%(r.Max-r.Min+1)
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(bind, strconv.Itoa(port)))
|
||||
if err == nil {
|
||||
return l, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("端口范围 %s 内无可用端口", r)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePortRange(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
wantMin int
|
||||
wantMax int
|
||||
wantErr bool
|
||||
}{
|
||||
{"", 0, 0, false}, // 空 = 未配置
|
||||
{"20000-21000", 20000, 21000, false},
|
||||
{"20000 - 21000", 20000, 21000, false}, // 两侧空白
|
||||
{"5900", 5900, 5900, false}, // 单端口
|
||||
{"abc", 0, 0, true},
|
||||
{"21000-20000", 0, 0, true}, // 逆序
|
||||
{"0-100", 0, 0, true}, // 零起点无效
|
||||
{"65536", 0, 0, true}, // 越界
|
||||
}
|
||||
for _, c := range cases {
|
||||
r, err := ParsePortRange(c.in)
|
||||
if c.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("ParsePortRange(%q) 期望报错, 得到 %v", c.in, r)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("ParsePortRange(%q) 意外报错: %v", c.in, err)
|
||||
continue
|
||||
}
|
||||
if r.Min != c.wantMin || r.Max != c.wantMax {
|
||||
t.Errorf("ParsePortRange(%q) = %v, 期望 %d-%d", c.in, r, c.wantMin, c.wantMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortRangeListen(t *testing.T) {
|
||||
r, err := ParsePortRange("20000-20002")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 占满 3 个端口后第 4 次应失败
|
||||
var ls []net.Listener
|
||||
defer func() {
|
||||
for _, l := range ls {
|
||||
l.Close()
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 3; i++ {
|
||||
l, err := r.listen("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("第 %d 次取端口失败: %v", i+1, err)
|
||||
}
|
||||
ls = append(ls, l)
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
if port < 20000 || port > 20002 {
|
||||
t.Fatalf("取得端口 %d 超出范围", port)
|
||||
}
|
||||
}
|
||||
if _, err := r.listen("127.0.0.1"); err == nil {
|
||||
t.Fatal("范围占满后应返回错误")
|
||||
}
|
||||
// 释放后应可再取
|
||||
ls[0].Close()
|
||||
l, err := r.listen("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("释放后再取失败: %v", err)
|
||||
}
|
||||
l.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user