mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 07:15:01 +03:00
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:
@@ -7,8 +7,11 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
@@ -944,7 +947,7 @@ type Client struct {
|
||||
// before -- fully backward compatible for callers that never set this.
|
||||
AllowedIPsByInbound map[int][]string `json:"allowedIPsByInbound,omitempty"`
|
||||
PreSharedKey string `json:"preSharedKey,omitempty"`
|
||||
KeepAlive *int `json:"keepAlive,omitempty"` // Seconds between PersistentKeepalive packets; 0 sends none, omit to keep the stored value
|
||||
KeepAlive *KeepAliveValue `json:"keepAlive,omitempty"` // Numeric values or AmneziaWG ranges; 0 disables, omission preserves.
|
||||
ForwardedPorts string `json:"forwardedPorts,omitempty"` // AmneziaWG per-client port-forwarding spec, e.g. "80,443,8000-8100"
|
||||
Secret string `json:"secret,omitempty" example:"ee1234567890abcdef1234567890abcd7777772e636c6f7564666c6172652e636f6d"`
|
||||
AdTag string `json:"adTag,omitempty" example:"0123456789abcdef0123456789abcdef"`
|
||||
@@ -969,39 +972,41 @@ type Client struct {
|
||||
}
|
||||
|
||||
type ClientRecord struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||
SubID string `json:"subId" gorm:"index;column:sub_id"`
|
||||
UUID string `json:"uuid" gorm:"column:uuid"`
|
||||
Password string `json:"password"`
|
||||
Auth string `json:"auth"`
|
||||
Flow string `json:"flow"`
|
||||
Security string `json:"security"`
|
||||
Reverse string `json:"reverse" gorm:"column:reverse"`
|
||||
PrivateKey string `json:"privateKey" gorm:"column:wg_private_key"`
|
||||
PublicKey string `json:"publicKey" gorm:"column:wg_public_key"`
|
||||
AllowedIPs string `json:"allowedIPs" gorm:"column:wg_allowed_ips"`
|
||||
PreSharedKey string `json:"preSharedKey" gorm:"column:wg_pre_shared_key"`
|
||||
KeepAlive int `json:"keepAlive" gorm:"column:wg_keep_alive;default:0"`
|
||||
ForwardedPorts string `json:"forwardedPorts" gorm:"column:wg_forwarded_ports"`
|
||||
Secret string `json:"secret" gorm:"column:secret"`
|
||||
AdTag string `json:"adTag" gorm:"column:ad_tag;default:''"`
|
||||
LimitIP int `json:"limitIp" gorm:"column:limit_ip"`
|
||||
LimitHwid int `json:"limitHwid" gorm:"column:limit_hwid;default:0"`
|
||||
TotalGB int64 `json:"totalGB" gorm:"column:total_gb"`
|
||||
ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time"`
|
||||
Enable bool `json:"enable" gorm:"default:true"`
|
||||
TgID int64 `json:"tgId" gorm:"column:tg_id;index:idx_clients_tg_id"`
|
||||
Group string `json:"group" gorm:"column:group_name;default:'';index:idx_client_record_group"`
|
||||
Comment string `json:"comment"`
|
||||
Reset int `json:"reset" gorm:"default:0"`
|
||||
ResetDay int `json:"resetDay" gorm:"column:reset_day;default:0"`
|
||||
ResetWeekday int `json:"resetWeekday" gorm:"column:reset_weekday;default:0"`
|
||||
ResetMax int `json:"resetMax" gorm:"column:reset_max;default:0"`
|
||||
TrafficReset string `json:"trafficReset" gorm:"column:traffic_reset;default:never;index:idx_clients_traffic_reset"`
|
||||
TrafficResetDay int `json:"trafficResetDay" gorm:"column:traffic_reset_day;default:1"`
|
||||
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||
SubID string `json:"subId" gorm:"index;column:sub_id"`
|
||||
UUID string `json:"uuid" gorm:"column:uuid"`
|
||||
Password string `json:"password"`
|
||||
Auth string `json:"auth"`
|
||||
Flow string `json:"flow"`
|
||||
Security string `json:"security"`
|
||||
Reverse string `json:"reverse" gorm:"column:reverse"`
|
||||
PrivateKey string `json:"privateKey" gorm:"column:wg_private_key"`
|
||||
PublicKey string `json:"publicKey" gorm:"column:wg_public_key"`
|
||||
AllowedIPs string `json:"allowedIPs" gorm:"column:wg_allowed_ips"`
|
||||
PreSharedKey string `json:"preSharedKey" gorm:"column:wg_pre_shared_key"`
|
||||
KeepAlive KeepAliveValue `json:"keepAlive" gorm:"-"` // Numeric values or AmneziaWG ranges.
|
||||
KeepAliveLegacy int `json:"-" gorm:"column:wg_keep_alive;default:0"`
|
||||
KeepAliveRange KeepAliveValue `json:"-" gorm:"column:wg_keep_alive_range;type:text;default:''"`
|
||||
ForwardedPorts string `json:"forwardedPorts" gorm:"column:wg_forwarded_ports"`
|
||||
Secret string `json:"secret" gorm:"column:secret"`
|
||||
AdTag string `json:"adTag" gorm:"column:ad_tag;default:''"`
|
||||
LimitIP int `json:"limitIp" gorm:"column:limit_ip"`
|
||||
LimitHwid int `json:"limitHwid" gorm:"column:limit_hwid;default:0"`
|
||||
TotalGB int64 `json:"totalGB" gorm:"column:total_gb"`
|
||||
ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time"`
|
||||
Enable bool `json:"enable" gorm:"default:true"`
|
||||
TgID int64 `json:"tgId" gorm:"column:tg_id;index:idx_clients_tg_id"`
|
||||
Group string `json:"group" gorm:"column:group_name;default:'';index:idx_client_record_group"`
|
||||
Comment string `json:"comment"`
|
||||
Reset int `json:"reset" gorm:"default:0"`
|
||||
ResetDay int `json:"resetDay" gorm:"column:reset_day;default:0"`
|
||||
ResetWeekday int `json:"resetWeekday" gorm:"column:reset_weekday;default:0"`
|
||||
ResetMax int `json:"resetMax" gorm:"column:reset_max;default:0"`
|
||||
TrafficReset string `json:"trafficReset" gorm:"column:traffic_reset;default:never;index:idx_clients_traffic_reset"`
|
||||
TrafficResetDay int `json:"trafficResetDay" gorm:"column:traffic_reset_day;default:1"`
|
||||
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
|
||||
// Owned solely by the node-snapshot sweep, which soft-orphans instead of
|
||||
// deleting; orphans from any other cause stay at zero and are never reaped.
|
||||
SyncOrphanedAt int64 `json:"-" gorm:"column:sync_orphaned_at;default:0"`
|
||||
@@ -1009,6 +1014,47 @@ type ClientRecord struct {
|
||||
|
||||
func (ClientRecord) TableName() string { return "clients" }
|
||||
|
||||
func (r *ClientRecord) BeforeSave(*gorm.DB) error {
|
||||
legacy, ranged, err := splitKeepAliveStorage(r.KeepAlive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.KeepAliveLegacy = legacy
|
||||
r.KeepAliveRange = ranged
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ClientRecord) AfterFind(*gorm.DB) error {
|
||||
if r.KeepAliveLegacy != 0 {
|
||||
r.KeepAlive = KeepAliveValue(strconv.Itoa(r.KeepAliveLegacy))
|
||||
} else if r.KeepAliveRange != "" {
|
||||
r.KeepAlive = r.KeepAliveRange
|
||||
} else {
|
||||
r.KeepAlive = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitKeepAliveStorage(value KeepAliveValue) (int, KeepAliveValue, error) {
|
||||
if value == "" {
|
||||
return 0, "", nil
|
||||
}
|
||||
if n, err := strconv.Atoi(string(value)); err == nil && strconv.Itoa(n) == string(value) {
|
||||
if n == 0 {
|
||||
return 0, "0", nil
|
||||
}
|
||||
return n, "", nil
|
||||
}
|
||||
if err := value.Validate(true); err != nil || !strings.Contains(string(value), "-") {
|
||||
return 0, "", fmt.Errorf("invalid stored keepAlive %q", value)
|
||||
}
|
||||
return 0, value, nil
|
||||
}
|
||||
|
||||
func KeepAliveStorage(value KeepAliveValue) (int, KeepAliveValue, error) {
|
||||
return splitKeepAliveStorage(value)
|
||||
}
|
||||
|
||||
type ClientGroup struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"uniqueIndex;not null"`
|
||||
@@ -1171,25 +1217,39 @@ type Host struct {
|
||||
|
||||
func (Host) TableName() string { return "hosts" }
|
||||
|
||||
// KeepAliveSeconds is the client's PersistentKeepalive, 0 when unset.
|
||||
// KeepAliveSeconds returns a scalar value; ranges are emitted by native AWG config.
|
||||
func (c Client) KeepAliveSeconds() int {
|
||||
if c.KeepAlive == nil {
|
||||
return 0
|
||||
}
|
||||
return *c.KeepAlive
|
||||
return c.KeepAlive.Seconds()
|
||||
}
|
||||
|
||||
// KeepAlivePtr wraps an explicit PersistentKeepalive, 0 included -- distinct
|
||||
// from a nil KeepAlive, which means the field was never sent.
|
||||
func KeepAlivePtr(v int) *int { return &v }
|
||||
func (c Client) KeepAliveText() string {
|
||||
if c.KeepAlive == nil {
|
||||
return ""
|
||||
}
|
||||
return c.KeepAlive.String()
|
||||
}
|
||||
|
||||
// nonZeroKeepAlive keeps a stored 0 nil: wg_keep_alive cannot tell "off" from
|
||||
// "never set", and an explicit 0 would emit keepAlive on every protocol.
|
||||
func nonZeroKeepAlive(seconds int) *int {
|
||||
if seconds <= 0 {
|
||||
func KeepAlivePtr(v int) *KeepAliveValue {
|
||||
value := KeepAliveValue(fmt.Sprintf("%d", v))
|
||||
return &value
|
||||
}
|
||||
|
||||
func keepAlivePtr(value KeepAliveValue) *KeepAliveValue {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return KeepAlivePtr(seconds)
|
||||
v := value
|
||||
return &v
|
||||
}
|
||||
|
||||
func keepAliveValue(value *KeepAliveValue) KeepAliveValue {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func (c *Client) ToRecord() *ClientRecord {
|
||||
@@ -1221,7 +1281,7 @@ func (c *Client) ToRecord() *ClientRecord {
|
||||
PublicKey: c.PublicKey,
|
||||
AllowedIPs: strings.Join(c.AllowedIPs, ","),
|
||||
PreSharedKey: c.PreSharedKey,
|
||||
KeepAlive: c.KeepAliveSeconds(),
|
||||
KeepAlive: keepAliveValue(c.KeepAlive),
|
||||
ForwardedPorts: c.ForwardedPorts,
|
||||
Secret: c.Secret,
|
||||
AdTag: c.AdTag,
|
||||
@@ -1280,7 +1340,7 @@ func (r *ClientRecord) ToClient() *Client {
|
||||
PublicKey: r.PublicKey,
|
||||
AllowedIPs: splitWireguardAllowedIPs(r.AllowedIPs),
|
||||
PreSharedKey: r.PreSharedKey,
|
||||
KeepAlive: nonZeroKeepAlive(r.KeepAlive),
|
||||
KeepAlive: keepAlivePtr(r.KeepAlive),
|
||||
ForwardedPorts: r.ForwardedPorts,
|
||||
Secret: r.Secret,
|
||||
AdTag: r.AdTag,
|
||||
@@ -1524,8 +1584,8 @@ func MergeClientRecord(existing *ClientRecord, incoming *ClientRecord) []ClientM
|
||||
existing.AllowedIPs = incoming.AllowedIPs
|
||||
}
|
||||
}
|
||||
if existing.KeepAlive != incoming.KeepAlive && incoming.KeepAlive != 0 {
|
||||
if incomingNewer || existing.KeepAlive == 0 {
|
||||
if incoming.KeepAlive != "" && existing.KeepAlive != incoming.KeepAlive {
|
||||
if incomingNewer || existing.KeepAlive == "" {
|
||||
keep("keepAlive", existing.KeepAlive, incoming.KeepAlive, incoming.KeepAlive)
|
||||
existing.KeepAlive = incoming.KeepAlive
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user