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.
This commit is contained in:
parent
ee7ff1b77f
commit
1e787bb3e0
7 changed files with 283 additions and 11 deletions
56
conn/reserved_gate_lx_test.go
Normal file
56
conn/reserved_gate_lx_test.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* lx: unit coverage for the StdNetBind.hasReserved() gate that guards the
|
||||
* receive-side reserved-clear. receiveIP zeroes bytes 1-3 (Cloudflare WARP
|
||||
* "reserved") only when a non-zero reserved value is set for some endpoint;
|
||||
* otherwise an AmneziaWG magic header landing in bytes 1-3 (small s1/s2/s4
|
||||
* padding) would be corrupted and the packet dropped. This test pins the gate
|
||||
* itself; the end-to-end handshake proof lives in the device package.
|
||||
*/
|
||||
|
||||
package conn
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func stdNetBindForTest(t *testing.T) *StdNetBind {
|
||||
t.Helper()
|
||||
b, ok := NewStdNetBind(nil).(*StdNetBind)
|
||||
if !ok {
|
||||
t.Fatalf("NewStdNetBind did not return *StdNetBind")
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestStdNetBindHasReserved(t *testing.T) {
|
||||
b := stdNetBindForTest(t)
|
||||
if b.hasReserved() {
|
||||
t.Fatal("fresh bind must report no reserved value")
|
||||
}
|
||||
|
||||
ep := netip.MustParseAddrPort("127.0.0.1:51820")
|
||||
|
||||
// An all-zero reserved value is indistinguishable from "unset" and must
|
||||
// not arm the clear.
|
||||
b.SetReservedForEndpoint(ep, [3]byte{0, 0, 0})
|
||||
if b.hasReserved() {
|
||||
t.Fatal("all-zero reserved must not count as reserved")
|
||||
}
|
||||
|
||||
// Any non-zero byte (WARP anycast tag) arms the clear.
|
||||
b.SetReservedForEndpoint(ep, [3]byte{0, 0, 1})
|
||||
if !b.hasReserved() {
|
||||
t.Fatal("non-zero reserved (byte 3) must count as reserved")
|
||||
}
|
||||
|
||||
// A second endpoint's non-zero value must also be seen.
|
||||
b2 := stdNetBindForTest(t)
|
||||
ep2 := netip.MustParseAddrPort("192.0.2.1:2408")
|
||||
b2.SetReservedForEndpoint(ep, [3]byte{0, 0, 0})
|
||||
b2.SetReservedForEndpoint(ep2, [3]byte{0xAB, 0, 0})
|
||||
if !b2.hasReserved() {
|
||||
t.Fatal("non-zero reserved on any endpoint must count as reserved")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue