feat: 通道组——一次开放多台主机 / 多个端口,共用邀请与授权

- 批量创建:主机支持 IP、主机名、末段范围(10.0.1.11-13)和 主机:端口,
  与端口列表展开为 主机 × 端口,自动分配连续的访问端口,创建前可预览
- 授权以组为单位:一条邀请链接、一次审批即可访问组内全部地址;
  计时从首次连上任一地址开始,撤销一次全组断开;后加入组的通道自动获得组授权
- 网关授权状态按作用范围(通道 / 通道组)存储,通道的动态白名单统一由状态库重新计算,
  通道授权与组授权互不干扰;组内最后一个通道关闭或组被解散时,组的邀请与授权失效
- 闲置联动:组内任一地址在用时整组都不算闲置;支持整组开关
- 已有通道可并入组或移出组;组可改名、追加目标、解散(保留或删除成员)
- App 与网页面板:组卡片、新建 / 编辑通道组、组邀请与“连接与授权”,授权页列出全部地址
- 测试:网关 9 个、Agent 5 个通道组用例
This commit is contained in:
DeepFal committed 2026-10-13 20:16:43 +08:00
1 parent 86e159b3f0
commit 3797bef88a
22 files changed
+3894 -471

No files matched your search

+12
View File
@@ -96,6 +96,18 @@ func (w *Whitelist) RestoreDynamic(leases map[string]time.Time) {
}
}
// ReplaceDynamic sets the dynamic leases to exactly the given set (the access state store is the source of truth)
func (w *Whitelist) ReplaceDynamic(leases map[string]time.Time) {
w.mu.Lock()
defer w.mu.Unlock()
w.dynamic = make(map[string]time.Time, len(leases))
for ip, exp := range leases {
if parsed := net.ParseIP(strings.TrimSpace(ip)); parsed != nil {
w.dynamic[parsed.String()] = exp
}
}
}
// RemoveDynamic revokes the lease for ip (all leases when ip is empty) and returns how many were removed
func (w *Whitelist) RemoveDynamic(ip string) int {
w.mu.Lock()
+28 -2
View File
@@ -17,6 +17,7 @@ const (
TypeInviteResp = "invite_resp"
TypeRevokeInvite = "revoke_invite"
TypeDecideRequest = "decide_request"
TypeDeleteGroup = "delete_group"
TypeMetrics = "metrics_update"
TypePing = "ping"
TypePong = "pong"
@@ -68,6 +69,10 @@ type TunnelConfig struct {
Notes string `json:"notes,omitempty"`
LeaseDuration string `json:"lease_duration,omitempty"` // 已废弃:放通时长改为在每个邀请上设置,保留字段仅为兼容旧配置
RequireApproval bool `json:"require_approval,omitempty"` // 已废弃:是否审批改为在每个邀请上设置,保留字段仅为兼容旧配置
// GroupID 所属通道组:组内通道共用邀请、授权与审批,闲置计时联动
GroupID string `json:"group_id,omitempty"`
// GroupName 由 Agent 同步到服务端时填写,用于授权页展示(不持久化)
GroupName string `json:"group_name,omitempty"`
}
// RegisterTunnelMessage registers a tunnel on server
@@ -110,13 +115,21 @@ type KillConnMessage struct {
ConnID string `json:"conn_id"`
}
// RevokeLeaseMessage removes a temporary (web-auth) whitelist lease; empty IP revokes all leases of the tunnel
// RevokeLeaseMessage removes a temporary (web-auth) whitelist lease; empty IP revokes all leases of the scope.
// 设置 GroupID 时作用于整个通道组的授权(TunnelID 忽略),下同。
type RevokeLeaseMessage struct {
Type string `json:"type"`
TunnelID string `json:"tunnel_id"`
GroupID string `json:"group_id,omitempty"`
IP string `json:"ip,omitempty"`
}
// DeleteGroupMessage drops the invites, leases and requests of a dissolved tunnel group
type DeleteGroupMessage struct {
Type string `json:"type"`
GroupID string `json:"group_id"`
}
// Lease durations (Invite.LeaseDuration):通过邀请获得授权后可访问多久
const (
LeaseDuration1h = "1h"
@@ -185,10 +198,11 @@ type Invite struct {
Uses []InviteUse `json:"uses,omitempty"`
}
// AccessRequest is an access application on a tunnel that requires approval
// AccessRequest is an access application on a tunnel (or tunnel group) that requires approval
type AccessRequest struct {
ID string `json:"id"`
TunnelID string `json:"tunnel_id"`
GroupID string `json:"group_id,omitempty"`
Name string `json:"name"`
IP string `json:"ip"`
Invite string `json:"invite,omitempty"`
@@ -201,6 +215,7 @@ type CreateInviteMessage struct {
Type string `json:"type"`
RequestID string `json:"request_id"`
TunnelID string `json:"tunnel_id"`
GroupID string `json:"group_id,omitempty"`
Note string `json:"note,omitempty"`
ValidMinutes int `json:"valid_minutes"`
MaxUses int `json:"max_uses"`
@@ -223,6 +238,7 @@ type InviteRespMessage struct {
type RevokeInviteMessage struct {
Type string `json:"type"`
TunnelID string `json:"tunnel_id"`
GroupID string `json:"group_id,omitempty"`
Code string `json:"code"`
}
@@ -230,6 +246,7 @@ type RevokeInviteMessage struct {
type DecideRequestMessage struct {
Type string `json:"type"`
TunnelID string `json:"tunnel_id"`
GroupID string `json:"group_id,omitempty"`
RequestID string `json:"request_id"`
Approve bool `json:"approve"`
}
@@ -279,6 +296,14 @@ type TunnelMetrics struct {
Pending []AccessRequest `json:"pending,omitempty"`
}
// GroupMetrics carries the access state shared by all tunnels of a group
type GroupMetrics struct {
GroupID string `json:"group_id"`
DynamicLeases []DynamicLease `json:"dynamic_leases,omitempty"`
Invites []Invite `json:"invites,omitempty"`
Pending []AccessRequest `json:"pending,omitempty"`
}
// SecurityEvent records blocked or flagged connection attempts
type SecurityEvent struct {
ID string `json:"id"`
@@ -293,6 +318,7 @@ type SecurityEvent struct {
type MetricsMessage struct {
Type string `json:"type"`
Tunnels []TunnelMetrics `json:"tunnels"`
Groups []GroupMetrics `json:"groups,omitempty"`
ActiveConns []ActiveConnInfo `json:"active_conns"`
SecurityEvents []SecurityEvent `json:"security_events,omitempty"`
}