mirror of
https://github.com/amnezia-vpn/amneziawg-go.git
synced 2026-10-13 21:34:25 +00:00
[PR #184] fix: send first TUN packet with the configured S4 padding #194
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#194
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/184
Author: @AYastrebov
Created: 8/25/2026
Status: 🔄 Open
Base:
master← Head:fix/tun-padding-race-on-first-packet📝 Commits (3)
b84b56ffix: send first TUN packet with the configured S4 padding0e204detest: cover the S4 padding captured before the interface is configuredfcbeecftest: close devices built before a later one aborts the test📊 Changes
2 files changed (+176 additions, -2 deletions)
View changed files
📝
device/send.go(+27 -2)➕
device/send_awg_test.go(+149 -0)📄 Description
Problem
The first packet an AmneziaWG interface sends is normally dropped by the peer.
RoutineReadFromTUNis started byNewDevice, beforeIpcSethas applied any interface configuration. It has to choose a read offset before it can read, so it loads S4 and then blocks intun.Read— capturing, on that first pass, the default S4 of0rather than the configured value.RoutineEncryptionwrites the transport header atbuffer[padding:padding+MessageTransportHeaderSize]and requires the payload to follow it directly, so that datagram goes out with its type field at offset 0 while the peer looks for it at offset S4. The peer decodes ciphertext there, matches no header range, and drops the packet as unknown.The same window applies to any later
s4change over UAPI, which took effect one batch of packets late.Evidence
This is what makes
TestAWGDevicePingflaky — it passed roughly 1 run in 5 on master. On a second machine (Linux, 20 cores) it is not flaky at all but simply broken: 0 runs out of 6 passed on master, and 6 out of 6 with this change. The window is a scheduling race, so how often it is lost depends on the machine. Each device in that test sends exactly one packet, so the dropped first packet has no chance to recover and the test times out.Instrumenting the padding each TUN reader captured correlated perfectly with the outcome:
pong(dev0→dev1) failsRun 3 is the clincher: dev0 captured 0, and
pongis exactly the direction dev0 sends.Receiver-side instrumentation agreed — the dropped datagram was 64 bytes (16 header + 32 content + 16 tag, i.e. zero crypto padding) carrying a random value at offset 25, against a configured
h4of32345-32350.Fix
The offset still has to be chosen before the read, so S4 is re-read once the packets are in hand and they are re-aligned within their buffer if it moved.
copyis memmove-safe for overlapping slices; a packet is dropped if a larger S4 leaves no room for it.Test
The bug is a scheduling race, so the test removes the scheduling from it. A TUN wrapper closes a channel on its first
Readbefore delegating, which proves the reader has already captured its S4 and parked. The test waits on that for both devices, asserts the captured offset still corresponds to S4 = 0 (i.e. the window is genuinely open), and only then appliess1–s4/h1–h4. The ping that follows is the first packet either interface sends.Verification
go build ./...,gofmt -l device/— cleango test ./...— all packages passgo test -race ./device/— passesTestAWGDevicePing— 10/10, previously ~1 in 5 (and 6/6 against 0/6 on a second machine)go vetstill reports the pre-existing copylock warning atdevice/pools_test.go:67, which is unrelated to this change.🤖 Generated with Claude Code
https://claude.ai/code/session_01TGVkr62MPBrFFzTZdXPs7e
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.