x/net/ipv4: add support for source-specific multicast

This CL introduces methods for the manipulation of source-specifc
group into both PacketConn and RawConn as follows:

JoinSourceSpecificGroup(*net.Interface, net.Addr, net.Addr) error
LeaveSourceSpecificGroup(*net.Interface, net.Addr, net.Addr) error
ExcludeSourceSpecificGroup(*net.Interface, net.Addr, net.Addr) error
IncludeSourceSpecificGroup(*net.Interface, net.Addr, net.Addr) error

Fixes golang/go#8266.

LGTM=iant
R=iant
CC=golang-codereviews
https://golang.org/cl/174030043
diff --git a/ipv4/multicastsockopt_test.go b/ipv4/multicastsockopt_test.go
index cda97bd..56df5be 100644
--- a/ipv4/multicastsockopt_test.go
+++ b/ipv4/multicastsockopt_test.go
@@ -16,10 +16,13 @@
 
 var packetConnMulticastSocketOptionTests = []struct {
 	net, proto, addr string
-	gaddr            net.Addr
+	grp, src         net.Addr
 }{
-	{"udp4", "", "224.0.0.0:0", &net.UDPAddr{IP: net.IPv4(224, 0, 0, 249)}}, // see RFC 4727
-	{"ip4", ":icmp", "0.0.0.0", &net.IPAddr{IP: net.IPv4(224, 0, 0, 250)}},  // see RFC 4727
+	{"udp4", "", "224.0.0.0:0", &net.UDPAddr{IP: net.IPv4(224, 0, 0, 249)}, nil}, // see RFC 4727
+	{"ip4", ":icmp", "0.0.0.0", &net.IPAddr{IP: net.IPv4(224, 0, 0, 250)}, nil},  // see RFC 4727
+
+	{"udp4", "", "232.0.0.0:0", &net.UDPAddr{IP: net.IPv4(232, 0, 1, 249)}, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}}, // see RFC 5771
+	{"ip4", ":icmp", "0.0.0.0", &net.IPAddr{IP: net.IPv4(232, 0, 1, 250)}, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}},  // see RFC 5771
 }
 
 func TestPacketConnMulticastSocketOptions(t *testing.T) {
@@ -34,18 +37,33 @@
 
 	for _, tt := range packetConnMulticastSocketOptionTests {
 		if tt.net == "ip4" && os.Getuid() != 0 {
-			t.Skip("must be root")
+			t.Log("must be root")
+			continue
 		}
 		c, err := net.ListenPacket(tt.net+tt.proto, tt.addr)
 		if err != nil {
-			t.Fatalf("net.ListenPacket failed: %v", err)
+			t.Fatal(err)
 		}
 		defer c.Close()
+		p := ipv4.NewPacketConn(c)
+		defer p.Close()
 
-		testMulticastSocketOptions(t, ipv4.NewPacketConn(c), ifi, tt.gaddr)
+		if tt.src == nil {
+			testMulticastSocketOptions(t, p, ifi, tt.grp)
+		} else {
+			testSourceSpecificMulticastSocketOptions(t, p, ifi, tt.grp, tt.src)
+		}
 	}
 }
 
