From 7daacc6a485c84559b84e654d3725cd1de81a700 Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Fri, 16 Oct 2026 18:12:58 -0500 Subject: [PATCH] overlay/darwin: test tun framing and zero-alloc Read/Write (#1906) --- overlay/tun_darwin_test.go | 121 +++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 overlay/tun_darwin_test.go diff --git a/overlay/tun_darwin_test.go b/overlay/tun_darwin_test.go new file mode 100644 index 00000000..a6611d6a --- /dev/null +++ b/overlay/tun_darwin_test.go @@ -0,0 +1,121 @@ +//go:build !ios && !e2e_testing + +package overlay + +import ( + "bytes" + "os" + "syscall" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTun_WriteFraming(t *testing.T) { + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + defer w.Close() + tn := &tun{f: w} + + for _, tc := range []struct { + name string + pkt []byte + af byte + }{ + // Literal families rather than syscall.AF_*, so a wrong constant in Write can't also fix the expectation. + {"ipv4", []byte{0x45, 1, 2, 3, 4, 5}, 2}, + {"ipv6", []byte{0x60, 1, 2, 3, 4, 5}, 30}, + } { + t.Run(tc.name, func(t *testing.T) { + n, err := tn.Write(tc.pkt) + require.NoError(t, err) + require.Equal(t, len(tc.pkt), n) + + got := make([]byte, 64) + n, err = r.Read(got) + require.NoError(t, err) + require.Equal(t, append([]byte{0, 0, 0, tc.af}, tc.pkt...), got[:n]) + }) + } + + _, err = tn.Write([]byte{0x10, 1, 2, 3}) + require.Error(t, err) + _, err = tn.Write([]byte{}) + require.ErrorIs(t, err, syscall.EIO) + + // The rejected writes must not have put anything on the fd, so the next read sees only this packet. + pkt := []byte{0x45, 9, 9} + _, err = tn.Write(pkt) + require.NoError(t, err) + got := make([]byte, 64) + n, err := r.Read(got) + require.NoError(t, err) + require.Equal(t, append([]byte{0, 0, 0, 2}, pkt...), got[:n]) +} + +func TestTun_ReadStripsHeader(t *testing.T) { + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + defer w.Close() + tn := &tun{f: r} + + pkt := []byte{0x45, 1, 2, 3, 4, 5} + _, err = w.Write(append([]byte{0, 0, 0, syscall.AF_INET}, pkt...)) + require.NoError(t, err) + + got := make([]byte, 64) + n, err := tn.Read(got) + require.NoError(t, err) + require.Equal(t, pkt, got[:n]) +} + +func TestTun_ReadShort(t *testing.T) { + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + defer w.Close() + tn := &tun{f: r} + + // Fewer bytes than the protocol header is not a packet. + _, err = w.Write([]byte{0, 0}) + require.NoError(t, err) + n, err := tn.Read(make([]byte, 64)) + require.NoError(t, err) + require.Zero(t, n) + + // The next packet reads cleanly only if the short read consumed exactly what was written. + pkt := []byte{0x45, 1, 2, 3} + _, err = w.Write(append([]byte{0, 0, 0, syscall.AF_INET}, pkt...)) + require.NoError(t, err) + got := make([]byte, 64) + n, err = tn.Read(got) + require.NoError(t, err) + require.Equal(t, pkt, got[:n]) +} + +// A heap allocation per packet rarely shows in a short throughput run, only across GC pauses, so +// this pins Read and Write to zero allocations directly. +func TestTun_ReadWriteDoNotAllocate(t *testing.T) { + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + defer w.Close() + tw := &tun{f: w} + tr := &tun{f: r} + + pkts := [][]byte{bytes.Repeat([]byte{0x45}, 1300), bytes.Repeat([]byte{0x60}, 1300)} + buf := make([]byte, 1500) + i := 0 + allocs := testing.AllocsPerRun(1000, func() { + i++ + if _, err := tw.Write(pkts[i%len(pkts)]); err != nil { + t.Fatal(err) + } + if _, err := tr.Read(buf); err != nil { + t.Fatal(err) + } + }) + require.Zero(t, allocs) +}