mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 08:27:01 +02:00
147c202c27
Co-authored-by: Jack Doan <me@jackdoan.com>
180 lines
5.2 KiB
Go
180 lines
5.2 KiB
Go
//go:build linux && !android && !e2e_testing
|
|
|
|
package udp
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"runtime"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
func testLogger() *slog.Logger {
|
|
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
|
}
|
|
|
|
// TestShutdownWakesAfterRx_Mechanism exercises the kernel quirk our teardown
|
|
// relies on: once a socket has received a packet, shutdown(2) wakes a blocked
|
|
// recvmmsg with n>=1/Len==0 (not n==0). recvmmsg must turn that into net.ErrClosed
|
|
// once Close set closed, so a parked reader exits instead of spinning.
|
|
func TestShutdownWakesAfterRx_Mechanism(t *testing.T) {
|
|
c, err := NewListener(testLogger(), netip.MustParseAddr("127.0.0.1"), 0, true, 64)
|
|
if err != nil {
|
|
t.Fatalf("NewListener: %v", err)
|
|
}
|
|
sc := c.(*StdConn)
|
|
addr, err := sc.LocalAddr()
|
|
if err != nil {
|
|
t.Fatalf("LocalAddr: %v", err)
|
|
}
|
|
msgs, _, _ := sc.PrepareRawMessages(sc.batch)
|
|
|
|
// Receive a real packet so the socket has carried data.
|
|
send, err := net.Dial("udp", addr.String())
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
if _, err := send.Write([]byte("hello")); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
n, err := sc.recvmmsg(msgs)
|
|
t.Logf("drain of real packet: n=%d err=%v msgs[0].Len=%d", n, err, msgs[0].Len)
|
|
_ = send.Close()
|
|
|
|
// Block a reader on the now-empty queue, then tear down as Close() does.
|
|
// recvmmsg must return net.ErrClosed (not hang, not spin) even post-rx.
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
_, err := sc.recvmmsg(msgs)
|
|
done <- err
|
|
}()
|
|
time.Sleep(150 * time.Millisecond) // let it park in recvmmsg
|
|
|
|
sc.closed.Store(true)
|
|
if serr := unix.Shutdown(sc.sysFd, unix.SHUT_RDWR); serr != nil {
|
|
t.Logf("shutdown returned %v (expected ENOTCONN on unconnected UDP)", serr)
|
|
}
|
|
|
|
select {
|
|
case err := <-done:
|
|
if !errors.Is(err, net.ErrClosed) {
|
|
t.Errorf("recvmmsg after post-rx shutdown returned %v, want net.ErrClosed", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("HANG: recvmmsg did not return after shutdown following a received packet")
|
|
}
|
|
_ = unix.Close(sc.sysFd)
|
|
}
|
|
|
|
// TestListenOutTeardown_TrafficPatterns reproduces the field report: a blocking
|
|
// reader must tear down cleanly on Close() regardless of what the socket has
|
|
// carried. The three cases the report called out:
|
|
//
|
|
// no traffic ever -> works (shutdown wakes recvmmsg with n==0)
|
|
// ping once, then idle -> historically HUNG: once the socket has received a
|
|
// packet, shutdown(2) wakes recvmmsg with n>=1/Len==0,
|
|
// which an n==0-only teardown check misses
|
|
// continuous traffic -> works (a real packet is always arriving)
|
|
//
|
|
// All three must return within the deadline; a hang dumps goroutines so the
|
|
// stuck reader is visible.
|
|
func TestListenOutTeardown_TrafficPatterns(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
traffic func(send net.Conn, stop <-chan struct{})
|
|
}{
|
|
{"no_traffic_ever", func(net.Conn, <-chan struct{}) {}},
|
|
{"ping_once_then_idle", func(send net.Conn, _ <-chan struct{}) {
|
|
_, _ = send.Write([]byte("hello"))
|
|
}},
|
|
{"continuous", func(send net.Conn, stop <-chan struct{}) {
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
_, _ = send.Write([]byte("hello"))
|
|
time.Sleep(2 * time.Millisecond)
|
|
}
|
|
}
|
|
}},
|
|
}
|
|
|
|
// batch 1 exercises the recvmsg path, batch 64 the recvmmsg path; both must
|
|
// tear down cleanly.
|
|
for _, batch := range []int{1, 64} {
|
|
for _, tc := range cases {
|
|
t.Run(fmt.Sprintf("batch%d/%s", batch, tc.name), func(t *testing.T) {
|
|
runTeardownCase(t, batch, tc.name, tc.traffic)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func runTeardownCase(t *testing.T, batch int, name string, traffic func(send net.Conn, stop <-chan struct{})) {
|
|
c, err := NewListener(testLogger(), netip.MustParseAddr("127.0.0.1"), 0, true, batch)
|
|
if err != nil {
|
|
t.Fatalf("NewListener: %v", err)
|
|
}
|
|
sc := c.(*StdConn)
|
|
addr, err := sc.LocalAddr()
|
|
if err != nil {
|
|
t.Fatalf("LocalAddr: %v", err)
|
|
}
|
|
|
|
var received atomic.Int64
|
|
loopDone := make(chan error, 1)
|
|
go func() {
|
|
loopDone <- sc.ListenOut(func(netip.AddrPort, []byte) {
|
|
received.Add(1)
|
|
})
|
|
}()
|
|
|
|
send, err := net.Dial("udp", addr.String())
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
defer send.Close()
|
|
|
|
stop := make(chan struct{})
|
|
trafficDone := make(chan struct{})
|
|
go func() {
|
|
traffic(send, stop)
|
|
close(trafficDone)
|
|
}()
|
|
|
|
// Let the pattern run and, for the idle case, the reader park again on an
|
|
// empty queue with the socket already having received a packet.
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
start := time.Now()
|
|
if err := sc.Close(); err != nil {
|
|
t.Fatalf("Close: %v", err)
|
|
}
|
|
close(stop)
|
|
|
|
select {
|
|
case err := <-loopDone:
|
|
// Clean teardown surfaces as net.ErrClosed (propagated like the other
|
|
// platforms); the caller absorbs it via its closed flag.
|
|
if err != nil && !errors.Is(err, net.ErrClosed) {
|
|
t.Fatalf("%s: ListenOut returned unexpected error on teardown: %v", name, err)
|
|
}
|
|
t.Logf("%s: closed in %v (received %d packets)", name, time.Since(start), received.Load())
|
|
case <-time.After(3 * time.Second):
|
|
buf := make([]byte, 1<<20)
|
|
n := runtime.Stack(buf, true)
|
|
t.Fatalf("%s: HANG, ListenOut did not return within 3s of Close\n%s", name, buf[:n])
|
|
}
|
|
<-trafficDone
|
|
}
|