[GH-ISSUE #168] AWG 2.0 server with S3-S4 junk offsets cannot communicate with AWG clients that lack S3-S4 support #102

Open
opened 2026-10-13 21:06:32 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @otternetwork on GitHub (Apr 26, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/168

An AmneziaWG 2.0 server configured with S3-S4 junk offsets (S3>0, S4>0) cannot communicate with AmneziaWG clients that do not support S3-S4 junk offsets. This includes AWG 1.0 clients and AWG 2.0 clients configured without S3-S4. The S3/S4 junk prefix is applied unconditionally at the device level to all AWG peers, with no per-peer detection or fallback.

This is the same class of backward-compatibility regression as #162 (per-peer advanced_security for legacy WireGuard clients) and #163 (per-peer ranged_headers for AWG 1.0 vs 2.0 headers), but for the S3-S4 junk offset parameters.

Client classes affected

A server configured with S1>0, S2>0, S3>0, S4>0 and custom H1-H4 should be able to serve all of:

Client type S1-S2 S3-S4 H1-H4 Status
Legacy WireGuard 0 0 standard (1-4) Fixed by #162 via peer->advanced_security
AWG without S3-S4 >0 0 custom Broken — server prepends S4 junk to transport, client cannot parse
AWG with S3-S4 >0 >0 custom Works

Root cause

Send path — transport data (S4)

In src/send.c, wg_packet_encrypt_worker prepends S4 junk to transport data packets for ALL peers with advanced_security:

encrypt_packet(
    PACKET_PEER(first)->advanced_security ?
    mh_peerheader(&wg->headers[MSGIDX_TRANSPORT],
                  PACKET_PEER(first)->ranged_headers) :
    MESSAGE_DATA,
    PACKET_PEER(first)->advanced_security ?
    wg->junk_size[MSGIDX_TRANSPORT] : 0,
    ...

When S4>0, every AWG peer receives S4-prefixed transport packets regardless of whether the peer supports S4. An AWG client without S3-S4 sees an unexpected junk prefix and cannot find the transport header at the expected offset.

Similarly, wg_packet_send_handshake_cookie prepends S3 junk for all non-legacy peers:

cookie_junk = wg->junk_size[MSGIDX_HANDSHAKE_COOKIE];

A client without S3 expects the cookie header at offset 0, but receives it at offset S3.

Receive path — incoming transport data

In src/receive.c, prepare_awg_message tries to match transport packets at offset S4:

if (skb->len >= wg->junk_size[MSGIDX_TRANSPORT] + MESSAGE_TRANSPORT_SIZE) {
    skb_pull(skb, wg->junk_size[MSGIDX_TRANSPORT]);
    if (mh_validate(SKB_TYPE_LE32(skb), &wg->headers[MSGIDX_TRANSPORT]))
        return MESSAGE_TRANSPORT_SIZE;

When S4>0, an incoming packet from an AWG client without S4 has the AWG H4 header at offset 0, not at offset S4. The size check may also fail since the packet is smaller by S4 bytes. There is no fallback to try the AWG H4 header at offset 0 (without the S4 junk prefix).

The same issue exists for incoming cookie replies (S3 offset).

Relationship to existing per-peer flags

The codebase already has the pattern for per-peer capability detection:

  • peer->advanced_security — distinguishes legacy WireGuard peers from AWG peers. Detected during handshake consumption in noise.c by checking whether the incoming H1 header matches the AWG range. Fixes #162.
  • peer->ranged_headers — distinguishes AWG 1.0 (fixed headers) from AWG 2.0 (ranged headers). Detected by checking whether the incoming H1 value differs from the range start. Fixes #163.

A third per-peer flag is needed: peer->junk_offsets — distinguishes AWG peers with S3-S4 support from AWG peers without it.

Detection approach

Detection follows the same try-and-fallback pattern already used in prepare_awg_message for S1-S2. When S4>0, the function first tries to match at offset S4 with AWG H4. If that fails, it should try AWG H4 at offset 0 (no S4 junk). The result (whether junk offsets were present) is threaded through to wg_packet_consume_data, which sets the per-peer flag after the peer is identified via key_idx.

Note: when S4=0, the "strip S4" block already performs a no-op skb_pull(skb, 0), so the fallback is only needed when S4>0.

During cookie send (wg_packet_send_handshake_cookie), there is no peer context — the peer hasn't been fully identified yet. S3 junk on cookies must follow the server's device-level configuration for AWG peers. This is acceptable because cookie replies are rare (only under load), temporary, and the client will retry the handshake.

Suggested fix

  1. Add bool junk_offsets to struct wg_peer, following the advanced_security / ranged_headers pattern
  2. Default it from device config at peer creation: true when advanced_security && (S3>0 || S4>0)
  3. Detect during receive: prepare_awg_message tries AWG-with-S4 first, falls back to AWG-without-S4
  4. Thread detection result to wg_packet_consume_data via function arguments (same as had_junk_offsets bool)
  5. Gate S4 junk in wg_packet_encrypt_worker on peer->advanced_security && peer->junk_offsets
  6. Expose via UAPI (WGPEER_A_JUNK_OFFSETS) and netlink for userspace visibility
  7. Leave S3 cookie path unchanged (no peer context available)
  8. Leave S1/S2 paths unchanged (S1/S2 must match between client and server for handshake to succeed)
Originally created by @otternetwork on GitHub (Apr 26, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/168 An AmneziaWG 2.0 server configured with S3-S4 junk offsets (S3>0, S4>0) cannot communicate with AmneziaWG clients that do not support S3-S4 junk offsets. This includes AWG 1.0 clients and AWG 2.0 clients configured without S3-S4. The S3/S4 junk prefix is applied unconditionally at the device level to all AWG peers, with no per-peer detection or fallback. This is the same class of backward-compatibility regression as #162 (per-peer `advanced_security` for legacy WireGuard clients) and #163 (per-peer `ranged_headers` for AWG 1.0 vs 2.0 headers), but for the S3-S4 junk offset parameters. ## Client classes affected A server configured with S1>0, S2>0, S3>0, S4>0 and custom H1-H4 should be able to serve all of: | Client type | S1-S2 | S3-S4 | H1-H4 | Status | |---|---|---|---|---| | Legacy WireGuard | 0 | 0 | standard (1-4) | Fixed by #162 via `peer->advanced_security` | | AWG without S3-S4 | >0 | 0 | custom | **Broken** — server prepends S4 junk to transport, client cannot parse | | AWG with S3-S4 | >0 | >0 | custom | Works | ## Root cause ### Send path — transport data (S4) In `src/send.c`, `wg_packet_encrypt_worker` prepends S4 junk to transport data packets for ALL peers with `advanced_security`: ```c encrypt_packet( PACKET_PEER(first)->advanced_security ? mh_peerheader(&wg->headers[MSGIDX_TRANSPORT], PACKET_PEER(first)->ranged_headers) : MESSAGE_DATA, PACKET_PEER(first)->advanced_security ? wg->junk_size[MSGIDX_TRANSPORT] : 0, ... ``` When S4>0, every AWG peer receives S4-prefixed transport packets regardless of whether the peer supports S4. An AWG client without S3-S4 sees an unexpected junk prefix and cannot find the transport header at the expected offset. ### Send path — cookie reply (S3) Similarly, `wg_packet_send_handshake_cookie` prepends S3 junk for all non-legacy peers: ```c cookie_junk = wg->junk_size[MSGIDX_HANDSHAKE_COOKIE]; ``` A client without S3 expects the cookie header at offset 0, but receives it at offset S3. ### Receive path — incoming transport data In `src/receive.c`, `prepare_awg_message` tries to match transport packets at offset S4: ```c if (skb->len >= wg->junk_size[MSGIDX_TRANSPORT] + MESSAGE_TRANSPORT_SIZE) { skb_pull(skb, wg->junk_size[MSGIDX_TRANSPORT]); if (mh_validate(SKB_TYPE_LE32(skb), &wg->headers[MSGIDX_TRANSPORT])) return MESSAGE_TRANSPORT_SIZE; ``` When S4>0, an incoming packet from an AWG client without S4 has the AWG H4 header at offset 0, not at offset S4. The size check may also fail since the packet is smaller by S4 bytes. There is no fallback to try the AWG H4 header at offset 0 (without the S4 junk prefix). The same issue exists for incoming cookie replies (S3 offset). ## Relationship to existing per-peer flags The codebase already has the pattern for per-peer capability detection: - `peer->advanced_security` — distinguishes legacy WireGuard peers from AWG peers. Detected during handshake consumption in `noise.c` by checking whether the incoming H1 header matches the AWG range. Fixes #162. - `peer->ranged_headers` — distinguishes AWG 1.0 (fixed headers) from AWG 2.0 (ranged headers). Detected by checking whether the incoming H1 value differs from the range start. Fixes #163. A third per-peer flag is needed: `peer->junk_offsets` — distinguishes AWG peers with S3-S4 support from AWG peers without it. ## Detection approach Detection follows the same try-and-fallback pattern already used in `prepare_awg_message` for S1-S2. When S4>0, the function first tries to match at offset S4 with AWG H4. If that fails, it should try AWG H4 at offset 0 (no S4 junk). The result (whether junk offsets were present) is threaded through to `wg_packet_consume_data`, which sets the per-peer flag after the peer is identified via `key_idx`. Note: when S4=0, the "strip S4" block already performs a no-op `skb_pull(skb, 0)`, so the fallback is only needed when S4>0. ## Cookie S3 limitation During cookie send (`wg_packet_send_handshake_cookie`), there is no peer context — the peer hasn't been fully identified yet. S3 junk on cookies must follow the server's device-level configuration for AWG peers. This is acceptable because cookie replies are rare (only under load), temporary, and the client will retry the handshake. ## Suggested fix 1. Add `bool junk_offsets` to `struct wg_peer`, following the `advanced_security` / `ranged_headers` pattern 2. Default it from device config at peer creation: `true` when `advanced_security && (S3>0 || S4>0)` 3. Detect during receive: `prepare_awg_message` tries AWG-with-S4 first, falls back to AWG-without-S4 4. Thread detection result to `wg_packet_consume_data` via function arguments (same as `had_junk_offsets` bool) 5. Gate S4 junk in `wg_packet_encrypt_worker` on `peer->advanced_security && peer->junk_offsets` 6. Expose via UAPI (`WGPEER_A_JUNK_OFFSETS`) and netlink for userspace visibility 7. Leave S3 cookie path unchanged (no peer context available) 8. Leave S1/S2 paths unchanged (S1/S2 must match between client and server for handshake to succeed)
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-linux-kernel-module#102
No description provided.