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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user