mirror of
https://github.com/amnezia-vpn/amneziawg-go.git
synced 2026-10-13 21:34:25 +00:00
[PR #183] fix: random trailers cause ~3.5% of transport packets to be misclassified as handshakes #191
Labels
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
amnezia-vpn/amneziawg-go#191
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/amnezia-vpn/amneziawg-go/pull/183
Author: @AYastrebov
Created: 8/25/2026
Status: 🔄 Open
Base:
master← Head:fix/random-trailers-transport-misclassification📝 Commits (3)
afe2fc5fix: stop random trailers stealing ~3.5% of transport packets67a2b96fix: keep exact-size handshakes ahead of the transport test2b855b9test: pin that equal S1-S4 avoids the classification collision📊 Changes
2 files changed (+344 additions, -6 deletions)
View changed files
📝
device/receive.go(+60 -6)➕
device/receive_awg_test.go(+284 -0)📄 Description
Summary
With
RandomTrailersenabled andS1–S4set to different values, roughly 3.5% of all transport packets are silently dropped as unparseable handshake messages. That is enough to collapse a TCP sender across the tunnel — I measured a real link fall from ~30 Mbit/s to ~0.4 Mbit/s, with the loss disappearing entirely whenRandomTrailerswas turned off and nothing else changed.The config dependency matters, and I want to put it up front rather than bury it: setting
S1 = S2 = S3 = S4avoids the collision completely, and upstream documentation already recommends exactly that when random trailers are in use, "to reduce the risk of incorrect packet type detection."So this is not a claim that the protocol is broken. It is a claim that the classifier silently depends on a configuration invariant it never checks, never warns about, and that generators in the wild do violate — and that when the invariant is broken the failure mode is 3.5% packet loss with no counter and no log line.
The bug
DeterminePacketTypeAndPaddingtests the four message types in order: init, response, cookie, then transport. Random trailers relax the three handshake size tests from==to>, which is necessary — a trailer makes a handshake message longer than its fixed size:A transport packet is longer than any handshake message, so that relaxed test matches every full-size data packet too. The only thing then separating a data packet from a handshake misparse is the type-range check.
Each branch reads its candidate type field at its own
Soffset:That is the crux. In a transport packet the real type field sits at
S4. WhenS1 != S4, the initiation branch reads four bytes from somewhere else — junk prefix or ciphertext — which are uniformly random, and a header range spanningnof the 2^32 values claimsn / 2^32of them. The client config generator emits H1–H4 ranges 50,000,000 wide, which is 1.164% each, and three handshake ranges are tried before transport:When all four
Svalues are equal, every branch reads the real type field instead. That value was drawn from H4, and since H1–H4 must not overlap it cannot match H1, H2 or H3 — the collision rate drops to exactly zero.Measured against unpatched
master, 200,000 synthetic transport packets, 50M-wide ranges:S1/S2/S3/S4Those packets fail their MAC and are dropped with no counter and no log line. By the Mathis model, 3.5% loss over a 170 ms path at a 1373-byte MSS caps a flow at about 0.35 Mbit/s, which matches what I measured.
Why fix it in code if the docs already say to use equal values
docker-amneziawgcontainer emitted independently randomisedS1–S4alongsideRandomTrailers = onfor everyAWG_VERSION=3.1deployment until I fixed it this week; it is unlikely to be the only one.The fix
Test transport first when random trailers are on.
Classification is a purely local decision that never appears on the wire, so the order costs no interoperability — a patched peer and an unpatched peer interoperate. It moves the same collision onto handshake messages (~1.16% via H4), which are rare and retried, instead of onto every data packet.
With the patch applied, both configurations are clean:
S1/S2/S3/S4A stronger fix would verify MAC1 before accepting a handshake match that only passed because of the relaxed size test, which would remove both collisions rather than trading one for the other. That is a larger change and I am happy to take this in that direction if you prefer it.
Tests
TestDeterminePacketTypeTransportNotStolenByRelaxedHandshakeSizesfeeds 200,000 synthetic transport packets (S4 padding prefix, a type value from H4, ciphertext-shaped body) throughDeterminePacketTypeAndPaddingwith realistic 50M-wide ranges and counts how many come back as anything other than transport.On master:
With this change:
PASS, 0 misclassified.TestDeterminePacketTypeEqualPaddingsNeverCollidepins the configuration dependency in code rather than leaving it in prose. It passes both before and after the fix — it is a guard, not a reproducer, documenting why some deployments never saw this and failing if a future change makes classification depend on something other than the type ranges.TestDeterminePacketTypeTransportExactWithoutRandomTrailerspins theRandomTrailers = offpath, which was never affected and must stay that way.Notes
awg_determine_type_and_paddinginsrc/receive.c, therandom_trailers ? skb->len >= expected_len : skb->len == expected_lentests), so it needs the same change there. Happy to open that PR too.TestAWGDevicePingfails on unmodifiedmaster, before and after this change. It is unrelated to this PR — #184 fixes it.Verification
Measured end to end against a real AmneziaWG 3.1 server, changing one parameter at a time on both ends. The
RandomTrailersrows are the ones that carry the argument; the other rows are single runs on a ~170 ms link and are indicative only.RandomTrailersRandomTrailersaloneRandomTrailersis the only parameter that reproduces the collapse, and it reproduces it alone. Fixing the classifier on the receiving side restored that side's throughput to parity with an AmneziaWG 2.0 tunnel to the same host.Independently reproduced on a second setup — kernel module 3.1.20260812 server,
amneziawg-go3.1.20260814 client, 22 ms path — where the same misconfiguration measured 1.7 Mbit/s up against a 100 Mbit/s baseline, and settingS1 = S2 = S3 = S4with no other change restored it to 95–99 Mbit/s.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.