//go:build linux package wshub import ( "net" "syscall" "testing" "time" ) // setMulticastIf pins a socket's outgoing multicast interface (IP_MULTICAST_IF), // which is exactly what UDPStreamer/UDPSServer does with its `Interface` key. func setMulticastIf(t *testing.T, conn *net.UDPConn, ip [4]byte) { t.Helper() rc, err := conn.SyscallConn() if err != nil { t.Fatalf("SyscallConn: %v", err) } var sockErr error if err := rc.Control(func(fd uintptr) { sockErr = syscall.SetsockoptInet4Addr(int(fd), syscall.IPPROTO_IP, syscall.IP_MULTICAST_IF, ip) }); err != nil { t.Fatalf("Control: %v", err) } if sockErr != nil { t.Fatalf("IP_MULTICAST_IF: %v", sockErr) } } func TestInterfaceForIPResolvesLoopback(t *testing.T) { ifi := interfaceForIP(net.ParseIP("127.0.0.1")) if ifi == nil { t.Fatal("no interface resolved for 127.0.0.1") } if ifi.Flags&net.FlagLoopback == 0 { t.Fatalf("resolved %q for 127.0.0.1, which is not a loopback interface", ifi.Name) } } func TestInterfaceForIPUnknownAddressIsNil(t *testing.T) { // Unspecified and unassigned addresses must fall back to "let the kernel // choose" rather than resolving to an arbitrary interface. if ifi := interfaceForIP(net.IPv4zero); ifi != nil { t.Fatalf("0.0.0.0 resolved to %q, want nil", ifi.Name) } if ifi := interfaceForIP(nil); ifi != nil { t.Fatalf("nil IP resolved to %q, want nil", ifi.Name) } if ifi := interfaceForIP(net.ParseIP("203.0.113.42")); ifi != nil { t.Fatalf("unassigned address resolved to %q, want nil", ifi.Name) } } // TestMulticastJoinOnControlInterfaceReceivesData is the regression test for the // bug that left the web UI permanently blank: the hub joined the group with a // nil interface, so imr_interface stayed INADDR_ANY and the kernel picked the // default-route interface. A UDPStreamer configured with Interface = "127.0.0.1" // sends out the loopback instead, and every datagram was silently dropped. // // The sender here mimics that server exactly (IP_MULTICAST_IF = 127.0.0.1); the // receiver joins the way runMulticastSession now does, via the interface that // owns the control connection's local address. func TestMulticastJoinOnControlInterfaceReceivesData(t *testing.T) { const group = "239.255.13.37" ifi := interfaceForIP(net.ParseIP("127.0.0.1")) if ifi == nil { t.Skip("no loopback interface available") } rx, err := net.ListenMulticastUDP("udp4", ifi, &net.UDPAddr{IP: net.ParseIP(group), Port: 0}) if err != nil { t.Fatalf("join %s on %s: %v", group, ifi.Name, err) } defer rx.Close() port := rx.LocalAddr().(*net.UDPAddr).Port tx, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")}) if err != nil { t.Fatalf("sender socket: %v", err) } defer tx.Close() setMulticastIf(t, tx, [4]byte{127, 0, 0, 1}) payload := []byte("UDPS-multicast-probe") dst := &net.UDPAddr{IP: net.ParseIP(group), Port: port} // Datagrams are lossy even on loopback if the join has not settled, so send // a few and accept the first that lands. done := make(chan struct{}) defer close(done) go func() { for { select { case <-done: return default: } tx.WriteToUDP(payload, dst) time.Sleep(20 * time.Millisecond) } }() buf := make([]byte, 128) if err := rx.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil { t.Fatal(err) } n, _, err := rx.ReadFromUDP(buf) if err != nil { t.Fatalf("no multicast received on %s within 3s: %v", ifi.Name, err) } if got := string(buf[:n]); got != string(payload) { t.Fatalf("payload = %q, want %q", got, payload) } }