mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +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:
@@ -38,9 +38,14 @@ import { resolveExternalLinkExpiry } from '@/lib/clients/external-link';
|
||||
import { useDatepicker } from '@/hooks/useDatepicker';
|
||||
import { useClientHwids } from '@/hooks/useClientHwids';
|
||||
import { DateTimePicker, SelectAllClearButtons } from '@/components/form';
|
||||
import { FormField } from '@/components/form/rhf';
|
||||
import { FormField, rhfZodValidate } from '@/components/form/rhf';
|
||||
import ClientHwidListModal from '@/components/clients/ClientHwidList';
|
||||
import { TLS_FLOW_CONTROL, TRAFFIC_RESETS } from '@/schemas/primitives';
|
||||
import {
|
||||
OptionalAmneziaWGKeepAliveInputSchema,
|
||||
OptionalNumericKeepAliveInputSchema,
|
||||
keepAliveInputValue,
|
||||
} from '@/schemas/primitives/keepalive';
|
||||
import type {
|
||||
ClientRecord,
|
||||
InboundOption,
|
||||
@@ -137,7 +142,7 @@ type Values = ClientFormValues & {
|
||||
wgAllowedIPs: string;
|
||||
awgAllowedIPs: string;
|
||||
awgForwardedPorts: string;
|
||||
wgKeepAlive: number;
|
||||
wgKeepAlive: number | string | undefined;
|
||||
secret: string;
|
||||
adTag: string;
|
||||
};
|
||||
@@ -389,7 +394,7 @@ export default function ClientFormModal({
|
||||
wgAllowedIPs: wgTunnelIPs ?? client.allowedIPs ?? '',
|
||||
awgAllowedIPs: awgTunnelIPs ?? client.allowedIPs ?? '',
|
||||
awgForwardedPorts: client.forwardedPorts || '',
|
||||
wgKeepAlive: client.keepAlive ?? 0,
|
||||
wgKeepAlive: client.keepAlive,
|
||||
secret: client.secret || '',
|
||||
adTag: client.adTag || '',
|
||||
};
|
||||
@@ -641,6 +646,25 @@ export default function ClientFormModal({
|
||||
|
||||
async function onSubmit() {
|
||||
const values = methods.getValues();
|
||||
const unchangedLegacyKeepAlive =
|
||||
isEdit &&
|
||||
typeof client?.keepAlive === 'number' &&
|
||||
client.keepAlive > 65535 &&
|
||||
values.wgKeepAlive === client.keepAlive;
|
||||
if (showAmneziawg && !unchangedLegacyKeepAlive) {
|
||||
const keepAliveSchema = showWireguard
|
||||
? OptionalNumericKeepAliveInputSchema
|
||||
: OptionalAmneziaWGKeepAliveInputSchema;
|
||||
const keepAliveResult = keepAliveSchema.safeParse(values.wgKeepAlive);
|
||||
if (!keepAliveResult.success) {
|
||||
methods.setError('wgKeepAlive', {
|
||||
type: 'validate',
|
||||
message: keepAliveResult.error.issues[0]?.message ?? 'validation.invalid',
|
||||
});
|
||||
return;
|
||||
}
|
||||
methods.clearErrors('wgKeepAlive');
|
||||
}
|
||||
const schema = isEdit ? ClientFormSchema : ClientCreateFormSchema;
|
||||
const validated = schema.safeParse({
|
||||
email: values.email,
|
||||
@@ -712,7 +736,9 @@ 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;
|
||||
if (!unchangedLegacyKeepAlive && values.wgKeepAlive !== undefined) {
|
||||
clientPayload.keepAlive = values.wgKeepAlive;
|
||||
}
|
||||
clientPayload.publicKey = values.wgPublicKey;
|
||||
if (values.wgPreSharedKey) {
|
||||
clientPayload.preSharedKey = values.wgPreSharedKey;
|
||||
@@ -1339,10 +1365,34 @@ export default function ClientFormModal({
|
||||
<FormField
|
||||
name="wgKeepAlive"
|
||||
label={t('pages.clients.tunnelKeepAlive')}
|
||||
extra={t('pages.clients.tunnelKeepAliveHint')}
|
||||
transform={{ output: (v) => Number(v) || 0 }}
|
||||
extra={t(
|
||||
showAmneziawg && !showWireguard
|
||||
? 'pages.clients.amneziaWgKeepAliveHint'
|
||||
: 'pages.clients.tunnelKeepAliveHint',
|
||||
)}
|
||||
transform={{
|
||||
output:
|
||||
showAmneziawg && !showWireguard
|
||||
? keepAliveInputValue
|
||||
: (value) => Number(value) || 0,
|
||||
}}
|
||||
rules={
|
||||
showAmneziawg
|
||||
? {
|
||||
validate: rhfZodValidate(
|
||||
showWireguard
|
||||
? OptionalNumericKeepAliveInputSchema
|
||||
: OptionalAmneziaWGKeepAliveInputSchema,
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<InputNumber min={0} max={65535} style={{ width: '100%' }} />
|
||||
{showAmneziawg && !showWireguard ? (
|
||||
<Input placeholder="25" />
|
||||
) : (
|
||||
<InputNumber min={0} max={65535} style={{ width: '100%' }} />
|
||||
)}
|
||||
</FormField>
|
||||
{showAmneziawg && (
|
||||
<FormField
|
||||
|
||||
@@ -3,6 +3,7 @@ import { formatInboundLabel } from '@/lib/inbounds/label';
|
||||
import { preferPublicHost, resolveShareHost } from '@/lib/xray/inbound-link';
|
||||
import { effectiveMtu } from '@/lib/xray/amneziawg-obfuscation';
|
||||
import { joinConfLines } from '@/lib/xray/conf-lines';
|
||||
import { AmneziaWGKeepAliveRangeSchema } from '@/schemas/primitives/keepalive';
|
||||
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
|
||||
|
||||
// AmneziaWG clients are wire-identical to WireGuard clients (same
|
||||
@@ -17,7 +18,7 @@ export function isAmneziaWGClient(client: ClientRecord | null | undefined): bool
|
||||
client.publicKey ||
|
||||
client.allowedIPs ||
|
||||
client.preSharedKey ||
|
||||
client.keepAlive
|
||||
client.keepAlive !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +103,11 @@ export function buildAmneziaWGClientConfig(
|
||||
lines.push('[Peer]', `PublicKey = ${server?.publicKey || ''}`);
|
||||
if (client.preSharedKey) lines.push(`PresharedKey = ${client.preSharedKey}`);
|
||||
lines.push('AllowedIPs = 0.0.0.0/0, ::/0', `Endpoint = ${endpoint}`);
|
||||
if (client.keepAlive && client.keepAlive > 0)
|
||||
if (typeof client.keepAlive === 'string') {
|
||||
if (!AmneziaWGKeepAliveRangeSchema.safeParse(client.keepAlive).success) return '';
|
||||
lines.push(`PersistentKeepalive = ${client.keepAlive}`);
|
||||
} else if (client.keepAlive && client.keepAlive > 0) {
|
||||
lines.push(`PersistentKeepalive = ${client.keepAlive}`);
|
||||
}
|
||||
return joinConfLines(lines);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function isWireguardClient(client: ClientRecord | null | undefined): bool
|
||||
client.publicKey ||
|
||||
client.allowedIPs ||
|
||||
client.preSharedKey ||
|
||||
client.keepAlive
|
||||
typeof client.keepAlive === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function buildWireguardClientConfig(
|
||||
lines.push('[Peer]', `PublicKey = ${inbound?.wgPublicKey || ''}`);
|
||||
if (client.preSharedKey) lines.push(`PresharedKey = ${client.preSharedKey}`);
|
||||
lines.push('AllowedIPs = 0.0.0.0/0, ::/0', `Endpoint = ${endpoint}`);
|
||||
if (client.keepAlive && client.keepAlive > 0)
|
||||
if (typeof client.keepAlive === 'number' && client.keepAlive > 0)
|
||||
lines.push(`PersistentKeepalive = ${client.keepAlive}`);
|
||||
return joinConfLines(lines);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user