// 端口范围:临时/永久隧道注册时未指定监听端口(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) }