[PR #137] [CLOSED] device: apply S4 transport padding to keepalive packets #161

Closed
opened 2026-10-13 21:04:09 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/amnezia-vpn/amneziawg-go/pull/137
Author: @remotetohome
Created: 5/13/2026
Status: ❌ Closed

Base: master ← Head: fix/transport-s4-keepalive-padding


📝 Commits (1)

  • 5e025ba device: apply S4 transport padding to keepalive packets

📊 Changes

2 files changed (+146 additions, -8 deletions)

View changed files

📝 device/device_test.go (+138 -0)
📝 device/send.go (+8 -8)

📄 Description

@ygurov - was finally able to hunt this down, but got it working.

I reproduced my initial test. I set the s3/4 back to non-zero values (38/22) on the server side (linux kernel).

I then tested the exact same client profile side by side on my Kubuntu laptop.. just as before:

  1. Control: awg-quick (kernel) = 258/242 Mbps, healthy.
  2. Linux AmneziaVPN desktop client (awg-go) = Handshake okay but no traffic passes. Tunnel dead in 28s.

After monitoring both the server and desktop client via the daemon socket (then putting Claude to work on the logs), I was able to isolate and draft fix - then retested (exact reproduction with same profie) = desktop client works!

This issue is awg-go isn't adding the s4 padding on the keepalive packets so when the server strips the expected s4 padding from the keepalive, it actually mangles the real data instead and then drops it. The tunnel then dies soon after initial setup.

Had Claude attend to the PR writeup:


Summary

RoutineSequentialSender skipped S4 padding for keepalive packets (len(elem.packet) != MessageKeepaliveSize guard), while the kernel module's encrypt_packet() (send.c:210) unconditionally prepends S4 random bytes to all transport packets including keepalives. This size mismatch caused the kernel module to reject awg-go client keepalives when S4 > 0:

  1. awg-go sends a 32-byte keepalive (no S4 prefix)
  2. Kernel module strips the first S4 bytes (expecting them to be random padding)
  3. Remaining bytes are too short, or the H4 field at offset 0 doesn't validate
  4. Keepalive is silently dropped

Without keepalives, the server-side keypair for the client never gets promoted from next to current (requires ReceivedWithKeypair), and the connection dies shortly after handshake.

The fix

Move the S4 padding block outside the keepalive-size check (2 lines changed in send.go). The dataSent flag — which controls timersDataSent() — remains gated on the size check, preserving WireGuard timer semantics (keepalives don't trigger data-sent timers).

Reproduction

Same laptop, same server (amneziawg.ko v1.0.20260329-2), same AWG profile (S3=38, S4=22, full STUN preset), three client implementations:

Client Implementation Result
awg-quick kernel module 258/242 Mbps, healthy
AmneziaVPN 4.8.11.4 desktop upstream awg-go Handshake OK → 0 data → dead
Patched awg-go (this PR) CGO_ENABLED=0 Keepalives flowing, 5/5 pings, 94ms avg

Server-side awg show confirmed: upstream awg-go client transmitted 0 bytes of transport data across 4 connection attempts. Patched binary: immediate symmetric traffic flow with keepalives every 25 seconds.

Patched awg-go verbose log (keepalive cycle)
(awgtest0) 22:41:02 peer(sNG6…/iBs) - Sending handshake initiation
(awgtest0) 22:41:03 peer(sNG6…/iBs) - Received handshake response
(awgtest0) 22:41:03 peer(sNG6…/iBs) - Receiving keepalive packet
(awgtest0) 22:41:17 peer(sNG6…/iBs) - Sending keepalive packet
(awgtest0) 22:41:42 peer(sNG6…/iBs) - Sending keepalive packet
(awgtest0) 22:41:56 peer(sNG6…/iBs) - Sending keepalive packet

No "Retrying handshake" messages — connection stays alive indefinitely.

Test coverage

Added TestKeepaliveIncludesS4Padding: uses a spy bind to capture raw wire packets, verifies keepalive size is exactly S4 + MessageKeepaliveSize (not bare MessageKeepaliveSize). Passes with both CGO_ENABLED=0 and CGO_ENABLED=1.

Cross-references

  • Related to #110
  • Related: amnezia-client#2582 (AmneziaVPN-Android transport failure with non-zero S3/S4)
  • Related: #89 (transport packet msg-type confusion)

Acknowledgments

  • @FrankBacon24 for filing #110 with the initial diagnostic analysis
  • @websharik for confirming reproduction on amd64 with CGO disabled

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/amnezia-vpn/amneziawg-go/pull/137 **Author:** [@remotetohome](https://github.com/remotetohome) **Created:** 5/13/2026 **Status:** ❌ Closed **Base:** `master` ← **Head:** `fix/transport-s4-keepalive-padding` --- ### 📝 Commits (1) - [`5e025ba`](https://github.com/amnezia-vpn/amneziawg-go/commit/5e025baaa28ba54066c15a82ed51e54cf3d2f1dc) device: apply S4 transport padding to keepalive packets ### 📊 Changes **2 files changed** (+146 additions, -8 deletions) <details> <summary>View changed files</summary> 📝 `device/device_test.go` (+138 -0) 📝 `device/send.go` (+8 -8) </details> ### 📄 Description @ygurov - was finally able to hunt this down, but got it working. I reproduced my initial test. I set the s3/4 back to non-zero values (38/22) on the server side (linux kernel). I then tested the exact same client profile side by side on my Kubuntu laptop.. just as before: 1) Control: awg-quick (kernel) = 258/242 Mbps, healthy. 2) Linux AmneziaVPN desktop client (awg-go) = Handshake okay but no traffic passes. Tunnel dead in 28s. After monitoring both the server and desktop client via the daemon socket (then putting Claude to work on the logs), I was able to isolate and draft fix - then retested (exact reproduction with same profie) = desktop client works! This issue is awg-go isn't adding the s4 padding on the keepalive packets so when the server strips the expected s4 padding from the keepalive, it actually mangles the real data instead and then drops it. The tunnel then dies soon after initial setup. Had Claude attend to the PR writeup: ------- ## Summary `RoutineSequentialSender` skipped S4 padding for keepalive packets (`len(elem.packet) != MessageKeepaliveSize` guard), while the kernel module's `encrypt_packet()` ([send.c:210](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/blob/master/src/send.c#L216)) unconditionally prepends S4 random bytes to **all** transport packets including keepalives. This size mismatch caused the kernel module to reject awg-go client keepalives when S4 > 0: 1. awg-go sends a 32-byte keepalive (no S4 prefix) 2. Kernel module strips the first S4 bytes (expecting them to be random padding) 3. Remaining bytes are too short, or the H4 field at offset 0 doesn't validate 4. Keepalive is silently dropped Without keepalives, the server-side keypair for the client never gets promoted from `next` to `current` (requires `ReceivedWithKeypair`), and the connection dies shortly after handshake. ### The fix Move the S4 padding block outside the keepalive-size check (2 lines changed in `send.go`). The `dataSent` flag — which controls `timersDataSent()` — remains gated on the size check, preserving WireGuard timer semantics (keepalives don't trigger data-sent timers). ## Reproduction Same laptop, same server (amneziawg.ko v1.0.20260329-2), same AWG profile (S3=38, S4=22, full STUN preset), three client implementations: | Client | Implementation | Result | |--------|---------------|--------| | `awg-quick` | kernel module | 258/242 Mbps, healthy | | AmneziaVPN 4.8.11.4 desktop | upstream awg-go | Handshake OK → 0 data → dead | | **Patched awg-go** (this PR) | `CGO_ENABLED=0` | **Keepalives flowing, 5/5 pings, 94ms avg** | Server-side `awg show` confirmed: upstream awg-go client transmitted 0 bytes of transport data across 4 connection attempts. Patched binary: immediate symmetric traffic flow with keepalives every 25 seconds. <details> <summary>Patched awg-go verbose log (keepalive cycle)</summary> ``` (awgtest0) 22:41:02 peer(sNG6…/iBs) - Sending handshake initiation (awgtest0) 22:41:03 peer(sNG6…/iBs) - Received handshake response (awgtest0) 22:41:03 peer(sNG6…/iBs) - Receiving keepalive packet (awgtest0) 22:41:17 peer(sNG6…/iBs) - Sending keepalive packet (awgtest0) 22:41:42 peer(sNG6…/iBs) - Sending keepalive packet (awgtest0) 22:41:56 peer(sNG6…/iBs) - Sending keepalive packet ``` No "Retrying handshake" messages — connection stays alive indefinitely. </details> ## Test coverage Added `TestKeepaliveIncludesS4Padding`: uses a spy bind to capture raw wire packets, verifies keepalive size is exactly `S4 + MessageKeepaliveSize` (not bare `MessageKeepaliveSize`). Passes with both `CGO_ENABLED=0` and `CGO_ENABLED=1`. ## Cross-references - Related to #110 - Related: [amnezia-client#2582](https://github.com/amnezia-vpn/amnezia-client/issues/2582) (AmneziaVPN-Android transport failure with non-zero S3/S4) - Related: #89 (transport packet msg-type confusion) ## Acknowledgments - @FrankBacon24 for filing #110 with the initial diagnostic analysis - @websharik for confirming reproduction on amd64 with CGO disabled --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-13 21:04:09 +00:00
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amneziawg-go#161
No description provided.