Skip to content

device: first transport packet can use stale S4 after UAPI configuration #168

Description

@coremraz

Suggested title

device: first transport packet can use stale S4 after UAPI configuration

Affected upstream source

  • Repository: https://github.com/amnezia-vpn/amneziawg-go
  • Version: v3.0.3
  • Immutable revision: cf9d2dd202821301f7039093b0a1b3d4b574c47c
  • At investigation time, upstream master was identical to that revision.
  • Linux test environment: golang:1.25.12, Docker with CAP_NET_ADMIN and
    /dev/net/tun.

Safe reproduction

This reproducer uses only upstream tests, localhost UDP and freshly generated
test keys. It contains no gateway address, production key, client config or
credential.

docker run --rm --cap-add=NET_ADMIN --device /dev/net/tun \
  -v $PWD:/src:ro -w /src golang:1.25.12 \
  go test ./device -run '^TestAWGDevicePing$' -race=false -count=10

Observed on the pinned revision: the AWG test fails after a successful
handshake with a ping/pong transit timeout. A profile bisect showed that
S4=25 is sufficient: seven of ten isolated runs failed; H1–H4 alone and
S1–S3 alone passed.

Causal evidence

Device.NewDevice starts RoutineReadFromTUN before the caller has applied
UAPI configuration. In device/send.go, that routine loads
device.paddings.transport, calculates offset, and then blocks in
tun.device.Read. If UAPI commits S4=25 while that read is waiting, the
first returned packet is still associated with the stale local S4=0.

The receiver has already applied S4 and therefore calls
DeterminePacketTypeAndPadding expecting the transport header at offset 25.
It rejects the packet before key lookup or AEAD decrypt.

Temporary instrumentation in a disposable source copy correlated one failing
run as follows:

  1. sender TUN reader started the blocking read with S4=0;
  2. UAPI committed S4=25 on both devices;
  3. sender returned the first packet with its original S4=0 snapshot;
  4. receiver reported s4=25, expected H4/type 4, observed type 4 at byte
    zero and random bytes at byte 25, then logged Received message with unknown type.

This is a stale snapshot across a blocking syscall, not a key exchange,
header-protection-key, routing or NAT failure.

Candidate direction for maintainer review

The following was validated only in an ephemeral Docker copy; it is not a
proposed final patch. Immediately after Read, re-load the current transport
padding. If it changed, move every just-read payload from the original offset
to the current offset using overlap-safe copy, then use that same current
padding for the outgoing element:

count, readErr = device.tun.device.Read(bufs, sizes, offset)
currentPadding := device.paddings.transport.Load()
if currentPadding != padding {
    currentOffset := MessageTransportHeaderSize + int(currentPadding)
    for i := 0; i < count; i++ {
        copy(bufs[i][currentOffset:currentOffset+sizes[i]],
            bufs[i][offset:offset+sizes[i]])
    }
    padding = currentPadding
    offset = currentOffset
}

With that source-copy change:

  • S4-only reproduction passed 10/10 with -race=false;
  • full TestAWGDevicePing passed 10/10 with -race=false;
  • full TestAWGDevicePing passed 10/10 with -race.

Please advise whether re-basing the packet buffer after Read is the intended
configuration-update behavior, or whether initial UAPI configuration should
instead gate the first TUN read. A final upstream patch should include a
deterministic regression test for this startup ordering.

Downstream safety posture

No production data plane has been enabled from this finding. The downstream
canary remains zero-peer with UDP closed and forwarding/NAT disabled. No local
fork or S4 workaround will be deployed before a reviewed upstream or otherwise
immutable-pinned fix, exact-artifact verification, and closed synthetic-peer
staging.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions