Commit 40ad15ca authored by Mikio Hara's avatar Mikio Hara

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
parent 53bb7518
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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,37 +43,45 @@ func TestPacketConnReadWriteMulticastUDP(t *testing.T) {
t.Skipf("not available on %q", runtime.GOOS)
}
c, err := net.ListenPacket("udp6", "[ff02::114]:0") // see RFC 4727
for _, tt := range packetConnReadWriteMulticastUDPTests {
c, err := net.ListenPacket("udp6", tt.addr)
if err != nil {
t.Fatalf("net.ListenPacket failed: %v", err)
t.Fatal(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)
}
grp := *tt.grp
grp.Port = c.LocalAddr().(*net.UDPAddr).Port
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 tt.src == nil {
if err := p.JoinGroup(ifi, &grp); err != nil {
t.Fatal(err)
}
defer p.LeaveGroup(ifi, &grp)
} else {
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.Fatalf("ipv6.PacketConn.SetMulticastInterface failed: %v", err)
t.Fatal(err)
}
if _, err := p.MulticastInterface(); err != nil {
t.Fatalf("ipv6.PacketConn.MulticastInterface failed: %v", err)
t.Fatal(err)
}
if err := p.SetMulticastLoopback(true); err != nil {
t.Fatalf("ipv6.PacketConn.SetMulticastLoopback failed: %v", err)
t.Fatal(err)
}
if _, err := p.MulticastLoopback(); err != nil {
t.Fatalf("ipv6.PacketConn.MulticastLoopback failed: %v", err)
t.Fatal(err)
}
cm := ipv6.ControlMessage{
......@@ -78,28 +95,38 @@ func TestPacketConnReadWriteMulticastUDP(t *testing.T) {
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)
t.Logf("not supported on %q", runtime.GOOS)
continue
}
t.Fatalf("ipv6.PacketConn.SetControlMessage failed: %v", err)
t.Fatal(err)
}
if err := p.SetDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
t.Fatalf("ipv6.PacketConn.SetDeadline failed: %v", err)
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)
if n, err := p.WriteTo(wb, &cm, &grp); err != nil {
t.Fatal(err)
} else if n != len(wb) {
t.Fatalf("ipv6.PacketConn.WriteTo failed: short write: %v", n)
t.Fatal(err)
}
rb := make([]byte, 128)
if n, cm, _, err := p.ReadFrom(rb); err != nil {
t.Fatalf("ipv6.PacketConn.ReadFrom failed: %v", err)
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) {
......@@ -118,34 +145,44 @@ func TestPacketConnReadWriteMulticastICMP(t *testing.T) {
t.Skipf("not available on %q", runtime.GOOS)
}
for _, tt := range packetConnReadWriteMulticastICMPTests {
c, err := net.ListenPacket("ip6:ipv6-icmp", "::")
if err != nil {
t.Fatalf("net.ListenPacket failed: %v", err)
t.Fatal(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)
pshicmp := icmp.IPv6PseudoHeader(c.LocalAddr().(*net.IPAddr).IP, tt.grp.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 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)
}
defer p.LeaveSourceSpecificGroup(ifi, tt.grp, tt.src)
}
if err := p.SetMulticastInterface(ifi); err != nil {
t.Fatalf("ipv6.PacketConn.SetMulticastInterface failed: %v", err)
t.Fatal(err)
}
if _, err := p.MulticastInterface(); err != nil {
t.Fatalf("ipv6.PacketConn.MulticastInterface failed: %v", err)
t.Fatal(err)
}
if err := p.SetMulticastLoopback(true); err != nil {
t.Fatalf("ipv6.PacketConn.SetMulticastLoopback failed: %v", err)
t.Fatal(err)
}
if _, err := p.MulticastLoopback(); err != nil {
t.Fatalf("ipv6.PacketConn.MulticastLoopback failed: %v", err)
t.Fatal(err)
}
cm := ipv6.ControlMessage{
......@@ -159,7 +196,7 @@ func TestPacketConnReadWriteMulticastICMP(t *testing.T) {
f.SetAll(true)
f.Set(ipv6.ICMPTypeEchoReply, false)
if err := p.SetICMPFilter(&f); err != nil {
t.Fatalf("ipv6.PacketConn.SetICMPFilter failed: %v", err)
t.Fatal(err)
}
var psh []byte
......@@ -167,12 +204,13 @@ func TestPacketConnReadWriteMulticastICMP(t *testing.T) {
if toggle {
psh = nil
if err := p.SetChecksum(true, 2); err != nil {
t.Fatalf("ipv6.PacketConn.SetChecksum failed: %v", err)
t.Fatal(err)
}
} else {
psh = pshicmp
// Some platforms never allow to disable the
// kernel checksum processing.
// Some platforms never allow to
// disable the kernel checksum
// processing.
p.SetChecksum(false, -1)
}
wb, err := (&icmp.Message{
......@@ -183,33 +221,35 @@ func TestPacketConnReadWriteMulticastICMP(t *testing.T) {
},
}).Marshal(psh)
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)
t.Logf("not supported on %q", runtime.GOOS)
continue
}
t.Fatalf("ipv6.PacketConn.SetControlMessage failed: %v", err)
t.Fatal(err)
}
if err := p.SetDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
t.Fatalf("ipv6.PacketConn.SetDeadline failed: %v", err)
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)
if n, err := p.WriteTo(wb, &cm, tt.grp); err != nil {
t.Fatal(err)
} else if n != len(wb) {
t.Fatalf("ipv6.PacketConn.WriteTo failed: short write: %v", n)
t.Fatalf("got %v; expected %v", n, len(wb))
}
rb := make([]byte, 128)
if n, cm, _, err := p.ReadFrom(rb); err != nil {
t.Fatalf("ipv6.PacketConn.ReadFrom failed: %v", err)
t.Fatal(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)
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)
}
}
}
}
}
......@@ -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 tt.src == nil {
testMulticastSocketOptions(t, p, ifi, tt.grp)
} else {
testSourceSpecificMulticastSocketOptions(t, p, ifi, tt.grp, tt.src)
}
}
if v, err := p.MulticastHopLimit(); err != nil {
t.Fatalf("ipv6.PacketConn.MulticastHopLimit failed: %v", err)
}
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
}
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.Fatalf("got unexpected multicast hop limit %v; expected %v", v, hoplim)
t.Errorf("got unexpected multicast hop limit %v; expected %v", v, hoplim)
return
}
for _, toggle := range []bool{true, false} {
if err := p.SetMulticastLoopback(toggle); err != nil {
t.Fatalf("ipv6.PacketConn.SetMulticastLoopback failed: %v", err)
if err := c.SetMulticastLoopback(toggle); err != nil {
t.Error(err)
return
}
if v, err := p.MulticastLoopback(); err != nil {
t.Fatalf("ipv6.PacketConn.MulticastLoopback failed: %v", err)
if v, err := c.MulticastLoopback(); err != nil {
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 := p.JoinGroup(ifi, tt.gaddr); err != nil {
t.Fatalf("ipv6.PacketConn.JoinGroup(%v, %v) failed: %v", ifi, tt.gaddr, err)
if err := c.JoinGroup(ifi, grp); err != nil {
t.Error(err)
return
}
if err := c.LeaveGroup(ifi, grp); 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)
}
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
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment