mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
bd1c27b03d
* fix(amneziawg): stop H1-H4 generator misclassifying transport packets Both the Go generator and its frontend mirror picked a random *range* per H1-H4 field with only a minimum width enforced (no maximum). amneziawg-go's packet classifier only ever compares a fixed-size ciphertext prefix against these bounds, so a wide range buys no DPI resistance -- the boundaries themselves are never observable on the wire. It does cost real throughput: with randomTrailers on (the default here), the handshake-size checks relax from == to >, so a wide H-range misclassifies a proportional fraction of ordinary transport packets as handshakes and silently drops them (amnezia-vpn/amneziawg-go#183). A single value per field is strictly safer than any range, with no obfuscation trade-off. Live-tested: narrowing H1-H4 alone took AmneziaWG upload from 2-3 Mbit/s to 200+ Mbit/s on one box, and ~20 Mbit/s to 120-156 Mbit/s on another, single-variable, no other change. * fix(amneziawgnet): raise tunQueueDepth to absorb slow-start bursts 1024 was sized for a single-connection buffering problem (the gVisor-to-amneziawg-go TUN handoff channel needing slack for the download direction). tcpip.Stack.Stats() during a real many-connection download (20-28 concurrent TCP flows, e.g. a segmented speed test) showed SlowStartRetransmits jump by ~770 in a single second the moment CurrentEstablished crossed ~20 -- consistent with many connections' simultaneous slow-start growth briefly exceeding 1024 outstanding packets and gVisor treating the resulting silent drops as real network loss. * fix(amneziawg): trim comment blocks to the repo's 2-line cap Review feedback: four comment blocks in the previous commits exceeded CLAUDE.md's 2-line-per-block hard rule (up to 13 lines). Trimmed each to the one non-obvious fact plus the amneziawg-go#183 reference; the fuller rationale already lives in the commit message. Also refreshed the stale H1-H4 range example in docs/content/docs/en/config/amneziawg.mdx to match the new single-value generator output.
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { generateAwgObfuscation } from '@/lib/xray/amneziawg-obfuscation';
|
|
import { AmneziawgServerSchema } from '@/schemas/protocols/inbound/amneziawg';
|
|
import { ServerSettingsSchema } from '@/generated/zod';
|
|
|
|
/*
|
|
* Parses "lo-hi" and asserts min <= lo <= hi <= max; mirrors the bounds the
|
|
* Go generator's own test pins (internal/amneziawg/params_test.go), so the
|
|
* two generators cannot drift apart silently.
|
|
*/
|
|
function expectRangeWithin(value: string, min: number, max: number): [number, number] {
|
|
const m = /^(\d+)-(\d+)$/.exec(value);
|
|
expect(m, `${value} is not a lo-hi range`).not.toBeNull();
|
|
const lo = Number(m![1]);
|
|
const hi = Number(m![2]);
|
|
expect(lo).toBeGreaterThanOrEqual(min);
|
|
expect(hi).toBeLessThanOrEqual(max);
|
|
expect(lo).toBeLessThanOrEqual(hi);
|
|
return [lo, hi];
|
|
}
|
|
|
|
/* Parses a plain integer and asserts min <= n <= max (see expectRangeWithin above for the range form). */
|
|
function expectIntWithin(value: string, min: number, max: number): number {
|
|
const m = /^(\d+)$/.exec(value);
|
|
expect(m, `${value} is not a plain integer`).not.toBeNull();
|
|
const n = Number(m![1]);
|
|
expect(n).toBeGreaterThanOrEqual(min);
|
|
expect(n).toBeLessThanOrEqual(max);
|
|
return n;
|
|
}
|
|
|
|
describe('generateAwgObfuscation', () => {
|
|
it('stays inside the Go generator ranges and invariants', () => {
|
|
for (let i = 0; i < 200; i++) {
|
|
const o = generateAwgObfuscation();
|
|
|
|
expect(o.jc).toBeGreaterThanOrEqual(3);
|
|
expect(o.jc).toBeLessThanOrEqual(6);
|
|
expect(o.jmin).toBeGreaterThanOrEqual(40);
|
|
expect(o.jmin).toBeLessThanOrEqual(89);
|
|
expect(o.jmax - o.jmin).toBeGreaterThanOrEqual(50);
|
|
expect(o.jmax - o.jmin).toBeLessThanOrEqual(250);
|
|
expect(o.s1 + 56).not.toBe(o.s2);
|
|
expect(o.s3).toBeGreaterThanOrEqual(12);
|
|
expect(o.s3).toBeLessThanOrEqual(55);
|
|
expect(o.s4).toBeGreaterThanOrEqual(12);
|
|
expect(o.s4).toBeLessThanOrEqual(27);
|
|
|
|
const hValues = [o.h1, o.h2, o.h3, o.h4].map((h) => expectIntWithin(h, 5, 2147483647));
|
|
for (let j = 1; j < 4; j++) {
|
|
expect(hValues[j], 'H values must be strictly increasing across bands').toBeGreaterThan(
|
|
hValues[j - 1],
|
|
);
|
|
}
|
|
|
|
expect(o.i1).toMatch(/^<r \d+>$/);
|
|
expect(o.i2).toBe('');
|
|
expect(o.i5).toBe('');
|
|
|
|
const key = atob(o.headerProtectionKey);
|
|
expect(key.length, 'headerProtectionKey must decode to 32 bytes').toBe(32);
|
|
|
|
expectRangeWithin(o.contentPaddingAddition, 8, 64);
|
|
const [, rekeyHi] = expectRangeWithin(o.rekeyAfterTime, 100, 160);
|
|
const [rejectLo] = expectRangeWithin(o.rejectAfterTime, 130, 310);
|
|
expect(
|
|
rejectLo,
|
|
'reject window must start >= 30s above the rekey window',
|
|
).toBeGreaterThanOrEqual(rekeyHi + 30);
|
|
expectRangeWithin(o.rekeyTimeout, 3, 10);
|
|
expectRangeWithin(o.keepaliveTimeout, 8, 20);
|
|
expectRangeWithin(o.maxHandshakeAttempts, 15, 50);
|
|
|
|
expect(o.randomTrailers).toBe(true);
|
|
expect(o.disableCookies).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('produces values the hand-written schema accepts unchanged', () => {
|
|
const parsed = AmneziawgServerSchema.parse({
|
|
...generateAwgObfuscation(),
|
|
privateKey: 'p',
|
|
publicKey: 'P',
|
|
});
|
|
expect(parsed.headerProtectionKey).not.toBe('');
|
|
});
|
|
});
|
|
|
|
/*
|
|
* Drift guard for the three-way mirror: the hand-written AmneziawgServerSchema,
|
|
* the Go ServerSettings struct, and the openapigen output must agree on the
|
|
* field set. Comparing hand-written vs generated keys catches a field added on
|
|
* one side but forgotten on the other before it silently drops from configs.
|
|
*/
|
|
describe('AmneziawgServerSchema parity with generated ServerSettings', () => {
|
|
it('declares exactly the generated key set', () => {
|
|
const handwritten = Object.keys(AmneziawgServerSchema.shape).sort();
|
|
const generated = Object.keys(ServerSettingsSchema.shape).sort();
|
|
expect(handwritten).toEqual(generated);
|
|
});
|
|
});
|