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
|
|
@ -359,7 +359,7 @@ func (s *StdNetBind) receiveIP(
|
|||
if sizes[i] == 0 {
|
||||
continue
|
||||
}
|
||||
if msg.N > 3 {
|
||||
if msg.N > 3 && s.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved)
|
||||
common.ClearArray(bufs[i][1:4])
|
||||
}
|
||||
ep := &StdNetEndpoint{AddrPort: M.AddrPortFromNet(msg.Addr)} // TODO: remove allocation
|
||||
|
|
@ -539,6 +539,20 @@ func (s *StdNetBind) SetReservedForEndpoint(destination netip.AddrPort, reserved
|
|||
s.reservedForEndpoint[destination] = reserved
|
||||
}
|
||||
|
||||
// lx: hasReserved reports whether any Cloudflare "reserved" value is set. The
|
||||
// receive path must only zero bytes 1-3 when a reserved value exists (WARP);
|
||||
// otherwise an AmneziaWG magic header that lands in bytes 1-3 (small s1/s2/s4
|
||||
// padding) would be corrupted and the packet dropped. The send path already
|
||||
// gates its stamp on a per-endpoint `loaded` check, so no change is needed there.
|
||||
func (s *StdNetBind) hasReserved() bool {
|
||||
for _, reserved := range s.reservedForEndpoint {
|
||||
if reserved != [3]uint8{} {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *StdNetBind) send(conn *net.UDPConn, pc batchWriter, msgs []ipv6.Message) error {
|
||||
var (
|
||||
n int
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ func (bind *WinRingBind) receiveIPv4(bufs [][]byte, sizes []int, eps []Endpoint)
|
|||
bind.mu.RLock()
|
||||
defer bind.mu.RUnlock()
|
||||
n, ep, err := bind.v4.Receive(bufs[0], &bind.isOpen)
|
||||
if n > 3 {
|
||||
if n > 3 && bind.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved)
|
||||
common.ClearArray(bufs[0][1:4])
|
||||
}
|
||||
sizes[0] = n
|
||||
|
|
@ -473,7 +473,7 @@ func (bind *WinRingBind) receiveIPv6(bufs [][]byte, sizes []int, eps []Endpoint)
|
|||
bind.mu.RLock()
|
||||
defer bind.mu.RUnlock()
|
||||
n, ep, err := bind.v6.Receive(bufs[0], &bind.isOpen)
|
||||
if n > 3 {
|
||||
if n > 3 && bind.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved)
|
||||
common.ClearArray(bufs[0][1:4])
|
||||
}
|
||||
sizes[0] = n
|
||||
|
|
@ -576,6 +576,18 @@ func (bind *WinRingBind) SetReservedForEndpoint(destination netip.AddrPort, rese
|
|||
bind.reservedForEndpoint[*endpoint.(*WinRingEndpoint)] = reserved
|
||||
}
|
||||
|
||||
// lx: hasReserved reports whether any Cloudflare "reserved" value is set. See
|
||||
// the StdNetBind.hasReserved comment — the unconditional receive clear would
|
||||
// corrupt an AmneziaWG magic header sitting in bytes 1-3 (small padding).
|
||||
func (bind *WinRingBind) hasReserved() bool {
|
||||
for _, reserved := range bind.reservedForEndpoint {
|
||||
if reserved != [3]uint8{} {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *StdNetBind) BindSocketToInterface4(interfaceIndex uint32, blackhole bool) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ func (s *StdNetBind) receiveSingle(conn *net.UDPConn, bufs [][]byte, sizes []int
|
|||
return 0, err
|
||||
}
|
||||
sizes[0] = n
|
||||
if n > 3 {
|
||||
if n > 3 && s.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved)
|
||||
bufs[0][1] = 0
|
||||
bufs[0][2] = 0
|
||||
bufs[0][3] = 0
|
||||
|
|
@ -299,7 +299,7 @@ func (s *StdNetBind) makeReceiveMsgX(conn *net.UDPConn, isV6 bool) (ReceiveFunc,
|
|||
numMsgs := int(n)
|
||||
for i := 0; i < numMsgs; i++ {
|
||||
sizes[i] = int(state.hdrs[i].DataLen)
|
||||
if sizes[i] > 3 {
|
||||
if sizes[i] > 3 && s.hasReserved() { // lx: only strip reserved bytes for WARP (see hasReserved)
|
||||
bufs[i][1] = 0
|
||||
bufs[i][2] = 0
|
||||
bufs[i][3] = 0
|
||||
|
|
|
|||
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