github.com/SagerNet/gvisor@v0.0.0-20210707092255-7731c139d75c/pkg/tcpip/network/ipv6/mld_test.go (about)

     1  // Copyright 2020 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ipv6_test
    16  
    17  import (
    18  	"bytes"
    19  	"math/rand"
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/SagerNet/gvisor/pkg/tcpip"
    24  	"github.com/SagerNet/gvisor/pkg/tcpip/buffer"
    25  	"github.com/SagerNet/gvisor/pkg/tcpip/checker"
    26  	"github.com/SagerNet/gvisor/pkg/tcpip/faketime"
    27  	"github.com/SagerNet/gvisor/pkg/tcpip/header"
    28  	"github.com/SagerNet/gvisor/pkg/tcpip/link/channel"
    29  	"github.com/SagerNet/gvisor/pkg/tcpip/network/ipv6"
    30  	"github.com/SagerNet/gvisor/pkg/tcpip/stack"
    31  	"github.com/SagerNet/gvisor/pkg/tcpip/testutil"
    32  )
    33  
    34  var (
    35  	linkLocalAddr       = testutil.MustParse6("fe80::1")
    36  	globalAddr          = testutil.MustParse6("a80::1")
    37  	globalMulticastAddr = testutil.MustParse6("ff05:100::2")
    38  
    39  	linkLocalAddrSNMC = header.SolicitedNodeAddr(linkLocalAddr)
    40  	globalAddrSNMC    = header.SolicitedNodeAddr(globalAddr)
    41  )
    42  
    43  func validateMLDPacket(t *testing.T, p buffer.View, localAddress, remoteAddress tcpip.Address, mldType header.ICMPv6Type, groupAddress tcpip.Address) {
    44  	t.Helper()
    45  
    46  	checker.IPv6WithExtHdr(t, p,
    47  		checker.IPv6ExtHdr(
    48  			checker.IPv6HopByHopExtensionHeader(checker.IPv6RouterAlert(header.IPv6RouterAlertMLD)),
    49  		),
    50  		checker.SrcAddr(localAddress),
    51  		checker.DstAddr(remoteAddress),
    52  		checker.TTL(header.MLDHopLimit),
    53  		checker.MLD(mldType, header.MLDMinimumSize,
    54  			checker.MLDMaxRespDelay(0),
    55  			checker.MLDMulticastAddress(groupAddress),
    56  		),
    57  	)
    58  }
    59  
    60  func TestIPv6JoinLeaveSolicitedNodeAddressPerformsMLD(t *testing.T) {
    61  	const nicID = 1
    62  
    63  	s := stack.New(stack.Options{
    64  		NetworkProtocols: []stack.NetworkProtocolFactory{ipv6.NewProtocolWithOptions(ipv6.Options{
    65  			MLD: ipv6.MLDOptions{
    66  				Enabled: true,
    67  			},
    68  		})},
    69  	})
    70  	e := channel.New(1, header.IPv6MinimumMTU, "")
    71  	if err := s.CreateNIC(nicID, e); err != nil {
    72  		t.Fatalf("CreateNIC(%d, _): %s", nicID, err)
    73  	}
    74  
    75  	// The stack will join an address's solicited node multicast address when
    76  	// an address is added. An MLD report message should be sent for the
    77  	// solicited-node group.
    78  	if err := s.AddAddress(nicID, ipv6.ProtocolNumber, linkLocalAddr); err != nil {
    79  		t.Fatalf("AddAddress(%d, %d, %s) = %s", nicID, ipv6.ProtocolNumber, linkLocalAddr, err)
    80  	}
    81  	if p, ok := e.Read(); !ok {
    82  		t.Fatal("expected a report message to be sent")
    83  	} else {
    84  		validateMLDPacket(t, stack.PayloadSince(p.Pkt.NetworkHeader()), linkLocalAddr, linkLocalAddrSNMC, header.ICMPv6MulticastListenerReport, linkLocalAddrSNMC)
    85  	}
    86  
    87  	// The stack will leave an address's solicited node multicast address when
    88  	// an address is removed. An MLD done message should be sent for the
    89  	// solicited-node group.
    90  	if err := s.RemoveAddress(nicID, linkLocalAddr); err != nil {
    91  		t.Fatalf("RemoveAddress(%d, %s) = %s", nicID, linkLocalAddr, err)
    92  	}
    93  	if p, ok := e.Read(); !ok {
    94  		t.Fatal("expected a done message to be sent")
    95  	} else {
    96  		validateMLDPacket(t, stack.PayloadSince(p.Pkt.NetworkHeader()), header.IPv6Any, header.IPv6AllRoutersLinkLocalMulticastAddress, header.ICMPv6MulticastListenerDone, linkLocalAddrSNMC)
    97  	}
    98  }
    99  
   100  func TestSendQueuedMLDReports(t *testing.T) {
   101  	const (
   102  		nicID      = 1
   103  		maxReports = 2
   104  	)
   105  
   106  	tests := []struct {
   107  		name            string
   108  		dadTransmits    uint8
   109  		retransmitTimer time.Duration
   110  	}{
   111  		{
   112  			name:            "DAD Disabled",
   113  			dadTransmits:    0,
   114  			retransmitTimer: 0,
   115  		},
   116  		{
   117  			name:            "DAD Enabled",
   118  			dadTransmits:    1,
   119  			retransmitTimer: time.Second,
   120  		},
   121  	}
   122  
   123  	nonce := [...]byte{
   124  		1, 2, 3, 4, 5, 6,
   125  	}
   126  
   127  	const maxNSMessages = 2
   128  	secureRNGBytes := make([]byte, len(nonce)*maxNSMessages)
   129  	for b := secureRNGBytes[:]; len(b) > 0; b = b[len(nonce):] {
   130  		if n := copy(b, nonce[:]); n != len(nonce) {
   131  			t.Fatalf("got copy(...) = %d, want = %d", n, len(nonce))
   132  		}
   133  	}
   134  
   135  	for _, test := range tests {
   136  		t.Run(test.name, func(t *testing.T) {
   137  			dadResolutionTime := test.retransmitTimer * time.Duration(test.dadTransmits)
   138  			clock := faketime.NewManualClock()
   139  			var secureRNG bytes.Reader
   140  			secureRNG.Reset(secureRNGBytes[:])
   141  			s := stack.New(stack.Options{
   142  				SecureRNG:  &secureRNG,
   143  				RandSource: rand.NewSource(time.Now().UnixNano()),
   144  				NetworkProtocols: []stack.NetworkProtocolFactory{ipv6.NewProtocolWithOptions(ipv6.Options{
   145  					DADConfigs: stack.DADConfigurations{
   146  						DupAddrDetectTransmits: test.dadTransmits,
   147  						RetransmitTimer:        test.retransmitTimer,
   148  					},
   149  					MLD: ipv6.MLDOptions{
   150  						Enabled: true,
   151  					},
   152  				})},
   153  				Clock: clock,
   154  			})
   155  
   156  			// Allow space for an extra packet so we can observe packets that were
   157  			// unexpectedly sent.
   158  			e := channel.New(maxReports+int(test.dadTransmits)+1 /* extra */, header.IPv6MinimumMTU, "")
   159  			if err := s.CreateNIC(nicID, e); err != nil {
   160  				t.Fatalf("CreateNIC(%d, _): %s", nicID, err)
   161  			}
   162  
   163  			resolveDAD := func(addr, snmc tcpip.Address) {
   164  				clock.Advance(dadResolutionTime)
   165  				if p, ok := e.Read(); !ok {
   166  					t.Fatal("expected DAD packet")
   167  				} else {
   168  					checker.IPv6(t, stack.PayloadSince(p.Pkt.NetworkHeader()),
   169  						checker.SrcAddr(header.IPv6Any),
   170  						checker.DstAddr(snmc),
   171  						checker.TTL(header.NDPHopLimit),
   172  						checker.NDPNS(
   173  							checker.NDPNSTargetAddress(addr),
   174  							checker.NDPNSOptions([]header.NDPOption{header.NDPNonceOption(nonce[:])}),
   175  						))
   176  				}
   177  			}
   178  
   179  			var reportCounter uint64
   180  			reportStat := s.Stats().ICMP.V6.PacketsSent.MulticastListenerReport
   181  			if got := reportStat.Value(); got != reportCounter {
   182  				t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter)
   183  			}
   184  			var doneCounter uint64
   185  			doneStat := s.Stats().ICMP.V6.PacketsSent.MulticastListenerDone
   186  			if got := doneStat.Value(); got != doneCounter {
   187  				t.Errorf("got doneStat.Value() = %d, want = %d", got, doneCounter)
   188  			}
   189  
   190  			// Joining a group without an assigned address should send an MLD report
   191  			// with the unspecified address.
   192  			if err := s.JoinGroup(ipv6.ProtocolNumber, nicID, globalMulticastAddr); err != nil {
   193  				t.Fatalf("JoinGroup(%d, %d, %s): %s", ipv6.ProtocolNumber, nicID, globalMulticastAddr, err)
   194  			}
   195  			reportCounter++
   196  			if got := reportStat.Value(); got != reportCounter {
   197  				t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter)
   198  			}
   199  			if p, ok := e.Read(); !ok {
   200  				t.Errorf("expected MLD report for %s", globalMulticastAddr)
   201  			} else {
   202  				validateMLDPacket(t, stack.PayloadSince(p.Pkt.NetworkHeader()), header.IPv6Any, globalMulticastAddr, header.ICMPv6MulticastListenerReport, globalMulticastAddr)
   203  			}
   204  			clock.Advance(time.Hour)
   205  			if p, ok := e.Read(); ok {
   206  				t.Errorf("got unexpected packet = %#v", p)
   207  			}
   208  			if t.Failed() {
   209  				t.FailNow()
   210  			}
   211  
   212  			// Adding a global address should not send reports for the already joined
   213  			// group since we should only send queued reports when a link-local
   214  			// address is assigned.
   215  			//
   216  			// Note, we will still expect to send a report for the global address's
   217  			// solicited node address from the unspecified address as per  RFC 3590
   218  			// section 4.
   219  			if err := s.AddAddressWithOptions(nicID, ipv6.ProtocolNumber, globalAddr, stack.FirstPrimaryEndpoint); err != nil {
   220  				t.Fatalf("AddAddressWithOptions(%d, %d, %s, %d): %s", nicID, ipv6.ProtocolNumber, globalAddr, stack.FirstPrimaryEndpoint, err)
   221  			}
   222  			reportCounter++
   223  			if got := reportStat.Value(); got != reportCounter {
   224  				t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter)
   225  			}
   226  			if p, ok := e.Read(); !ok {
   227  				t.Errorf("expected MLD report for %s", globalAddrSNMC)
   228  			} else {
   229  				validateMLDPacket(t, stack.PayloadSince(p.Pkt.NetworkHeader()), header.IPv6Any, globalAddrSNMC, header.ICMPv6MulticastListenerReport, globalAddrSNMC)
   230  			}
   231  			if dadResolutionTime != 0 {
   232  				// Reports should not be sent when the address resolves.
   233  				resolveDAD(globalAddr, globalAddrSNMC)
   234  				if got := reportStat.Value(); got != reportCounter {
   235  					t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter)
   236  				}
   237  			}
   238  			// Leave the group since we don't care about the global address's
   239  			// solicited node multicast group membership.
   240  			if err := s.LeaveGroup(ipv6.ProtocolNumber, nicID, globalAddrSNMC); err != nil {
   241  				t.Fatalf("LeaveGroup(%d, %d, %s): %s", ipv6.ProtocolNumber, nicID, globalAddrSNMC, err)
   242  			}
   243  			if got := doneStat.Value(); got != doneCounter {
   244  				t.Errorf("got doneStat.Value() = %d, want = %d", got, doneCounter)
   245  			}
   246  			if p, ok := e.Read(); ok {
   247  				t.Errorf("got unexpected packet = %#v", p)
   248  			}
   249  			if t.Failed() {
   250  				t.FailNow()
   251  			}
   252  
   253  			// Adding a link-local address should send a report for its solicited node
   254  			// address and globalMulticastAddr.
   255  			if err := s.AddAddressWithOptions(nicID, ipv6.ProtocolNumber, linkLocalAddr, stack.CanBePrimaryEndpoint); err != nil {
   256  				t.Fatalf("AddAddressWithOptions(%d, %d, %s, %d): %s", nicID, ipv6.ProtocolNumber, linkLocalAddr, stack.CanBePrimaryEndpoint, err)
   257  			}
   258  			if dadResolutionTime != 0 {
   259  				reportCounter++
   260  				if got := reportStat.Value(); got != reportCounter {
   261  					t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter)
   262  				}
   263  				if p, ok := e.Read(); !ok {
   264  					t.Errorf("expected MLD report for %s", linkLocalAddrSNMC)
   265  				} else {
   266  					validateMLDPacket(t, stack.PayloadSince(p.Pkt.NetworkHeader()), header.IPv6Any, linkLocalAddrSNMC, header.ICMPv6MulticastListenerReport, linkLocalAddrSNMC)
   267  				}
   268  				resolveDAD(linkLocalAddr, linkLocalAddrSNMC)
   269  			}
   270  
   271  			// We expect two batches of reports to be sent (1 batch when the
   272  			// link-local address is assigned, and another after the maximum
   273  			// unsolicited report interval.
   274  			for i := 0; i < 2; i++ {
   275  				// We expect reports to be sent (one for globalMulticastAddr and another
   276  				// for linkLocalAddrSNMC).
   277  				reportCounter += maxReports
   278  				if got := reportStat.Value(); got != reportCounter {
   279  					t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter)
   280  				}
   281  
   282  				addrs := map[tcpip.Address]bool{
   283  					globalMulticastAddr: false,
   284  					linkLocalAddrSNMC:   false,
   285  				}
   286  				for range addrs {
   287  					p, ok := e.Read()
   288  					if !ok {
   289  						t.Fatalf("expected MLD report for %s and %s; addrs = %#v", globalMulticastAddr, linkLocalAddrSNMC, addrs)
   290  					}
   291  
   292  					addr := header.IPv6(stack.PayloadSince(p.Pkt.NetworkHeader())).DestinationAddress()
   293  					if seen, ok := addrs[addr]; !ok {
   294  						t.Fatalf("got unexpected packet destined to %s", addr)
   295  					} else if seen {
   296  						t.Fatalf("got another packet destined to %s", addr)
   297  					}
   298  
   299  					addrs[addr] = true
   300  					validateMLDPacket(t, stack.PayloadSince(p.Pkt.NetworkHeader()), linkLocalAddr, addr, header.ICMPv6MulticastListenerReport, addr)
   301  
   302  					clock.Advance(ipv6.UnsolicitedReportIntervalMax)
   303  				}
   304  			}
   305  
   306  			// Should not send any more reports.
   307  			clock.Advance(time.Hour)
   308  			if p, ok := e.Read(); ok {
   309  				t.Errorf("got unexpected packet = %#v", p)
   310  			}
   311  		})
   312  	}
   313  }
   314  
   315  // createAndInjectMLDPacket creates and injects an MLD packet with the
   316  // specified fields.
   317  func createAndInjectMLDPacket(e *channel.Endpoint, mldType header.ICMPv6Type, hopLimit uint8, srcAddress tcpip.Address, withRouterAlertOption bool, routerAlertValue header.IPv6RouterAlertValue) {
   318  	var extensionHeaders header.IPv6ExtHdrSerializer
   319  	if withRouterAlertOption {
   320  		extensionHeaders = header.IPv6ExtHdrSerializer{
   321  			header.IPv6SerializableHopByHopExtHdr{
   322  				&header.IPv6RouterAlertOption{Value: routerAlertValue},
   323  			},
   324  		}
   325  	}
   326  
   327  	extensionHeadersLength := extensionHeaders.Length()
   328  	payloadLength := extensionHeadersLength + header.ICMPv6HeaderSize + header.MLDMinimumSize
   329  	buf := buffer.NewView(header.IPv6MinimumSize + payloadLength)
   330  
   331  	ip := header.IPv6(buf)
   332  	ip.Encode(&header.IPv6Fields{
   333  		PayloadLength:     uint16(payloadLength),
   334  		HopLimit:          hopLimit,
   335  		TransportProtocol: header.ICMPv6ProtocolNumber,
   336  		SrcAddr:           srcAddress,
   337  		DstAddr:           header.IPv6AllNodesMulticastAddress,
   338  		ExtensionHeaders:  extensionHeaders,
   339  	})
   340  
   341  	icmp := header.ICMPv6(ip.Payload()[extensionHeadersLength:])
   342  	icmp.SetType(mldType)
   343  	mld := header.MLD(icmp.MessageBody())
   344  	mld.SetMaximumResponseDelay(0)
   345  	mld.SetMulticastAddress(header.IPv6Any)
   346  	icmp.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
   347  		Header: icmp,
   348  		Src:    srcAddress,
   349  		Dst:    header.IPv6AllNodesMulticastAddress,
   350  	}))
   351  
   352  	e.InjectInbound(ipv6.ProtocolNumber, stack.NewPacketBuffer(stack.PacketBufferOptions{
   353  		Data: buf.ToVectorisedView(),
   354  	}))
   355  }
   356  
   357  func TestMLDPacketValidation(t *testing.T) {
   358  	const nicID = 1
   359  	linkLocalAddr2 := testutil.MustParse6("fe80::2")
   360  
   361  	tests := []struct {
   362  		name                     string
   363  		messageType              header.ICMPv6Type
   364  		srcAddr                  tcpip.Address
   365  		includeRouterAlertOption bool
   366  		routerAlertValue         header.IPv6RouterAlertValue
   367  		hopLimit                 uint8
   368  		expectValidMLD           bool
   369  		getMessageTypeStatValue  func(tcpip.Stats) uint64
   370  	}{
   371  		{
   372  			name:                     "valid",
   373  			messageType:              header.ICMPv6MulticastListenerQuery,
   374  			includeRouterAlertOption: true,
   375  			routerAlertValue:         header.IPv6RouterAlertMLD,
   376  			srcAddr:                  linkLocalAddr2,
   377  			hopLimit:                 header.MLDHopLimit,
   378  			expectValidMLD:           true,
   379  			getMessageTypeStatValue:  func(stats tcpip.Stats) uint64 { return stats.ICMP.V6.PacketsReceived.MulticastListenerQuery.Value() },
   380  		},
   381  		{
   382  			name:                     "bad hop limit",
   383  			messageType:              header.ICMPv6MulticastListenerReport,
   384  			includeRouterAlertOption: true,
   385  			routerAlertValue:         header.IPv6RouterAlertMLD,
   386  			srcAddr:                  linkLocalAddr2,
   387  			hopLimit:                 header.MLDHopLimit + 1,
   388  			expectValidMLD:           false,
   389  			getMessageTypeStatValue:  func(stats tcpip.Stats) uint64 { return stats.ICMP.V6.PacketsReceived.MulticastListenerReport.Value() },
   390  		},
   391  		{
   392  			name:                     "src ip not link local",
   393  			messageType:              header.ICMPv6MulticastListenerReport,
   394  			includeRouterAlertOption: true,
   395  			routerAlertValue:         header.IPv6RouterAlertMLD,
   396  			srcAddr:                  globalAddr,
   397  			hopLimit:                 header.MLDHopLimit,
   398  			expectValidMLD:           false,
   399  			getMessageTypeStatValue:  func(stats tcpip.Stats) uint64 { return stats.ICMP.V6.PacketsReceived.MulticastListenerReport.Value() },
   400  		},
   401  		{
   402  			name:                     "missing router alert ip option",
   403  			messageType:              header.ICMPv6MulticastListenerDone,
   404  			includeRouterAlertOption: false,
   405  			srcAddr:                  linkLocalAddr2,
   406  			hopLimit:                 header.MLDHopLimit,
   407  			expectValidMLD:           false,
   408  			getMessageTypeStatValue:  func(stats tcpip.Stats) uint64 { return stats.ICMP.V6.PacketsReceived.MulticastListenerDone.Value() },
   409  		},
   410  		{
   411  			name:                     "incorrect router alert value",
   412  			messageType:              header.ICMPv6MulticastListenerDone,
   413  			includeRouterAlertOption: true,
   414  			routerAlertValue:         header.IPv6RouterAlertRSVP,
   415  			srcAddr:                  linkLocalAddr2,
   416  			hopLimit:                 header.MLDHopLimit,
   417  			expectValidMLD:           false,
   418  			getMessageTypeStatValue:  func(stats tcpip.Stats) uint64 { return stats.ICMP.V6.PacketsReceived.MulticastListenerDone.Value() },
   419  		},
   420  	}
   421  
   422  	for _, test := range tests {
   423  		t.Run(test.name, func(t *testing.T) {
   424  			s := stack.New(stack.Options{
   425  				NetworkProtocols: []stack.NetworkProtocolFactory{ipv6.NewProtocolWithOptions(ipv6.Options{
   426  					MLD: ipv6.MLDOptions{
   427  						Enabled: true,
   428  					},
   429  				})},
   430  			})
   431  			e := channel.New(nicID, header.IPv6MinimumMTU, "")
   432  			if err := s.CreateNIC(nicID, e); err != nil {
   433  				t.Fatalf("CreateNIC(%d, _): %s", nicID, err)
   434  			}
   435  			stats := s.Stats()
   436  			// Verify that every relevant stats is zero'd before we send a packet.
   437  			if got := test.getMessageTypeStatValue(s.Stats()); got != 0 {
   438  				t.Errorf("got test.getMessageTypeStatValue(s.Stats()) = %d, want = 0", got)
   439  			}
   440  			if got := stats.ICMP.V6.PacketsReceived.Invalid.Value(); got != 0 {
   441  				t.Errorf("got stats.ICMP.V6.PacketsReceived.Invalid.Value() = %d, want = 0", got)
   442  			}
   443  			if got := stats.IP.PacketsDelivered.Value(); got != 0 {
   444  				t.Fatalf("got stats.IP.PacketsDelivered.Value() = %d, want = 0", got)
   445  			}
   446  			createAndInjectMLDPacket(e, test.messageType, test.hopLimit, test.srcAddr, test.includeRouterAlertOption, test.routerAlertValue)
   447  			// We always expect the packet to pass IP validation.
   448  			if got := stats.IP.PacketsDelivered.Value(); got != 1 {
   449  				t.Fatalf("got stats.IP.PacketsDelivered.Value() = %d, want = 1", got)
   450  			}
   451  			// Even when the MLD-specific validation checks fail, we expect the
   452  			// corresponding MLD counter to be incremented.
   453  			if got := test.getMessageTypeStatValue(s.Stats()); got != 1 {
   454  				t.Errorf("got test.getMessageTypeStatValue(s.Stats()) = %d, want = 1", got)
   455  			}
   456  			var expectedInvalidCount uint64
   457  			if !test.expectValidMLD {
   458  				expectedInvalidCount = 1
   459  			}
   460  			if got := stats.ICMP.V6.PacketsReceived.Invalid.Value(); got != expectedInvalidCount {
   461  				t.Errorf("got stats.ICMP.V6.PacketsReceived.Invalid.Value() = %d, want = %d", got, expectedInvalidCount)
   462  			}
   463  		})
   464  	}
   465  }
   466  
   467  func TestMLDSkipProtocol(t *testing.T) {
   468  	const nicID = 1
   469  
   470  	tests := []struct {
   471  		name         string
   472  		group        tcpip.Address
   473  		expectReport bool
   474  	}{
   475  		{
   476  			name:         "Reserverd0",
   477  			group:        "\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   478  			expectReport: false,
   479  		},
   480  		{
   481  			name:         "Interface Local",
   482  			group:        "\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   483  			expectReport: false,
   484  		},
   485  		{
   486  			name:         "Link Local",
   487  			group:        "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   488  			expectReport: true,
   489  		},
   490  		{
   491  			name:         "Realm Local",
   492  			group:        "\xff\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   493  			expectReport: true,
   494  		},
   495  		{
   496  			name:         "Admin Local",
   497  			group:        "\xff\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   498  			expectReport: true,
   499  		},
   500  		{
   501  			name:         "Site Local",
   502  			group:        "\xff\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   503  			expectReport: true,
   504  		},
   505  		{
   506  			name:         "Unassigned(6)",
   507  			group:        "\xff\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   508  			expectReport: true,
   509  		},
   510  		{
   511  			name:         "Unassigned(7)",
   512  			group:        "\xff\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   513  			expectReport: true,
   514  		},
   515  		{
   516  			name:         "Organization Local",
   517  			group:        "\xff\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   518  			expectReport: true,
   519  		},
   520  		{
   521  			name:         "Unassigned(9)",
   522  			group:        "\xff\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   523  			expectReport: true,
   524  		},
   525  		{
   526  			name:         "Unassigned(A)",
   527  			group:        "\xff\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   528  			expectReport: true,
   529  		},
   530  		{
   531  			name:         "Unassigned(B)",
   532  			group:        "\xff\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   533  			expectReport: true,
   534  		},
   535  		{
   536  			name:         "Unassigned(C)",
   537  			group:        "\xff\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   538  			expectReport: true,
   539  		},
   540  		{
   541  			name:         "Unassigned(D)",
   542  			group:        "\xff\x0d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   543  			expectReport: true,
   544  		},
   545  		{
   546  			name:         "Global",
   547  			group:        "\xff\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   548  			expectReport: true,
   549  		},
   550  		{
   551  			name:         "ReservedF",
   552  			group:        "\xff\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11",
   553  			expectReport: true,
   554  		},
   555  	}
   556  
   557  	for _, test := range tests {
   558  		t.Run(test.name, func(t *testing.T) {
   559  			s := stack.New(stack.Options{
   560  				NetworkProtocols: []stack.NetworkProtocolFactory{ipv6.NewProtocolWithOptions(ipv6.Options{
   561  					MLD: ipv6.MLDOptions{
   562  						Enabled: true,
   563  					},
   564  				})},
   565  			})
   566  			e := channel.New(1, header.IPv6MinimumMTU, "")
   567  			if err := s.CreateNIC(nicID, e); err != nil {
   568  				t.Fatalf("CreateNIC(%d, _): %s", nicID, err)
   569  			}
   570  			if err := s.AddAddress(nicID, ipv6.ProtocolNumber, linkLocalAddr); err != nil {
   571  				t.Fatalf("AddAddress(%d, %d, %s) = %s", nicID, ipv6.ProtocolNumber, linkLocalAddr, err)
   572  			}
   573  			if p, ok := e.Read(); !ok {
   574  				t.Fatal("expected a report message to be sent")
   575  			} else {
   576  				validateMLDPacket(t, stack.PayloadSince(p.Pkt.NetworkHeader()), linkLocalAddr, linkLocalAddrSNMC, header.ICMPv6MulticastListenerReport, linkLocalAddrSNMC)
   577  			}
   578  
   579  			if err := s.JoinGroup(ipv6.ProtocolNumber, nicID, test.group); err != nil {
   580  				t.Fatalf("s.JoinGroup(%d, %d, %s): %s", ipv6.ProtocolNumber, nicID, test.group, err)
   581  			}
   582  			if isInGroup, err := s.IsInGroup(nicID, test.group); err != nil {
   583  				t.Fatalf("IsInGroup(%d, %s): %s", nicID, test.group, err)
   584  			} else if !isInGroup {
   585  				t.Fatalf("got IsInGroup(%d, %s) = false, want = true", nicID, test.group)
   586  			}
   587  
   588  			if !test.expectReport {
   589  				if p, ok := e.Read(); ok {
   590  					t.Fatalf("got e.Read() = (%#v, true), want = (_, false)", p)
   591  				}
   592  
   593  				return
   594  			}
   595  
   596  			if p, ok := e.Read(); !ok {
   597  				t.Fatal("expected a report message to be sent")
   598  			} else {
   599  				validateMLDPacket(t, stack.PayloadSince(p.Pkt.NetworkHeader()), linkLocalAddr, test.group, header.ICMPv6MulticastListenerReport, test.group)
   600  			}
   601  		})
   602  	}
   603  }