magicsock: use local STUN server in tests
import (
"fmt"
+ "log"
"net"
"strings"
+ "sync"
"testing"
"time"
+
+ "tailscale.com/stun"
)
func TestListen(t *testing.T) {
@@ -20,14 +24,13 @@ func TestListen(t *testing.T) {
}
}
- // TODO(crawshaw): break test dependency on the network
- // using "gortc.io/stun" (like stunner_test.go).
- stunServers := DefaultSTUN
+ stunAddr, stunCleanupFn := serveSTUN(t)
+ defer stunCleanupFn()
port := pickPort(t)
conn, err := Listen(Options{
Port: port,
- STUN: stunServers,
+ STUN: []string{stunAddr.String()},
EndpointsFunc: epFunc,
})
if err != nil {
@@ -119,3 +122,60 @@ func TestPickDERPFallback(t *testing.T) {
t.Errorf("not sticky: got %v; want %v", got, someNode)
}
}
+
+type stunStats struct {
+ mu sync.Mutex
+ readIPv4 int
+ readIPv6 int
+}
+
+func serveSTUN(t *testing.T) (addr net.Addr, cleanupFn func()) {
+ t.Helper()
+
+ // TODO(crawshaw): use stats to test re-STUN logic
+ var stats stunStats
+
+ pc, err := net.ListenPacket("udp4", ":3478")
+ if err != nil {
+ t.Fatalf("failed to open STUN listener: %v", err)
+ }
+
+ go runSTUN(pc, &stats)
+ return pc.LocalAddr(), func() { pc.Close() }
+}
+
+func runSTUN(pc net.PacketConn, stats *stunStats) {
+ var buf [64 << 10]byte
+ for {
+ n, addr, err := pc.ReadFrom(buf[:])
+ if err != nil {
+ if strings.Contains(err.Error(), "closed network connection") {
+ log.Printf("STUN server shutdown")
+ return
+ }
+ continue
+ }
+ ua := addr.(*net.UDPAddr)
+ pkt := buf[:n]
+ if !stun.Is(pkt) {
+ continue
+ }
+ txid, err := stun.ParseBindingRequest(pkt)
+ if err != nil {
+ continue
+ }
+
+ stats.mu.Lock()
+ if ua.IP.To4() != nil {
+ stats.readIPv4++
+ } else {
+ stats.readIPv6++
+ }
+ stats.mu.Unlock()
+
+ res := stun.Response(txid, ua.IP, uint16(ua.Port))
+ if _, err := pc.WriteTo(res, addr); err != nil {
+ log.Printf("STUN server write failed: %v", err)
+ }
+ }
+}
