wireguard-go-awg2-lx/device/obf_guards_test.go
Leadaxe 1e787bb3e0 lx: gate reserved-byte clear on receive so AmneziaWG magic survives
The Cloudflare "reserved" bytes (1-3) were zeroed unconditionally on
every received datagram across all StdNetBind/WinRingBind receive paths.
AmneziaWG reads its magic header as LittleEndian.Uint32(packet[padding:])
where padding is s1/s2/s4; with small padding (0-3) the magic overlaps
bytes 1-3, so clearing them collapses it out of the ranged h1-h4 window
and every packet is dropped (handshake included) — the AWG endpoint
never comes up. Plain WG (types 1-4, bytes 1-3 already zero) and large
padding are unaffected, which is why it went unnoticed.

Gate all five receive clears (bind_std receiveIP, msgx_darwin
receiveSingle + makeReceiveMsgX, bind_windows receiveIPv4/v6) behind a
new hasReserved() so bytes 1-3 are only touched when a WARP reserved
value is actually configured. Send paths already gate on a per-endpoint
loaded/non-zero check, so they are left unchanged. The reserved map is
populated before the receive goroutines start and never mutated after,
so the lock-free read is safe.

Tests: awg_stdnetbind_reserved_lx_test.go brings up two Devices over
StdNetBind with zero padding (magic in bytes 0-3) and asserts delivery
(red before the fix, green after); reserved_gate_lx_test.go pins the
hasReserved() gate.
2026-08-05 16:55:02 +03:00

108 lines
3.2 KiB
Go

/* SPDX-License-Identifier: MIT
*
* Guards around AWG obfuscation config values: these tests pin the
* crash-on-config-value fixes (swapped jmin/jmax, out-of-range obfuscator
* lengths, full-range magic headers).
*/
package device
import (
"context"
"encoding/hex"
"fmt"
"testing"
)
func TestParseObfLen(t *testing.T) {
cases := []struct {
val string
want int
wantErr bool
}{
{"0", 0, false},
{"100", 100, false},
{fmt.Sprintf("%d", MaxMessageSize), MaxMessageSize, false},
{"-1", 0, true}, // would panic slice bounds in Obfuscate
{fmt.Sprintf("%d", MaxMessageSize+1), 0, true}, // would OOM the handshake make
{"2000000000", 0, true},
{"abc", 0, true},
}
for _, c := range cases {
got, err := parseObfLen(c.val)
if c.wantErr != (err != nil) {
t.Errorf("parseObfLen(%q): err = %v, wantErr = %v", c.val, err, c.wantErr)
}
if err == nil && got != c.want {
t.Errorf("parseObfLen(%q) = %d, want %d", c.val, got, c.want)
}
}
}
func TestMagicHeaderGenerateFullRange(t *testing.T) {
// end-start+1 computed in uint32 wraps to 0 for the full range and
// panics rand.Int; the fix widens to int64 before the arithmetic.
h := &magicHeader{start: 0, end: ^uint32(0)}
for i := 0; i < 8; i++ {
v := h.Generate()
if !h.Validate(v) {
t.Fatalf("generated value %d outside range", v)
}
}
}
// TestJunkSwappedBounds brings up a device pair whose junk config has
// jmin > jmax (passes per-field UAPI validation); without the swap guard
// the first handshake panics rand.Int with a non-positive bound.
func TestJunkSwappedBounds(t *testing.T) {
skA, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey A: %v", err)
}
skB, err := newPrivateKey()
if err != nil {
t.Fatalf("newPrivateKey B: %v", err)
}
pkA := skA.publicKey()
pkB := skB.publicKey()
bindA, bindB := newChanBindPair()
tunA := newChanTun()
tunB := newChanTun()
devA := NewDevice(context.Background(), tunA, bindA, NewLogger(LogLevelError, "devA: "), 1)
devB := NewDevice(context.Background(), tunB, bindB, NewLogger(LogLevelError, "devB: "), 1)
t.Cleanup(devA.Close)
t.Cleanup(devB.Close)
// jmin deliberately greater than jmax: each field alone is valid.
junk := "jc=2\njmin=100\njmax=50\n"
cfgA := fmt.Sprintf(
"private_key=%s\n%sreplace_peers=true\npublic_key=%s\nendpoint=127.0.0.1:2\nallowed_ip=%s/32\n",
hex.EncodeToString(skA[:]), junk, hex.EncodeToString(pkB[:]), testIPB)
cfgB := fmt.Sprintf(
"private_key=%s\n%sreplace_peers=true\npublic_key=%s\nendpoint=127.0.0.1:1\nallowed_ip=%s/32\n",
hex.EncodeToString(skB[:]), junk, hex.EncodeToString(pkA[:]), testIPA)
if err := devA.IpcSet(cfgA); err != nil {
t.Fatalf("IpcSet A: %v", err)
}
if err := devB.IpcSet(cfgB); err != nil {
t.Fatalf("IpcSet B: %v", err)
}
if err := devA.Up(); err != nil {
t.Fatalf("Up A: %v", err)
}
if err := devB.Up(); err != nil {
t.Fatalf("Up B: %v", err)
}
// Drive a packet end-to-end: the handshake (junk packets included)
// must complete without panicking the process.
pkt := buildIPv4Packet(testIPA, testIPB, 28)
devA.InputPacket(testIPB.AsSlice(), [][]byte{pkt})
awaitPacket(t, tunB, pkt, func() {
devA.InputPacket(testIPB.AsSlice(), [][]byte{pkt})
})
}