Files
3x-ui/frontend/src/schemas/protocols/inbound/amneziawg.ts
T
MHSanaei 425a2bbf08 fix(amneziawg): bound what a handshake allocates and the tunnel MTU
Every handshake initiation builds Jc junk packets of Jmin..Jmax bytes
and the I1-I5 signature packets, and the limits allowed the full uint32
range and lengths of any size, so one connection made the embedded
device allocate without bound. The MTU reaches the device as an int32
and a wider value made it panic. Both ended the whole panel at the next
connection, after every restart.

Jc is now at most 128, Jmin/Jmax and each signature packet at most one
65535-byte datagram, and the MTU at most 65535. Saves refuse more, the
device setup refuses rows stored before, and the stored MTU is capped
where it is read. The form schemas carry the same limits.
2026-10-31 14:50:18 +01:00

108 lines
5.2 KiB
TypeScript

import { z } from 'zod';
import { OptionalClientKeepAliveSchema } from '@/schemas/primitives/keepalive';
// AntD InputNumber emits null (not undefined) when the user clears it, and
// the form store hands that null straight to safeParse on submit — a bare
// .optional() would reject it and block the save.
const optionalClearedInt = (schema: z.ZodNumber) =>
z.preprocess((v) => (v == null ? undefined : v), schema.optional());
// Same null-absorbing preprocess for fields that keep a schema default:
// clearing the InputNumber refills the default instead of blocking the save.
const clearedToDefault = <T extends z.ZodType>(schema: T) =>
z.preprocess((v) => (v == null ? undefined : v), schema);
// An AmneziaWG client (multi-client model). Same key/address fields as
// WireguardClientSchema — the panel's generic ClientRecord already has those
// exact keys (privateKey/publicKey/preSharedKey/allowedIPs/keepAlive), so
// bulk operations, the QR modal and subscriptions all work unmodified — plus
// one AmneziaWG-only addition, forwardedPorts (WireGuard's Xray-native
// inbound has no host-level iptables layer to hang per-client DNAT off of).
// Keys are optional on the wire — the backend generates them when absent.
export const AmneziawgClientSchema = z.object({
privateKey: z.string().optional(),
publicKey: z.string().optional(),
preSharedKey: z.string().optional(),
allowedIPs: z.array(z.string()).default([]),
keepAlive: OptionalClientKeepAliveSchema,
forwardedPorts: z.string().default(''),
email: z.string().min(1),
limitIp: z.number().int().min(0).default(0),
totalGB: z.number().int().min(0).default(0),
expiryTime: z.number().int().default(0),
enable: z.boolean().default(true),
tgId: z
.union([z.number(), z.string()])
.transform((v) => Number(v) || 0)
.default(0),
subId: z.string().default(''),
comment: z.string().default(''),
reset: z.number().int().min(0).default(0),
created_at: z.number().int().optional(),
updated_at: z.number().int().optional(),
});
export type AmneziawgClient = z.infer<typeof AmneziawgClientSchema>;
// Server-wide AmneziaWG 3.1 obfuscation parameters and tunnel identity,
// mirroring internal/amneziawg.ServerSettings on the Go side exactly (same
// field names) — the listen port is not duplicated here, it's the inbound's
// own port like every other protocol. H1-H4 blank falls back to the classic
// 1/2/3/4 magic header on save; blank optional fields omit their feature
// from the rendered config.
export const AmneziawgServerSchema = z.object({
privateKey: z.string().optional(),
publicKey: z.string().optional(),
subnetIp: z.string().default('10.8.1.0'),
subnetCidr: clearedToDefault(z.number().int().min(1).max(32).default(24)),
mtu: optionalClearedInt(z.number().int().min(1).max(65535)),
primaryDns: z.string().default('8.8.8.8'),
secondaryDns: z.string().default('8.8.4.4'),
externalInterface: z.string().default(''),
ipv6Enabled: z.boolean().default(false),
ipv6Subnet: z.string().default(''),
ipv6ExternalInterface: z.string().default(''),
// routeThroughXray is vestigial on the Go side (see ServerSettings' own
// doc comment) -- the embedded relay is always on, this field is read by
// nothing. Kept here anyway, with no corresponding form control, purely so
// z.object's default unknown-key stripping doesn't silently drop it from
// an existing stored settings blob on the next save.
routeThroughXray: z.boolean().default(false).optional(),
// kernelMode runs this inbound on the DKMS kernel module instead of the
// embedded userspace stack -- no Xray routing, sniffing, IPv6 or port forwarding.
kernelMode: z.boolean().default(false).optional(),
// Upper bounds match amneziawg-go's own UAPI parsers (device/uapi.go):
// Every handshake allocates jc packets of jmin..jmax bytes; s1-s4 are uint16.
jc: clearedToDefault(z.number().int().min(0).max(128).default(5)),
jmin: clearedToDefault(z.number().int().min(0).max(65535).default(10)),
jmax: clearedToDefault(z.number().int().min(0).max(65535).default(50)),
s1: clearedToDefault(z.number().int().min(0).max(1552).default(30)),
s2: clearedToDefault(z.number().int().min(0).max(1608).default(45)),
s3: clearedToDefault(z.number().int().min(0).max(1636).default(10)),
s4: clearedToDefault(z.number().int().min(0).max(32).default(5)),
h1: z.string().default(''),
h2: z.string().default(''),
h3: z.string().default(''),
h4: z.string().default(''),
i1: z.string().default(''),
i2: z.string().default(''),
i3: z.string().default(''),
i4: z.string().default(''),
i5: z.string().default(''),
headerProtectionKey: z.string().default(''),
contentPaddingAddition: z.string().default(''),
rekeyAfterTime: z.string().default(''),
rekeyTimeout: z.string().default(''),
rejectAfterTime: z.string().default(''),
keepaliveTimeout: z.string().default(''),
maxHandshakeAttempts: z.string().default(''),
randomTrailers: z.boolean().default(false),
disableCookies: z.boolean().default(false),
});
export type AmneziawgServer = z.infer<typeof AmneziawgServerSchema>;
export const AmneziawgInboundSettingsSchema = z.object({
server: AmneziawgServerSchema,
clients: z.array(AmneziawgClientSchema).default([]),
});
export type AmneziawgInboundSettings = z.infer<typeof AmneziawgInboundSettingsSchema>;