mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-01 19:52:09 +03:00
fix(amneziawg): bound S1-S3 by the receive buffer, reject overlapping H (#6642)
* fix(amneziawg): bound S1-S3 by the receive buffer, reject overlapping H The native AmneziaWG validator, both Zod schemas, both forms and the docs now follow the rules amneziawg-go actually enforces. S1-S3. A padded handshake message is 148+S1, 92+S2 or 64+S3 bytes (device/send.go). The peer reads each datagram into a [MaxMessageSize]byte buffer, where MaxMessageSize = MaxSegmentSize (device/pools.go, constants.go). MaxSegmentSize is 65535 on Linux/Android, 2016 on Windows and 1700 on iOS (device/queueconstants_*.go). The limits are therefore S1 <= 1552, S2 <= 1608 and S3 <= 1636. Before, S1/S2 allowed 65535, which iOS peers silently drop, and S3 was capped at 64, a number inherited from the coinman-dev/3ax-ui port in #6105 with no stated reason. That cap blocked real configs such as Amnezia Premium's S3=1045. RandomTrailers only tops a packet up to 500 bytes (DefaultUdpWindow), so it never pushes a message past these limits. H1-H4. amneziawg-go refuses the whole device when the header ranges overlap ("headers must not overlap", device/uapi.go mergeWithDevice), and so does the kernel module (src/netlink.c). The panel did not check this, so an inbound with overlapping ranges saved and then failed to apply. A blank H is never sent, so the engine keeps its default, WireGuard's own type 1-4; the check treats blank fields that way. The docs said 1-4 "must not be used". They are valid and are the engine default, only unobfuscated without a HeaderProtectionKey. The docs also said amneziawg-go rejects S1+56 == S2. It does not (IpcSet accepts it). The panel keeps that rule as a fingerprint guard, and the docs now say so. Tests: the new params_test cases and the Zod bounds fail on the old code. TestValidatedObfuscationAlwaysApplies runs every accepted set through a real amneziawg-go IpcSet and now covers overlap, blank-H defaults, H=1-4, the exact S bounds and the full Amnezia Premium set. Before this fix it failed with "headers must not overlap". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(amneziawg): bound only inbound padding by the iOS receive buffer The 1700-byte iOS buffer limits what an inbound's clients can receive, but ValidateObfuscation also runs for outbounds, and the Xray template save re-validates every AmneziaWG outbound. An outbound whose remote server uses S1 above 1552 would have blocked every Xray settings save, though its values come from that server and are received on Linux. ValidateObfuscation keeps amneziawg-go's uint16 UAPI width for S1-S3; ValidateServerObfuscation adds the receive-buffer bounds and is what inbounds call. The outbound schema and form follow the same split. --------- Co-authored-by: Kirill Rudenko <rudenko@npp-energy.ru> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -103,13 +103,13 @@ export default function AmneziawgFields({
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</FormField>
|
||||
<FormField name={['settings', 'server', 's1']} label={t('pages.xray.amneziawg.s1')}>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
<InputNumber min={0} max={1552} style={{ width: '100%' }} />
|
||||
</FormField>
|
||||
<FormField name={['settings', 'server', 's2']} label={t('pages.xray.amneziawg.s2')}>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
<InputNumber min={0} max={1608} style={{ width: '100%' }} />
|
||||
</FormField>
|
||||
<FormField name={['settings', 'server', 's3']} label={t('pages.xray.amneziawg.s3')}>
|
||||
<InputNumber min={0} max={64} style={{ width: '100%' }} />
|
||||
<InputNumber min={0} max={1636} style={{ width: '100%' }} />
|
||||
</FormField>
|
||||
<FormField name={['settings', 'server', 's4']} label={t('pages.xray.amneziawg.s4')}>
|
||||
<InputNumber min={0} max={32} style={{ width: '100%' }} />
|
||||
|
||||
@@ -70,9 +70,9 @@ export default function AmneziawgFields() {
|
||||
<ObfNumber name="jc" label={t('pages.xray.amneziawg.jc')} min={0} />
|
||||
<ObfNumber name="jmin" label={t('pages.xray.amneziawg.jmin')} min={0} />
|
||||
<ObfNumber name="jmax" label={t('pages.xray.amneziawg.jmax')} min={0} />
|
||||
<ObfNumber name="s1" label={t('pages.xray.amneziawg.s1')} min={0} />
|
||||
<ObfNumber name="s2" label={t('pages.xray.amneziawg.s2')} min={0} />
|
||||
<ObfNumber name="s3" label={t('pages.xray.amneziawg.s3')} min={0} max={64} />
|
||||
<ObfNumber name="s1" label={t('pages.xray.amneziawg.s1')} min={0} max={65535} />
|
||||
<ObfNumber name="s2" label={t('pages.xray.amneziawg.s2')} min={0} max={65535} />
|
||||
<ObfNumber name="s3" label={t('pages.xray.amneziawg.s3')} min={0} max={65535} />
|
||||
<ObfNumber name="s4" label={t('pages.xray.amneziawg.s4')} min={0} max={32} />
|
||||
<ObfText name="h1" label={t('pages.xray.amneziawg.h1')} placeholder="100-800" />
|
||||
<ObfText name="h2" label={t('pages.xray.amneziawg.h2')} placeholder="900-1600" />
|
||||
|
||||
@@ -71,9 +71,9 @@ export const AmneziawgServerSchema = z.object({
|
||||
jc: clearedToDefault(z.number().int().min(0).max(4294967295).default(5)),
|
||||
jmin: clearedToDefault(z.number().int().min(0).max(4294967295).default(10)),
|
||||
jmax: clearedToDefault(z.number().int().min(0).max(4294967295).default(50)),
|
||||
s1: clearedToDefault(z.number().int().min(0).max(65535).default(30)),
|
||||
s2: clearedToDefault(z.number().int().min(0).max(65535).default(45)),
|
||||
s3: clearedToDefault(z.number().int().min(0).max(64).default(10)),
|
||||
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(''),
|
||||
|
||||
@@ -22,9 +22,10 @@ export const AmneziaWGOutboundSettingsSchema = z.object({
|
||||
jc: z.number().int().min(0).default(0),
|
||||
jmin: z.number().int().min(0).default(40),
|
||||
jmax: z.number().int().min(0).default(100),
|
||||
s1: z.number().int().min(0).default(15),
|
||||
s2: z.number().int().min(0).default(80),
|
||||
s3: z.number().int().min(0).max(64).default(12),
|
||||
// The remote server sets S1-S3; only amneziawg-go's uint16 UAPI width bounds them here.
|
||||
s1: z.number().int().min(0).max(65535).default(15),
|
||||
s2: z.number().int().min(0).max(65535).default(80),
|
||||
s3: z.number().int().min(0).max(65535).default(12),
|
||||
s4: z.number().int().min(0).max(32).default(12),
|
||||
h1: z.string().default(''),
|
||||
h2: z.string().default(''),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { AmneziawgServerSchema } from '@/schemas/protocols/inbound/amneziawg';
|
||||
import { AmneziaWGOutboundSettingsSchema } from '@/schemas/protocols/outbound/amneziawg';
|
||||
|
||||
// AntD InputNumber emits null when cleared; a cleared numeric field must
|
||||
// refill its schema default instead of failing validation and blocking the save.
|
||||
@@ -33,27 +34,27 @@ describe('AmneziawgServerSchema cleared numeric fields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// The form must reject what amneziawg-go's UAPI parsers reject (device/uapi.go:
|
||||
// jc/jmin/jmax uint32, s1-s4 uint16), or the save silently outlives the apply.
|
||||
// The form must reject what cannot apply or be received: jc/jmin/jmax past uint32 (device/uapi.go),
|
||||
// S1-S3 past amneziawg-go's 1700-byte iOS receive buffer, S4 past 32 (MTU headroom).
|
||||
describe('AmneziawgServerSchema obfuscation bounds', () => {
|
||||
const overWidth: Array<[string, number]> = [
|
||||
['s1', 65536],
|
||||
['s2', 70000],
|
||||
['s3', 65],
|
||||
['s1', 1553],
|
||||
['s2', 1609],
|
||||
['s3', 1637],
|
||||
['s4', 33],
|
||||
['jc', 4294967296],
|
||||
['jmin', 4294967296],
|
||||
['jmax', 5000000000],
|
||||
];
|
||||
|
||||
it.each(overWidth)('rejects %s above the width amneziawg-go parses', (field, value) => {
|
||||
it.each(overWidth)('rejects %s past what amneziawg-go can apply or receive', (field, value) => {
|
||||
expect(AmneziawgServerSchema.safeParse({ [field]: value }).success).toBe(false);
|
||||
});
|
||||
|
||||
const atLimit: Array<[string, number]> = [
|
||||
['s1', 65535],
|
||||
['s2', 65535],
|
||||
['s3', 64],
|
||||
['s1', 1552],
|
||||
['s2', 1608],
|
||||
['s3', 1636],
|
||||
['s4', 32],
|
||||
['jc', 4294967295],
|
||||
];
|
||||
@@ -69,3 +70,15 @@ describe('AmneziawgServerSchema obfuscation bounds', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// An outbound's S values come from the remote server and are received on Linux,
|
||||
// so only amneziawg-go's uint16 UAPI width bounds them, not the iOS buffer.
|
||||
describe('AmneziaWGOutboundSettingsSchema padding bounds', () => {
|
||||
it.each(['s1', 's2', 's3'])('accepts %s past the inbound iOS cap', (field) => {
|
||||
expect(AmneziaWGOutboundSettingsSchema.safeParse({ [field]: 2000 }).success).toBe(true);
|
||||
});
|
||||
|
||||
it.each(['s1', 's2', 's3'])('rejects %s past uint16', (field) => {
|
||||
expect(AmneziaWGOutboundSettingsSchema.safeParse({ [field]: 65536 }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user