mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-04 17:37:19 +00:00
fix(amneziawg): H1-H4 generator + queue-depth throughput fixes (#6330)
* 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.
This commit is contained in:
@@ -141,10 +141,10 @@ S1 = 87
|
|||||||
S2 = 44
|
S2 = 44
|
||||||
S3 = 21
|
S3 = 21
|
||||||
S4 = 9
|
S4 = 9
|
||||||
H1 = 462980921-463150218
|
H1 = 463065432
|
||||||
H2 = 1177681572-1177787900
|
H2 = 912345678
|
||||||
H3 = 1907413509-1907903969
|
H3 = 1345678901
|
||||||
H4 = 2029908558-2030313135
|
H4 = 1987654321
|
||||||
I1 = <r 148>
|
I1 = <r 148>
|
||||||
HeaderProtectionKey = 8Iu83eHDA3fMKKSGaEsVW9Ycd2lYYzc0MYlk1jJTvE4=
|
HeaderProtectionKey = 8Iu83eHDA3fMKKSGaEsVW9Ycd2lYYzc0MYlk1jJTvE4=
|
||||||
ContentPaddingAddition = 17-49
|
ContentPaddingAddition = 17-49
|
||||||
|
|||||||
@@ -51,21 +51,17 @@ const generateHeaderProtectionKey = (): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Four non-overlapping "low-high" ranges for H1-H4: split the space into
|
* Four distinct values for H1-H4, one per band; low bound >= 5 (1-4 are vanilla WG message types).
|
||||||
* four bands and take a random sub-range from each (>= 1000 wide, low
|
* Single values, not ranges: with randomTrailers on, a wide range misclassifies transport packets as handshakes (amnezia-vpn/amneziawg-go#183).
|
||||||
* bound >= 5 since 1-4 are reserved for vanilla WireGuard message types).
|
|
||||||
*/
|
*/
|
||||||
const generateHRanges = (): [string, string, string, string] => {
|
const generateHValues = (): [string, string, string, string] => {
|
||||||
const hMax = 2147483647;
|
const hMax = 2147483647;
|
||||||
const hMinWidth = 1000;
|
|
||||||
const lo = 5;
|
const lo = 5;
|
||||||
const bandSize = Math.floor((hMax - lo + 1) / 4);
|
const bandSize = Math.floor((hMax - lo + 1) / 4);
|
||||||
return Array.from({ length: 4 }, (_, i) => {
|
return Array.from({ length: 4 }, (_, i) => {
|
||||||
const bandLo = lo + i * bandSize;
|
const bandLo = lo + i * bandSize;
|
||||||
const bandHi = bandLo + bandSize - 1;
|
const bandHi = bandLo + bandSize - 1;
|
||||||
const start = randInt(bandLo, bandHi - hMinWidth - 1);
|
return `${randInt(bandLo, bandHi)}`;
|
||||||
const end = randInt(start + hMinWidth, bandHi - 1);
|
|
||||||
return `${start}-${end}`;
|
|
||||||
}) as [string, string, string, string];
|
}) as [string, string, string, string];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,7 +72,7 @@ export function generateAwgObfuscation(): AwgObfuscation {
|
|||||||
while (s1 + 56 === s2) {
|
while (s1 + 56 === s2) {
|
||||||
s2 = randInt(15, 150);
|
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
|
* Timing windows bracket WireGuard's stock constants (rekey 120s, reject
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ function expectRangeWithin(value: string, min: number, max: number): [number, nu
|
|||||||
return [lo, 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', () => {
|
describe('generateAwgObfuscation', () => {
|
||||||
it('stays inside the Go generator ranges and invariants', () => {
|
it('stays inside the Go generator ranges and invariants', () => {
|
||||||
for (let i = 0; i < 200; i++) {
|
for (let i = 0; i < 200; i++) {
|
||||||
@@ -37,9 +47,11 @@ describe('generateAwgObfuscation', () => {
|
|||||||
expect(o.s4).toBeGreaterThanOrEqual(12);
|
expect(o.s4).toBeGreaterThanOrEqual(12);
|
||||||
expect(o.s4).toBeLessThanOrEqual(27);
|
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++) {
|
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(/^<r \d+>$/);
|
expect(o.i1).toMatch(/^<r \d+>$/);
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ import (
|
|||||||
// but the amneziawg-windows-client config editor rejects anything above.
|
// but the amneziawg-windows-client config editor rejects anything above.
|
||||||
const awgHMax = 2147483647
|
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
|
// hMaxValid is the largest value ValidateObfuscation accepts for an H
|
||||||
// parameter: uint32 max, the kernel's own limit.
|
// parameter: uint32 max, the kernel's own limit.
|
||||||
const hMaxValid int64 = 4294967295
|
const hMaxValid int64 = 4294967295
|
||||||
@@ -56,7 +53,7 @@ func GenerateObfuscation31() Obfuscation31 {
|
|||||||
o.S3 = randInt(12, 55) // cookie padding (max 64)
|
o.S3 = randInt(12, 55) // cookie padding (max 64)
|
||||||
o.S4 = randInt(12, 27) // transport padding (max 32)
|
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]
|
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
|
// CPS signature packet, N random bytes before each handshake. I2-I5 stay
|
||||||
@@ -109,19 +106,16 @@ func generateHeaderProtectionKey() string {
|
|||||||
return base64.StdEncoding.EncodeToString(key)
|
return base64.StdEncoding.EncodeToString(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateHRanges returns four non-overlapping "low-high" ranges for H1-H4,
|
// generateHValues returns one distinct value per H1-H4 band; low bound >= 5 (1-4 are vanilla WG message types).
|
||||||
// one per band of the space so non-overlap needs no retries. The low bound is
|
// Single values, not ranges: with RandomTrailers on, a wide range misclassifies transport packets as handshakes (amnezia-vpn/amneziawg-go#183).
|
||||||
// >= 5: values 1-4 are reserved for vanilla WireGuard message types.
|
func generateHValues() [4]string {
|
||||||
func generateHRanges() [4]string {
|
|
||||||
const lo = 5
|
const lo = 5
|
||||||
bandSize := (awgHMax - lo + 1) / 4
|
bandSize := (awgHMax - lo + 1) / 4
|
||||||
var out [4]string
|
var out [4]string
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
bandLo := lo + i*bandSize
|
bandLo := lo + i*bandSize
|
||||||
bandHi := bandLo + bandSize - 1
|
bandHi := bandLo + bandSize - 1
|
||||||
start := randInt(bandLo, bandHi-hMinWidth-1)
|
out[i] = fmt.Sprintf("%d", randInt(bandLo, bandHi))
|
||||||
end := randInt(start+hMinWidth, bandHi-1)
|
|
||||||
out[i] = fmt.Sprintf("%d-%d", start, end)
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,24 +95,19 @@ func assertRangeWithin(t *testing.T, name, v string, min, max int64) (lo, hi int
|
|||||||
return lo, hi
|
return lo, hi
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateHRangesNonOverlapping(t *testing.T) {
|
func TestGenerateHValuesDistinct(t *testing.T) {
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
h := generateHRanges()
|
h := generateHValues()
|
||||||
var prevHi int64
|
var prev int64
|
||||||
for i, r := range h {
|
for i, v := range h {
|
||||||
lo, hi, ok := strings.Cut(r, "-")
|
n, err := strconv.ParseInt(v, 10, 64)
|
||||||
if !ok {
|
if err != nil {
|
||||||
t.Fatalf("H%d = %q is not a range", i+1, r)
|
t.Fatalf("H%d = %q is not a plain integer: %v", i+1, v, err)
|
||||||
}
|
}
|
||||||
loN, _ := strconv.ParseInt(lo, 10, 64)
|
if n <= prev {
|
||||||
hiN, _ := strconv.ParseInt(hi, 10, 64)
|
t.Fatalf("H%d = %q is not strictly greater than the previous value (%d)", i+1, v, prev)
|
||||||
if loN <= prevHi {
|
|
||||||
t.Fatalf("H%d = %q overlaps or touches the previous range (prev high=%d)", i+1, r, prevHi)
|
|
||||||
}
|
}
|
||||||
if hiN-loN < hMinWidth {
|
prev = n
|
||||||
t.Fatalf("H%d = %q is narrower than hMinWidth=%d", i+1, r, hMinWidth)
|
|
||||||
}
|
|
||||||
prevHi = hiN
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// tunQueueDepth is the outbound queue depth for channel endpoint and handoff.
|
// 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,
|
// stackTun implements amneziawg-go tun.Device over a gVisor channel endpoint,
|
||||||
// exposing *stack.Stack for forwarder attachment.
|
// exposing *stack.Stack for forwarder attachment.
|
||||||
|
|||||||
Reference in New Issue
Block a user