117 lines
3.5 KiB
Go
117 lines
3.5 KiB
Go
//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)
|
|
}
|
|
}
|