diff --git a/docs/public/openapi.json b/docs/public/openapi.json index 2b3179a19..314a06c21 100644 --- a/docs/public/openapi.json +++ b/docs/public/openapi.json @@ -1365,6 +1365,8 @@ "type": "string" }, "keepAlive": { + "description": "Seconds between PersistentKeepalive packets; 0 sends none, omit to keep the stored value", + "nullable": true, "type": "integer" }, "limitIp": { diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 2b3179a19..314a06c21 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -1365,6 +1365,8 @@ "type": "string" }, "keepAlive": { + "description": "Seconds between PersistentKeepalive packets; 0 sends none, omit to keep the stored value", + "nullable": true, "type": "integer" }, "limitIp": { diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index 4e197e143..fcd88ee00 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -329,7 +329,7 @@ export const EXAMPLES: Record = { "forwardedPorts": "", "group": "", "id": "", - "keepAlive": 0, + "keepAlive": null, "limitIp": 0, "password": "", "preSharedKey": "", diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index dd2314aa7..cb524291b 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -1339,6 +1339,8 @@ export const SCHEMAS: Record = { "type": "string" }, "keepAlive": { + "description": "Seconds between PersistentKeepalive packets; 0 sends none, omit to keep the stored value", + "nullable": true, "type": "integer" }, "limitIp": { diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index 8cbb92f81..26bb6743b 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -325,7 +325,7 @@ export interface Client { forwardedPorts?: string; group?: string; id?: string; - keepAlive?: number; + keepAlive?: number | null; limitIp: number; password?: string; preSharedKey?: string; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index 90c32b2ab..a6039170e 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -346,7 +346,7 @@ export const ClientSchema = z.object({ forwardedPorts: z.string().optional(), group: z.string().optional(), id: z.string().optional(), - keepAlive: z.number().int().optional(), + keepAlive: z.number().int().nullable().optional(), limitIp: z.number().int(), password: z.string().optional(), preSharedKey: z.string().optional(), diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx index 2767b91b9..991127a0d 100644 --- a/frontend/src/pages/clients/ClientFormModal.tsx +++ b/frontend/src/pages/clients/ClientFormModal.tsx @@ -133,6 +133,7 @@ type Values = ClientFormValues & { wgAllowedIPs: string; awgAllowedIPs: string; awgForwardedPorts: string; + wgKeepAlive: number; secret: string; adTag: string; }; @@ -169,6 +170,7 @@ const EMPTY: Values = { wgAllowedIPs: '', awgAllowedIPs: '', awgForwardedPorts: '', + wgKeepAlive: 25, secret: '', adTag: '', }; @@ -379,6 +381,7 @@ export default function ClientFormModal({ wgAllowedIPs: wgTunnelIPs ?? client.allowedIPs ?? '', awgAllowedIPs: awgTunnelIPs ?? client.allowedIPs ?? '', awgForwardedPorts: client.forwardedPorts || '', + wgKeepAlive: client.keepAlive ?? 0, secret: client.secret || '', adTag: client.adTag || '', }; @@ -694,6 +697,7 @@ export default function ClientFormModal({ // so both protocols share this one field set — see wgPrivateKey etc. // below and the AmneziaWG-labeled variants of the same inputs. clientPayload.privateKey = values.wgPrivateKey; + clientPayload.keepAlive = values.wgKeepAlive; clientPayload.publicKey = values.wgPublicKey; if (values.wgPreSharedKey) { clientPayload.preSharedKey = values.wgPreSharedKey; @@ -1272,6 +1276,14 @@ export default function ClientFormModal({ )} + Number(v) || 0 }} + > + + {showAmneziawg && ( 0 { - peer["keepAlive"] = c.KeepAlive + if ka := c.KeepAliveSeconds(); ka > 0 { + peer["keepAlive"] = ka } return peer } @@ -890,7 +890,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"` + KeepAlive *int `json:"keepAlive,omitempty"` // Seconds between PersistentKeepalive packets; 0 sends none, omit to keep the stored value 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"` @@ -1113,6 +1113,27 @@ type Host struct { func (Host) TableName() string { return "hosts" } +// KeepAliveSeconds is the client's PersistentKeepalive, 0 when unset. +func (c Client) KeepAliveSeconds() int { + if c.KeepAlive == nil { + return 0 + } + return *c.KeepAlive +} + +// 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 } + +// 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 { + return nil + } + return KeepAlivePtr(seconds) +} + func (c *Client) ToRecord() *ClientRecord { rec := &ClientRecord{ Email: c.Email, @@ -1141,7 +1162,7 @@ func (c *Client) ToRecord() *ClientRecord { PublicKey: c.PublicKey, AllowedIPs: strings.Join(c.AllowedIPs, ","), PreSharedKey: c.PreSharedKey, - KeepAlive: c.KeepAlive, + KeepAlive: c.KeepAliveSeconds(), ForwardedPorts: c.ForwardedPorts, Secret: c.Secret, AdTag: c.AdTag, @@ -1199,7 +1220,7 @@ func (r *ClientRecord) ToClient() *Client { PublicKey: r.PublicKey, AllowedIPs: splitWireguardAllowedIPs(r.AllowedIPs), PreSharedKey: r.PreSharedKey, - KeepAlive: r.KeepAlive, + KeepAlive: nonZeroKeepAlive(r.KeepAlive), ForwardedPorts: r.ForwardedPorts, Secret: r.Secret, AdTag: r.AdTag, diff --git a/internal/database/model/model_wireguard_test.go b/internal/database/model/model_wireguard_test.go index 6c5a38489..fb7e4d1e3 100644 --- a/internal/database/model/model_wireguard_test.go +++ b/internal/database/model/model_wireguard_test.go @@ -1,7 +1,9 @@ package model import ( + "encoding/json" "reflect" + "strings" "testing" ) @@ -13,7 +15,7 @@ func TestClientToRecordRoundTripWireGuard(t *testing.T) { PublicKey: "cGVlci1wdWJsaWMta2V5LWJhc2U2NC0zMmJ5dGVzISE=", AllowedIPs: []string{"10.0.0.2/32", "fd00::2/128"}, PreSharedKey: "cHNrLWJhc2U2NC0zMmJ5dGVzLXBsYWNlaG9sZGVyISE=", - KeepAlive: 25, + KeepAlive: KeepAlivePtr(25), } rec := c.ToRecord() @@ -29,7 +31,7 @@ func TestClientToRecordRoundTripWireGuard(t *testing.T) { {"PrivateKey", c.PrivateKey, got.PrivateKey}, {"PublicKey", c.PublicKey, got.PublicKey}, {"PreSharedKey", c.PreSharedKey, got.PreSharedKey}, - {"KeepAlive", c.KeepAlive, got.KeepAlive}, + {"KeepAlive", c.KeepAliveSeconds(), got.KeepAliveSeconds()}, } { if f.a != f.b { t.Errorf("%s round-trip = %v, want %v", f.name, f.b, f.a) @@ -40,6 +42,23 @@ func TestClientToRecordRoundTripWireGuard(t *testing.T) { } } +// ToClient feeds the settings JSON of every protocol, not just the tunnels, and +// that JSON reaches xray-core verbatim through GenXrayInboundConfig. +func TestClientToClientOmitsUnsetKeepAlive(t *testing.T) { + rec := &ClientRecord{Email: "vless@example.test", UUID: "11111111-2222-3333-4444-555555555555", Enable: true} + + if got := rec.ToClient().KeepAlive; got != nil { + t.Fatalf("KeepAlive for a record that never set one = %d, want nil", *got) + } + blob, err := json.Marshal(map[string][]Client{"clients": {*rec.ToClient()}}) + if err != nil { + t.Fatalf("marshal settings payload: %v", err) + } + if strings.Contains(string(blob), "keepAlive") { + t.Fatalf("settings payload carries keepAlive for a non-tunnel client: %s", blob) + } +} + func TestClientRecordEmptyAllowedIPs(t *testing.T) { rec := &ClientRecord{Email: "bob@example.test", AllowedIPs: ""} if got := rec.ToClient().AllowedIPs; got != nil { diff --git a/internal/sub/clash_service.go b/internal/sub/clash_service.go index 7bcd0d7ca..b4da51247 100644 --- a/internal/sub/clash_service.go +++ b/internal/sub/clash_service.go @@ -579,8 +579,8 @@ func (s *SubClashService) buildWireguardProxy(subReq *SubService, inbound *model if client.PreSharedKey != "" { proxy["pre-shared-key"] = client.PreSharedKey } - if client.KeepAlive > 0 { - proxy["persistent-keepalive"] = client.KeepAlive + if ka := client.KeepAliveSeconds(); ka > 0 { + proxy["persistent-keepalive"] = ka } for _, addr := range client.AllowedIPs { ip := stripCIDR(addr) @@ -667,8 +667,8 @@ func (s *SubClashService) buildAmneziaWGProxy(subReq *SubService, inbound *model if client.PreSharedKey != "" { proxy["pre-shared-key"] = client.PreSharedKey } - if client.KeepAlive > 0 { - proxy["persistent-keepalive"] = client.KeepAlive + if ka := client.KeepAliveSeconds(); ka > 0 { + proxy["persistent-keepalive"] = ka } for _, addr := range amneziaWGClientAddresses(parsed.Clients, client) { diff --git a/internal/sub/clash_service_test.go b/internal/sub/clash_service_test.go index f87851cf5..ef3b66f88 100644 --- a/internal/sub/clash_service_test.go +++ b/internal/sub/clash_service_test.go @@ -909,7 +909,7 @@ func TestBuildWireguardProxyForClash(t *testing.T) { Email: "user", PrivateKey: clientPriv, PreSharedKey: "psk-value", - KeepAlive: 25, + KeepAlive: model.KeepAlivePtr(25), AllowedIPs: []string{"10.0.0.2/32", "fd00::2/128"}, } @@ -989,7 +989,7 @@ func TestBuildAmneziaWGProxyForClash(t *testing.T) { Email: "user", PrivateKey: clientPriv, PreSharedKey: "psk-value", - KeepAlive: 25, + KeepAlive: model.KeepAlivePtr(25), AllowedIPs: []string{"10.8.1.2/32", "fd00::2/128"}, } diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index 6e009972f..7f7b0a154 100644 --- a/internal/sub/json_service.go +++ b/internal/sub/json_service.go @@ -998,8 +998,8 @@ func (s *SubJsonService) genWireguard(inbound *model.Inbound, client model.Clien if client.PreSharedKey != "" { peer["preSharedKey"] = client.PreSharedKey } - if client.KeepAlive > 0 { - peer["keepAlive"] = client.KeepAlive + if ka := client.KeepAliveSeconds(); ka > 0 { + peer["keepAlive"] = ka } settings := map[string]any{ diff --git a/internal/sub/json_service_test.go b/internal/sub/json_service_test.go index 6492395a0..e0f0d5a14 100644 --- a/internal/sub/json_service_test.go +++ b/internal/sub/json_service_test.go @@ -462,7 +462,7 @@ func TestSubJsonServiceWireguard(t *testing.T) { Email: "user", PrivateKey: clientPriv, PreSharedKey: "psk-value", - KeepAlive: 25, + KeepAlive: model.KeepAlivePtr(25), AllowedIPs: []string{"10.0.0.2/32", "fd00::2/128"}, } diff --git a/internal/sub/service.go b/internal/sub/service.go index afb2b4bec..1d3fe6455 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -776,8 +776,8 @@ func (s *SubService) genWireguardLink(inbound *model.Inbound, email string) stri if client.PreSharedKey != "" { params["presharedkey"] = client.PreSharedKey } - if client.KeepAlive > 0 { - params["keepalive"] = strconv.Itoa(client.KeepAlive) + if ka := client.KeepAliveSeconds(); ka > 0 { + params["keepalive"] = strconv.Itoa(ka) } return buildLinkWithParams(link, params, s.genRemark(inbound, email, "", "")) } @@ -878,8 +878,8 @@ func amneziaWGConfigText(server *amneziawg.ServerSettings, client *model.Client, } b.WriteString("AllowedIPs = 0.0.0.0/0, ::/0\n") fmt.Fprintf(&b, "Endpoint = %s:%d", host, port) - if client.KeepAlive > 0 { - fmt.Fprintf(&b, "\nPersistentKeepalive = %d", client.KeepAlive) + if ka := client.KeepAliveSeconds(); ka > 0 { + fmt.Fprintf(&b, "\nPersistentKeepalive = %d", ka) } return b.String() diff --git a/internal/sub/service_amneziawg_test.go b/internal/sub/service_amneziawg_test.go index cfa1f66da..39ea977ac 100644 --- a/internal/sub/service_amneziawg_test.go +++ b/internal/sub/service_amneziawg_test.go @@ -215,7 +215,7 @@ func TestAmneziaWGConfigTextPeerFieldOrder(t *testing.T) { server := &amneziawg.ServerSettings{PublicKey: "serverPub", PrimaryDNS: "8.8.8.8", MTU: 1420} t.Run("every optional field set", func(t *testing.T) { - client := &model.Client{PrivateKey: "clientPriv", AllowedIPs: []string{"10.8.1.2/32"}, PreSharedKey: "psk", KeepAlive: 25} + client := &model.Client{PrivateKey: "clientPriv", AllowedIPs: []string{"10.8.1.2/32"}, PreSharedKey: "psk", KeepAlive: model.KeepAlivePtr(25)} conf := amneziaWGConfigText(server, client, "203.0.113.7", 51820, "remark") if got := peerFields(t, conf); !slices.Equal(got, peerFieldOrder) { t.Fatalf("peer fields = %v, want %v\n%s", got, peerFieldOrder, conf) diff --git a/internal/web/runtime/local.go b/internal/web/runtime/local.go index 8298eaada..41e7e3ef4 100644 --- a/internal/web/runtime/local.go +++ b/internal/web/runtime/local.go @@ -242,7 +242,7 @@ func (l *Local) AddClient(ctx context.Context, ib *model.Inbound, client model.C "publicKey": client.PublicKey, "allowedIPs": client.AllowedIPs, "preSharedKey": client.PreSharedKey, - "keepAlive": wgKeepAlive(client.KeepAlive), + "keepAlive": wgKeepAlive(client.KeepAliveSeconds()), } return l.AddUser(ctx, ib, user) } @@ -283,7 +283,7 @@ func (l *Local) UpdateUser(ctx context.Context, ib *model.Inbound, oldEmail stri "publicKey": payload.PublicKey, "allowedIPs": payload.AllowedIPs, "preSharedKey": payload.PreSharedKey, - "keepAlive": wgKeepAlive(payload.KeepAlive), + "keepAlive": wgKeepAlive(payload.KeepAliveSeconds()), } return l.AddUser(ctx, ib, user) } diff --git a/internal/web/service/client_inbound_apply.go b/internal/web/service/client_inbound_apply.go index c36d10b26..46b04f4bc 100644 --- a/internal/web/service/client_inbound_apply.go +++ b/internal/web/service/client_inbound_apply.go @@ -587,7 +587,7 @@ func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model "publicKey": client.PublicKey, "allowedIPs": client.AllowedIPs, "preSharedKey": client.PreSharedKey, - "keepAlive": keepAliveStr(client.KeepAlive), + "keepAlive": keepAliveStr(client.KeepAliveSeconds()), }) if err1 == nil { logger.Debug("Client added on", rt.Name(), ":", client.Email) @@ -734,7 +734,7 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo if clients[0].PreSharedKey == "" { clients[0].PreSharedKey = old.PreSharedKey } - if clients[0].KeepAlive == 0 { + if clients[0].KeepAlive == nil { clients[0].KeepAlive = old.KeepAlive } // ForwardedPorts is AmneziaWG-only (WireGuard's own inbound never @@ -801,8 +801,8 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo if clients[0].PreSharedKey != "" { newMap["preSharedKey"] = clients[0].PreSharedKey } - if clients[0].KeepAlive > 0 { - newMap["keepAlive"] = clients[0].KeepAlive + if ka := clients[0].KeepAliveSeconds(); ka > 0 { + newMap["keepAlive"] = ka } if oldInbound.Protocol == model.AmneziaWG && clients[0].ForwardedPorts != "" { newMap["forwardedPorts"] = clients[0].ForwardedPorts @@ -1014,7 +1014,7 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo "publicKey": clients[0].PublicKey, "allowedIPs": clients[0].AllowedIPs, "preSharedKey": clients[0].PreSharedKey, - "keepAlive": keepAliveStr(clients[0].KeepAlive), + "keepAlive": keepAliveStr(clients[0].KeepAliveSeconds()), }) if err1 == nil { logger.Debug("Client edited on", rt.Name(), ":", clients[0].Email) diff --git a/internal/web/service/client_update_keepalive_test.go b/internal/web/service/client_update_keepalive_test.go new file mode 100644 index 000000000..f0b19ccde --- /dev/null +++ b/internal/web/service/client_update_keepalive_test.go @@ -0,0 +1,102 @@ +package service + +import ( + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database/model" +) + +func inboundKeepAlive(t *testing.T, inboundSvc *InboundService, ibId int, email string) int { + t.Helper() + ib, err := inboundSvc.GetInbound(ibId) + if err != nil { + t.Fatalf("GetInbound %d: %v", ibId, err) + } + clients, err := inboundSvc.GetClients(ib) + if err != nil { + t.Fatalf("GetClients %d: %v", ibId, err) + } + for i := range clients { + if clients[i].Email == email { + return clients[i].KeepAliveSeconds() + } + } + t.Fatalf("email %q not found on inbound %d", email, ibId) + return 0 +} + +// seedKeepAliveClient attaches one WireGuard client already carrying a +// PersistentKeepalive, and returns its inbound and client-record id. +func seedKeepAliveClient(t *testing.T, email string, keepAlive int) (*model.Inbound, int) { + t.Helper() + svc := &ClientService{} + + seeded := model.Client{ + Email: email, + SubID: "sub-" + email, + Enable: true, + AllowedIPs: []string{"10.0.0.5/32"}, + KeepAlive: model.KeepAlivePtr(keepAlive), + } + ib := mkInbound(t, 51820, model.WireGuard, clientsSettings(t, []model.Client{seeded})) + if err := svc.SyncInbound(nil, ib.Id, []model.Client{seeded}); err != nil { + t.Fatalf("seed linkage: %v", err) + } + return ib, lookupClientRecord(t, email).Id +} + +// Carrying forward on a zero incoming keepalive was a 0 -> 0 no-op while no UI +// could set the field; once the client form could, "0 disables it" was unreachable. +func TestUpdateCanClearKeepAliveOnAnExistingClient(t *testing.T) { + setupBulkDB(t) + inboundSvc := &InboundService{} + svc := &ClientService{} + + ib, recId := seedKeepAliveClient(t, "ka@x", 25) + if got := inboundKeepAlive(t, inboundSvc, ib.Id, "ka@x"); got != 25 { + t.Fatalf("seeded keepAlive = %d, want 25", got) + } + + updated := model.Client{ + Email: "ka@x", + Enable: true, + AllowedIPs: []string{"10.0.0.5/32"}, + KeepAlive: model.KeepAlivePtr(0), + } + if _, err := svc.Update(inboundSvc, recId, updated, 0); err != nil { + t.Fatalf("Update: %v", err) + } + + 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) + } +} + +// The other half of the contract: a payload that never mentions keepAlive (a +// metadata-only edit from the bot or the API) leaves the stored value alone. +func TestUpdateWithoutKeepAlivePreservesTheStoredValue(t *testing.T) { + setupBulkDB(t) + inboundSvc := &InboundService{} + svc := &ClientService{} + + ib, recId := seedKeepAliveClient(t, "ka@x", 25) + + updated := model.Client{ + Email: "ka@x", + Enable: true, + AllowedIPs: []string{"10.0.0.5/32"}, + } + if _, err := svc.Update(inboundSvc, recId, updated, 0); err != nil { + t.Fatalf("Update: %v", err) + } + + 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) + } +} diff --git a/internal/web/service/client_wireguard.go b/internal/web/service/client_wireguard.go index 67c555f71..2a14be36b 100644 --- a/internal/web/service/client_wireguard.go +++ b/internal/web/service/client_wireguard.go @@ -249,8 +249,8 @@ func defaultWireguardClients(settingsJSON string, existing, clients []model.Clie if c.PreSharedKey != "" { m["preSharedKey"] = c.PreSharedKey } - if c.KeepAlive > 0 { - m["keepAlive"] = c.KeepAlive + if ka := c.KeepAliveSeconds(); ka > 0 { + m["keepAlive"] = ka } interfaceClients[i] = m } diff --git a/internal/web/service/xray_wireguard_config_test.go b/internal/web/service/xray_wireguard_config_test.go index dc19504a3..443cb72a5 100644 --- a/internal/web/service/xray_wireguard_config_test.go +++ b/internal/web/service/xray_wireguard_config_test.go @@ -130,7 +130,7 @@ func wgPeerList(t *testing.T, settings map[string]any) []map[string]any { func TestGetXrayConfigWireGuardPeers(t *testing.T) { clients := []model.Client{ - {Email: "alice@wg.test", Enable: true, PublicKey: "pub-alice", AllowedIPs: []string{"10.0.0.2/32"}, KeepAlive: 25}, + {Email: "alice@wg.test", Enable: true, PublicKey: "pub-alice", AllowedIPs: []string{"10.0.0.2/32"}, KeepAlive: model.KeepAlivePtr(25)}, {Email: "bob@wg.test", Enable: true, PublicKey: "pub-bob", AllowedIPs: []string{"10.0.0.3/32"}}, } seedWGInbound(t, "wg-multi", 51820, clients) diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index 82299d203..a7e876e4d 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "اتركه فارغًا للتعيين التلقائي؛ افصل بين الإدخالات بفواصل", "amneziaWgForwardedPorts": "المنافذ المُعاد توجيهها", "amneziaWgForwardedPortsHint": "المنافذ/النطاقات المُعاد توجيهها (DNAT) لهذا العميل، مثل 80, 443, 8000-8100. اتركها فارغة إن لم تكن مطلوبة.", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "إعدادات AmneziaWG", "mtprotoSecret": "سر MTProto", "mtprotoSecretHint": "سر FakeTLS الخاص بالعميل. أعد التوليد لتغييره.", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index b26a190c5..e8af507ae 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "Leave empty to auto-assign; separate entries with commas", "amneziaWgForwardedPorts": "Forwarded Ports", "amneziaWgForwardedPortsHint": "Ports/ranges DNAT'd to this client, e.g. 80, 443, 8000-8100. Leave empty for none.", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "AmneziaWG config", "mtprotoSecret": "MTProto secret", "mtprotoSecretHint": "The client's FakeTLS secret. Regenerate to rotate it.", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index 18d14e69e..1d755d63b 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "Déjalo vacío para asignar automáticamente; separa las entradas con comas", "amneziaWgForwardedPorts": "Puertos reenviados", "amneziaWgForwardedPortsHint": "Puertos/rangos redirigidos (DNAT) a este cliente, p. ej. 80, 443, 8000-8100. Déjalo vacío si no aplica.", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "Configuración de AmneziaWG", "mtprotoSecret": "Secreto MTProto", "mtprotoSecretHint": "El secreto FakeTLS del cliente. Vuelve a generarlo para cambiarlo.", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 4f965a947..00c5693e9 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "برای تخصیص خودکار خالی بگذارید؛ ورودی‌ها را با کاما جدا کنید", "amneziaWgForwardedPorts": "پورت‌های هدایت‌شده", "amneziaWgForwardedPortsHint": "پورت‌ها/محدوده‌های DNAT شده به این کلاینت، مثلاً 80, 443, 8000-8100. برای غیرفعال بودن خالی بگذارید.", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "پیکربندی AmneziaWG", "mtprotoSecret": "سکرت MTProto", "mtprotoSecretHint": "سکرت FakeTLS این کلاینت. برای تعویض، دوباره تولید کنید.", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 60b5ba6b6..d9bbe5f82 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "Biarkan kosong untuk penetapan otomatis; pisahkan entri dengan koma", "amneziaWgForwardedPorts": "Port yang Diteruskan", "amneziaWgForwardedPortsHint": "Port/rentang yang di-DNAT ke klien ini, mis. 80, 443, 8000-8100. Biarkan kosong jika tidak ada.", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "Konfigurasi AmneziaWG", "mtprotoSecret": "Secret MTProto", "mtprotoSecretHint": "Secret FakeTLS klien. Buat ulang untuk menggantinya.", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 785be61f1..1662e63d4 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "空欄で自動割り当て。複数指定はカンマ区切り", "amneziaWgForwardedPorts": "転送ポート", "amneziaWgForwardedPortsHint": "このクライアントに転送するポート/範囲。例: 80, 443, 8000-8100。空欄で転送なし。", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "AmneziaWG 設定", "mtprotoSecret": "MTProto シークレット", "mtprotoSecretHint": "このクライアントの FakeTLS シークレット。変更するには再生成します。", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 607873355..92b39a4a2 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "Deixe vazio para atribuir automaticamente; separe as entradas com vírgulas", "amneziaWgForwardedPorts": "Portas encaminhadas", "amneziaWgForwardedPortsHint": "Portas/intervalos redirecionados (DNAT) para este cliente, ex. 80, 443, 8000-8100. Deixe vazio se não aplicável.", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "Configuração do AmneziaWG", "mtprotoSecret": "Segredo MTProto", "mtprotoSecretHint": "O segredo FakeTLS do cliente. Gere novamente para trocá-lo.", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index fd9fc4c2c..7ab8cbfa5 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "Оставьте пустым для автоназначения; разделяйте записи запятыми", "amneziaWgForwardedPorts": "Проброс портов", "amneziaWgForwardedPortsHint": "Порты/диапазоны, DNAT'ящиеся на этого клиента, например 80, 443, 8000-8100. Оставьте пустым, если не нужно.", + "tunnelKeepAlive": "Keepalive (секунды)", + "tunnelKeepAliveHint": "Как часто клиент шлёт keepalive-пакет. Обычное значение 25: оно держит NAT-маппинг открытым и возвращает простаивающий пир сам после любого разрыва, включая рестарт панели. 0 отключает — тогда молчащий клиент остаётся отключённым до первого своего пакета.", "amneziaWgConfig": "Конфиг AmneziaWG", "mtprotoSecret": "Секрет MTProto", "mtprotoSecretHint": "Секрет FakeTLS клиента. Перегенерируйте, чтобы сменить.", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 91def2c5a..4243ea7d4 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "Otomatik atama için boş bırakın; girişleri virgülle ayırın", "amneziaWgForwardedPorts": "Yönlendirilen Portlar", "amneziaWgForwardedPortsHint": "Bu istemciye DNAT ile yönlendirilen port/aralıklar, örn. 80, 443, 8000-8100. Yoksa boş bırakın.", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "AmneziaWG Yapılandırması", "mtprotoSecret": "MTProto sırrı", "mtprotoSecretHint": "İstemcinin FakeTLS sırrı. Değiştirmek için yeniden oluşturun.", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 19676454a..7a35273e1 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "Залиште порожнім для автопризначення; розділяйте записи комами", "amneziaWgForwardedPorts": "Перенаправлені порти", "amneziaWgForwardedPortsHint": "Порти/діапазони, що перенаправляються (DNAT) на цього клієнта, напр. 80, 443, 8000-8100. Залиште порожнім, якщо не потрібно.", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "Конфігурація AmneziaWG", "mtprotoSecret": "Секрет MTProto", "mtprotoSecretHint": "Секрет FakeTLS клієнта. Згенеруйте заново, щоб змінити.", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index a470d99d9..d3bf7b698 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "Để trống để tự động gán; phân tách các mục bằng dấu phẩy", "amneziaWgForwardedPorts": "Cổng chuyển tiếp", "amneziaWgForwardedPortsHint": "Cổng/dải cổng được chuyển tiếp (DNAT) đến client này, vd. 80, 443, 8000-8100. Để trống nếu không cần.", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "Cấu hình AmneziaWG", "mtprotoSecret": "Secret MTProto", "mtprotoSecretHint": "Secret FakeTLS của client. Tạo lại để thay đổi.", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 0c9596c38..8d600ab52 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "留空则自动分配;多个条目用逗号分隔", "amneziaWgForwardedPorts": "转发端口", "amneziaWgForwardedPortsHint": "转发到此客户端的端口/范围,例如 80, 443, 8000-8100。留空则不转发。", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "AmneziaWG 配置", "mtprotoSecret": "MTProto 密钥", "mtprotoSecretHint": "该客户端的 FakeTLS 密钥。重新生成即可更换。", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index c01ed3054..2b0088ccd 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -879,6 +879,8 @@ "amneziaWgAllowedIPsHint": "留空則自動分配;多個條目用逗號分隔", "amneziaWgForwardedPorts": "轉發連接埠", "amneziaWgForwardedPortsHint": "轉發到此客戶端的連接埠/範圍,例如 80, 443, 8000-8100。留空則不轉發。", + "tunnelKeepAlive": "Keepalive (seconds)", + "tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.", "amneziaWgConfig": "AmneziaWG 設定", "mtprotoSecret": "MTProto 金鑰", "mtprotoSecretHint": "該用戶端的 FakeTLS 金鑰。重新產生即可更換。",