From b70e17ed7312317d52bb2caf4c315cfd340ff055 Mon Sep 17 00:00:00 2001
From: YoungReckless4 <132759713+YoungReckless4@users.noreply.github.com>
Date: Mon, 31 Aug 2026 14:39:58 +0300
Subject: [PATCH] feat(clients): let admins set PersistentKeepalive on tunnel
clients
model.Client already carries KeepAlive, and every AmneziaWG/WireGuard client
config emitter already writes PersistentKeepalive when it is above zero -- but
nothing in the UI could set it, so it stayed 0 and the line was never emitted.
Without it a peer that goes quiet has nothing to trigger a handshake: WireGuard
only initiates when it has data to send. An idle client stays disconnected
after any interruption -- a NAT mapping timing out, a device sleeping, the
panel restarting -- until the user generates traffic themselves.
New clients default to 25, the conventional value, which also keeps the NAT
mapping open. Existing clients keep whatever they have, and 0 remains valid and
means "do not send keepalives".
---
frontend/src/pages/clients/ClientFormModal.tsx | 11 +++++++++++
internal/web/translation/ar-EG.json | 2 ++
internal/web/translation/en-US.json | 2 ++
internal/web/translation/es-ES.json | 2 ++
internal/web/translation/fa-IR.json | 2 ++
internal/web/translation/id-ID.json | 2 ++
internal/web/translation/ja-JP.json | 2 ++
internal/web/translation/pt-BR.json | 2 ++
internal/web/translation/ru-RU.json | 2 ++
internal/web/translation/tr-TR.json | 2 ++
internal/web/translation/uk-UA.json | 2 ++
internal/web/translation/vi-VN.json | 2 ++
internal/web/translation/zh-CN.json | 2 ++
internal/web/translation/zh-TW.json | 2 ++
14 files changed, 37 insertions(+)
diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx
index f7b28f42c..3f5912557 100644
--- a/frontend/src/pages/clients/ClientFormModal.tsx
+++ b/frontend/src/pages/clients/ClientFormModal.tsx
@@ -132,6 +132,7 @@ type Values = ClientFormValues & {
wgAllowedIPs: string;
awgAllowedIPs: string;
awgForwardedPorts: string;
+ wgKeepAlive: number;
secret: string;
adTag: string;
};
@@ -168,6 +169,7 @@ const EMPTY: Values = {
wgAllowedIPs: '',
awgAllowedIPs: '',
awgForwardedPorts: '',
+ wgKeepAlive: 25,
secret: '',
adTag: '',
};
@@ -378,6 +380,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 || '',
};
@@ -693,6 +696,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;
@@ -1271,6 +1275,13 @@ export default function ClientFormModal({
)}
+
+
+
{showAmneziawg && (