lx: fix transport padding buffer overrun + harden AWG config guards
Transport padding (s4) crashed the whole process with "index out of range" in RoutineSequentialSender on the first data packet: InputPacket/InputPackets sized elem.buffer without headroom for the in-buffer right-shift that prepends the random prefix. - send.go: reserve paddings.transport in both injection-path allocLength computations; replace the manual backward byte loop with an overlap-safe copy; defensively grow the buffer (pool-backed) if it still lacks headroom, dropping packets that cannot fit a single WG message instead of overrunning. - receive.go: drop the rxBytes/timers block duplicated by the AWG re-graft (rx accounting was doubled, keepKeyFreshReceiving fired twice per batch). - send.go: swap jmin/jmax when configured inverted (UAPI validates the fields only individually; a swapped pair panicked rand.Int with a non-positive bound on the first handshake). - obf*.go: bound obfuscator length args to [0, MaxMessageSize] (negative panicked slice bounds, huge ones OOMed the handshake). - magic-header.go: widen to int64 before end-start+1 so a full-range header cannot wrap to a zero rand.Int bound. Tests: transport_padding_test.go reproduces the on-device crash byte-for-byte (red on the previous commit, green now) across both injection paths and the tun path; obf_guards_test.go pins the config-value guards.
This commit is contained in:
parent
831d483366
commit
ee7ff1b77f
9 changed files with 502 additions and 12 deletions
361
device/transport_padding_test.go
Normal file
361
device/transport_padding_test.go
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Regression test for the AWG transport-padding (uapi "s4") out-of-bounds
|
||||
* crash: RoutineSequentialSender shifts elem.packet right by
|
||||
* device.paddings.transport bytes inside elem.buffer to prepend a random
|
||||
* padding prefix, but the injection paths (InputPacket/InputPackets)
|
||||
* allocated elem.buffer tightly, without headroom for that shift:
|
||||
*
|
||||
* panic: runtime error: index out of range [123] with length 76
|
||||
* device.(*Peer).RoutineSequentialSender
|
||||
*
|
||||
* (payload 28 bytes -> allocLength 76, sealed packet 64, s4=60 -> 63+60=123).
|
||||
*
|
||||
* The tests spin up two real Devices wired together through an in-memory
|
||||
* conn.Bind (Go channels) and an in-memory tun.Device, configure s4=60 on
|
||||
* both sides, and pass a small IPv4 packet end to end via both outbound
|
||||
* paths: Device.InputPacket (the crashing path) and the regular tun read
|
||||
* loop (in-place shift path).
|
||||
*/
|
||||
|
||||
package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/wireguard-go/conn"
|
||||
"github.com/sagernet/wireguard-go/tun"
|
||||
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
const testTransportPadding = 60 // uapi s4, matches the on-device crash
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// In-memory conn.Bind over Go channels (minimal bindtest.ChannelBind clone).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type chanEndpoint uint16
|
||||
|
||||
func (e chanEndpoint) ClearSrc() {}
|
||||
func (e chanEndpoint) SrcToString() string { return "" }
|
||||
func (e chanEndpoint) DstToString() string { return fmt.Sprintf("127.0.0.1:%d", uint16(e)) }
|
||||
func (e chanEndpoint) DstToBytes() []byte { return []byte{byte(e), byte(e >> 8)} }
|
||||
func (e chanEndpoint) DstIP() netip.Addr { return netip.AddrFrom4([4]byte{127, 0, 0, 1}) }
|
||||
func (e chanEndpoint) SrcIP() netip.Addr { return netip.Addr{} }
|
||||
|
||||
type chanBind struct {
|
||||
rx, tx chan []byte
|
||||
source chanEndpoint // "port" this bind listens on
|
||||
target chanEndpoint // endpoint of the opposite bind
|
||||
|
||||
mu sync.Mutex
|
||||
closeSignal chan struct{} // recreated on every Open (BindUpdate closes+reopens)
|
||||
}
|
||||
|
||||
// newChanBindPair returns two Binds whose Send/Receive are cross-wired.
|
||||
func newChanBindPair() (*chanBind, *chanBind) {
|
||||
aToB := make(chan []byte, 1024)
|
||||
bToA := make(chan []byte, 1024)
|
||||
a := &chanBind{rx: bToA, tx: aToB, source: 1, target: 2}
|
||||
b := &chanBind{rx: aToB, tx: bToA, source: 2, target: 1}
|
||||
return a, b
|
||||
}
|
||||
|
||||
func (b *chanBind) currentCloseSignal() chan struct{} {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.closeSignal
|
||||
}
|
||||
|
||||
func (b *chanBind) Open(port uint16) ([]conn.ReceiveFunc, uint16, error) {
|
||||
b.mu.Lock()
|
||||
b.closeSignal = make(chan struct{})
|
||||
closeSignal := b.closeSignal
|
||||
b.mu.Unlock()
|
||||
fn := func(packets [][]byte, sizes []int, eps []conn.Endpoint) (int, error) {
|
||||
select {
|
||||
case <-closeSignal:
|
||||
// Must be net.ErrClosed: RoutineReceiveIncoming treats anything
|
||||
// else as a transient error and death-spirals before exiting.
|
||||
return 0, net.ErrClosed
|
||||
case pkt, ok := <-b.rx:
|
||||
if !ok {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
sizes[0] = copy(packets[0], pkt)
|
||||
eps[0] = b.target
|
||||
return 1, nil
|
||||
}
|
||||
}
|
||||
return []conn.ReceiveFunc{fn}, uint16(b.source), nil
|
||||
}
|
||||
|
||||
func (b *chanBind) Close() error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.closeSignal != nil {
|
||||
select {
|
||||
case <-b.closeSignal:
|
||||
default:
|
||||
close(b.closeSignal)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *chanBind) SetMark(mark uint32) error { return nil }
|
||||
|
||||
func (b *chanBind) Send(bufs [][]byte, ep conn.Endpoint, offset int) error {
|
||||
closeSignal := b.currentCloseSignal()
|
||||
if closeSignal == nil {
|
||||
return net.ErrClosed
|
||||
}
|
||||
for _, buf := range bufs {
|
||||
pkt := make([]byte, len(buf)-offset)
|
||||
copy(pkt, buf[offset:])
|
||||
select {
|
||||
case <-closeSignal:
|
||||
return net.ErrClosed
|
||||
case b.tx <- pkt:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *chanBind) ParseEndpoint(s string) (conn.Endpoint, error) { return b.target, nil }
|
||||
|
||||
func (b *chanBind) BatchSize() int { return 1 }
|
||||
|
||||
func (b *chanBind) SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// In-memory tun.Device over Go channels (minimal tuntest.ChannelTUN clone).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type chanTun struct {
|
||||
toDevice chan []byte // packets the device Reads (outbound plaintext)
|
||||
fromDevice chan []byte // packets the device Writes (inbound plaintext)
|
||||
events chan tun.Event
|
||||
closed chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func newChanTun() *chanTun {
|
||||
return &chanTun{
|
||||
toDevice: make(chan []byte, 1024),
|
||||
fromDevice: make(chan []byte, 1024),
|
||||
events: make(chan tun.Event, 4),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *chanTun) File() *os.File { return nil }
|
||||
|
||||
func (t *chanTun) Read(bufs [][]byte, sizes []int, offset int) (int, error) {
|
||||
select {
|
||||
case <-t.closed:
|
||||
return 0, os.ErrClosed
|
||||
case pkt, ok := <-t.toDevice:
|
||||
if !ok {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
sizes[0] = copy(bufs[0][offset:], pkt)
|
||||
return 1, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *chanTun) Write(bufs [][]byte, offset int) (int, error) {
|
||||
for _, buf := range bufs {
|
||||
pkt := make([]byte, len(buf)-offset)
|
||||
copy(pkt, buf[offset:])
|
||||
select {
|
||||
case <-t.closed:
|
||||
return 0, os.ErrClosed
|
||||
case t.fromDevice <- pkt:
|
||||
}
|
||||
}
|
||||
return len(bufs), nil
|
||||
}
|
||||
|
||||
func (t *chanTun) MTU() (int, error) { return DefaultMTU, nil }
|
||||
func (t *chanTun) Name() (string, error) { return "chantun", nil }
|
||||
func (t *chanTun) Events() <-chan tun.Event { return t.events }
|
||||
func (t *chanTun) BatchSize() int { return 1 }
|
||||
|
||||
func (t *chanTun) Close() error {
|
||||
t.closeOnce.Do(func() {
|
||||
close(t.closed)
|
||||
close(t.events)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test scaffolding.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
testIPA = netip.AddrFrom4([4]byte{10, 0, 0, 1})
|
||||
testIPB = netip.AddrFrom4([4]byte{10, 0, 0, 2})
|
||||
)
|
||||
|
||||
// buildIPv4Packet builds a minimal, routable IPv4/UDP packet whose header
|
||||
// fields satisfy the receive-side validation in RoutineSequentialReceiver
|
||||
// (version, total-length field, allowed source address).
|
||||
func buildIPv4Packet(src, dst netip.Addr, payloadLen int) []byte {
|
||||
total := ipv4.HeaderLen + payloadLen
|
||||
pkt := make([]byte, total)
|
||||
pkt[0] = 0x45 // version 4, IHL 5
|
||||
binary.BigEndian.PutUint16(pkt[IPv4offsetTotalLength:IPv4offsetTotalLength+2], uint16(total))
|
||||
pkt[8] = 64 // TTL
|
||||
pkt[9] = 17 // protocol: UDP
|
||||
copy(pkt[IPv4offsetSrc:], src.AsSlice())
|
||||
copy(pkt[IPv4offsetDst:], dst.AsSlice())
|
||||
for i := ipv4.HeaderLen; i < total; i++ {
|
||||
pkt[i] = byte(i) // deterministic payload
|
||||
}
|
||||
return pkt
|
||||
}
|
||||
|
||||
type paddedPair struct {
|
||||
devA, devB *Device
|
||||
tunA, tunB *chanTun
|
||||
}
|
||||
|
||||
// newPaddedDevicePair builds two Up()'d devices peered with each other over
|
||||
// the channel bind, both configured with s4 (transport padding) enabled.
|
||||
func newPaddedDevicePair(t *testing.T) *paddedPair {
|
||||
t.Helper()
|
||||
|
||||
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)
|
||||
|
||||
cfgA := fmt.Sprintf(
|
||||
"private_key=%s\ns4=%d\nreplace_peers=true\npublic_key=%s\nendpoint=127.0.0.1:2\nallowed_ip=%s/32\n",
|
||||
hex.EncodeToString(skA[:]), testTransportPadding, hex.EncodeToString(pkB[:]), testIPB)
|
||||
cfgB := fmt.Sprintf(
|
||||
"private_key=%s\ns4=%d\nreplace_peers=true\npublic_key=%s\nendpoint=127.0.0.1:1\nallowed_ip=%s/32\n",
|
||||
hex.EncodeToString(skB[:]), testTransportPadding, 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 devA.paddings.transport != testTransportPadding {
|
||||
t.Fatalf("s4 not applied: paddings.transport = %d", devA.paddings.transport)
|
||||
}
|
||||
|
||||
if err := devA.Up(); err != nil {
|
||||
t.Fatalf("Up A: %v", err)
|
||||
}
|
||||
if err := devB.Up(); err != nil {
|
||||
t.Fatalf("Up B: %v", err)
|
||||
}
|
||||
|
||||
return &paddedPair{devA: devA, devB: devB, tunA: tunA, tunB: tunB}
|
||||
}
|
||||
|
||||
// awaitPacket waits for want to arrive on the receiving tun, periodically
|
||||
// re-sending via resend (injection has no delivery guarantee before the
|
||||
// handshake completes).
|
||||
func awaitPacket(t *testing.T, from *chanTun, want []byte, resend func()) {
|
||||
t.Helper()
|
||||
deadline := time.After(20 * time.Second)
|
||||
retry := time.NewTicker(1 * time.Second)
|
||||
defer retry.Stop()
|
||||
for {
|
||||
select {
|
||||
case got := <-from.fromDevice:
|
||||
if bytes.Equal(got, want) {
|
||||
return
|
||||
}
|
||||
t.Logf("ignoring unexpected packet, len=%d", len(got))
|
||||
case <-retry.C:
|
||||
resend()
|
||||
case <-deadline:
|
||||
t.Fatal("timed out waiting for packet on peer tun")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestTransportPaddingInputPacket exercises the exact crash path: an injected
|
||||
// packet (Device.InputPacket) whose buffer was allocated by payload size.
|
||||
// With s4=60 and a 28-byte IPv4 packet the pre-fix buffer was 76 bytes and
|
||||
// the padding shift indexed [123] -> index out of range.
|
||||
func TestTransportPaddingInputPacket(t *testing.T) {
|
||||
pair := newPaddedDevicePair(t)
|
||||
|
||||
// 20-byte header + 8-byte payload = 28 bytes, the on-device crash size.
|
||||
pkt := buildIPv4Packet(testIPA, testIPB, 8)
|
||||
dst := testIPB.AsSlice()
|
||||
|
||||
send := func() { pair.devA.InputPacket(dst, [][]byte{pkt}) }
|
||||
send()
|
||||
awaitPacket(t, pair.tunB, pkt, send)
|
||||
}
|
||||
|
||||
// TestTransportPaddingInputPackets covers the batched injection path
|
||||
// (Device.InputPackets), which had the same tight allocation.
|
||||
func TestTransportPaddingInputPackets(t *testing.T) {
|
||||
pair := newPaddedDevicePair(t)
|
||||
|
||||
pkt := buildIPv4Packet(testIPA, testIPB, 8)
|
||||
refs := []*InputPacketRef{{
|
||||
Destination: testIPB.AsSlice(),
|
||||
PacketSlices: [][]byte{pkt[:12], pkt[12:]}, // multi-slice on purpose
|
||||
}}
|
||||
|
||||
send := func() {
|
||||
if unmatched := pair.devA.InputPackets(refs); len(unmatched) != 0 {
|
||||
t.Fatalf("InputPackets returned %d unmatched refs", len(unmatched))
|
||||
}
|
||||
}
|
||||
send()
|
||||
awaitPacket(t, pair.tunB, pkt, send)
|
||||
}
|
||||
|
||||
// TestTransportPaddingTunPath covers the regular outbound path (tun read
|
||||
// loop), whose MaxMessageSize buffers take the in-place shift branch.
|
||||
func TestTransportPaddingTunPath(t *testing.T) {
|
||||
pair := newPaddedDevicePair(t)
|
||||
|
||||
pkt := buildIPv4Packet(testIPA, testIPB, 8)
|
||||
|
||||
send := func() { pair.tunA.toDevice <- pkt }
|
||||
send()
|
||||
awaitPacket(t, pair.tunB, pkt, send)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue