Skip to content

fix(amneziawg): H1-H4 generator + queue-depth throughput fixes - #6330

Open
kuzzrus wants to merge 3 commits into
MHSanaei:mainfrom
kuzzrus:fix/awg-h-values-and-queue-depth
Open

fix(amneziawg): H1-H4 generator + queue-depth throughput fixes#6330
kuzzrus wants to merge 3 commits into
MHSanaei:mainfrom
kuzzrus:fix/awg-h-values-and-queue-depth

Conversation

@kuzzrus

@kuzzrus kuzzrus commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Two independent fixes for AmneziaWG throughput, both found and live-tested while investigating #6303 ("AmneziaWG import speed is extremely poor").

1. H1-H4 generator: single value instead of a range

GenerateObfuscation31 (and its frontend mirror, generateAwgObfuscation) picked a random range for each of H1-H4, with only a minimum width enforced (hMinWidth = 1000), no maximum. amneziawg-go's packet classifier (DeterminePacketTypeAndPadding) only ever compares a fixed-size ciphertext prefix against these bounds to decide "handshake vs. transport" — so a wider range buys zero DPI resistance (the range boundaries themselves are never observable on the wire, only whichever single value the classifier lets through matters). It does cost real throughput: randomTrailers is generated true here, and with it on, amneziawg-go's handshake-size checks relax from == to >, so a wide H-range starts misclassifying a proportional fraction of ordinary transport packets as handshakes and silently dropping them. This was independently found and filed the same week as amnezia-vpn/amneziawg-go#183, with a synthetic-packet regression test proving the mechanism.

Live-tested on two boxes, single-variable (only H1-H4 narrowed, nothing else changed): upload went from 2-3 Mbit/s to 200+ Mbit/s on one, and a flat ~20 Mbit/s to 120-156 Mbit/s on another.

A single value per H field is strictly safer than any range — the same reasoning @alekskomp's own native-Amnezia-Docker config in #6303 already uses (H1=1, H2=2, H3=3, H4=4). This changes what the generator produces, not what's accepted: a manually-entered range still validates fine (ValidateObfuscation/validateUintRange untouched).

Fixed in both places that generate these values, since they're independent implementations that could silently drift apart again: internal/amneziawg/params.go (Go, server-side) and frontend/src/lib/xray/amneziawg-obfuscation.ts (what the inbound form's "Regenerate" button calls).

2. tunQueueDepth: 1024 → 8192

A separate contributor to the same "AmneziaWG slower than expected" symptom, specifically on the download side. 1024 was originally sized for a single-connection buffering problem (the gVisor-to-amneziawg-go TUN handoff channel — see the pre-existing comment above the constant). Watching tcpip.Stack.Stats() live 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 several connections' simultaneous slow-start growth briefly exceeding 1024 outstanding packets, with gVisor's channel endpoint (whose queue.Write is non-blocking and drops silently when full) treating the resulting drops as ordinary network loss.

Context

Both root causes, with the live-test numbers, are written up in more detail in my comment on #6303.

One thing worth flagging for review: GenerateObfuscation31 always turns RandomTrailers on for a freshly generated set (it's a normal toggle a user can flip off afterward, but "on" is what every new/regenerated inbound gets) — which is exactly the setting that makes the H1-H4 misclassification bug bite. This PR doesn't touch that default; a single H-value is safe regardless of randomTrailers, so the two are orthogonal. Flagging only so reviewers have the full picture — not proposing a change there.

Test plan

  • go vet / go test on internal/amneziawg and internal/amneziawgnet
  • tsc --noEmit, oxlint, oxfmt --check, vitest run on the touched frontend files
  • Live-tested the H1-H4 fix on two real servers (numbers above)

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.
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.
Comment thread internal/amneziawg/params.go Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

No blocking issues — 0 🔴 / 1 🟡 / 0 🟣.

Reviewed head: 6b73226989698d529d2b993b236dc5409ce44e77 (5 files, +69 −38).

🟡 Nit

Coverage

  • Generator change (Go + TS) — band arithmetic checked: bands are [5,536870914] [536870915,1073741824] [1073741825,1610612734] [1610612735,2147483644], disjoint, ascending, top <= awgHMax; the largest intermediate fits int32, so the 386/armv5-7 builds are safe (and they passed).
  • Wire format — every H1-H4 consumer passes the string through verbatim with a 1/2/3/4 fallback and never splits on -: internal/sub/service.go:740, internal/amneziawgnet/device.go:206, frontend/src/lib/xray/inbound-link.ts:990, frontend/src/pages/clients/amneziawgConfig.ts:32. A bare integer is already accepted by parseUintRange (internal/amneziawg/params.go:337-341) and pinned by the pre-existing TestValidateObfuscationAcceptsSingleValueRanges; the frontend schema is z.string() (frontend/src/schemas/protocols/inbound/amneziawg.ts:76-79). No divergence introduced.
  • Third implementationdocs/lib/xray/ has no AWG obfuscation generator, so the two files changed are the full set; nothing left to drift.
  • Tests fail without the fixstrconv.ParseInt (params_test.go:103) and the plain-integer regex (amneziawg-obfuscation.test.ts:29) both reject a lo-hi string, so reverting the generator turns both red. strings stays used in params_test.go; expectRangeWithin stays used at test lines 68-77.
  • tunQueueDepth 1024 -> 8192 — both uses are plain capacities (channel.New, make(chan *buffer.View), netstack.go:91,94); no fixed-size array, no gVisor limit, ~64 KB of pointer slots per device. It strictly reduces the chance of WriteNotify's blocking send stalling a gVisor sender.
  • CI on this headgo-test, golangci, frontend, codegen, govulncheck, fuzz-smoke, postgres-durable-first, CodeQL and all 8 build-matrix legs: success. race was still in progress at review time.
  • Not verified here — the upstream claim that DeterminePacketTypeAndPadding relaxes handshake-size checks from == to > under RandomTrailers: the amneziawg-go module source is outside this sandbox's readable paths, so that mechanism rests on the author's live tests and fix: random trailers cause ~3.5% of transport packets to be misclassified as handshakes amnezia-vpn/amneziawg-go#183, not on a citation I could check.
  • Observation, not a findingdocs/content/docs/en/config/amneziawg.mdx:144-147 still shows range-style sample output (H1 = 462980921-463150218). The prose above it ("a single integer or a low-high range") stays correct and nothing breaks, but the example no longer matches what a fresh inbound generates.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant