- 批量创建:主机支持 IP、主机名、末段范围(10.0.1.11-13)和 主机:端口, 与端口列表展开为 主机 × 端口,自动分配连续的访问端口,创建前可预览 - 授权以组为单位:一条邀请链接、一次审批即可访问组内全部地址; 计时从首次连上任一地址开始,撤销一次全组断开;后加入组的通道自动获得组授权 - 网关授权状态按作用范围(通道 / 通道组)存储,通道的动态白名单统一由状态库重新计算, 通道授权与组授权互不干扰;组内最后一个通道关闭或组被解散时,组的邀请与授权失效 - 闲置联动:组内任一地址在用时整组都不算闲置;支持整组开关 - 已有通道可并入组或移出组;组可改名、追加目标、解散(保留或删除成员) - App 与网页面板:组卡片、新建 / 编辑通道组、组邀请与“连接与授权”,授权页列出全部地址 - 测试:网关 9 个、Agent 5 个通道组用例
210 lines
5.8 KiB
Go
210 lines
5.8 KiB
Go
package client
|
||
|
||
import (
|
||
"fmt"
|
||
"net"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// maxBatchTargets 一次批量创建的目标上限,避免误操作占满端口池
|
||
const maxBatchTargets = 64
|
||
|
||
var (
|
||
listSeparators = regexp.MustCompile(`[\s,,、;;]+`)
|
||
shortRange = regexp.MustCompile(`^(\d{1,3}\.\d{1,3}\.\d{1,3}\.)(\d{1,3})-(\d{1,3})$`)
|
||
fullRange = regexp.MustCompile(`^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})-(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$`)
|
||
hostnameRe = regexp.MustCompile(`^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$`)
|
||
)
|
||
|
||
func splitList(s string) []string {
|
||
var out []string
|
||
for _, item := range listSeparators.Split(strings.TrimSpace(s), -1) {
|
||
if item != "" {
|
||
out = append(out, item)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// parsePorts accepts "22, 8080-8082"
|
||
func parsePorts(s string) ([]int, error) {
|
||
var out []int
|
||
for _, item := range splitList(s) {
|
||
lo, hi := item, item
|
||
if i := strings.Index(item, "-"); i > 0 {
|
||
lo, hi = item[:i], item[i+1:]
|
||
}
|
||
a, err1 := strconv.Atoi(lo)
|
||
b, err2 := strconv.Atoi(hi)
|
||
if err1 != nil || err2 != nil || a <= 0 || b > 65535 || a > b {
|
||
return nil, fmt.Errorf("无效的端口 %q", item)
|
||
}
|
||
if b-a >= maxBatchTargets {
|
||
return nil, fmt.Errorf("端口范围 %q 过大", item)
|
||
}
|
||
for p := a; p <= b; p++ {
|
||
out = append(out, p)
|
||
}
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// expandIPv4Range expands "10.0.1.11-13" or "10.0.1.11-10.0.1.13"
|
||
func expandIPv4Range(item string) ([]string, bool, error) {
|
||
var from, to net.IP
|
||
if m := shortRange.FindStringSubmatch(item); m != nil {
|
||
from, to = net.ParseIP(m[1]+m[2]).To4(), net.ParseIP(m[1]+m[3]).To4()
|
||
} else if m := fullRange.FindStringSubmatch(item); m != nil {
|
||
from, to = net.ParseIP(m[1]).To4(), net.ParseIP(m[2]).To4()
|
||
} else {
|
||
return nil, false, nil
|
||
}
|
||
if from == nil || to == nil {
|
||
return nil, true, fmt.Errorf("无效的地址范围 %q", item)
|
||
}
|
||
a := uint32(from[0])<<24 | uint32(from[1])<<16 | uint32(from[2])<<8 | uint32(from[3])
|
||
b := uint32(to[0])<<24 | uint32(to[1])<<16 | uint32(to[2])<<8 | uint32(to[3])
|
||
if a > b {
|
||
return nil, true, fmt.Errorf("地址范围 %q 起止颠倒", item)
|
||
}
|
||
if b-a >= maxBatchTargets {
|
||
return nil, true, fmt.Errorf("地址范围 %q 过大", item)
|
||
}
|
||
var out []string
|
||
for v := a; v <= b; v++ {
|
||
out = append(out, net.IPv4(byte(v>>24), byte(v>>16), byte(v>>8), byte(v)).String())
|
||
}
|
||
return out, true, nil
|
||
}
|
||
|
||
func validHost(h string) bool {
|
||
return net.ParseIP(h) != nil || hostnameRe.MatchString(h)
|
||
}
|
||
|
||
// ExpandTargets turns host and port lists into "host:port" targets, host by host.
|
||
//
|
||
// hosts 用逗号、空格或换行分隔,每一项可以是 IP、主机名、IPv4 范围("10.0.1.11-13" 或
|
||
// "10.0.1.11-10.0.1.13"),也可以直接写成 "主机:端口";ports 形如 "22, 8080-8082",
|
||
// 作用于没有写端口的主机。结果去重并保持输入顺序。
|
||
func ExpandTargets(hosts, ports string) ([]string, error) {
|
||
portList, err := parsePorts(ports)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
seen := make(map[string]bool)
|
||
var out []string
|
||
add := func(t string) error {
|
||
if !seen[t] {
|
||
seen[t] = true
|
||
out = append(out, t)
|
||
}
|
||
if len(out) > maxBatchTargets {
|
||
return fmt.Errorf("一次最多添加 %d 个目标", maxBatchTargets)
|
||
}
|
||
return nil
|
||
}
|
||
items := splitList(hosts)
|
||
if len(items) == 0 {
|
||
return nil, fmt.Errorf("请填写至少一个目标主机")
|
||
}
|
||
for _, item := range items {
|
||
// 自带端口的完整目标:"10.0.1.11:22"、"[fd00::1]:22"
|
||
if h, p, err := net.SplitHostPort(item); err == nil {
|
||
n, perr := strconv.Atoi(p)
|
||
if !validHost(h) || perr != nil || n <= 0 || n > 65535 {
|
||
return nil, fmt.Errorf("无效的目标 %q", item)
|
||
}
|
||
if err := add(net.JoinHostPort(h, p)); err != nil {
|
||
return nil, err
|
||
}
|
||
continue
|
||
}
|
||
hostsOfItem, isRange, err := expandIPv4Range(item)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !isRange {
|
||
if !validHost(item) {
|
||
return nil, fmt.Errorf("无效的主机 %q", item)
|
||
}
|
||
hostsOfItem = []string{item}
|
||
}
|
||
if len(portList) == 0 {
|
||
return nil, fmt.Errorf("请填写端口,或把目标写成 主机:端口")
|
||
}
|
||
for _, h := range hostsOfItem {
|
||
for _, p := range portList {
|
||
if err := add(net.JoinHostPort(h, strconv.Itoa(p))); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// memberNames names batch-created tunnels after the group: "组名 (主机)" when every target
|
||
// shares one port, otherwise "组名 (主机:端口)".
|
||
func memberNames(group string, targets []string) []string {
|
||
ports := make(map[string]bool)
|
||
for _, t := range targets {
|
||
if _, p, err := net.SplitHostPort(t); err == nil {
|
||
ports[p] = true
|
||
}
|
||
}
|
||
names := make([]string, len(targets))
|
||
for i, t := range targets {
|
||
label := t
|
||
if len(ports) == 1 {
|
||
if h, _, err := net.SplitHostPort(t); err == nil {
|
||
label = h
|
||
}
|
||
}
|
||
names[i] = fmt.Sprintf("%s (%s)", group, label)
|
||
}
|
||
return names
|
||
}
|
||
|
||
// allocatePortsLocked picks n free listen ports, preferring one contiguous block so the mapping
|
||
// is easy to read. Caller must hold cm.mu.
|
||
func (cm *ConfigManager) allocatePortsLocked(n int) ([]int, error) {
|
||
used := make(map[int]bool)
|
||
for _, t := range cm.cfg.Tunnels {
|
||
used[t.ListenPort] = true
|
||
}
|
||
// 与单个通道的推荐顺序一致:先 33660 往上,再从端口池开头补
|
||
var order []int
|
||
for p := 33660; p <= PortMax; p++ {
|
||
order = append(order, p)
|
||
}
|
||
for p := PortMin; p < 33660; p++ {
|
||
order = append(order, p)
|
||
}
|
||
free := func(p int) bool { return p != ControlPort && !used[p] }
|
||
|
||
for i := range order {
|
||
ok := true
|
||
for k := 0; k < n; k++ {
|
||
if i+k >= len(order) || !free(order[i+k]) || (k > 0 && order[i+k] != order[i+k-1]+1) {
|
||
ok = false
|
||
break
|
||
}
|
||
}
|
||
if ok {
|
||
return order[i : i+n], nil
|
||
}
|
||
}
|
||
var out []int
|
||
for _, p := range order {
|
||
if free(p) {
|
||
out = append(out, p)
|
||
if len(out) == n {
|
||
return out, nil
|
||
}
|
||
}
|
||
}
|
||
return nil, fmt.Errorf("端口池剩余 %d 个空闲端口,不够分配 %d 个", len(out), n)
|
||
}
|