diff --git a/docs/content/docs/en/config/amneziawg.mdx b/docs/content/docs/en/config/amneziawg.mdx index 507de6887..805ab2689 100644 --- a/docs/content/docs/en/config/amneziawg.mdx +++ b/docs/content/docs/en/config/amneziawg.mdx @@ -141,10 +141,10 @@ S1 = 87 S2 = 44 S3 = 21 S4 = 9 -H1 = 462980921-463150218 -H2 = 1177681572-1177787900 -H3 = 1907413509-1907903969 -H4 = 2029908558-2030313135 +H1 = 463065432 +H2 = 912345678 +H3 = 1345678901 +H4 = 1987654321 I1 = HeaderProtectionKey = 8Iu83eHDA3fMKKSGaEsVW9Ycd2lYYzc0MYlk1jJTvE4= ContentPaddingAddition = 17-49 diff --git a/frontend/src/lib/xray/amneziawg-obfuscation.ts b/frontend/src/lib/xray/amneziawg-obfuscation.ts index 59dd5782a..b80d58344 100644 --- a/frontend/src/lib/xray/amneziawg-obfuscation.ts +++ b/frontend/src/lib/xray/amneziawg-obfuscation.ts @@ -51,21 +51,17 @@ const generateHeaderProtectionKey = (): string => { }; /* - * Four non-overlapping "low-high" ranges for H1-H4: split the space into - * four bands and take a random sub-range from each (>= 1000 wide, low - * bound >= 5 since 1-4 are reserved for vanilla WireGuard message types). + * Four distinct values for H1-H4, one per band; low bound >= 5 (1-4 are vanilla WG message types). + * Single values, not ranges: with randomTrailers on, a wide range misclassifies transport packets as handshakes (amnezia-vpn/amneziawg-go#183). */ -const generateHRanges = (): [string, string, string, string] => { +const generateHValues = (): [string, string, string, string] => { const hMax = 2147483647; - const hMinWidth = 1000; const lo = 5; const bandSize = Math.floor((hMax - lo + 1) / 4); return Array.from({ length: 4 }, (_, i) => { const bandLo = lo + i * bandSize; const bandHi = bandLo + bandSize - 1; - const start = randInt(bandLo, bandHi - hMinWidth - 1); - const end = randInt(start + hMinWidth, bandHi - 1); - return `${start}-${end}`; + return `${randInt(bandLo, bandHi)}`; }) as [string, string, string, string]; }; @@ -76,7 +72,7 @@ export function generateAwgObfuscation(): AwgObfuscation { while (s1 + 56 === s2) { s2 = randInt(15, 150); } - const [h1, h2, h3, h4] = generateHRanges(); + const [h1, h2, h3, h4] = generateHValues(); /* * Timing windows bracket WireGuard's stock constants (rekey 120s, reject diff --git a/frontend/src/test/amneziawg-obfuscation.test.ts b/frontend/src/test/amneziawg-obfuscation.test.ts index e62f617af..af957b1f4 100644 --- a/frontend/src/test/amneziawg-obfuscation.test.ts +++ b/frontend/src/test/amneziawg-obfuscation.test.ts @@ -20,6 +20,16 @@ function expectRangeWithin(value: string, min: number, max: number): [number, nu 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++) { @@ -37,9 +47,11 @@ describe('generateAwgObfuscation', () => { expect(o.s4).toBeGreaterThanOrEqual(12); expect(o.s4).toBeLessThanOrEqual(27); - const hBounds = [o.h1, o.h2, o.h3, o.h4].map((h) => expectRangeWithin(h, 5, 2147483647)); + const hValues = [o.h1, o.h2, o.h3, o.h4].map((h) => expectIntWithin(h, 5, 2147483647)); for (let j = 1; j < 4; j++) { - expect(hBounds[j][0], 'H ranges must not overlap').toBeGreaterThan(hBounds[j - 1][1]); + expect(hValues[j], 'H values must be strictly increasing across bands').toBeGreaterThan( + hValues[j - 1], + ); } expect(o.i1).toMatch(/^$/); diff --git a/internal/amneziawg/params.go b/internal/amneziawg/params.go index 9bbc92386..54842f086 100644 --- a/internal/amneziawg/params.go +++ b/internal/amneziawg/params.go @@ -15,9 +15,6 @@ import ( // but the amneziawg-windows-client config editor rejects anything above. const awgHMax = 2147483647 -// hMinWidth is the minimum width of each generated H1-H4 range. -const hMinWidth = 1000 - // hMaxValid is the largest value ValidateObfuscation accepts for an H // parameter: uint32 max, the kernel's own limit. const hMaxValid int64 = 4294967295 @@ -56,7 +53,7 @@ func GenerateObfuscation31() Obfuscation31 { o.S3 = randInt(12, 55) // cookie padding (max 64) o.S4 = randInt(12, 27) // transport padding (max 32) - h := generateHRanges() + h := generateHValues() o.H1, o.H2, o.H3, o.H4 = h[0], h[1], h[2], h[3] // CPS signature packet, N random bytes before each handshake. I2-I5 stay @@ -109,19 +106,16 @@ func generateHeaderProtectionKey() string { return base64.StdEncoding.EncodeToString(key) } -// generateHRanges returns four non-overlapping "low-high" ranges for H1-H4, -// one per band of the space so non-overlap needs no retries. The low bound is -// >= 5: values 1-4 are reserved for vanilla WireGuard message types. -func generateHRanges() [4]string { +// generateHValues returns one distinct value per H1-H4 band; low bound >= 5 (1-4 are vanilla WG message types). +// Single values, not ranges: with RandomTrailers on, a wide range misclassifies transport packets as handshakes (amnezia-vpn/amneziawg-go#183). +func generateHValues() [4]string { const lo = 5 bandSize := (awgHMax - lo + 1) / 4 var out [4]string for i := 0; i < 4; i++ { bandLo := lo + i*bandSize bandHi := bandLo + bandSize - 1 - start := randInt(bandLo, bandHi-hMinWidth-1) - end := randInt(start+hMinWidth, bandHi-1) - out[i] = fmt.Sprintf("%d-%d", start, end) + out[i] = fmt.Sprintf("%d", randInt(bandLo, bandHi)) } return out } diff --git a/internal/amneziawg/params_test.go b/internal/amneziawg/params_test.go index ef9e6903e..7ba57b5c9 100644 --- a/internal/amneziawg/params_test.go +++ b/internal/amneziawg/params_test.go @@ -95,24 +95,19 @@ func assertRangeWithin(t *testing.T, name, v string, min, max int64) (lo, hi int return lo, hi } -func TestGenerateHRangesNonOverlapping(t *testing.T) { +func TestGenerateHValuesDistinct(t *testing.T) { for i := 0; i < 50; i++ { - h := generateHRanges() - var prevHi int64 - for i, r := range h { - lo, hi, ok := strings.Cut(r, "-") - if !ok { - t.Fatalf("H%d = %q is not a range", i+1, r) + h := generateHValues() + var prev int64 + for i, v := range h { + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + t.Fatalf("H%d = %q is not a plain integer: %v", i+1, v, err) } - loN, _ := strconv.ParseInt(lo, 10, 64) - hiN, _ := strconv.ParseInt(hi, 10, 64) - if loN <= prevHi { - t.Fatalf("H%d = %q overlaps or touches the previous range (prev high=%d)", i+1, r, prevHi) + if n <= prev { + t.Fatalf("H%d = %q is not strictly greater than the previous value (%d)", i+1, v, prev) } - if hiN-loN < hMinWidth { - t.Fatalf("H%d = %q is narrower than hMinWidth=%d", i+1, r, hMinWidth) - } - prevHi = hiN + prev = n } } } diff --git a/internal/amneziawgnet/netstack.go b/internal/amneziawgnet/netstack.go index a75df0146..5b3045141 100644 --- a/internal/amneziawgnet/netstack.go +++ b/internal/amneziawgnet/netstack.go @@ -24,7 +24,8 @@ import ( ) // tunQueueDepth is the outbound queue depth for channel endpoint and handoff. -const tunQueueDepth = 1024 +// 1024 starved simultaneous TCP slow-starts; channel.Endpoint drops silently when full. +const tunQueueDepth = 8192 // stackTun implements amneziawg-go tun.Device over a gVisor channel endpoint, // exposing *stack.Stack for forwarder attachment.