mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 15:25: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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -334,6 +334,17 @@ func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model
|
||||
if err := validateClientsRenewal(clients); err != nil {
|
||||
return false, err
|
||||
}
|
||||
inbound, err := inboundSvc.GetInbound(data.Id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
existingClients, err := inboundSvc.GetClients(inbound)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := validateClientsKeepAliveAgainstExisting(inbound.Protocol, clients, existingClients); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var settings map[string]any
|
||||
err = json.Unmarshal([]byte(data.Settings), &settings)
|
||||
@@ -372,11 +383,6 @@ func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model
|
||||
return false, err
|
||||
}
|
||||
|
||||
existingClients, err := inboundSvc.GetClients(oldInbound)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// A client already on this inbound is skipped instead of appended again:
|
||||
// checkEmailsExistForClients exempts a matching subId so one identity can
|
||||
// live on several inbounds, which let retried or raced adds duplicate the
|
||||
@@ -670,7 +676,10 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
||||
if err := validateClientsRenewal(clients); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
inbound, err := inboundSvc.GetInbound(data.Id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var settings map[string]any
|
||||
err = json.Unmarshal([]byte(data.Settings), &settings)
|
||||
if err != nil {
|
||||
@@ -691,6 +700,15 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for i, client := range clients {
|
||||
identityEmail := client.Email
|
||||
if i == 0 {
|
||||
identityEmail = oldEmail
|
||||
}
|
||||
if err := validateClientKeepAliveAgainstExisting(inbound.Protocol, client, oldClients, identityEmail); err != nil {
|
||||
return false, fmt.Errorf("client %q: %w", client.Email, err)
|
||||
}
|
||||
}
|
||||
|
||||
newClientId := ""
|
||||
switch oldInbound.Protocol {
|
||||
@@ -844,8 +862,8 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
||||
if clients[0].PreSharedKey != "" {
|
||||
newMap["preSharedKey"] = clients[0].PreSharedKey
|
||||
}
|
||||
if ka := clients[0].KeepAliveSeconds(); ka > 0 {
|
||||
newMap["keepAlive"] = ka
|
||||
if clients[0].KeepAlive != nil {
|
||||
newMap["keepAlive"] = clients[0].KeepAlive
|
||||
}
|
||||
if oldInbound.Protocol == model.AmneziaWG && clients[0].ForwardedPorts != "" {
|
||||
newMap["forwardedPorts"] = clients[0].ForwardedPorts
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
func validateClientKeepAlive(protocol model.Protocol, client model.Client) error {
|
||||
if client.KeepAlive == nil {
|
||||
return nil
|
||||
}
|
||||
switch protocol {
|
||||
case model.AmneziaWG:
|
||||
return client.KeepAlive.Validate(true)
|
||||
case model.WireGuard:
|
||||
return client.KeepAlive.Validate(false)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateClientsKeepAlive(protocol model.Protocol, clients []model.Client) error {
|
||||
for _, client := range clients {
|
||||
if err := validateClientKeepAlive(protocol, client); err != nil {
|
||||
return fmt.Errorf("client %q: %w", client.Email, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateClientKeepAliveAgainstExisting(protocol model.Protocol, client model.Client, existing []model.Client, identityEmail ...string) error {
|
||||
if err := validateClientKeepAlive(protocol, client); err == nil || client.KeepAlive == nil {
|
||||
return err
|
||||
}
|
||||
if protocol != model.AmneziaWG && protocol != model.WireGuard {
|
||||
return nil
|
||||
}
|
||||
value := client.KeepAlive.String()
|
||||
identity := client.Email
|
||||
if len(identityEmail) > 0 && identityEmail[0] != "" {
|
||||
identity = identityEmail[0]
|
||||
}
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || strconv.FormatInt(parsed, 10) != value {
|
||||
return validateClientKeepAlive(protocol, client)
|
||||
}
|
||||
for _, old := range existing {
|
||||
if strings.EqualFold(old.Email, identity) && old.KeepAlive != nil && old.KeepAlive.String() == value {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return validateClientKeepAlive(protocol, client)
|
||||
}
|
||||
|
||||
func validateClientsKeepAliveAgainstExisting(protocol model.Protocol, clients, existing []model.Client) error {
|
||||
for _, client := range clients {
|
||||
if err := validateClientKeepAliveAgainstExisting(protocol, client, existing); err != nil {
|
||||
return fmt.Errorf("client %q: %w", client.Email, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
||||
)
|
||||
|
||||
func newKeepAliveRangeClient(t *testing.T, email, value string) model.Client {
|
||||
t.Helper()
|
||||
privateKey, publicKey, err := wgutil.GenerateWireguardKeypair()
|
||||
if err != nil {
|
||||
t.Fatalf("generate peer keypair: %v", err)
|
||||
}
|
||||
keepAlive := model.KeepAliveValue(value)
|
||||
return model.Client{
|
||||
Email: email, Enable: true, PrivateKey: privateKey, PublicKey: publicKey,
|
||||
KeepAlive: &keepAlive,
|
||||
}
|
||||
}
|
||||
|
||||
func keepAliveTestAWGInbound(t *testing.T, port int) *model.Inbound {
|
||||
t.Helper()
|
||||
return mkInbound(t, port, model.AmneziaWG, `{"server":{"privateKey":"server","publicKey":"server-pub","subnetIp":"10.8.1.0","subnetCidr":24},"clients":[]}`)
|
||||
}
|
||||
|
||||
func TestClientCreateAllowsAmneziaWGKeepAliveRangeAndPersistsNativeValue(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
inboundSvc := &InboundService{}
|
||||
clientSvc := &ClientService{}
|
||||
inbound := keepAliveTestAWGInbound(t, 51871)
|
||||
client := newKeepAliveRangeClient(t, "range@awg", "15-25")
|
||||
|
||||
if _, err := clientSvc.CreateOne(inboundSvc, inbound.Id, client); err != nil {
|
||||
t.Fatalf("create AmneziaWG client with keepAlive range: %v", err)
|
||||
}
|
||||
var record model.ClientRecord
|
||||
if err := database.GetDB().Where("email = ?", client.Email).First(&record).Error; err != nil {
|
||||
t.Fatalf("load saved client: %v", err)
|
||||
}
|
||||
if record.KeepAlive != "15-25" {
|
||||
t.Fatalf("saved keepAlive = %q, want 15-25", record.KeepAlive)
|
||||
}
|
||||
savedInbound, err := inboundSvc.GetInbound(inbound.Id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clients, err := inboundSvc.GetClients(savedInbound)
|
||||
if err != nil {
|
||||
t.Fatalf("load client settings: %v", err)
|
||||
}
|
||||
if len(clients) != 1 || clients[0].KeepAliveText() != "15-25" {
|
||||
t.Fatalf("inbound keepAlive = %#v, want 15-25", clients)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCreateRejectsKeepAliveRangeForWireGuardBeforeAnyWrite(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
inboundSvc := &InboundService{}
|
||||
clientSvc := &ClientService{}
|
||||
wireguard := mkInbound(t, 51872, model.WireGuard, `{"secretKey":"server","clients":[]}`)
|
||||
awg := keepAliveTestAWGInbound(t, 51873)
|
||||
client := newKeepAliveRangeClient(t, "range@mixed", "15-25")
|
||||
|
||||
_, err := clientSvc.Create(inboundSvc, &ClientCreatePayload{
|
||||
Client: client, InboundIds: []int{awg.Id, wireguard.Id},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "keepAlive ranges are supported only for AmneziaWG clients") {
|
||||
t.Fatalf("mixed-protocol create error = %v, want WireGuard range rejection", err)
|
||||
}
|
||||
var records int64
|
||||
if err := database.GetDB().Model(&model.ClientRecord{}).Where("email = ?", client.Email).Count(&records).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if records != 0 {
|
||||
t.Fatalf("client records written before protocol validation: %d", records)
|
||||
}
|
||||
for _, inbound := range []*model.Inbound{awg, wireguard} {
|
||||
saved, err := inboundSvc.GetInbound(inbound.Id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var settings struct {
|
||||
Clients []model.Client `json:"clients"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(saved.Settings), &settings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(settings.Clients) != 0 {
|
||||
t.Fatalf("client partially written to %s inbound: %#v", inbound.Protocol, settings.Clients)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientUpdateRejectsRangeWhenSharedWithWireGuard(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
inboundSvc := &InboundService{}
|
||||
clientSvc := &ClientService{}
|
||||
awg := keepAliveTestAWGInbound(t, 51874)
|
||||
wireguard := mkInbound(t, 51875, model.WireGuard, `{"secretKey":"server","clients":[]}`)
|
||||
seed := newKeepAliveRangeClient(t, "shared@tunnels", "25")
|
||||
if _, err := clientSvc.Create(inboundSvc, &ClientCreatePayload{
|
||||
Client: seed, InboundIds: []int{awg.Id, wireguard.Id},
|
||||
}); err != nil {
|
||||
t.Fatalf("create scalar client on both protocols: %v", err)
|
||||
}
|
||||
record, err := clientSvc.GetRecordByEmail(nil, seed.Email)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
update := model.Client{Email: seed.Email, Enable: true, KeepAlive: seed.KeepAlive}
|
||||
*update.KeepAlive = "15-25"
|
||||
if _, err := clientSvc.Update(inboundSvc, record.Id, update, 0); err == nil || !strings.Contains(err.Error(), "keepAlive ranges are supported only for AmneziaWG clients") {
|
||||
t.Fatalf("shared client update error = %v, want WireGuard range rejection", err)
|
||||
}
|
||||
var unchanged model.ClientRecord
|
||||
if err := database.GetDB().Where("id = ?", record.Id).First(&unchanged).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if unchanged.KeepAlive != "25" {
|
||||
t.Fatalf("shared record keepAlive after rejected update = %q, want 25", unchanged.KeepAlive)
|
||||
}
|
||||
for _, inbound := range []*model.Inbound{awg, wireguard} {
|
||||
saved, err := inboundSvc.GetInbound(inbound.Id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clients, err := inboundSvc.GetClients(saved)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(clients) != 1 || clients[0].KeepAliveText() != "25" {
|
||||
t.Fatalf("%s inbound keepAlive after rejected update = %#v, want 25", inbound.Protocol, clients)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCreateRejectsKeepAliveOutsideNativeRangeBeforeWrite(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
inboundSvc := &InboundService{}
|
||||
clientSvc := &ClientService{}
|
||||
inbound := keepAliveTestAWGInbound(t, 51876)
|
||||
client := newKeepAliveRangeClient(t, "too-large@awg", "65536")
|
||||
if _, err := clientSvc.CreateOne(inboundSvc, inbound.Id, client); err == nil || !strings.Contains(err.Error(), "keepAlive must be an integer from 0 to 65535") {
|
||||
t.Fatalf("out-of-range create error = %v, want keepAlive bound error", err)
|
||||
}
|
||||
var records int64
|
||||
if err := database.GetDB().Model(&model.ClientRecord{}).Where("email = ?", client.Email).Count(&records).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if records != 0 {
|
||||
t.Fatalf("out-of-range client was written: %d records", records)
|
||||
}
|
||||
saved, err := inboundSvc.GetInbound(inbound.Id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clients, err := inboundSvc.GetClients(saved)
|
||||
if err != nil || len(clients) != 0 {
|
||||
t.Fatalf("out-of-range client settings = %#v, %v; want no clients", clients, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCreateAllowsRangeForSharedAmneziaWGAndVLESSClient(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
inboundSvc := &InboundService{}
|
||||
clientSvc := &ClientService{}
|
||||
awg := keepAliveTestAWGInbound(t, 51877)
|
||||
vless := mkInbound(t, 51878, model.VLESS, `{"clients":[]}`)
|
||||
client := newKeepAliveRangeClient(t, "range@awg-vless", "15-25")
|
||||
if _, err := clientSvc.Create(inboundSvc, &ClientCreatePayload{Client: client, InboundIds: []int{awg.Id, vless.Id}}); err != nil {
|
||||
t.Fatalf("create client shared by AmneziaWG and VLESS: %v", err)
|
||||
}
|
||||
var record model.ClientRecord
|
||||
if err := database.GetDB().Where("email = ?", client.Email).First(&record).Error; err != nil {
|
||||
t.Fatalf("read shared client: %v", err)
|
||||
}
|
||||
if record.KeepAlive != "15-25" {
|
||||
t.Fatalf("shared client keepAlive = %q, want 15-25", record.KeepAlive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientUpdateAllowsRangeForSharedAmneziaWGAndVLESSClient(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
inboundSvc := &InboundService{}
|
||||
clientSvc := &ClientService{}
|
||||
awg := keepAliveTestAWGInbound(t, 51879)
|
||||
vless := mkInbound(t, 51880, model.VLESS, `{"clients":[]}`)
|
||||
client := newKeepAliveRangeClient(t, "update@awg-vless", "25")
|
||||
if _, err := clientSvc.Create(inboundSvc, &ClientCreatePayload{Client: client, InboundIds: []int{awg.Id, vless.Id}}); err != nil {
|
||||
t.Fatalf("create scalar client shared by AmneziaWG and VLESS: %v", err)
|
||||
}
|
||||
record, err := clientSvc.GetRecordByEmail(nil, client.Email)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
updated := model.KeepAliveValue("15-25")
|
||||
client.KeepAlive = &updated
|
||||
if _, err := clientSvc.Update(inboundSvc, record.Id, client, 0); err != nil {
|
||||
t.Fatalf("update shared client to an AmneziaWG range: %v", err)
|
||||
}
|
||||
var saved model.ClientRecord
|
||||
if err := database.GetDB().Where("id = ?", record.Id).First(&saved).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.KeepAlive != "15-25" {
|
||||
t.Fatalf("updated shared client keepAlive = %q, want 15-25", saved.KeepAlive)
|
||||
}
|
||||
client.KeepAlive = model.KeepAlivePtr(25)
|
||||
if _, err := clientSvc.Update(inboundSvc, record.Id, client, 0); err != nil {
|
||||
t.Fatalf("change shared client range to scalar: %v", err)
|
||||
}
|
||||
client.KeepAlive = model.KeepAlivePtr(0)
|
||||
if _, err := clientSvc.Update(inboundSvc, record.Id, client, 0); err != nil {
|
||||
t.Fatalf("set explicit zero after range: %v", err)
|
||||
}
|
||||
client.KeepAlive = nil
|
||||
if _, err := clientSvc.Update(inboundSvc, record.Id, client, 0); err != nil {
|
||||
t.Fatalf("update with omitted keepAlive after explicit zero: %v", err)
|
||||
}
|
||||
if err := database.GetDB().Where("id = ?", record.Id).First(&saved).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.KeepAlive != "0" {
|
||||
t.Fatalf("omitted update changed explicit zero to %q", saved.KeepAlive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetClientEnableByEmailPreservesLegacyAmneziaWGKeepAlive(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
inboundSvc := &InboundService{}
|
||||
clientSvc := &ClientService{}
|
||||
legacy := newKeepAliveRangeClient(t, "legacy-enable@awg", "25")
|
||||
inbound := keepAliveTestAWGInbound(t, 51881)
|
||||
if _, err := clientSvc.CreateOne(inboundSvc, inbound.Id, legacy); err != nil {
|
||||
t.Fatalf("create client: %v", err)
|
||||
}
|
||||
legacy.KeepAlive = model.KeepAlivePtr(70000)
|
||||
if err := saveKeepAliveTestClientSettings(t, inboundSvc, inbound.Id, legacy); err != nil {
|
||||
t.Fatalf("seed legacy client settings: %v", err)
|
||||
}
|
||||
|
||||
changed, _, err := clientSvc.SetClientEnableByEmail(inboundSvc, legacy.Email, false)
|
||||
if err != nil {
|
||||
t.Fatalf("disable existing client with legacy keepAlive: %v", err)
|
||||
}
|
||||
if !changed {
|
||||
t.Fatal("SetClientEnableByEmail reported no change")
|
||||
}
|
||||
saved, err := inboundSvc.GetInbound(inbound.Id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clients, err := inboundSvc.GetClients(saved)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(clients) != 1 || clients[0].Enable || clients[0].KeepAliveText() != "70000" {
|
||||
t.Fatalf("saved legacy client = %#v, want disabled with keepAlive 70000", clients)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateInboundPreservesUnchangedLegacyAmneziaWGKeepAlive(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
inboundSvc := &InboundService{}
|
||||
legacy := newKeepAliveRangeClient(t, "legacy-remark@awg", "-5")
|
||||
legacy.AllowedIPs = []string{"10.8.1.3/32"}
|
||||
inbound := mkInbound(t, 51882, model.AmneziaWG, clientsSettings(t, []model.Client{legacy}))
|
||||
loaded, err := inboundSvc.GetInbound(inbound.Id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded.Remark = "updated remark"
|
||||
if _, _, err := inboundSvc.UpdateInbound(loaded); err != nil {
|
||||
t.Fatalf("save unrelated inbound remark with legacy keepAlive: %v", err)
|
||||
}
|
||||
saved, err := inboundSvc.GetInbound(inbound.Id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clients, err := inboundSvc.GetClients(saved)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.Remark != "updated remark" {
|
||||
t.Fatalf("saved inbound remark = %q, want updated remark", saved.Remark)
|
||||
}
|
||||
if len(clients) != 1 || clients[0].KeepAliveText() != "-5" {
|
||||
t.Fatalf("saved client keepAlive = %#v, want -5", clients)
|
||||
}
|
||||
}
|
||||
|
||||
func saveKeepAliveTestClientSettings(t *testing.T, inboundSvc *InboundService, inboundID int, client model.Client) error {
|
||||
t.Helper()
|
||||
inbound, err := inboundSvc.GetInbound(inboundID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clients, err := inboundSvc.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range clients {
|
||||
if clients[i].Email == client.Email {
|
||||
clients[i].KeepAlive = client.KeepAlive
|
||||
clients[i].AllowedIPs = client.AllowedIPs
|
||||
clients[i].PrivateKey = client.PrivateKey
|
||||
clients[i].PublicKey = client.PublicKey
|
||||
}
|
||||
}
|
||||
return database.GetDB().Model(&model.Inbound{}).Where("id = ?", inboundID).Update("settings", clientsSettings(t, clients)).Error
|
||||
}
|
||||
|
||||
func TestClientUpdateGrandfathersOnlyUnchangedLegacyKeepAlive(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
inboundSvc := &InboundService{}
|
||||
clientSvc := &ClientService{}
|
||||
client := newKeepAliveRangeClient(t, "legacy-update@awg", "25")
|
||||
inbound := keepAliveTestAWGInbound(t, 51883)
|
||||
if _, err := clientSvc.CreateOne(inboundSvc, inbound.Id, client); err != nil {
|
||||
t.Fatalf("create client: %v", err)
|
||||
}
|
||||
client.KeepAlive = model.KeepAlivePtr(70000)
|
||||
if err := saveKeepAliveTestClientSettings(t, inboundSvc, inbound.Id, client); err != nil {
|
||||
t.Fatalf("seed legacy client settings: %v", err)
|
||||
}
|
||||
fresh, err := inboundSvc.GetInbound(inbound.Id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clients, err := inboundSvc.GetClients(fresh)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := clientSvc.SyncInbound(nil, inbound.Id, clients); err != nil {
|
||||
t.Fatalf("sync legacy record: %v", err)
|
||||
}
|
||||
record, err := clientSvc.GetRecordByEmail(nil, client.Email)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unchanged := model.Client{Email: client.Email, Enable: true, KeepAlive: model.KeepAlivePtr(70000), Comment: "updated comment"}
|
||||
if _, err := clientSvc.Update(inboundSvc, record.Id, unchanged, 0); err != nil {
|
||||
t.Fatalf("update unrelated field with unchanged legacy keepAlive: %v", err)
|
||||
}
|
||||
renamed := unchanged
|
||||
renamed.Email = "legacy-renamed@awg"
|
||||
if _, err := clientSvc.Update(inboundSvc, record.Id, renamed, 0); err != nil {
|
||||
t.Fatalf("rename client with unchanged legacy keepAlive: %v", err)
|
||||
}
|
||||
record, err = clientSvc.GetByID(record.Id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if record.Email != "legacy-renamed@awg" || record.Comment != "updated comment" || record.KeepAlive != "70000" {
|
||||
t.Fatalf("saved renamed record = %#v, want renamed/comment updated with keepAlive 70000", record)
|
||||
}
|
||||
changed := renamed
|
||||
changed.KeepAlive = model.KeepAlivePtr(70001)
|
||||
if _, err := clientSvc.Update(inboundSvc, record.Id, changed, 0); err == nil || !strings.Contains(err.Error(), "keepAlive must be an integer from 0 to 65535") {
|
||||
t.Fatalf("changed invalid legacy value error = %v, want keepAlive bound error", err)
|
||||
}
|
||||
stored, err := clientSvc.GetByID(record.Id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.KeepAlive != "70000" {
|
||||
t.Fatalf("record after rejected changed legacy value = %q, want 70000", stored.KeepAlive)
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,9 @@ func applyClientRecordMerge(row *model.ClientRecord, incoming *model.ClientRecor
|
||||
row.AllowedIPs = incoming.AllowedIPs
|
||||
}
|
||||
row.PreSharedKey = incoming.PreSharedKey
|
||||
row.KeepAlive = incoming.KeepAlive
|
||||
if incoming.KeepAlive != "" {
|
||||
row.KeepAlive = incoming.KeepAlive
|
||||
}
|
||||
row.SubID = incoming.SubID
|
||||
row.LimitIP = incoming.LimitIP
|
||||
row.TotalGB = incoming.TotalGB
|
||||
|
||||
@@ -70,8 +70,8 @@ func TestUpdateCanClearKeepAliveOnAnExistingClient(t *testing.T) {
|
||||
if got := inboundKeepAlive(t, inboundSvc, ib.Id, "ka@x"); got != 0 {
|
||||
t.Fatalf("inbound keepAlive after an explicit 0 = %d, want 0", got)
|
||||
}
|
||||
if got := lookupClientRecord(t, "ka@x").KeepAlive; got != 0 {
|
||||
t.Fatalf("stored wg_keep_alive after an explicit 0 = %d, want 0", got)
|
||||
if got := lookupClientRecord(t, "ka@x").KeepAlive.String(); got != "0" {
|
||||
t.Fatalf("stored wg_keep_alive after an explicit 0 = %s, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestUpdateWithoutKeepAlivePreservesTheStoredValue(t *testing.T) {
|
||||
if got := inboundKeepAlive(t, inboundSvc, ib.Id, "ka@x"); got != 25 {
|
||||
t.Fatalf("inbound keepAlive after an edit that omitted it = %d, want 25", got)
|
||||
}
|
||||
if got := lookupClientRecord(t, "ka@x").KeepAlive; got != 25 {
|
||||
t.Fatalf("stored wg_keep_alive after an edit that omitted it = %d, want 25", got)
|
||||
if got := lookupClientRecord(t, "ka@x").KeepAlive.String(); got != "25" {
|
||||
t.Fatalf("stored wg_keep_alive after an edit that omitted it = %s, want 25", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1338,6 +1338,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
if err := validateClientsRenewal(clients); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
if err := validateClientsKeepAlive(inbound.Protocol, clients); err != nil {
|
||||
return inbound, false, err
|
||||
}
|
||||
for _, traffic := range inbound.ClientStats {
|
||||
if err := validateClientRenewal(model.Client{Reset: traffic.Reset, ResetDay: traffic.ResetDay, ResetWeekday: traffic.ResetWeekday}); err != nil {
|
||||
return inbound, false, err
|
||||
@@ -1912,7 +1915,7 @@ func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) {
|
||||
return needRestart, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) validateUpdatedInboundClients(inbound *model.Inbound) error {
|
||||
func (s *InboundService) validateUpdatedInboundClients(inbound, existing *model.Inbound) error {
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1920,6 +1923,16 @@ func (s *InboundService) validateUpdatedInboundClients(inbound *model.Inbound) e
|
||||
if err := validateClientsRenewal(clients); err != nil {
|
||||
return err
|
||||
}
|
||||
var existingClients []model.Client
|
||||
if inbound.Protocol == existing.Protocol {
|
||||
existingClients, err = s.GetClients(existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := validateClientsKeepAliveAgainstExisting(inbound.Protocol, clients, existingClients); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, client := range clients {
|
||||
switch inbound.Protocol {
|
||||
case model.Hysteria:
|
||||
@@ -2038,7 +2051,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
inbound.Enable = stored.Enable
|
||||
}
|
||||
// On the clients actually saved: a protocol switch keeps the stored ones.
|
||||
if err := s.validateUpdatedInboundClients(inbound); err != nil {
|
||||
if err := s.validateUpdatedInboundClients(inbound, stored); err != nil {
|
||||
return err
|
||||
}
|
||||
conflict, cErr := checkPortConflictTx(tx, inbound, inbound.Id)
|
||||
|
||||
@@ -56,7 +56,7 @@ func seedWGInbound(t *testing.T, tag string, port int, clients []model.Client) {
|
||||
}
|
||||
}
|
||||
|
||||
func seedDualTunnelClient(t *testing.T, enabled bool, wgKeepAlive *int) string {
|
||||
func seedDualTunnelClient(t *testing.T, enabled bool, wgKeepAlive *model.KeepAliveValue) string {
|
||||
t.Helper()
|
||||
setupSettingTestDB(t)
|
||||
db := database.GetDB()
|
||||
@@ -202,8 +202,8 @@ func TestGetXrayConfigWireGuardUsesInboundLocalTunnelFields(t *testing.T) {
|
||||
if err := database.GetDB().Where("email = ?", email).First(&shared).Error; err != nil {
|
||||
t.Fatalf("read shared client: %v", err)
|
||||
}
|
||||
if shared.AllowedIPs != "10.8.1.5/32" || shared.PreSharedKey != "awg-psk" || shared.PublicKey != "pub-awg" || shared.KeepAlive != 25 {
|
||||
t.Fatalf("test setup did not persist AmneziaWG identity: allowedIPs=%q preSharedKey=%q publicKey=%q keepAlive=%d", shared.AllowedIPs, shared.PreSharedKey, shared.PublicKey, shared.KeepAlive)
|
||||
if shared.AllowedIPs != "10.8.1.5/32" || shared.PreSharedKey != "awg-psk" || shared.PublicKey != "pub-awg" || shared.KeepAlive.Seconds() != 25 {
|
||||
t.Fatalf("test setup did not persist AmneziaWG identity: allowedIPs=%q preSharedKey=%q publicKey=%q keepAlive=%d", shared.AllowedIPs, shared.PreSharedKey, shared.PublicKey, shared.KeepAlive.Seconds())
|
||||
}
|
||||
|
||||
peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-dual"))
|
||||
@@ -232,8 +232,8 @@ func TestGetXrayConfigWireGuardOmitsKeepAliveAbsentFromSettings(t *testing.T) {
|
||||
if err := database.GetDB().Where("email = ?", email).First(&shared).Error; err != nil {
|
||||
t.Fatalf("read shared client: %v", err)
|
||||
}
|
||||
if shared.PublicKey != "pub-awg" || shared.KeepAlive != 25 {
|
||||
t.Fatalf("test setup did not persist AmneziaWG identity: publicKey=%q keepAlive=%d", shared.PublicKey, shared.KeepAlive)
|
||||
if shared.PublicKey != "pub-awg" || shared.KeepAlive.Seconds() != 25 {
|
||||
t.Fatalf("test setup did not persist AmneziaWG identity: publicKey=%q keepAlive=%d", shared.PublicKey, shared.KeepAlive.Seconds())
|
||||
}
|
||||
|
||||
peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-dual"))
|
||||
|
||||
Reference in New Issue
Block a user