wireguard-go-awg2-lx/tun/checksum_test.go
Adrian Dewhurst 71393c576b tun: fix checksum test failures on non-4KiB page sizes
When generating page-aligned random bytes, random data started at the
beginning of the buffer that will be chopped off. When the page size
differs, the start of the returned slice is different than expected for
the expected checksums, causing the tests to fail.
2024-07-31 16:30:15 -04:00

619 lines
13 KiB
Go

package tun
import (
"fmt"
"math"
"math/rand"
"net/netip"
"sort"
"syscall"
"testing"
"unsafe"
"gvisor.dev/gvisor/pkg/tcpip"
gvisorChecksum "gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
type archChecksumDetails struct {
name string
available bool
f func([]byte, uint16) uint16
}
func fillRandomBuffer(seed int64, buf []byte) {
rng := rand.New(rand.NewSource(seed))
n, err := rng.Read(buf)
if err != nil {
panic(err)
}
if n != len(buf) {
panic("incomplete random buffer")
}
}
func deterministicRandomBytes(seed int64, length int) []byte {
buf := make([]byte, length)
fillRandomBuffer(seed, buf)
return buf
}
func getPageAlignedRandomBytes(seed int64, length int) []byte {
alignment := syscall.Getpagesize()
buf := make([]byte, length+(alignment-1))
bufPtr := uintptr(unsafe.Pointer(&buf[0]))
alignedBufPtr := (bufPtr + uintptr(alignment-1)) & ^uintptr(alignment-1)
alignedStart := int(alignedBufPtr - bufPtr)
buf = buf[alignedStart : alignedStart+length]
fillRandomBuffer(seed, buf)
return buf
}
func TestChecksum(t *testing.T) {
alignedBuf := getPageAlignedRandomBytes(10, 8192)
allOnes := make([]byte, 65535)
for i := range allOnes {
allOnes[i] = 0xff
}
allFE := make([]byte, 65535)
for i := range allFE {
allFE[i] = 0xfe
}
tests := []struct {
name string
data []byte
initial uint16
want uint16
}{
{
name: "empty",
data: []byte{},
initial: 0,
want: 0,
},
{
name: "max initial",
data: []byte{},
initial: math.MaxUint16,
want: 0xffff,
},
{
name: "odd length",
data: []byte{0x01, 0x02, 0x01},
initial: 0,
want: 0x0202,
},
{
name: "tiny",
data: []byte{0x01, 0x02, 0x01, 0x02, 0x01, 0x02},
initial: 0,
want: 0x0306,
},
{
name: "initial",
data: []byte{0x01, 0x02, 0x01, 0x02, 0x01, 0x02},
initial: 0x1000,
want: 0x1306,
},
// cleanup0 through cleanup15 is 1024 (handled by large SIMD loops) +
// 32 (handled by small SIMD loops) + n, where n ranges from 0 to 15
// to cover all of the leftover byte sizes that are possible after small
// SIMD loops that handle 16 bytes.
{
name: "cleanup0",
data: deterministicRandomBytes(1, 1056),
initial: 0,
want: 0x11ec,
},
{
name: "cleanup1",
data: deterministicRandomBytes(1, 1057),
initial: 0,
want: 0xc5ec,
},
{
name: "cleanup2",
data: deterministicRandomBytes(1, 1058),
initial: 0,
want: 0xc6ad,
},
{
name: "cleanup3",
data: deterministicRandomBytes(1, 1059),
initial: 0,
want: 0x86ae,
},
{
name: "cleanup4",
data: deterministicRandomBytes(1, 1060),
initial: 0,
want: 0x878e,
},
{
name: "cleanup5",
data: deterministicRandomBytes(1, 1061),
initial: 0,
want: 0xdb8e,
},
{
name: "cleanup6",
data: deterministicRandomBytes(1, 1062),
initial: 0,
want: 0xdbd5,
},
{
name: "cleanup7",
data: deterministicRandomBytes(1, 1063),
initial: 0,
want: 0xcfd6,
},
{
name: "cleanup8",
data: deterministicRandomBytes(1, 1064),
initial: 0,
want: 0xd090,
},
{
name: "cleanup9",
data: deterministicRandomBytes(1, 1065),
initial: 0,
want: 0x0791,
},
{
name: "cleanup10",
data: deterministicRandomBytes(1, 1066),
initial: 0,
want: 0x079f,
},
{
name: "cleanup11",
data: deterministicRandomBytes(1, 1067),
initial: 0,
want: 0xba9f,
},
{
name: "cleanup12",
data: deterministicRandomBytes(1, 1068),
initial: 0,
want: 0xbb0c,
},
{
name: "cleanup13",
data: deterministicRandomBytes(1, 1069),
initial: 0,
want: 0x770d,
},
{
name: "cleanup14",
data: deterministicRandomBytes(1, 1070),
initial: 0,
want: 0x780a,
},
{
name: "cleanup15",
data: deterministicRandomBytes(1, 1071),
initial: 0,
want: 0x640b,
},
// small1 through small15 covers small sizes that are not large enough
// to do overlapped reads.
{
name: "small1",
data: deterministicRandomBytes(2, 1),
initial: 0x1122,
want: 0x4022,
},
{
name: "small2",
data: deterministicRandomBytes(2, 2),
initial: 0x1122,
want: 0x40a4,
},
{
name: "small3",
data: deterministicRandomBytes(2, 3),
initial: 0x1122,
want: 0xc2a4,
},
{
name: "small4",
data: deterministicRandomBytes(2, 4),
initial: 0x1122,
want: 0xc36f,
},
{
name: "small5",
data: deterministicRandomBytes(2, 5),
initial: 0x1122,
want: 0xa570,
},
{
name: "small6",
data: deterministicRandomBytes(2, 6),
initial: 0x1122,
want: 0xa669,
},
{
name: "small7",
data: deterministicRandomBytes(2, 7),
initial: 0x1122,
want: 0x0f6a,
},
{
name: "small8",
data: deterministicRandomBytes(2, 8),
initial: 0x1122,
want: 0x0fd9,
},
{
name: "small9",
data: deterministicRandomBytes(2, 9),
initial: 0x1122,
want: 0x40d9,
},
{
name: "small10",
data: deterministicRandomBytes(2, 10),
initial: 0x1122,
want: 0x411d,
},
{
name: "small11",
data: deterministicRandomBytes(2, 11),
initial: 0x1122,
want: 0x011e,
},
{
name: "small12",
data: deterministicRandomBytes(2, 12),
initial: 0x1122,
want: 0x01c8,
},
{
name: "small13",
data: deterministicRandomBytes(2, 13),
initial: 0x1122,
want: 0x4dc8,
},
{
name: "small14",
data: deterministicRandomBytes(2, 14),
initial: 0x1122,
want: 0x4eb5,
},
{
name: "small15",
data: deterministicRandomBytes(2, 15),
initial: 0x1122,
want: 0xa4b5,
},
// other small-ish sizes
{
name: "small16",
data: deterministicRandomBytes(1, 16),
initial: 0,
want: 0x02fa,
},
{
name: "small32",
data: deterministicRandomBytes(1, 32),
initial: 0,
want: 0x03ee,
},
{
name: "small64",
data: deterministicRandomBytes(1, 64),
initial: 0,
want: 0x3f85,
},
{
name: "medium",
data: deterministicRandomBytes(1, 1400),
initial: 0,
want: 0xbea5,
},
{
name: "big",
data: deterministicRandomBytes(2, 65000),
initial: 0,
want: 0x3ba7,
},
{
name: "big-initial",
data: deterministicRandomBytes(2, 65000),
initial: 0x1234,
want: 0x4ddb,
},
{
// big-small-loop is intended to exercise a few iterations of a big
// initial loop of 128 bytes or larger + a smaller loop of 16 bytes
// + some leftover
name: "big-small-loop",
data: deterministicRandomBytes(3, 1094),
initial: 0x9999,
want: 0xe65b,
},
{
name: "page-aligned",
data: alignedBuf[:4096],
initial: 0,
want: 0x963b,
},
{
name: "32-aligned",
data: alignedBuf[32:4128],
initial: 0,
want: 0x30c4,
},
{
name: "16-aligned",
data: alignedBuf[16:4112],
initial: 0,
want: 0xaeff,
},
{
name: "8-aligned",
data: alignedBuf[8:4104],
initial: 0,
want: 0x6c3b,
},
{
name: "4-aligned",
data: alignedBuf[4:4100],
initial: 0,
want: 0x2e4a,
},
{
name: "2-aligned",
data: alignedBuf[2:4098],
initial: 0,
want: 0xc702,
},
{
name: "unaligned",
data: alignedBuf[1:4097],
initial: 0,
want: 0x3bc7,
},
{
name: "unalignedAndOdd",
data: alignedBuf[1:4096],
initial: 0,
want: 0x3b13,
},
{
name: "fe1282",
data: allFE[:1282],
initial: 0,
want: 0x7c7c,
},
{
name: "fe",
data: allFE,
initial: 0,
want: 0x7e81,
},
{
name: "maximum",
data: allOnes,
initial: 0,
want: 0xff00,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, fd := range archChecksumFuncs {
t.Run(fd.name, func(t *testing.T) {
if !fd.available {
t.Skip("can not run on this system")
}
if got := fd.f(tt.data, tt.initial); got != tt.want {
t.Errorf("%s checksum = %04x, want %04x", fd.name, got, tt.want)
}
})
}
t.Run("reference", func(t *testing.T) {
if got := gvisorChecksum.Checksum(tt.data, tt.initial); got != tt.want {
t.Errorf("reference checksum = %04x, want %04x", got, tt.want)
}
})
})
}
}
func TestPseudoHeaderChecksumNoFold(t *testing.T) {
tests := []struct {
name string
protocol uint8
srcAddr []byte
dstAddr []byte
totalLen uint16
want uint16
}{
{
name: "ipv4",
protocol: syscall.IPPROTO_TCP,
srcAddr: netip.MustParseAddr("192.168.1.1").AsSlice(),
dstAddr: netip.MustParseAddr("192.168.1.2").AsSlice(),
totalLen: 1492,
want: 0x892e,
},
{
name: "ipv6",
protocol: syscall.IPPROTO_TCP,
srcAddr: netip.MustParseAddr("2001:db8:3333:4444:5555:6666:7777:8888").AsSlice(),
dstAddr: netip.MustParseAddr("2001:db8:aaaa:bbbb:cccc:dddd:eeee:ffff").AsSlice(),
totalLen: 1492,
want: 0x947f,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run("pseudoHeaderChecksum32", func(t *testing.T) {
got := pseudoHeaderChecksum32(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen)
if got != tt.want {
t.Errorf("got %04x, want %04x", got, tt.want)
}
})
t.Run("pseudoHeaderChecksum64", func(t *testing.T) {
got := pseudoHeaderChecksum64(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen)
if got != tt.want {
t.Errorf("got %04x, want %04x", got, tt.want)
}
})
t.Run("reference", func(t *testing.T) {
got := header.PseudoHeaderChecksum(
tcpip.TransportProtocolNumber(tt.protocol),
tcpip.AddrFromSlice(tt.srcAddr),
tcpip.AddrFromSlice(tt.dstAddr),
tt.totalLen)
if got != tt.want {
t.Errorf("got %04x, want %04x", got, tt.want)
}
})
})
}
}
func FuzzChecksum(f *testing.F) {
buf := getPageAlignedRandomBytes(1234, 65536)
f.Add([]byte{}, uint16(0))
f.Add([]byte{}, uint16(0x1234))
f.Add([]byte{}, uint16(0))
f.Add(buf[:15], uint16(0x1234))
f.Add(buf[:256], uint16(0x1234))
f.Add(buf[:1280], uint16(0x1234))
f.Add(buf[:1288], uint16(0x1234))
f.Add(buf[1:1050], uint16(0x1234))
f.Fuzz(func(t *testing.T, data []byte, initial uint16) {
want := gvisorChecksum.Checksum(data, initial)
for _, fd := range archChecksumFuncs {
t.Run(fd.name, func(t *testing.T) {
if !fd.available {
t.Skip("can not run on this system")
}
if got := fd.f(data, initial); got != want {
t.Errorf("%s checksum = %04x, want %04x", fd.name, got, want)
}
})
}
})
}
var result uint16
func BenchmarkChecksum(b *testing.B) {
offsets := []int{ // offsets from page alignment
0,
1,
2,
4,
8,
16,
}
lengths := []int{
0,
7,
15,
16,
31,
64,
90,
95,
128,
256,
512,
1024,
1240,
1500,
2048,
4096,
8192,
9000,
9001,
16384,
65536,
}
if !sort.IntsAreSorted(offsets) {
b.Fatal("offsets are not sorted")
}
largestLength := lengths[len(lengths)-1]
if !sort.IntsAreSorted(lengths) {
b.Fatal("lengths are not sorted")
}
largestOffset := lengths[len(offsets)-1]
alignedBuf := getPageAlignedRandomBytes(1, largestOffset+largestLength)
var r uint16
for _, offset := range offsets {
name := fmt.Sprintf("%vAligned", offset)
if offset == 0 {
name = "pageAligned"
}
offsetBuf := alignedBuf[offset:]
b.Run(name, func(b *testing.B) {
for _, length := range lengths {
b.Run(fmt.Sprintf("%d", length), func(b *testing.B) {
for _, fd := range archChecksumFuncs {
b.Run(fd.name, func(b *testing.B) {
if !fd.available {
b.Skip("can not run on this system")
}
b.SetBytes(int64(length))
for i := 0; i < b.N; i++ {
r += fd.f(offsetBuf[:length], 0)
}
})
}
})
}
})
}
result = r
}
func BenchmarkPseudoHeaderChecksum(b *testing.B) {
tests := []struct {
name string
protocol uint8
srcAddr []byte
dstAddr []byte
totalLen uint16
want uint16
}{
{
name: "ipv4",
protocol: syscall.IPPROTO_TCP,
srcAddr: []byte{192, 168, 1, 1},
dstAddr: []byte{192, 168, 1, 2},
totalLen: 1492,
want: 0x892e,
},
{
name: "ipv6",
protocol: syscall.IPPROTO_TCP,
srcAddr: netip.MustParseAddr("2001:db8:3333:4444:5555:6666:7777:8888").AsSlice(),
dstAddr: netip.MustParseAddr("2001:db8:aaaa:bbbb:cccc:dddd:eeee:ffff").AsSlice(),
totalLen: 1492,
want: 0x892e,
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
b.Run("pseudoHeaderChecksum32", func(b *testing.B) {
for i := 0; i < b.N; i++ {
result += pseudoHeaderChecksum32(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen)
}
})
b.Run("pseudoHeaderChecksum64", func(b *testing.B) {
for i := 0; i < b.N; i++ {
result += pseudoHeaderChecksum64(tt.protocol, tt.srcAddr, tt.dstAddr, tt.totalLen)
}
})
})
}
}