feat(amneziawg): support persistent keepalive ranges (#6881)

AmneziaWG clients accept a randomised PersistentKeepalive range such as 15-25,
which makes keepalive timing harder to fingerprint; the panel only stored a
single number. A client's keepalive may now be a range, validated the way
AmneziaWG parses it. The downloadable .conf and vpn:// profile carry the range,
Clash/Mihomo output gets its lower bound, and plain WireGuard stays scalar-only.
The range lives in an additive wg_keep_alive_range column, so the integer
column and an older panel binary keep working after a rollback.
This commit is contained in:
YoungReckless4
2026-10-24 07:36:49 +03:00
committed by GitHub
parent 207c705300
commit 35e09191de
59 changed files with 1871 additions and 146 deletions
+53 -24
View File
@@ -432,6 +432,13 @@ func markInboundNodesDirty(inboundIds []int) error {
}
func (s *ClientService) fillProtocolDefaults(c *model.Client, ib *model.Inbound) error {
return s.fillProtocolDefaultsAgainstExisting(c, ib, nil)
}
func (s *ClientService) fillProtocolDefaultsAgainstExisting(c *model.Client, ib *model.Inbound, existing []model.Client, identityEmail ...string) error {
if err := validateClientKeepAliveAgainstExisting(ib.Protocol, *c, existing, identityEmail...); err != nil {
return err
}
switch ib.Protocol {
case model.VMESS, model.VLESS:
if c.ID == "" {
@@ -688,6 +695,23 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
if err := validateClientTrafficReset(updated.TrafficReset, updated.TrafficResetDay); err != nil {
return false, err
}
existingClientsByInbound := make(map[int][]model.Client, len(attachedIds))
if updated.KeepAlive != nil {
for _, inboundID := range attachedIds {
inbound, getErr := inboundSvc.GetInbound(inboundID)
if getErr != nil {
return false, fmt.Errorf("inbound %d: %w", inboundID, getErr)
}
clients, getErr := inboundSvc.GetClients(inbound)
if getErr != nil {
return false, fmt.Errorf("inbound %d: %w", inboundID, getErr)
}
existingClientsByInbound[inboundID] = clients
if err := validateClientKeepAliveAgainstExisting(inbound.Protocol, updated, clients, existing.Email); err != nil {
return false, fmt.Errorf("inbound %d: %w", inboundID, err)
}
}
}
normalizeClientTrafficReset(&updated)
if updated.SubID == "" {
updated.SubID = existing.SubID
@@ -765,7 +789,7 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
if existing.Email == "" {
continue
}
if err := s.fillProtocolDefaults(&updated, inbound); err != nil {
if err := s.fillProtocolDefaultsAgainstExisting(&updated, inbound, existingClientsByInbound[ibId], existing.Email); err != nil {
return false, err
}
clientForInbound := updated
@@ -822,32 +846,37 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
if len(inboundIds) == 0 {
merged := *existing
applyClientRecordMerge(&merged, updated.ToRecord())
keepAliveLegacy, keepAliveRange, keepAliveErr := model.KeepAliveStorage(merged.KeepAlive)
if keepAliveErr != nil {
return needRestart, keepAliveErr
}
if err := database.GetDB().Model(&model.ClientRecord{}).
Where("id = ?", id).
Updates(map[string]any{
"sub_id": merged.SubID,
"uuid": merged.UUID,
"password": merged.Password,
"auth": merged.Auth,
"secret": merged.Secret,
"flow": merged.Flow,
"security": merged.Security,
"wg_private_key": merged.PrivateKey,
"wg_public_key": merged.PublicKey,
"wg_allowed_ips": merged.AllowedIPs,
"wg_pre_shared_key": merged.PreSharedKey,
"wg_keep_alive": merged.KeepAlive,
"limit_ip": merged.LimitIP,
"total_gb": merged.TotalGB,
"expiry_time": merged.ExpiryTime,
"tg_id": merged.TgID,
"comment": merged.Comment,
"reset": merged.Reset,
"reset_day": merged.ResetDay,
"reset_weekday": merged.ResetWeekday,
"reset_max": merged.ResetMax,
"traffic_reset": merged.TrafficReset,
"traffic_reset_day": merged.TrafficResetDay,
"sub_id": merged.SubID,
"uuid": merged.UUID,
"password": merged.Password,
"auth": merged.Auth,
"secret": merged.Secret,
"flow": merged.Flow,
"security": merged.Security,
"wg_private_key": merged.PrivateKey,
"wg_public_key": merged.PublicKey,
"wg_allowed_ips": merged.AllowedIPs,
"wg_pre_shared_key": merged.PreSharedKey,
"wg_keep_alive": keepAliveLegacy,
"wg_keep_alive_range": keepAliveRange,
"limit_ip": merged.LimitIP,
"total_gb": merged.TotalGB,
"expiry_time": merged.ExpiryTime,
"tg_id": merged.TgID,
"comment": merged.Comment,
"reset": merged.Reset,
"reset_day": merged.ResetDay,
"reset_weekday": merged.ResetWeekday,
"reset_max": merged.ResetMax,
"traffic_reset": merged.TrafficReset,
"traffic_reset_day": merged.TrafficResetDay,
}).Error; err != nil {
return needRestart, err
}