Parcourir la source

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

This CL introduces methods for the manipulation of source-specifc
group into PacketConn 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#8752.

LGTM=iant
R=iant
CC=golang-codereviews
https://golang.org/cl/169510043
Mikio Hara il y a 11 ans
Parent
commit
40ad15caf3
6 fichiers modifiés avec 430 ajouts et 178 suppressions
  1. 4 0
      ipv6/control.go
  2. 95 1
      ipv6/dgramopt_posix.go
  3. 35 1
      ipv6/dgramopt_stub.go
  4. 2 1
      ipv6/doc.go
  5. 187 147
      ipv6/multicast_test.go
  6. 107 28
      ipv6/multicastsockopt_test.go

+ 4 - 0
ipv6/control.go

@@ -27,6 +27,10 @@ var (
 //	http://tools.ietf.org/html/rfc3493.html
 // RFC 3542  Advanced Sockets Application Program Interface (API) for IPv6
 //	http://tools.ietf.org/html/rfc3542
+// RFC 3678  Socket Interface Extensions for Multicast Source Filters
+//	http://tools.ietf.org/html/rfc3678
+// RFC 4607  Source-Specific Multicast for IP
+//	http://tools.ietf.org/html/rfc4607
 //
 // Note that RFC 3542 obsoletes RFC 2292 but OS X Snow Leopard and the
 // former still support RFC 2292 only.  Please be aware that almost

+ 95 - 1
ipv6/dgramopt_posix.go

@@ -93,7 +93,11 @@ func (c *dgramOpt) SetMulticastLoopback(on bool) error {
 	return setInt(fd, &sockOpts[ssoMulticastLoopback], boolint(on))
 }
 
-// JoinGroup joins the group address group on the interface ifi.
+// JoinGroup joins the group address group on the interface ifi. By
+// default all sources that can cast data to group are accepted. It's
+// possible to mute and unmute data transmission from a specific
+// source by using ExcludeSourceSpecificGroup and
+// IncludeSourceSpecificGroup.
 // It uses the system assigned multicast interface when ifi is nil,
 // although this is not recommended because the assignment depends on
 // platforms and sometimes it might require routing configuration.
@@ -113,6 +117,8 @@ func (c *dgramOpt) JoinGroup(ifi *net.Interface, group net.Addr) error {
 }
 
 // LeaveGroup leaves the group address group on the interface ifi.
+// It's allowed to leave the group which is formed by
+// JoinSourceSpecificGroup for convenience.
 func (c *dgramOpt) LeaveGroup(ifi *net.Interface, group net.Addr) error {
 	if !c.ok() {
 		return syscall.EINVAL
@@ -128,6 +134,94 @@ func (c *dgramOpt) LeaveGroup(ifi *net.Interface, group net.Addr) error {
 	return setGroup(fd, &sockOpts[ssoLeaveGroup], ifi, grp)
 }
 
+// JoinSourceSpecificGroup joins the source-specific group consisting
+// group and source on the interface ifi. It uses the system assigned
+// multicast interface when ifi is nil, although this is not
+// recommended because the assignment depends on platforms and
+// sometimes it might require routing configuration.
+func (c *dgramOpt) JoinSourceSpecificGroup(ifi *net.Interface, group, source net.Addr) error {
+	if !c.ok() {
+		return syscall.EINVAL
+	}
+	fd, err := c.sysfd()
+	if err != nil {
+		return err
+	}
+	grp := netAddrToIP16(group)
+	if grp == nil {
+		return errMissingAddress
+	}
+	src := netAddrToIP16(source)
+	if src == nil {
+		return errMissingAddress
+	}
+	return setSourceGroup(fd, &sockOpts[ssoJoinSourceGroup], ifi, grp, src)
+}
+
+// LeaveSourceSpecificGroup leaves the source-specific group on the
+// interface ifi.
+func (c *dgramOpt) LeaveSourceSpecificGroup(ifi *net.Interface, group, source net.Addr) error {
+	if !c.ok() {
+		return syscall.EINVAL
+	}
+	fd, err := c.sysfd()
+	if err != nil {
+		return err
+	}
+	grp := netAddrToIP16(group)
+	if grp == nil {
+		return errMissingAddress
+	}
+	src := netAddrToIP16(source)
+	if src == nil {
+		return errMissingAddress
+	}
+	return setSourceGroup(fd, &sockOpts[ssoLeaveSourceGroup], ifi, grp, src)
+}
+
+// ExcludeSourceSpecificGroup excludes the source-specific group from
+// the already joined groups by either JoinGroup or
+// JoinSourceSpecificGroup on the interface ifi.
+func (c *dgramOpt) ExcludeSourceSpecificGroup(ifi *net.Interface, group, source net.Addr) error {
+	if !c.ok() {
+		return syscall.EINVAL
+	}
+	fd, err := c.sysfd()
+	if err != nil {
+		return err
+	}
+	grp := netAddrToIP16(group)
+	if grp == nil {
+		return errMissingAddress
+	}
+	src := netAddrToIP16(source)
+	if src == nil {
+		return errMissingAddress
+	}
+	return setSourceGroup(fd, &sockOpts[ssoBlockSourceGroup], ifi, grp, src)
+}
+
+// IncludeSourceSpecificGroup includes the excluded source-specific
+// group by ExcludeSourceSpecificGroup again on the interface ifi.
+func (c *dgramOpt) IncludeSourceSpecificGroup(ifi *net.Interface, group, source net.Addr) error {
+	if !c.ok() {
+		return syscall.EINVAL
+	}
+	fd, err := c.sysfd()
+	if err != nil {
+		return err
+	}
+	grp := netAddrToIP16(group)
+	if grp == nil {
+		return errMissingAddress
+	}
+	src := netAddrToIP16(source)
+	if src == nil {
+		return errMissingAddress
+	}
+	return setSourceGroup(fd, &sockOpts[ssoUnblockSourceGroup], ifi, grp, src)
+}
+
 // Checksum reports whether the kernel will compute, store or verify a
 // checksum for both incoming and outgoing packets.  If on is true, it
 // returns an offset in bytes into the data of where the checksum

+ 35 - 1
ipv6/dgramopt_stub.go

@@ -44,7 +44,11 @@ func (c *dgramOpt) SetMulticastLoopback(on bool) error {
 	return errOpNoSupport
 }
 
-// JoinGroup joins the group address group on the interface ifi.
+// JoinGroup joins the group address group on the interface ifi. By
+// default all sources that can cast data to group are accepted. It's
+// possible to mute and unmute data transmission from a specific
+// source by using ExcludeSourceSpecificGroup and
+// IncludeSourceSpecificGroup.
 // It uses the system assigned multicast interface when ifi is nil,
 // although this is not recommended because the assignment depends on
 // platforms and sometimes it might require routing configuration.
@@ -53,10 +57,40 @@ func (c *dgramOpt) JoinGroup(ifi *net.Interface, group net.Addr) error {
 }
 
 // LeaveGroup leaves the group address group on the interface ifi.
+// It's allowed to leave the group which is formed by
+// JoinSourceSpecificGroup for convenience.
 func (c *dgramOpt) LeaveGroup(ifi *net.Interface, group net.Addr) error {
 	return errOpNoSupport
 }
 
+// JoinSourceSpecificGroup joins the source-specific group consisting
+// group and source on the interface ifi. It uses the system assigned
+// multicast interface when ifi is nil, although this is not
+// recommended because the assignment depends on platforms and
+// sometimes it might require routing configuration.
+func (c *dgramOpt) JoinSourceSpecificGroup(ifi *net.Interface, group, source net.Addr) error {
+	return errOpNoSupport
+}
+
+// LeaveSourceSpecificGroup leaves the source-specific group on the
+// interface ifi.
+func (c *dgramOpt) LeaveSourceSpecificGroup(ifi *net.Interface, group, source net.Addr) error {
+	return errOpNoSupport
+}
+
+// ExcludeSourceSpecificGroup excludes the source-specific group from
+// the already joined groups by either JoinGroup or
+// JoinSourceSpecificGroup on the interface ifi.
+func (c *dgramOpt) ExcludeSourceSpecificGroup(ifi *net.Interface, group, source net.Addr) error {
+	return errOpNoSupport
+}
+
+// IncludeSourceSpecificGroup includes the excluded source-specific
+// group by ExcludeSourceSpecificGroup again on the interface ifi.
+func (c *dgramOpt) IncludeSourceSpecificGroup(ifi *net.Interface, group, source net.Addr) error {
+	return errOpNoSupport
+}
+
 // Checksum reports whether the kernel will compute, store or verify a
 // checksum for both incoming and outgoing packets.  If on is true, it
 // returns an offset in bytes into the data of where the checksum

+ 2 - 1
ipv6/doc.go

@@ -7,7 +7,8 @@
 //
 // The package provides IP-level socket options that allow
 // manipulation of IPv6 facilities.  The IPv6 and socket options for
-// IPv6 are defined in RFC 2460, RFC 3493 and RFC 3542.
+// IPv6 are defined in RFC 2460, RFC 3493, RFC 3542, RFC 3678 and RFC
+// 4607.
 //
 //
 // Unicasting

+ 187 - 147
ipv6/multicast_test.go

@@ -18,6 +18,15 @@ import (
 	"golang.org/x/net/ipv6"
 )
 
+var packetConnReadWriteMulticastUDPTests = []struct {
+	addr     string
+	grp, src *net.UDPAddr
+}{
+	{"[ff02::]:0", &net.UDPAddr{IP: net.ParseIP("ff02::114")}, nil}, // see RFC 4727
+
+	{"[ff30::8000:0]:0", &net.UDPAddr{IP: net.ParseIP("ff30::8000:1")}, &net.UDPAddr{IP: net.IPv6loopback}}, // see RFC 5771
+}
+
 func TestPacketConnReadWriteMulticastUDP(t *testing.T) {
 	switch runtime.GOOS {
 	case "freebsd": // due to a bug on loopback marking
@@ -34,74 +43,92 @@ func TestPacketConnReadWriteMulticastUDP(t *testing.T) {
 		t.Skipf("not available on %q", runtime.GOOS)
 	}
 
-	c, err := net.ListenPacket("udp6", "[ff02::114]:0") // see RFC 4727
-	if err != nil {
-		t.Fatalf("net.ListenPacket failed: %v", err)
-	}
-	defer c.Close()
-
-	_, port, err := net.SplitHostPort(c.LocalAddr().String())
-	if err != nil {
-		t.Fatalf("net.SplitHostPort failed: %v", err)
-	}
-	dst, err := net.ResolveUDPAddr("udp6", "[ff02::114]:"+port) // see RFC 4727
-	if err != nil {
-		t.Fatalf("net.ResolveUDPAddr failed: %v", err)
-	}
-
-	p := ipv6.NewPacketConn(c)
-	defer p.Close()
-	if err := p.JoinGroup(ifi, dst); err != nil {
-		t.Fatalf("ipv6.PacketConn.JoinGroup on %v failed: %v", ifi, err)
-	}
-	if err := p.SetMulticastInterface(ifi); err != nil {
-		t.Fatalf("ipv6.PacketConn.SetMulticastInterface failed: %v", err)
-	}
-	if _, err := p.MulticastInterface(); err != nil {
-		t.Fatalf("ipv6.PacketConn.MulticastInterface failed: %v", err)
-	}
-	if err := p.SetMulticastLoopback(true); err != nil {
-		t.Fatalf("ipv6.PacketConn.SetMulticastLoopback failed: %v", err)
-	}
-	if _, err := p.MulticastLoopback(); err != nil {
-		t.Fatalf("ipv6.PacketConn.MulticastLoopback failed: %v", err)
-	}
-
-	cm := ipv6.ControlMessage{
-		TrafficClass: iana.DiffServAF11 | iana.CongestionExperienced,
-		Src:          net.IPv6loopback,
-		IfIndex:      ifi.Index,
-	}
-	cf := ipv6.FlagTrafficClass | ipv6.FlagHopLimit | ipv6.FlagSrc | ipv6.FlagDst | ipv6.FlagInterface | ipv6.FlagPathMTU
-	wb := []byte("HELLO-R-U-THERE")
-
-	for i, toggle := range []bool{true, false, true} {
-		if err := p.SetControlMessage(cf, toggle); err != nil {
-			if nettest.ProtocolNotSupported(err) {
-				t.Skipf("not supported on %q", runtime.GOOS)
+	for _, tt := range packetConnReadWriteMulticastUDPTests {
+		c, err := net.ListenPacket("udp6", tt.addr)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer c.Close()
+
+		grp := *tt.grp
+		grp.Port = c.LocalAddr().(*net.UDPAddr).Port
+		p := ipv6.NewPacketConn(c)
+		defer p.Close()
+		if tt.src == nil {
+			if err := p.JoinGroup(ifi, &grp); err != nil {
+				t.Fatal(err)
 			}
-			t.Fatalf("ipv6.PacketConn.SetControlMessage failed: %v", err)
-		}
-		if err := p.SetDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
-			t.Fatalf("ipv6.PacketConn.SetDeadline failed: %v", err)
-		}
-		cm.HopLimit = i + 1
-		if n, err := p.WriteTo(wb, &cm, dst); err != nil {
-			t.Fatalf("ipv6.PacketConn.WriteTo failed: %v", err)
-		} else if n != len(wb) {
-			t.Fatalf("ipv6.PacketConn.WriteTo failed: short write: %v", n)
-		}
-		rb := make([]byte, 128)
-		if n, cm, _, err := p.ReadFrom(rb); err != nil {
-			t.Fatalf("ipv6.PacketConn.ReadFrom failed: %v", err)
-		} else if !bytes.Equal(rb[:n], wb) {
-			t.Fatalf("got %v; expected %v", rb[:n], wb)
+			defer p.LeaveGroup(ifi, &grp)
 		} else {
-			t.Logf("rcvd cmsg: %v", cm)
+			if err := p.JoinSourceSpecificGroup(ifi, &grp, tt.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)
+					continue
+				}
+				t.Fatal(err)
+			}
+			defer p.LeaveSourceSpecificGroup(ifi, &grp, tt.src)
+		}
+		if err := p.SetMulticastInterface(ifi); err != nil {
+			t.Fatal(err)
+		}
+		if _, err := p.MulticastInterface(); err != nil {
+			t.Fatal(err)
+		}
+		if err := p.SetMulticastLoopback(true); err != nil {
+			t.Fatal(err)
+		}
+		if _, err := p.MulticastLoopback(); err != nil {
+			t.Fatal(err)
+		}
+
+		cm := ipv6.ControlMessage{
+			TrafficClass: iana.DiffServAF11 | iana.CongestionExperienced,
+			Src:          net.IPv6loopback,
+			IfIndex:      ifi.Index,
+		}
+		cf := ipv6.FlagTrafficClass | ipv6.FlagHopLimit | ipv6.FlagSrc | ipv6.FlagDst | ipv6.FlagInterface | ipv6.FlagPathMTU
+		wb := []byte("HELLO-R-U-THERE")
+
+		for i, toggle := range []bool{true, false, true} {
+			if err := p.SetControlMessage(cf, toggle); err != nil {
+				if nettest.ProtocolNotSupported(err) {
+					t.Logf("not supported on %q", runtime.GOOS)
+					continue
+				}
+				t.Fatal(err)
+			}
+			if err := p.SetDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
+				t.Fatal(err)
+			}
+			cm.HopLimit = i + 1
+			if n, err := p.WriteTo(wb, &cm, &grp); err != nil {
+				t.Fatal(err)
+			} else if n != len(wb) {
+				t.Fatal(err)
+			}
+			rb := make([]byte, 128)
+			if n, cm, _, err := p.ReadFrom(rb); err != nil {
+				t.Fatal(err)
+			} else if !bytes.Equal(rb[:n], wb) {
+				t.Fatalf("got %v; expected %v", rb[:n], wb)
+			} else {
+				t.Logf("rcvd cmsg: %v", cm)
+			}
 		}
 	}
 }
 
+var packetConnReadWriteMulticastICMPTests = []struct {
+	grp, src *net.IPAddr
+}{
+	{&net.IPAddr{IP: net.ParseIP("ff02::114")}, nil}, // see RFC 4727
+
+	{&net.IPAddr{IP: net.ParseIP("ff30::8000:1")}, &net.IPAddr{IP: net.IPv6loopback}}, // see RFC 5771
+}
+
 func TestPacketConnReadWriteMulticastICMP(t *testing.T) {
 	switch runtime.GOOS {
 	case "nacl", "plan9", "solaris", "windows":
@@ -118,97 +145,110 @@ func TestPacketConnReadWriteMulticastICMP(t *testing.T) {
 		t.Skipf("not available on %q", runtime.GOOS)
 	}
 
-	c, err := net.ListenPacket("ip6:ipv6-icmp", "::")
-	if err != nil {
-		t.Fatalf("net.ListenPacket failed: %v", err)
-	}
-	defer c.Close()
-
-	dst, err := net.ResolveIPAddr("ip6", "ff02::114") // see RFC 4727
-	if err != nil {
-		t.Fatalf("net.ResolveIPAddr failed: %v", err)
-	}
-
-	pshicmp := icmp.IPv6PseudoHeader(c.LocalAddr().(*net.IPAddr).IP, dst.IP)
-	p := ipv6.NewPacketConn(c)
-	defer p.Close()
-	if err := p.JoinGroup(ifi, dst); err != nil {
-		t.Fatalf("ipv6.PacketConn.JoinGroup on %v failed: %v", ifi, err)
-	}
-	if err := p.SetMulticastInterface(ifi); err != nil {
-		t.Fatalf("ipv6.PacketConn.SetMulticastInterface failed: %v", err)
-	}
-	if _, err := p.MulticastInterface(); err != nil {
-		t.Fatalf("ipv6.PacketConn.MulticastInterface failed: %v", err)
-	}
-	if err := p.SetMulticastLoopback(true); err != nil {
-		t.Fatalf("ipv6.PacketConn.SetMulticastLoopback failed: %v", err)
-	}
-	if _, err := p.MulticastLoopback(); err != nil {
-		t.Fatalf("ipv6.PacketConn.MulticastLoopback failed: %v", err)
-	}
-
-	cm := ipv6.ControlMessage{
-		TrafficClass: iana.DiffServAF11 | iana.CongestionExperienced,
-		Src:          net.IPv6loopback,
-		IfIndex:      ifi.Index,
-	}
-	cf := ipv6.FlagTrafficClass | ipv6.FlagHopLimit | ipv6.FlagSrc | ipv6.FlagDst | ipv6.FlagInterface | ipv6.FlagPathMTU
-
-	var f ipv6.ICMPFilter
-	f.SetAll(true)
-	f.Set(ipv6.ICMPTypeEchoReply, false)
-	if err := p.SetICMPFilter(&f); err != nil {
-		t.Fatalf("ipv6.PacketConn.SetICMPFilter failed: %v", err)
-	}
-
-	var psh []byte
-	for i, toggle := range []bool{true, false, true} {
-		if toggle {
-			psh = nil
-			if err := p.SetChecksum(true, 2); err != nil {
-				t.Fatalf("ipv6.PacketConn.SetChecksum failed: %v", err)
-			}
-		} else {
-			psh = pshicmp
-			// Some platforms never allow to disable the
-			// kernel checksum processing.
-			p.SetChecksum(false, -1)
-		}
-		wb, err := (&icmp.Message{
-			Type: ipv6.ICMPTypeEchoRequest, Code: 0,
-			Body: &icmp.Echo{
-				ID: os.Getpid() & 0xffff, Seq: i + 1,
-				Data: []byte("HELLO-R-U-THERE"),
-			},
-		}).Marshal(psh)
+	for _, tt := range packetConnReadWriteMulticastICMPTests {
+		c, err := net.ListenPacket("ip6:ipv6-icmp", "::")
 		if err != nil {
-			t.Fatalf("icmp.Message.Marshal failed: %v", err)
+			t.Fatal(err)
 		}
-		if err := p.SetControlMessage(cf, toggle); err != nil {
-			if nettest.ProtocolNotSupported(err) {
-				t.Skipf("not supported on %q", runtime.GOOS)
+		defer c.Close()
+
+		pshicmp := icmp.IPv6PseudoHeader(c.LocalAddr().(*net.IPAddr).IP, tt.grp.IP)
+		p := ipv6.NewPacketConn(c)
+		defer p.Close()
+		if tt.src == nil {
+			if err := p.JoinGroup(ifi, tt.grp); err != nil {
+				t.Fatal(err)
+			}
+			defer p.LeaveGroup(ifi, tt.grp)
+		} else {
+			if err := p.JoinSourceSpecificGroup(ifi, tt.grp, tt.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)
+					continue
+				}
+				t.Fatal(err)
 			}
-			t.Fatalf("ipv6.PacketConn.SetControlMessage failed: %v", err)
+			defer p.LeaveSourceSpecificGroup(ifi, tt.grp, tt.src)
 		}
-		if err := p.SetDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
-			t.Fatalf("ipv6.PacketConn.SetDeadline failed: %v", err)
+		if err := p.SetMulticastInterface(ifi); err != nil {
+			t.Fatal(err)
 		}
-		cm.HopLimit = i + 1
-		if n, err := p.WriteTo(wb, &cm, dst); err != nil {
-			t.Fatalf("ipv6.PacketConn.WriteTo failed: %v", err)
-		} else if n != len(wb) {
-			t.Fatalf("ipv6.PacketConn.WriteTo failed: short write: %v", n)
+		if _, err := p.MulticastInterface(); err != nil {
+			t.Fatal(err)
 		}
-		rb := make([]byte, 128)
-		if n, cm, _, err := p.ReadFrom(rb); err != nil {
-			t.Fatalf("ipv6.PacketConn.ReadFrom failed: %v", err)
-		} else {
-			t.Logf("rcvd cmsg: %v", cm)
-			if m, err := icmp.ParseMessage(iana.ProtocolIPv6ICMP, rb[:n]); err != nil {
-				t.Fatalf("icmp.ParseMessage failed: %v", err)
-			} else if m.Type != ipv6.ICMPTypeEchoReply || m.Code != 0 {
-				t.Fatalf("got type=%v, code=%v; expected type=%v, code=%v", m.Type, m.Code, ipv6.ICMPTypeEchoReply, 0)
+		if err := p.SetMulticastLoopback(true); err != nil {
+			t.Fatal(err)
+		}
+		if _, err := p.MulticastLoopback(); err != nil {
+			t.Fatal(err)
+		}
+
+		cm := ipv6.ControlMessage{
+			TrafficClass: iana.DiffServAF11 | iana.CongestionExperienced,
+			Src:          net.IPv6loopback,
+			IfIndex:      ifi.Index,
+		}
+		cf := ipv6.FlagTrafficClass | ipv6.FlagHopLimit | ipv6.FlagSrc | ipv6.FlagDst | ipv6.FlagInterface | ipv6.FlagPathMTU
+
+		var f ipv6.ICMPFilter
+		f.SetAll(true)
+		f.Set(ipv6.ICMPTypeEchoReply, false)
+		if err := p.SetICMPFilter(&f); err != nil {
+			t.Fatal(err)
+		}
+
+		var psh []byte
+		for i, toggle := range []bool{true, false, true} {
+			if toggle {
+				psh = nil
+				if err := p.SetChecksum(true, 2); err != nil {
+					t.Fatal(err)
+				}
+			} else {
+				psh = pshicmp
+				// Some platforms never allow to
+				// disable the kernel checksum
+				// processing.
+				p.SetChecksum(false, -1)
+			}
+			wb, err := (&icmp.Message{
+				Type: ipv6.ICMPTypeEchoRequest, Code: 0,
+				Body: &icmp.Echo{
+					ID: os.Getpid() & 0xffff, Seq: i + 1,
+					Data: []byte("HELLO-R-U-THERE"),
+				},
+			}).Marshal(psh)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if err := p.SetControlMessage(cf, toggle); err != nil {
+				if nettest.ProtocolNotSupported(err) {
+					t.Logf("not supported on %q", runtime.GOOS)
+					continue
+				}
+				t.Fatal(err)
+			}
+			if err := p.SetDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
+				t.Fatal(err)
+			}
+			cm.HopLimit = i + 1
+			if n, err := p.WriteTo(wb, &cm, tt.grp); err != nil {
+				t.Fatal(err)
+			} else if n != len(wb) {
+				t.Fatalf("got %v; expected %v", n, len(wb))
+			}
+			rb := make([]byte, 128)
+			if n, cm, _, err := p.ReadFrom(rb); err != nil {
+				t.Fatal(err)
+			} else {
+				t.Logf("rcvd cmsg: %v", cm)
+				if m, err := icmp.ParseMessage(iana.ProtocolIPv6ICMP, rb[:n]); err != nil {
+					t.Fatal(err)
+				} else if m.Type != ipv6.ICMPTypeEchoReply || m.Code != 0 {
+					t.Fatalf("got type=%v, code=%v; expected type=%v, code=%v", m.Type, m.Code, ipv6.ICMPTypeEchoReply, 0)
+				}
 			}
 		}
 	}

+ 107 - 28
ipv6/multicastsockopt_test.go

@@ -16,10 +16,13 @@ import (
 
 var packetConnMulticastSocketOptionTests = []struct {
 	net, proto, addr string
-	gaddr            net.Addr
+	grp, src         net.Addr
 }{
-	{"udp6", "", "[ff02::]:0", &net.UDPAddr{IP: net.ParseIP("ff02::114")}}, // see RFC 4727
-	{"ip6", ":ipv6-icmp", "::", &net.IPAddr{IP: net.ParseIP("ff02::114")}}, // see RFC 4727
+	{"udp6", "", "[ff02::]:0", &net.UDPAddr{IP: net.ParseIP("ff02::114")}, nil}, // see RFC 4727
+	{"ip6", ":ipv6-icmp", "::", &net.IPAddr{IP: net.ParseIP("ff02::115")}, nil}, // see RFC 4727
+
+	{"udp6", "", "[ff30::8000:0]:0", &net.UDPAddr{IP: net.ParseIP("ff30::8000:1")}, &net.UDPAddr{IP: net.IPv6loopback}}, // see RFC 5771
+	{"ip6", ":ipv6-icmp", "::", &net.IPAddr{IP: net.ParseIP("ff30::8000:2")}, &net.IPAddr{IP: net.IPv6loopback}},        // see RFC 5771
 }
 
 func TestPacketConnMulticastSocketOptions(t *testing.T) {
@@ -37,42 +40,118 @@ func TestPacketConnMulticastSocketOptions(t *testing.T) {
 
 	for _, tt := range packetConnMulticastSocketOptionTests {
 		if tt.net == "ip6" && 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 := ipv6.NewPacketConn(c)
+		defer p.Close()
 
-		hoplim := 255
-		if err := p.SetMulticastHopLimit(hoplim); err != nil {
-			t.Fatalf("ipv6.PacketConn.SetMulticastHopLimit failed: %v", err)
-		}
-		if v, err := p.MulticastHopLimit(); err != nil {
-			t.Fatalf("ipv6.PacketConn.MulticastHopLimit failed: %v", err)
-		} else if v != hoplim {
-			t.Fatalf("got unexpected multicast hop limit %v; expected %v", v, hoplim)
+		if tt.src == nil {
+			testMulticastSocketOptions(t, p, ifi, tt.grp)
+		} else {
+			testSourceSpecificMulticastSocketOptions(t, p, ifi, tt.grp, tt.src)
 		}
+	}
+}
 
-		for _, toggle := range []bool{true, false} {
-			if err := p.SetMulticastLoopback(toggle); err != nil {
-				t.Fatalf("ipv6.PacketConn.SetMulticastLoopback failed: %v", err)
-			}
-			if v, err := p.MulticastLoopback(); err != nil {
-				t.Fatalf("ipv6.PacketConn.MulticastLoopback failed: %v", err)
-			} else if v != toggle {
-				t.Fatalf("got unexpected multicast loopback %v; expected %v", v, toggle)
-			}
-		}
+type testIPv6MulticastConn interface {
+	MulticastHopLimit() (int, error)
+	SetMulticastHopLimit(ttl int) error
+	MulticastLoopback() (bool, error)
+	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
+}
 
-		if err := p.JoinGroup(ifi, tt.gaddr); err != nil {
-			t.Fatalf("ipv6.PacketConn.JoinGroup(%v, %v) failed: %v", ifi, tt.gaddr, err)
+func testMulticastSocketOptions(t *testing.T, c testIPv6MulticastConn, ifi *net.Interface, grp net.Addr) {
+	const hoplim = 255
+	if err := c.SetMulticastHopLimit(hoplim); err != nil {
+		t.Error(err)
+		return
+	}
+	if v, err := c.MulticastHopLimit(); err != nil {
+		t.Error(err)
+		return
+	} else if v != hoplim {
+		t.Errorf("got unexpected multicast hop limit %v; expected %v", v, hoplim)
+		return
+	}
+
+	for _, toggle := range []bool{true, false} {
+		if err := c.SetMulticastLoopback(toggle); err != nil {
+			t.Error(err)
+			return
 		}
-		if err := p.LeaveGroup(ifi, tt.gaddr); err != nil {
-			t.Fatalf("ipv6.PacketConn.LeaveGroup(%v, %v) failed: %v", ifi, tt.gaddr, err)
+		if v, err := c.MulticastLoopback(); err != nil {
+			t.Error(err)
+			return
+		} else if v != toggle {
+			t.Errorf("got unexpected multicast loopback %v; expected %v", v, toggle)
+			return
+		}
+	}
+
+	if err := c.JoinGroup(ifi, grp); err != nil {
+		t.Error(err)
+		return
+	}
+	if err := c.LeaveGroup(ifi, grp); err != nil {
+		t.Error(err)
+		return
+	}
+}
+
+func testSourceSpecificMulticastSocketOptions(t *testing.T, c testIPv6MulticastConn, 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
 	}
 }