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.
108 lines
3.2 KiB
Go
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})
|
|
})
|
|
}
|