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.
361 lines
11 KiB
Go
361 lines
11 KiB
Go
/* 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)
|
|
}
|