+var rawConnMulticastSocketOptionTests = []struct {
+	grp, src net.Addr
+}{
+	{&net.IPAddr{IP: net.IPv4(224, 0, 0, 250)}, nil}, // see RFC 4727
+
+	{&net.IPAddr{IP: net.IPv4(232, 0, 1, 250)}, &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)}}, // see RFC 5771
+}
+
 func TestRawConnMulticastSocketOptions(t *testing.T) {
 	switch runtime.GOOS {
 	case "nacl", "plan9", "solaris":
@@ -59,18 +77,24 @@
 		t.Skipf("not available on %q", runtime.GOOS)
 	}
 
-	c, err := net.ListenPacket("ip4:icmp", "0.0.0.0")
-	if err != nil {
-		t.Fatalf("net.ListenPacket failed: %v", err)
-	}
-	defer c.Close()
+	for _, tt := range rawConnMulticastSocketOptionTests {
+		c, err := net.ListenPacket("ip4:icmp", "0.0.0.0")
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer c.Close()
+		r, err := ipv4.NewRawConn(c)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer r.Close()
 
-	r, err := ipv4.NewRawConn(c)
-	if err != nil {
-		t.Fatalf("ipv4.NewRawConn failed: %v", err)
+		if tt.src == nil {
+			testMulticastSocketOptions(t, r, ifi, tt.grp)
+		} else {
+			testSourceSpecificMulticastSocketOptions(t, r, ifi, tt.grp, tt.src)
+		}
 	}
-
-	testMulticastSocketOptions(t, r, ifi, &net.IPAddr{IP: net.IPv4(224, 0, 0, 250)}) /// see RFC 4727
 }
 
 type testIPv4MulticastConn interface {
@@ -80,34 +104,92 @@
 	SetMulticastLoopback(bool) error
 	JoinGroup(*net.Interface, net.Addr) error
 	LeaveGroup(*net.Interface, net.Addr) error
+	JoinSourceSpecificGroup(*net.Interface, net.Addr, net.Addr) error
+	LeaveSourceSpecificGroup(*net.Interface, net.Addr, net.Addr) error
+	ExcludeSourceSpecificGroup(*net.Interface, net.Addr, net.Addr) error
+	IncludeSourceSpecificGroup(*net.Interface, net.Addr, net.Addr) error
 }
 
-func testMulticastSocketOptions(t *testing.T, c testIPv4MulticastConn, ifi *net.Interface, gaddr net.Addr) {
+func testMulticastSocketOptions(t *testing.T, c testIPv4MulticastConn, ifi *net.Interface, grp net.Addr) {
 	const ttl = 255
 	if err := c.SetMulticastTTL(ttl); err != nil {
-		t.Fatalf("ipv4.PacketConn.SetMulticastTTL failed: %v", err)
+		t.Error(err)
+		return
 	}
 	if v, err := c.MulticastTTL(); err != nil {
-		t.Fatalf("ipv4.PacketConn.MulticastTTL failed: %v", err)
+		t.Error(err)
+		return
 	} else if v != ttl {
-		t.Fatalf("got unexpected multicast TTL value %v; expected %v", v, ttl)
+		t.Errorf("got unexpected multicast ttl %v; expected %v", v, ttl)
+		return
 	}
 
 	for _, toggle := range []bool{true, false} {
 		if err := c.SetMulticastLoopback(toggle); err != nil {
-			t.Fatalf("ipv4.PacketConn.SetMulticastLoopback failed: %v", err)
+			t.Error(err)
+			return
 		}
 		if v, err := c.MulticastLoopback(); err != nil {
-			t.Fatalf("ipv4.PacketConn.MulticastLoopback failed: %v", err)
+			t.Error(err)
+			return
 		} else if v != toggle {
-			t.Fatalf("got unexpected multicast loopback %v; expected %v", v, toggle)
+			t.Errorf("got unexpected multicast loopback %v; expected %v", v, toggle)
+			return
 		}
 	}
 
-	if err := c.JoinGroup(ifi, gaddr); err != nil {
-		t.Fatalf("ipv4.PacketConn.JoinGroup(%v, %v) failed: %v", ifi, gaddr, err)
+	if err := c.JoinGroup(ifi, grp); err != nil {
+		t.Error(err)
+		return
 	}
-	if err := c.LeaveGroup(ifi, gaddr); err != nil {
-		t.Fatalf("ipv4.PacketConn.LeaveGroup(%v, %v) failed: %v", ifi, gaddr, err)
+	if err := c.LeaveGroup(ifi, grp); err != nil {
+		t.Error(err)
+		return
+	}
+}
+
+func testSourceSpecificMulticastSocketOptions(t *testing.T, c testIPv4MulticastConn, ifi *net.Interface, grp, src net.Addr) {
+	// MCAST_JOIN_GROUP -> MCAST_BLOCK_SOURCE -> MCAST_UNBLOCK_SOURCE -> MCAST_LEAVE_GROUP
+	if err := c.JoinGroup(ifi, grp); err != nil {
+		t.Error(err)
+		return
+	}
+	if err := c.ExcludeSourceSpecificGroup(ifi, grp, src); err != nil {
+		switch runtime.GOOS {
+		case "freebsd", "linux":
+		default: // platforms that don't support IGMPv2/3 fail here
+			t.Logf("not supported on %q", runtime.GOOS)
+			return
+		}
+		t.Error(err)
+		return
+	}
+	if err := c.IncludeSourceSpecificGroup(ifi, grp, src); err != nil {
+		t.Error(err)
+		return
+	}
+	if err := c.LeaveGroup(ifi, grp); err != nil {
+		t.Error(err)
+		return
+	}
+
+	// MCAST_JOIN_SOURCE_GROUP -> MCAST_LEAVE_SOURCE_GROUP
+	if err := c.JoinSourceSpecificGroup(ifi, grp, src); err != nil {
+		t.Error(err)
+		return
+	}
+	if err := c.LeaveSourceSpecificGroup(ifi, grp, src); err != nil {
+		t.Error(err)
+		return
+	}
+
+	// MCAST_JOIN_SOURCE_GROUP -> MCAST_LEAVE_GROUP
+	if err := c.JoinSourceSpecificGroup(ifi, grp, src); err != nil {
+		t.Error(err)
+		return
+	}
+	if err := c.LeaveGroup(ifi, grp); err != nil {
+		t.Error(err)
+		return
 	}
 }