From 4632d45dd8447667060f69ba02db4166f725f764 Mon Sep 17 00:00:00 2001 From: Ghanan Gowripalan Date: Wed, 11 Jan 2023 11:37:18 -0800 Subject: [PATCH] Perform MLDv2/IGMPv3 without SSM This change introduces support for MLDv2/IGMPv3. Note that this change does not yet introduce APIs to perform source filtering so SSM is not yet supported. Also note that this change does not yet coalesce records in a report as that will come in a follow-up. Updates #8346 PiperOrigin-RevId: 501336347 --- pkg/tcpip/checker/checker.go | 129 ++ pkg/tcpip/header/icmpv6.go | 4 + pkg/tcpip/header/igmp.go | 4 +- pkg/tcpip/header/igmp_test.go | 2 +- pkg/tcpip/header/igmpv3.go | 48 + pkg/tcpip/header/mldv2.go | 14 +- pkg/tcpip/header/mldv2_igmpv3_common.go | 7 +- pkg/tcpip/header/parse/parse.go | 13 +- pkg/tcpip/network/BUILD | 1 + pkg/tcpip/network/internal/ip/BUILD | 2 + .../internal/ip/generic_multicast_protocol.go | 603 ++++- .../ip/generic_multicast_protocol_test.go | 1455 +++++++++--- pkg/tcpip/network/internal/testutil/BUILD | 1 + .../network/internal/testutil/testutil.go | 59 + pkg/tcpip/network/ipv4/igmp.go | 174 +- pkg/tcpip/network/ipv4/igmp_test.go | 223 +- pkg/tcpip/network/ipv4/stats.go | 2 + pkg/tcpip/network/ipv6/BUILD | 1 + pkg/tcpip/network/ipv6/icmp.go | 18 +- pkg/tcpip/network/ipv6/icmp_test.go | 6 + pkg/tcpip/network/ipv6/mld.go | 147 +- pkg/tcpip/network/ipv6/mld_test.go | 832 ++++--- pkg/tcpip/network/ipv6/stats.go | 30 +- pkg/tcpip/network/multicast_group_test.go | 1993 ++++++++++++----- pkg/tcpip/tcpip.go | 8 + .../tests/integration/link_resolution_test.go | 14 +- 26 files changed, 4408 insertions(+), 1382 deletions(-) diff --git a/pkg/tcpip/checker/checker.go b/pkg/tcpip/checker/checker.go index 71927fda5..93c3df24c 100644 --- a/pkg/tcpip/checker/checker.go +++ b/pkg/tcpip/checker/checker.go @@ -1241,6 +1241,69 @@ func MLDMulticastAddress(want tcpip.Address) TransportChecker { } } +// MLDv2Report creates a checker that checks that the packet contains a valid +// MLDv2 report with the specified records. +func MLDv2Report(expectedReport header.MLDv2ReportSerializer) NetworkChecker { + return func(t *testing.T, h []header.Network) { + t.Helper() + + // Check normal ICMPv6 first. + ICMPv6( + ICMPv6Type(header.ICMPv6MulticastListenerV2Report), + ICMPv6Code(0))(t, h) + + last := h[len(h)-1] + icmp := header.ICMPv6(last.Payload()) + report := header.MLDv2Report(icmp.MessageBody()) + expectedRecords := expectedReport.Records + + records := report.MulticastAddressRecords() + for len(expectedRecords) != 0 { + record, res := records.Next() + if res != header.MLDv2ReportMulticastAddressRecordIteratorNextOk { + t.Fatalf("got records.Next() = (%#v, %d), want = (_, %d)", record, res, header.MLDv2ReportMulticastAddressRecordIteratorNextOk) + } + + if got, want := record.RecordType(), expectedRecords[0].RecordType; got != want { + t.Errorf("got record.RecordType() = %d, want = %d", got, want) + } + + if got := record.AuxDataLen(); got != 0 { + t.Errorf("got record.AuxDataLen() = %d, want = 0", got) + } + + if got, want := record.MulticastAddress(), expectedRecords[0].MulticastAddress; got != want { + t.Errorf("got record.MulticastAddress() = %s, want = %s", got, want) + } + + sources, ok := record.Sources() + if !ok { + t.Error("got record.Sources() = (_, false), want = (_, true)") + continue + } + + expectedSources := expectedRecords[0].Sources + for len(expectedSources) != 0 { + source, ok := sources.Next() + if !ok { + t.Fatal("got sources.Next() = (_, false), want = (_, true)") + } + if source != expectedSources[0] { + t.Errorf("got sources.Next() = %s, want = %s", source, expectedSources[0]) + } + + expectedSources = expectedSources[1:] + } + + expectedRecords = expectedRecords[1:] + } + + if record, res := records.Next(); res != header.MLDv2ReportMulticastAddressRecordIteratorNextDone { + t.Fatalf("got records.Next() = (%#v, %d), want = (_, %d)", record, res, header.MLDv2ReportMulticastAddressRecordIteratorNextDone) + } + } +} + // NDP creates a checker that checks that the packet contains a valid NDP // message for type of ty, with potentially additional checks specified by // checkers. @@ -1532,6 +1595,72 @@ func IGMPGroupAddress(want tcpip.Address) TransportChecker { } } +// IGMPv3Report creates a checker that checks that the packet contains a valid +// IGMPv3 report with the specified records. +func IGMPv3Report(expectedReport header.IGMPv3ReportSerializer) NetworkChecker { + return func(t *testing.T, h []header.Network) { + t.Helper() + + last := h[len(h)-1] + if p := last.TransportProtocol(); p != header.IGMPProtocolNumber { + t.Fatalf("Bad protocol, got %d, want %d", p, header.IGMPProtocolNumber) + } + + igmp := header.IGMP(last.Payload()) + if got := igmp.Type(); got != header.IGMPv3MembershipReport { + t.Errorf("got igmp.Type() = %d, want = %d", got, header.IGMPv3MembershipReport) + } + + report := header.IGMPv3Report(igmp) + expectedRecords := expectedReport.Records + + records := report.GroupAddressRecords() + for len(expectedRecords) != 0 { + record, res := records.Next() + if res != header.IGMPv3ReportGroupAddressRecordIteratorNextOk { + t.Fatalf("got records.Next() = (%#v, %d), want = (_, %d)", record, res, header.IGMPv3ReportGroupAddressRecordIteratorNextOk) + } + + if got, want := record.RecordType(), expectedRecords[0].RecordType; got != want { + t.Errorf("got record.RecordType() = %d, want = %d", got, want) + } + + if got := record.AuxDataLen(); got != 0 { + t.Errorf("got record.AuxDataLen() = %d, want = 0", got) + } + + if got, want := record.GroupAddress(), expectedRecords[0].GroupAddress; got != want { + t.Errorf("got record.GroupAddress() = %s, want = %s", got, want) + } + + sources, ok := record.Sources() + if !ok { + t.Error("got record.Sources() = (_, false), want = (_, true)") + continue + } + + expectedSources := expectedRecords[0].Sources + for len(expectedSources) != 0 { + source, ok := sources.Next() + if !ok { + t.Fatal("got sources.Next() = (_, false), want = (_, true)") + } + if source != expectedSources[0] { + t.Errorf("got sources.Next() = %s, want = %s", source, expectedSources[0]) + } + + expectedSources = expectedSources[1:] + } + + expectedRecords = expectedRecords[1:] + } + + if record, res := records.Next(); res != header.IGMPv3ReportGroupAddressRecordIteratorNextDone { + t.Fatalf("got records.Next() = (%#v, %d), want = (_, %d)", record, res, header.IGMPv3ReportGroupAddressRecordIteratorNextDone) + } + } +} + // IPv6ExtHdrChecker is a function to check an extension header. type IPv6ExtHdrChecker func(*testing.T, header.IPv6PayloadHeader) diff --git a/pkg/tcpip/header/icmpv6.go b/pkg/tcpip/header/icmpv6.go index d4b24f81b..4e75ac400 100644 --- a/pkg/tcpip/header/icmpv6.go +++ b/pkg/tcpip/header/icmpv6.go @@ -121,6 +121,10 @@ const ( ICMPv6MulticastListenerQuery ICMPv6Type = 130 ICMPv6MulticastListenerReport ICMPv6Type = 131 ICMPv6MulticastListenerDone ICMPv6Type = 132 + + // Multicast Listener Discovert Version 2 (MLDv2) messages, see RFC 3810. + + ICMPv6MulticastListenerV2Report ICMPv6Type = 143 ) // IsErrorType returns true if the receiver is an ICMP error type. diff --git a/pkg/tcpip/header/igmp.go b/pkg/tcpip/header/igmp.go index af7726c3e..a35458e0b 100644 --- a/pkg/tcpip/header/igmp.go +++ b/pkg/tcpip/header/igmp.go @@ -114,7 +114,7 @@ func (b IGMP) MaxRespTime() time.Duration { // messages, and specifies the maximum allowed time before sending a // responding report in units of 1/10 second. In all other messages, it // is set to zero by the sender and ignored by receivers. - return DecisecondToDuration(b[igmpMaxRespTimeOffset]) + return DecisecondToDuration(uint16(b[igmpMaxRespTimeOffset])) } // SetMaxRespTime sets the MaxRespTimeField. @@ -179,6 +179,6 @@ func IGMPCalculateChecksum(h IGMP) uint16 { // DecisecondToDuration converts a value representing deci-seconds to a // time.Duration. -func DecisecondToDuration(ds uint8) time.Duration { +func DecisecondToDuration(ds uint16) time.Duration { return time.Duration(ds) * time.Second / 10 } diff --git a/pkg/tcpip/header/igmp_test.go b/pkg/tcpip/header/igmp_test.go index aab54c9f7..8d6b08cdb 100644 --- a/pkg/tcpip/header/igmp_test.go +++ b/pkg/tcpip/header/igmp_test.go @@ -65,7 +65,7 @@ func TestIGMPHeader(t *testing.T) { respTime := byte(0x02) igmpHeader.SetMaxRespTime(respTime) - if got, want := igmpHeader.MaxRespTime(), header.DecisecondToDuration(respTime); got != want { + if got, want := igmpHeader.MaxRespTime(), header.DecisecondToDuration(uint16(respTime)); got != want { t.Errorf("got igmpHeader.MaxRespTime() = %s, want = %s", got, want) } diff --git a/pkg/tcpip/header/igmpv3.go b/pkg/tcpip/header/igmpv3.go index 5f4585321..0ffb4cd43 100644 --- a/pkg/tcpip/header/igmpv3.go +++ b/pkg/tcpip/header/igmpv3.go @@ -24,6 +24,14 @@ import ( ) const ( + // IGMPv3RoutersAddress is the address to send IGMPv3 reports to. + // + // As per RFC 3376 section 4.2.14, + // + // Version 3 Reports are sent with an IP destination address of + // 224.0.0.22, to which all IGMPv3-capable multicast routers listen. + IGMPv3RoutersAddress tcpip.Address = "\xe0\x00\x00\x16" + // IGMPv3QueryMinimumSize is the mimum size of a valid IGMPv3 query, // as per RFC 3376 section 4.1. IGMPv3QueryMinimumSize = 12 @@ -66,6 +74,46 @@ func (i IGMPv3Query) MaximumResponseCode() uint8 { return i[igmpv3QueryMaxRespCodeOffset] } +// IGMPv3MaximumResponseDelay returns the Maximum Response Delay in an IGMPv3 +// Maximum Response Code. +// +// As per RFC 3376 section 4.1.1, +// +// The Max Resp Code field specifies the maximum time allowed before +// sending a responding report. The actual time allowed, called the Max +// Resp Time, is represented in units of 1/10 second and is derived from +// the Max Resp Code as follows: +// +// If Max Resp Code < 128, Max Resp Time = Max Resp Code +// +// If Max Resp Code >= 128, Max Resp Code represents a floating-point +// value as follows: +// +// 0 1 2 3 4 5 6 7 +// +-+-+-+-+-+-+-+-+ +// |1| exp | mant | +// +-+-+-+-+-+-+-+-+ +// +// Max Resp Time = (mant | 0x10) << (exp + 3) +// +// Small values of Max Resp Time allow IGMPv3 routers to tune the "leave +// latency" (the time between the moment the last host leaves a group +// and the moment the routing protocol is notified that there are no +// more members). Larger values, especially in the exponential range, +// allow tuning of the burstiness of IGMP traffic on a network. +func IGMPv3MaximumResponseDelay(codeRaw uint8) time.Duration { + code := uint16(codeRaw) + if code < 128 { + return DecisecondToDuration(code) + } + + const mantBits = 4 + const expMask = 0b111 + exp := (code >> mantBits) & expMask + mant := code & ((1 << mantBits) - 1) + return DecisecondToDuration((mant | 0x10) << (exp + 3)) +} + // GroupAddress returns the group address. func (i IGMPv3Query) GroupAddress() tcpip.Address { return tcpip.Address(i[igmpv3QueryGroupAddressOffset:][:IPv4AddressSize]) diff --git a/pkg/tcpip/header/mldv2.go b/pkg/tcpip/header/mldv2.go index ed78fc964..8a6f7b057 100644 --- a/pkg/tcpip/header/mldv2.go +++ b/pkg/tcpip/header/mldv2.go @@ -24,6 +24,16 @@ import ( ) const ( + // MLDv2RoutersAddress is the address to send MLDv2 reports to. + // + // As per RFC 3810 section 5.2.14, + // + // Version 2 Multicast Listener Reports are sent with an IP destination + // address of FF02:0:0:0:0:0:0:16, to which all MLDv2-capable multicast + // routers listen (see section 11 for IANA considerations related to + // this special destination address). + MLDv2RoutersAddress tcpip.Address = "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16" + // MLDv2QueryMinimumSize is the minimum size for an MLDv2 message. MLDv2QueryMinimumSize = 24 @@ -35,6 +45,9 @@ const ( // field within MLDv2Query. mldv2QueryNumberOfSourcesOffset = 22 + // MLDv2ReportMinimumSize is the minimum size of an MLDv2 report. + MLDv2ReportMinimumSize = 24 + // mldv2QuerySourcesOffset is the offset to the Sources field within // MLDv2Query. mldv2QuerySourcesOffset = 24 @@ -140,7 +153,6 @@ func MLDv2MaximumResponseDelay(codeRaw uint16) time.Duration { exp := (code >> mantBits) & expMask mant := code & ((1 << mantBits) - 1) return (mant | 0x1000) << (exp + 3) * time.Millisecond - } // MulticastAddress returns the Multicast Address. diff --git a/pkg/tcpip/header/mldv2_igmpv3_common.go b/pkg/tcpip/header/mldv2_igmpv3_common.go index cef2436c7..b0bad2ed4 100644 --- a/pkg/tcpip/header/mldv2_igmpv3_common.go +++ b/pkg/tcpip/header/mldv2_igmpv3_common.go @@ -82,6 +82,11 @@ func mldv2AndIGMPv3QuerierQueryCodeToInterval(code uint8) time.Duration { return (mant | 0x10) << (exp + 3) * time.Second } +// MakeAddressIterator returns an AddressIterator. +func MakeAddressIterator(addressSize int, buf *bytes.Buffer) AddressIterator { + return AddressIterator{addressSize: addressSize, buf: buf} +} + // AddressIterator is an iterator over IPv6 addresses. type AddressIterator struct { addressSize int @@ -115,5 +120,5 @@ func makeAddressIterator(b []byte, expectedAddresses uint16, addressSize int) (A if len(b) < expectedLen { return AddressIterator{}, false } - return AddressIterator{addressSize: addressSize, buf: bytes.NewBuffer(b[:expectedLen])}, true + return MakeAddressIterator(addressSize, bytes.NewBuffer(b[:expectedLen])), true } diff --git a/pkg/tcpip/header/parse/parse.go b/pkg/tcpip/header/parse/parse.go index 0e29377cc..33a85fdbc 100644 --- a/pkg/tcpip/header/parse/parse.go +++ b/pkg/tcpip/header/parse/parse.go @@ -215,18 +215,15 @@ func ICMPv6(pkt stack.PacketBufferPtr) bool { header.ICMPv6RouterAdvert, header.ICMPv6NeighborSolicit, header.ICMPv6NeighborAdvert, - header.ICMPv6RedirectMsg: + header.ICMPv6RedirectMsg, + header.ICMPv6MulticastListenerQuery, + header.ICMPv6MulticastListenerReport, + header.ICMPv6MulticastListenerV2Report, + header.ICMPv6MulticastListenerDone: size := pkt.Data().Size() if _, ok := pkt.TransportHeader().Consume(size); !ok { panic(fmt.Sprintf("expected to consume the full data of size = %d bytes into transport header", size)) } - case header.ICMPv6MulticastListenerQuery, - header.ICMPv6MulticastListenerReport, - header.ICMPv6MulticastListenerDone: - size := header.ICMPv6HeaderSize + header.MLDMinimumSize - if _, ok := pkt.TransportHeader().Consume(size); !ok { - return false - } case header.ICMPv6DstUnreachable, header.ICMPv6PacketTooBig, header.ICMPv6TimeExceeded, diff --git a/pkg/tcpip/network/BUILD b/pkg/tcpip/network/BUILD index 4e75bd875..d118a0343 100644 --- a/pkg/tcpip/network/BUILD +++ b/pkg/tcpip/network/BUILD @@ -21,6 +21,7 @@ go_test( "//pkg/tcpip/header", "//pkg/tcpip/link/channel", "//pkg/tcpip/link/loopback", + "//pkg/tcpip/network/internal/testutil", "//pkg/tcpip/network/ipv4", "//pkg/tcpip/network/ipv6", "//pkg/tcpip/prependable", diff --git a/pkg/tcpip/network/internal/ip/BUILD b/pkg/tcpip/network/internal/ip/BUILD index fd944ce99..b875bdc43 100644 --- a/pkg/tcpip/network/internal/ip/BUILD +++ b/pkg/tcpip/network/internal/ip/BUILD @@ -18,6 +18,7 @@ go_library( deps = [ "//pkg/sync", "//pkg/tcpip", + "//pkg/tcpip/header", "//pkg/tcpip/stack", ], ) @@ -34,6 +35,7 @@ go_test( "//pkg/sync", "//pkg/tcpip", "//pkg/tcpip/faketime", + "//pkg/tcpip/header", "//pkg/tcpip/stack", "@com_github_google_go_cmp//cmp:go_default_library", ], diff --git a/pkg/tcpip/network/internal/ip/generic_multicast_protocol.go b/pkg/tcpip/network/internal/ip/generic_multicast_protocol.go index 428907e94..63adc23a9 100644 --- a/pkg/tcpip/network/internal/ip/generic_multicast_protocol.go +++ b/pkg/tcpip/network/internal/ip/generic_multicast_protocol.go @@ -21,6 +21,7 @@ import ( "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" ) const ( @@ -68,6 +69,38 @@ const ( // address and any multicast addresses of scope 0 (reserved) or 1 // (node-local). minQueryResponseTransmissionCount = 1 + + // DefaultRobustnessVariable is the default robustness variable + // + // As per RFC 3810 section 9.1 (for MLDv2), + // + // The Robustness Variable allows tuning for the expected packet loss on + // a link. If a link is expected to be lossy, the value of the + // Robustness Variable may be increased. MLD is robust to [Robustness + // Variable] - 1 packet losses. The value of the Robustness Variable + // MUST NOT be zero, and SHOULD NOT be one. Default value: 2. + // + // As per RFC 3376 section 8.1 (for IGMPv3), + // + // The Robustness Variable allows tuning for the expected packet loss on + // a network. If a network is expected to be lossy, the Robustness + // Variable may be increased. IGMP is robust to (Robustness Variable - + // 1) packet losses. The Robustness Variable MUST NOT be zero, and + // SHOULD NOT be one. Default: 2 + DefaultRobustnessVariable = 2 + + // DefaultQueryInterval is the default query interval. + // + // As per RFC 3810 section 9.2 (for MLDv2), + // + // The Query Interval variable denotes the interval between General + // Queries sent by the Querier. Default value: 125 seconds. + // + // As per RFC 3376 section 8.2 (for IGMPv3), + // + // The Query Interval is the interval between General Queries sent by + // the Querier. Default: 125 seconds. + DefaultQueryInterval = 125 * time.Second ) // multicastGroupState holds the Generic Multicast Protocol state for a @@ -77,7 +110,7 @@ type multicastGroupState struct { joins uint64 // transmissionLeft is the number of transmissions left to send. - transmissionLeft uint + transmissionLeft uint8 // lastToSendReport is true if we sent the last report for the group. It is // used to track whether there are other hosts on the subnet that are also @@ -98,6 +131,14 @@ type multicastGroupState struct { // // A zero value indicates that the job is not scheduled. delayedReportJobFiresAt time.Time + + // queriedIncludeSources holds sources that were queried for. + // + // Indicates that there is a pending source-specific query response for the + // multicast address. + queriedIncludeSources map[tcpip.Address]struct{} + + deleteScheduled bool } func (m *multicastGroupState) cancelDelayedReportJob() { @@ -106,6 +147,12 @@ func (m *multicastGroupState) cancelDelayedReportJob() { m.transmissionLeft = 0 } +func (m *multicastGroupState) clearQueriedIncludeSources() { + for source := range m.queriedIncludeSources { + delete(m.queriedIncludeSources, source) + } +} + // GenericMulticastProtocolOptions holds options for the generic multicast // protocol. type GenericMulticastProtocolOptions struct { @@ -126,6 +173,32 @@ type GenericMulticastProtocolOptions struct { MaxUnsolicitedReportDelay time.Duration } +// MulticastGroupProtocolV2ReportRecordType is the type of a +// MulticastGroupProtocolv2 multicast address record. +type MulticastGroupProtocolV2ReportRecordType int + +// MulticastGroupProtocolv2 multicast address record types. +const ( + _ MulticastGroupProtocolV2ReportRecordType = iota + MulticastGroupProtocolV2ReportRecordModeIsInclude + MulticastGroupProtocolV2ReportRecordModeIsExclude + MulticastGroupProtocolV2ReportRecordChangeToIncludeMode + MulticastGroupProtocolV2ReportRecordChangeToExcludeMode + MulticastGroupProtocolV2ReportRecordAllowNewSources + MulticastGroupProtocolV2ReportRecordBlockOldSources +) + +// MulticastGroupProtocolV2ReportBuilder is a builder for a V2 report. +type MulticastGroupProtocolV2ReportBuilder interface { + // AddRecord adds a record to the report. + AddRecord(recordType MulticastGroupProtocolV2ReportRecordType, groupAddress tcpip.Address) + + // Send sends the report. + // + // It is invalid to use this builder after this method is called. + Send() (sent bool, err tcpip.Error) +} + // MulticastGroupProtocol is a multicast group protocol whose core state machine // can be represented by GenericMulticastProtocolState. type MulticastGroupProtocol interface { @@ -152,8 +225,26 @@ type MulticastGroupProtocol interface { // ShouldPerformProtocol returns true iff the protocol should be performed for // the specified group. ShouldPerformProtocol(tcpip.Address) bool + + // NewReportV2Builder creates a new V2 builder. + NewReportV2Builder() MulticastGroupProtocolV2ReportBuilder + + // V2QueryMaxRespCodeToV2Delay takes a V2 query's maximum response code and + // returns the V2 delay. + V2QueryMaxRespCodeToV2Delay(code uint16) time.Duration + + // V2QueryMaxRespCodeToV1Delay takes a V2 query's maximum response code and + // returns the V1 delay. + V2QueryMaxRespCodeToV1Delay(code uint16) time.Duration } +type protocolMode int + +const ( + protocolModeV2 protocolMode = iota + protocolModeV1Compatibility +) + // GenericMulticastProtocolState is the per interface generic multicast protocol // state. // @@ -184,6 +275,30 @@ type GenericMulticastProtocolState struct { // protocolMU is the mutex used to protect the protocol. protocolMU *sync.RWMutex + + // V2 state. + robustnessVariable uint8 + queryInterval time.Duration + mode protocolMode + modeTimer tcpip.Timer + + generalQueryV2Timer tcpip.Timer + generalQueryV2TimerFiresAt time.Time + + stateChangedReportV2Timer tcpip.Timer + stateChangedReportV2TimerSet bool +} + +func (g *GenericMulticastProtocolState) cancelV2ReportTimers() { + if g.generalQueryV2Timer != nil { + g.generalQueryV2Timer.Stop() + g.generalQueryV2TimerFiresAt = time.Time{} + } + + if g.stateChangedReportV2Timer != nil { + g.stateChangedReportV2Timer.Stop() + g.stateChangedReportV2TimerSet = false + } } // Init initializes the Generic Multicast Protocol state. @@ -202,9 +317,12 @@ func (g *GenericMulticastProtocolState) Init(protocolMU *sync.RWMutex, opts Gene } *g = GenericMulticastProtocolState{ - opts: opts, - memberships: make(map[tcpip.Address]multicastGroupState), - protocolMU: protocolMU, + opts: opts, + memberships: make(map[tcpip.Address]multicastGroupState), + protocolMU: protocolMU, + robustnessVariable: DefaultRobustnessVariable, + queryInterval: DefaultQueryInterval, + mode: protocolModeV2, } } @@ -220,9 +338,47 @@ func (g *GenericMulticastProtocolState) MakeAllNonMemberLocked() { return } + if g.modeTimer != nil { + g.modeTimer.Stop() + } + g.cancelV2ReportTimers() + + var handler func(tcpip.Address, *multicastGroupState) + switch g.mode { + case protocolModeV2: + handler = func(groupAddress tcpip.Address, _ *multicastGroupState) { + // Send a report immediately to announce us leaving the group. + reportBuilder := g.opts.Protocol.NewReportV2Builder() + reportBuilder.AddRecord( + MulticastGroupProtocolV2ReportRecordChangeToIncludeMode, + groupAddress, + ) + // Nothing meaningful we can do with the error here - this method may be + // called when an interface is being disabled when we expect sends to + // fail. + _, _ = reportBuilder.Send() + } + case protocolModeV1Compatibility: + handler = g.transitionToNonMemberLocked + default: + panic(fmt.Sprintf("unrecognized mode = %d", g.mode)) + } + + g.mode = protocolModeV2 + for groupAddress, info := range g.memberships { - g.transitionToNonMemberLocked(groupAddress, &info) - g.memberships[groupAddress] = info + if !g.shouldPerformForGroup(groupAddress) { + continue + } + + handler(groupAddress, &info) + + if info.deleteScheduled { + delete(g.memberships, groupAddress) + } else { + info.transmissionLeft = 0 + g.memberships[groupAddress] = info + } } } @@ -249,9 +405,21 @@ func (g *GenericMulticastProtocolState) InitializeGroupsLocked() { // // Precondition: g.protocolMU must be locked. func (g *GenericMulticastProtocolState) SendQueuedReportsLocked() { + if g.stateChangedReportV2TimerSet { + return + } + for groupAddress, info := range g.memberships { if info.delayedReportJobFiresAt.IsZero() { - g.maybeSendReportLocked(groupAddress, &info) + switch g.mode { + case protocolModeV2: + g.sendV2ReportAndMaybeScheduleChangedTimer(groupAddress, &info, MulticastGroupProtocolV2ReportRecordChangeToExcludeMode) + case protocolModeV1Compatibility: + g.maybeSendReportLocked(groupAddress, &info) + default: + panic(fmt.Sprintf("unrecognized mode = %d", g.mode)) + } + g.memberships[groupAddress] = info } } @@ -261,37 +429,56 @@ func (g *GenericMulticastProtocolState) SendQueuedReportsLocked() { // // Precondition: g.protocolMU must be locked. func (g *GenericMulticastProtocolState) JoinGroupLocked(groupAddress tcpip.Address) { - if info, ok := g.memberships[groupAddress]; ok { - // The group has already been joined. + info, ok := g.memberships[groupAddress] + if ok { info.joins++ - g.memberships[groupAddress] = info - return - } - - info := multicastGroupState{ - // Since we just joined the group, its count is 1. - joins: 1, - lastToSendReport: false, - delayedReportJob: tcpip.NewJob(g.opts.Clock, g.protocolMU, func() { - if !g.opts.Protocol.Enabled() { - panic(fmt.Sprintf("delayed report job fired for group %s while the multicast group protocol is disabled", groupAddress)) - } - - info, ok := g.memberships[groupAddress] - if !ok { - panic(fmt.Sprintf("expected to find group state for group = %s", groupAddress)) - } - - info.delayedReportJobFiresAt = time.Time{} - g.maybeSendReportLocked(groupAddress, &info) + if info.joins > 1 { + // The group has already been joined. g.memberships[groupAddress] = info - }), - } + return + } + } else { + info = multicastGroupState{ + // Since we just joined the group, its count is 1. + joins: 1, + lastToSendReport: false, + delayedReportJob: tcpip.NewJob(g.opts.Clock, g.protocolMU, func() { + if !g.opts.Protocol.Enabled() { + panic(fmt.Sprintf("delayed report job fired for group %s while the multicast group protocol is disabled", groupAddress)) + } - if g.opts.Protocol.Enabled() { - g.initializeNewMemberLocked(groupAddress, &info) + info, ok := g.memberships[groupAddress] + if !ok { + panic(fmt.Sprintf("expected to find group state for group = %s", groupAddress)) + } + + info.delayedReportJobFiresAt = time.Time{} + + switch g.mode { + case protocolModeV2: + reportBuilder := g.opts.Protocol.NewReportV2Builder() + reportBuilder.AddRecord(MulticastGroupProtocolV2ReportRecordModeIsExclude, groupAddress) + // Nothing meaningful we can do with the error here - we only try to + // send a delayed report once. + _, _ = reportBuilder.Send() + case protocolModeV1Compatibility: + g.maybeSendReportLocked(groupAddress, &info) + default: + panic(fmt.Sprintf("unrecognized mode = %d", g.mode)) + } + + info.clearQueriedIncludeSources() + g.memberships[groupAddress] = info + }), + queriedIncludeSources: make(map[tcpip.Address]struct{}), + } } + info.deleteScheduled = false + info.clearQueriedIncludeSources() + info.delayedReportJobFiresAt = time.Time{} + info.lastToSendReport = false + g.initializeNewMemberLocked(groupAddress, &info) g.memberships[groupAddress] = info } @@ -299,8 +486,78 @@ func (g *GenericMulticastProtocolState) JoinGroupLocked(groupAddress tcpip.Addre // // Precondition: g.protocolMU must be read locked. func (g *GenericMulticastProtocolState) IsLocallyJoinedRLocked(groupAddress tcpip.Address) bool { - _, ok := g.memberships[groupAddress] - return ok + info, ok := g.memberships[groupAddress] + return ok && !info.deleteScheduled +} + +func (g *GenericMulticastProtocolState) sendV2ReportAndMaybeScheduleChangedTimer(groupAddress tcpip.Address, info *multicastGroupState, recordType MulticastGroupProtocolV2ReportRecordType) bool { + if info.transmissionLeft == 0 { + return false + } + + successfullySentAndHasMore := false + + // Send a report immediately to announce us leaving the group. + reportBuilder := g.opts.Protocol.NewReportV2Builder() + reportBuilder.AddRecord(recordType, groupAddress) + if sent, err := reportBuilder.Send(); sent && err == nil { + info.transmissionLeft-- + + successfullySentAndHasMore = info.transmissionLeft != 0 + + // Use the interface-wide state changed report for further transmissions. + if successfullySentAndHasMore && !g.stateChangedReportV2TimerSet { + delay := g.calculateDelayTimerDuration(g.opts.MaxUnsolicitedReportDelay) + if g.stateChangedReportV2Timer == nil { + // TODO(https://issuetracker.google.com/264799098): Create timer on + // initialization instead of lazily creating the timer since the timer + // does not change after being created. + g.stateChangedReportV2Timer = g.opts.Clock.AfterFunc(delay, func() { + g.protocolMU.Lock() + defer g.protocolMU.Unlock() + + nonEmptyReport := false + for groupAddress, info := range g.memberships { + if info.transmissionLeft == 0 || !g.shouldPerformForGroup(groupAddress) { + continue + } + + info.transmissionLeft-- + nonEmptyReport = true + + reportBuilder := g.opts.Protocol.NewReportV2Builder() + mode := MulticastGroupProtocolV2ReportRecordChangeToExcludeMode + if info.deleteScheduled { + mode = MulticastGroupProtocolV2ReportRecordChangeToIncludeMode + } + reportBuilder.AddRecord(mode, groupAddress) + // Nothing meaningful we can do with the error here. We will retry + // sending a state changed report again anyways. + _, _ = reportBuilder.Send() + + if info.deleteScheduled && info.transmissionLeft == 0 { + // No more transmissions left so we can actually delete the + // membership. + delete(g.memberships, groupAddress) + } else { + g.memberships[groupAddress] = info + } + } + + if nonEmptyReport { + g.stateChangedReportV2Timer.Reset(g.calculateDelayTimerDuration(g.opts.MaxUnsolicitedReportDelay)) + } else { + g.stateChangedReportV2TimerSet = false + } + }) + } else { + g.stateChangedReportV2Timer.Reset(delay) + } + g.stateChangedReportV2TimerSet = true + } + } + + return successfullySentAndHasMore } // LeaveGroupLocked handles leaving the group. @@ -310,13 +567,10 @@ func (g *GenericMulticastProtocolState) IsLocallyJoinedRLocked(groupAddress tcpi // Precondition: g.protocolMU must be locked. func (g *GenericMulticastProtocolState) LeaveGroupLocked(groupAddress tcpip.Address) bool { info, ok := g.memberships[groupAddress] - if !ok { + if !ok || info.joins == 0 { return false } - if info.joins == 0 { - panic(fmt.Sprintf("tried to leave group %s with a join count of 0", groupAddress)) - } info.joins-- if info.joins != 0 { // If we still have outstanding joins, then do nothing further. @@ -324,11 +578,210 @@ func (g *GenericMulticastProtocolState) LeaveGroupLocked(groupAddress tcpip.Addr return true } - g.transitionToNonMemberLocked(groupAddress, &info) - delete(g.memberships, groupAddress) + info.deleteScheduled = true + info.cancelDelayedReportJob() + + if !g.shouldPerformForGroup(groupAddress) { + delete(g.memberships, groupAddress) + return true + } + + switch g.mode { + case protocolModeV2: + info.transmissionLeft = g.robustnessVariable + if g.sendV2ReportAndMaybeScheduleChangedTimer(groupAddress, &info, MulticastGroupProtocolV2ReportRecordChangeToIncludeMode) { + g.memberships[groupAddress] = info + } else { + delete(g.memberships, groupAddress) + } + case protocolModeV1Compatibility: + g.transitionToNonMemberLocked(groupAddress, &info) + delete(g.memberships, groupAddress) + default: + panic(fmt.Sprintf("unrecognized mode = %d", g.mode)) + } + return true } +// HandleQueryV2Locked handles a V2 query. +// +// Precondition: g.protocolMU must be locked. +func (g *GenericMulticastProtocolState) HandleQueryV2Locked(groupAddress tcpip.Address, maxResponseCode uint16, sources header.AddressIterator, robustnessVariable uint8, queryInterval time.Duration) { + if !g.opts.Protocol.Enabled() { + return + } + + switch g.mode { + case protocolModeV1Compatibility: + g.handleQueryInnerLocked(groupAddress, g.opts.Protocol.V2QueryMaxRespCodeToV1Delay(maxResponseCode)) + return + case protocolModeV2: + default: + panic(fmt.Sprintf("unrecognized mode = %d", g.mode)) + } + + if robustnessVariable != 0 { + g.robustnessVariable = robustnessVariable + } + + if queryInterval != 0 { + g.queryInterval = queryInterval + } + + maxResponseTime := g.calculateDelayTimerDuration(g.opts.Protocol.V2QueryMaxRespCodeToV2Delay(maxResponseCode)) + + // As per RFC 3376 section 5.2, + // + // 1. If there is a pending response to a previous General Query + // scheduled sooner than the selected delay, no additional response + // needs to be scheduled. + // + // 2. If the received Query is a General Query, the interface timer is + // used to schedule a response to the General Query after the + // selected delay. Any previously pending response to a General + // Query is canceled. + // + // 3. If the received Query is a Group-Specific Query or a Group-and- + // Source-Specific Query and there is no pending response to a + // previous Query for this group, then the group timer is used to + // schedule a report. If the received Query is a Group-and-Source- + // Specific Query, the list of queried sources is recorded to be used + // when generating a response. + // + // 4. If there already is a pending response to a previous Query + // scheduled for this group, and either the new Query is a Group- + // Specific Query or the recorded source-list associated with the + // group is empty, then the group source-list is cleared and a single + // response is scheduled using the group timer. The new response is + // scheduled to be sent at the earliest of the remaining time for the + // pending report and the selected delay. + // + // 5. If the received Query is a Group-and-Source-Specific Query and + // there is a pending response for this group with a non-empty + // source-list, then the group source list is augmented to contain + // the list of sources in the new Query and a single response is + // scheduled using the group timer. The new response is scheduled to + // be sent at the earliest of the remaining time for the pending + // report and the selected delay. + // + // As per RFC 3810 section 6.2, + // + // 1. If there is a pending response to a previous General Query + // scheduled sooner than the selected delay, no additional response + // needs to be scheduled. + // + // 2. If the received Query is a General Query, the Interface Timer is + // used to schedule a response to the General Query after the + // selected delay. Any previously pending response to a General + // Query is canceled. + // + // 3. If the received Query is a Multicast Address Specific Query or a + // Multicast Address and Source Specific Query and there is no + // pending response to a previous Query for this multicast address, + // then the Multicast Address Timer is used to schedule a report. If + // the received Query is a Multicast Address and Source Specific + // Query, the list of queried sources is recorded to be used when + // generating a response. + // + // 4. If there is already a pending response to a previous Query + // scheduled for this multicast address, and either the new Query is + // a Multicast Address Specific Query or the recorded source list + // associated with the multicast address is empty, then the multicast + // address source list is cleared and a single response is scheduled, + // using the Multicast Address Timer. The new response is scheduled + // to be sent at the earliest of the remaining time for the pending + // report and the selected delay. + // + // 5. If the received Query is a Multicast Address and Source Specific + // Query and there is a pending response for this multicast address + // with a non-empty source list, then the multicast address source + // list is augmented to contain the list of sources in the new Query, + // and a single response is scheduled using the Multicast Address + // Timer. The new response is scheduled to be sent at the earliest + // of the remaining time for the pending report and the selected + // delay. + now := g.opts.Clock.Now() + if !g.generalQueryV2TimerFiresAt.IsZero() && g.generalQueryV2TimerFiresAt.Sub(now) <= maxResponseTime { + return + } + + if groupAddress.Unspecified() { + if g.generalQueryV2Timer == nil { + // TODO(https://issuetracker.google.com/264799098): Create timer on + // initialization instead of lazily creating the timer since the timer + // does not change after being created. + g.generalQueryV2Timer = g.opts.Clock.AfterFunc(maxResponseTime, func() { + g.protocolMU.Lock() + defer g.protocolMU.Unlock() + + g.generalQueryV2TimerFiresAt = time.Time{} + + // As per RFC 3810 section 6.3, + // + // If the expired timer is the Interface Timer (i.e., there is a + // pending response to a General Query), then one Current State + // Record is sent for each multicast address for which the specified + // interface has listening state, as described in section 4.2. The + // Current State Record carries the multicast address and its + // associated filter mode (MODE_IS_INCLUDE or MODE_IS_EXCLUDE) and + // Source list. Multiple Current State Records are packed into + // individual Report messages, to the extent possible. + // + // As per RFC 3376 section 5.2, + // + // If the expired timer is the interface timer (i.e., it is a pending + // response to a General Query), then one Current-State Record is + // sent for each multicast address for which the specified interface + // has reception state, as described in section 3.2. The Current- + // State Record carries the multicast address and its associated + // filter mode (MODE_IS_INCLUDE or MODE_IS_EXCLUDE) and source list. + // Multiple Current-State Records are packed into individual Report + // messages, to the extent possible. + reportBuilder := g.opts.Protocol.NewReportV2Builder() + for groupAddress, info := range g.memberships { + if info.deleteScheduled || !g.shouldPerformForGroup(groupAddress) { + continue + } + + // A MODE_IS_EXCLUDE record without any sources indicates that we are + // interested in traffic from all sources for the group. + // + // We currently only hold groups if we have an active interest in the + // group. + reportBuilder.AddRecord( + MulticastGroupProtocolV2ReportRecordModeIsExclude, + groupAddress, + ) + } + + _, _ = reportBuilder.Send() + }) + } else { + g.generalQueryV2Timer.Reset(maxResponseTime) + } + g.generalQueryV2TimerFiresAt = now.Add(maxResponseTime) + return + } + + if info, ok := g.memberships[groupAddress]; ok && !info.deleteScheduled && g.shouldPerformForGroup(groupAddress) { + if info.delayedReportJobFiresAt.IsZero() || (!sources.Done() && len(info.queriedIncludeSources) != 0) { + for { + source, ok := sources.Next() + if !ok { + break + } + + info.queriedIncludeSources[source] = struct{}{} + } + } else { + info.clearQueriedIncludeSources() + } + g.setDelayTimerForAddressLocked(groupAddress, &info, maxResponseTime) + g.memberships[groupAddress] = info + } +} + // HandleQueryLocked handles a query message with the specified maximum response // time. // @@ -344,6 +797,48 @@ func (g *GenericMulticastProtocolState) HandleQueryLocked(groupAddress tcpip.Add return } + // As per 3376 section 8.12 (for IGMPv3), + // + // The Older Version Querier Interval is the time-out for transitioning + // a host back to IGMPv3 mode once an older version query is heard. + // When an older version query is received, hosts set their Older + // Version Querier Present Timer to Older Version Querier Interval. + // + // This value MUST be ((the Robustness Variable) times (the Query + // Interval in the last Query received)) plus (one Query Response + // Interval). + // + // As per RFC 3810 section 9.12 (for MLDv2), + // + // The Older Version Querier Present Timeout is the time-out for + // transitioning a host back to MLDv2 Host Compatibility Mode. When an + // MLDv1 query is received, MLDv2 hosts set their Older Version Querier + // Present Timer to [Older Version Querier Present Timeout]. + // + // This value MUST be ([Robustness Variable] times (the [Query Interval] + // in the last Query received)) plus ([Query Response Interval]). + modeRevertDelay := time.Duration(g.robustnessVariable) * g.queryInterval + if g.modeTimer == nil { + // TODO(https://issuetracker.google.com/264799098): Create timer on + // initialization instead of lazily creating the timer since the timer + // does not change after being created. + g.modeTimer = g.opts.Clock.AfterFunc(modeRevertDelay, func() { + g.protocolMU.Lock() + defer g.protocolMU.Unlock() + g.mode = protocolModeV2 + }) + } else { + g.modeTimer.Reset(modeRevertDelay) + } + g.mode = protocolModeV1Compatibility + g.cancelV2ReportTimers() + + g.handleQueryInnerLocked(groupAddress, maxResponseTime) +} + +func (g *GenericMulticastProtocolState) handleQueryInnerLocked(groupAddress tcpip.Address, maxResponseTime time.Duration) { + maxResponseTime = g.calculateDelayTimerDuration(maxResponseTime) + // As per RFC 2236 section 2.4 (for IGMPv2), // // In a Membership Query message, the group address field is set to zero @@ -361,7 +856,7 @@ func (g *GenericMulticastProtocolState) HandleQueryLocked(groupAddress tcpip.Add g.setDelayTimerForAddressLocked(groupAddress, &info, maxResponseTime) g.memberships[groupAddress] = info } - } else if info, ok := g.memberships[groupAddress]; ok { + } else if info, ok := g.memberships[groupAddress]; ok && !info.deleteScheduled { g.setDelayTimerForAddressLocked(groupAddress, &info, maxResponseTime) g.memberships[groupAddress] = info } @@ -401,10 +896,21 @@ func (g *GenericMulticastProtocolState) HandleReportLocked(groupAddress tcpip.Ad // // Precondition: g.protocolMU must be locked. func (g *GenericMulticastProtocolState) initializeNewMemberLocked(groupAddress tcpip.Address, info *multicastGroupState) { + if !g.shouldPerformForGroup(groupAddress) { + return + } + info.lastToSendReport = false - if g.shouldPerformForGroup(groupAddress) { + + switch g.mode { + case protocolModeV2: + info.transmissionLeft = g.robustnessVariable + g.sendV2ReportAndMaybeScheduleChangedTimer(groupAddress, info, MulticastGroupProtocolV2ReportRecordChangeToExcludeMode) + case protocolModeV1Compatibility: info.transmissionLeft = unsolicitedTransmissionCount g.maybeSendReportLocked(groupAddress, info) + default: + panic(fmt.Sprintf("unrecognized mode = %d", g.mode)) } } @@ -443,7 +949,11 @@ func (g *GenericMulticastProtocolState) maybeSendReportLocked(groupAddress tcpip info.transmissionLeft-- if info.transmissionLeft > 0 { - g.setDelayTimerForAddressLocked(groupAddress, info, g.opts.MaxUnsolicitedReportDelay) + g.setDelayTimerForAddressLocked( + groupAddress, + info, + g.calculateDelayTimerDuration(g.opts.MaxUnsolicitedReportDelay), + ) } } } @@ -509,10 +1019,6 @@ func (g *GenericMulticastProtocolState) maybeSendLeave(groupAddress tcpip.Addres // // Precondition: g.protocolMU must be locked. func (g *GenericMulticastProtocolState) transitionToNonMemberLocked(groupAddress tcpip.Address, info *multicastGroupState) { - if !g.shouldPerformForGroup(groupAddress) { - return - } - info.cancelDelayedReportJob() g.maybeSendLeave(groupAddress, info.lastToSendReport) info.lastToSendReport = false @@ -548,7 +1054,6 @@ func (g *GenericMulticastProtocolState) setDelayTimerForAddressLocked(groupAddre return } - maxResponseTime = g.calculateDelayTimerDuration(maxResponseTime) info.delayedReportJob.Cancel() info.delayedReportJob.Schedule(maxResponseTime) info.delayedReportJobFiresAt = now.Add(maxResponseTime) diff --git a/pkg/tcpip/network/internal/ip/generic_multicast_protocol_test.go b/pkg/tcpip/network/internal/ip/generic_multicast_protocol_test.go index d1e2ea61c..ad0337752 100644 --- a/pkg/tcpip/network/internal/ip/generic_multicast_protocol_test.go +++ b/pkg/tcpip/network/internal/ip/generic_multicast_protocol_test.go @@ -15,6 +15,9 @@ package ip_test import ( + "bytes" + "fmt" + "math" "math/rand" "testing" "time" @@ -23,6 +26,7 @@ import ( "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/faketime" + "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/network/internal/ip" ) @@ -38,6 +42,7 @@ type mockMulticastGroupProtocolProtectedFields struct { sendLeaveGroupAddrCount map[tcpip.Address]int makeQueuePackets bool disabled bool + sentV2Reports map[tcpip.Address][]ip.MulticastGroupProtocolV2ReportRecordType } type mockMulticastGroupProtocol struct { @@ -48,17 +53,26 @@ type mockMulticastGroupProtocol struct { mu mockMulticastGroupProtocolProtectedFields } -func (m *mockMulticastGroupProtocol) init(opts ip.GenericMulticastProtocolOptions) { +func (m *mockMulticastGroupProtocol) init(opts ip.GenericMulticastProtocolOptions, v1Compatibility bool) { m.mu.Lock() defer m.mu.Unlock() m.initLocked() opts.Protocol = m m.mu.genericMulticastGroup.Init(&m.mu.RWMutex, opts) + + if v1Compatibility { + // A General V1 query should make us drop into V1 compatibility mode. + // + // Since we just init-ed, we know we don't have any groups to send + // reports for so this won't break any tests looking at packets. + m.mu.genericMulticastGroup.HandleQueryLocked("", math.MaxInt64) + } } func (m *mockMulticastGroupProtocol) initLocked() { m.mu.sendReportGroupAddrCount = make(map[tcpip.Address]int) m.mu.sendLeaveGroupAddrCount = make(map[tcpip.Address]int) + m.mu.sentV2Reports = make(map[tcpip.Address][]ip.MulticastGroupProtocolV2ReportRecordType) } func (m *mockMulticastGroupProtocol) setEnabled(v bool) { @@ -97,6 +111,12 @@ func (m *mockMulticastGroupProtocol) handleQuery(addr tcpip.Address, maxRespTime m.mu.genericMulticastGroup.HandleQueryLocked(addr, maxRespTime) } +func (m *mockMulticastGroupProtocol) handleQueryV2(addr tcpip.Address, maxResponseCode uint16, sources header.AddressIterator, robustnessVariable uint8, queryInterval time.Duration) { + m.mu.Lock() + defer m.mu.Unlock() + m.mu.genericMulticastGroup.HandleQueryV2Locked(addr, maxResponseCode, sources, robustnessVariable, queryInterval) +} + func (m *mockMulticastGroupProtocol) isLocallyJoined(addr tcpip.Address) bool { m.mu.RLock() defer m.mu.RUnlock() @@ -172,9 +192,65 @@ func (m *mockMulticastGroupProtocol) ShouldPerformProtocol(groupAddress tcpip.Ad return groupAddress != m.skipProtocolAddress } +type mockReportV2Record struct { + recordType ip.MulticastGroupProtocolV2ReportRecordType + groupAddress tcpip.Address +} + +type mockReportV2 struct { + records []mockReportV2Record +} + +type mockReportV2Builder struct { + m *mockMulticastGroupProtocol + report mockReportV2 +} + +// AddRecord implements ip.MulticastGroupProtocolV2ReportBuilder. +func (b *mockReportV2Builder) AddRecord(recordType ip.MulticastGroupProtocolV2ReportRecordType, groupAddress tcpip.Address) { + b.report.records = append(b.report.records, mockReportV2Record{recordType: recordType, groupAddress: groupAddress}) +} + +func recordsToMap(m map[tcpip.Address][]ip.MulticastGroupProtocolV2ReportRecordType, records []mockReportV2Record) { + for _, record := range records { + m[record.groupAddress] = append(m[record.groupAddress], record.recordType) + } +} + +// Send implements ip.MulticastGroupProtocolV2ReportBuilder. +func (b *mockReportV2Builder) Send() (sent bool, err tcpip.Error) { + if b.m.mu.TryLock() { + b.m.mu.Unlock() // +checklocksforce: TryLock. + b.m.t.Fatal("got write lock, expected to not take the lock; generic multicast protocol must take the write lock before sending v2 report") + } + if b.m.mu.TryRLock() { + b.m.mu.RUnlock() // +checklocksforce: TryLock. + b.m.t.Fatal("got read lock, expected to not take the lock; generic multicast protocol must take the write lock before sending v2 report") + } + + recordsToMap(b.m.mu.sentV2Reports, b.report.records) + return !b.m.mu.makeQueuePackets, nil +} + +// NewReportV2Builder implements ip.MulticastGroupProtocol. +func (m *mockMulticastGroupProtocol) NewReportV2Builder() ip.MulticastGroupProtocolV2ReportBuilder { + return &mockReportV2Builder{m: m} +} + +// V2QueryMaxRespCodeToV2Delay implements ip.MulticastGroupProtocol. +func (*mockMulticastGroupProtocol) V2QueryMaxRespCodeToV2Delay(code uint16) time.Duration { + return time.Duration(code) * time.Millisecond +} + +// V2QueryMaxRespCodeToV1Delay implements ip.MulticastGroupProtocol. +func (*mockMulticastGroupProtocol) V2QueryMaxRespCodeToV1Delay(code uint16) time.Duration { + return time.Duration(code) * time.Millisecond +} + type checkFields struct { sendReportGroupAddresses []tcpip.Address sendLeaveGroupAddresses []tcpip.Address + sentV2Reports []mockReportV2 } func (m *mockMulticastGroupProtocol) check(fields checkFields) string { @@ -191,16 +267,24 @@ func (m *mockMulticastGroupProtocol) check(fields checkFields) string { sendLeaveGroupAddrCount[a] = 1 } + sentV2Reports := make(map[tcpip.Address][]ip.MulticastGroupProtocolV2ReportRecordType) + for _, report := range fields.sentV2Reports { + recordsToMap(sentV2Reports, report.records) + } + diff := cmp.Diff( &mockMulticastGroupProtocol{ mu: mockMulticastGroupProtocolProtectedFields{ sendReportGroupAddrCount: sendReportGroupAddrCount, sendLeaveGroupAddrCount: sendLeaveGroupAddrCount, + sentV2Reports: sentV2Reports, }, }, m, cmp.AllowUnexported(mockMulticastGroupProtocol{}), cmp.AllowUnexported(mockMulticastGroupProtocolProtectedFields{}), + cmp.AllowUnexported(mockReportV2{}), + cmp.AllowUnexported(mockReportV2Record{}), // ignore mockMulticastGroupProtocol.mu and mockMulticastGroupProtocol.t cmp.FilterPath( func(p cmp.Path) bool { @@ -236,42 +320,75 @@ func TestJoinGroup(t *testing.T) { }, } + subTests := []struct { + name string + v1Compatibility bool + checkFields func(tcpip.Address) checkFields + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + checkFields: func(addr tcpip.Address) checkFields { + return checkFields{sendReportGroupAddresses: []tcpip.Address{addr}} + }, + }, + { + name: "V2", + v1Compatibility: false, + checkFields: func(addr tcpip.Address) checkFields { + return checkFields{sentV2Reports: []mockReportV2{{records: []mockReportV2Record{ + { + recordType: ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode, + groupAddress: addr, + }, + }}}} + }, + }, + } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr2} - clock := faketime.NewManualClock() + for _, subTest := range subTests { + t.Run(subTest.name, func(t *testing.T) { + mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr2} + clock := faketime.NewManualClock() - mgp.init(ip.GenericMulticastProtocolOptions{ - Rand: rand.New(rand.NewSource(0)), - Clock: clock, - MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, - }) + mgp.init(ip.GenericMulticastProtocolOptions{ + Rand: rand.New(rand.NewSource(0)), + Clock: clock, + MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, + }, subTest.v1Compatibility) - // Joining a group should send a report immediately and another after - // a random interval between 0 and the maximum unsolicited report delay. - mgp.joinGroup(test.addr) - if test.shouldSendReports { - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{test.addr}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + // Joining a group should send a report immediately and another after + // a random interval between 0 and the maximum unsolicited report delay. + mgp.joinGroup(test.addr) + if test.shouldSendReports { + expected := subTest.checkFields(test.addr) + if diff := mgp.check(expected); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } - // Generic multicast protocol timers are expected to take the job mutex. - clock.Advance(maxUnsolicitedReportDelay) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{test.addr}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - } + // Generic multicast protocol timers are expected to take the job mutex. + clock.Advance(maxUnsolicitedReportDelay) + if diff := mgp.check(expected); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + } - // Should have no more messages to send. - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + // Should have no more messages to send. + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + }) } }) } } func TestLeaveGroup(t *testing.T) { + const maxRespCode = 1 + tests := []struct { name string addr tcpip.Address @@ -289,44 +406,105 @@ func TestLeaveGroup(t *testing.T) { }, } + subTests := []struct { + name string + v1Compatibility bool + checkFields func(tcpip.Address, bool) checkFields + handleQuery func(*mockMulticastGroupProtocol, tcpip.Address) + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + checkFields: func(addr tcpip.Address, leave bool) checkFields { + if leave { + return checkFields{sendLeaveGroupAddresses: []tcpip.Address{addr}} + } + return checkFields{sendReportGroupAddresses: []tcpip.Address{addr}} + }, + handleQuery: func(mgp *mockMulticastGroupProtocol, groupAddress tcpip.Address) { + mgp.handleQuery(groupAddress, maxRespCode) + }, + }, + { + name: "V2", + v1Compatibility: false, + checkFields: func(addr tcpip.Address, leave bool) checkFields { + recordType := ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode + if leave { + recordType = ip.MulticastGroupProtocolV2ReportRecordChangeToIncludeMode + } + + return checkFields{sentV2Reports: []mockReportV2{{records: []mockReportV2Record{ + { + recordType: recordType, + groupAddress: addr, + }, + }}}} + }, + handleQuery: func(mgp *mockMulticastGroupProtocol, groupAddress tcpip.Address) { + mgp.handleQueryV2(groupAddress, maxRespCode, header.MakeAddressIterator(len(addr1), bytes.NewBuffer(nil)), 0, 0) + }, + }, + } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr2} - clock := faketime.NewManualClock() + for _, subTest := range subTests { + t.Run(subTest.name, func(t *testing.T) { + for _, queryAddr := range []tcpip.Address{test.addr, ""} { + t.Run(fmt.Sprintf("QueryAddr=%s", queryAddr), func(t *testing.T) { + mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr2} + clock := faketime.NewManualClock() - mgp.init(ip.GenericMulticastProtocolOptions{ - Rand: rand.New(rand.NewSource(1)), - Clock: clock, - MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, - }) + mgp.init(ip.GenericMulticastProtocolOptions{ + Rand: rand.New(rand.NewSource(1)), + Clock: clock, + MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, + }, subTest.v1Compatibility) - mgp.joinGroup(test.addr) - if test.shouldSendMessages { - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{test.addr}}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - } + mgp.joinGroup(test.addr) + if test.shouldSendMessages { + if diff := mgp.check(subTest.checkFields(test.addr, false /* leave */)); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + } - // Leaving a group should send a leave report immediately and cancel any - // delayed reports. - { + // The timer scheduled to send the query response should do + // nothing since we will leave the group before the response is + // sent. + subTest.handleQuery(&mgp, queryAddr) - if !mgp.leaveGroup(test.addr) { - t.Fatalf("got mgp.leaveGroup(%s) = false, want = true", test.addr) - } - } - if test.shouldSendMessages { - if diff := mgp.check(checkFields{sendLeaveGroupAddresses: []tcpip.Address{test.addr}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - } + // Leaving a group should send a leave report immediately and + // cancel any delayed reports. + if !mgp.leaveGroup(test.addr) { + t.Fatalf("got mgp.leaveGroup(%s) = false, want = true", test.addr) + } - // Should have no more messages to send. - // - // Generic multicast protocol timers are expected to take the job mutex. - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + // A query should not do anything since we left the group. + subTest.handleQuery(&mgp, queryAddr) + + if test.shouldSendMessages { + if diff := mgp.check(subTest.checkFields(test.addr, true /* leave */)); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + if !subTest.v1Compatibility { + clock.Advance(maxUnsolicitedReportDelay) + + if diff := mgp.check(subTest.checkFields(test.addr, true /* leave */)); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + } + } + + // Should have no more messages to send. + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + }) + } + }) } }) } @@ -365,45 +543,78 @@ func TestHandleReport(t *testing.T) { }, } + subTests := []struct { + name string + v1Compatibility bool + checkFields func([]tcpip.Address) checkFields + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + checkFields: func(addrs []tcpip.Address) checkFields { + return checkFields{sendReportGroupAddresses: addrs} + }, + }, + { + name: "V2", + v1Compatibility: false, + checkFields: func(addrs []tcpip.Address) checkFields { + var records []mockReportV2Record + for _, addr := range addrs { + records = append(records, mockReportV2Record{ + recordType: ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode, + groupAddress: addr, + }) + } + + return checkFields{sentV2Reports: []mockReportV2{{records: records}}} + }, + }, + } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr3} - clock := faketime.NewManualClock() + for _, subTest := range subTests { + t.Run(subTest.name, func(t *testing.T) { + mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr3} + clock := faketime.NewManualClock() - mgp.init(ip.GenericMulticastProtocolOptions{ - Rand: rand.New(rand.NewSource(2)), - Clock: clock, - MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, - }) + mgp.init(ip.GenericMulticastProtocolOptions{ + Rand: rand.New(rand.NewSource(2)), + Clock: clock, + MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, + }, subTest.v1Compatibility) - mgp.joinGroup(addr1) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - mgp.joinGroup(addr2) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr2}}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - mgp.joinGroup(addr3) - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + mgp.joinGroup(addr1) + if diff := mgp.check(subTest.checkFields([]tcpip.Address{addr1})); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + mgp.joinGroup(addr2) + if diff := mgp.check(subTest.checkFields([]tcpip.Address{addr2})); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + mgp.joinGroup(addr3) + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } - // Receiving a report for a group we have a timer scheduled for should - // cancel our delayed report timer for the group. - mgp.handleReport(test.reportAddr) - if len(test.expectReportsFor) != 0 { - // Generic multicast protocol timers are expected to take the job mutex. - clock.Advance(maxUnsolicitedReportDelay) - if diff := mgp.check(checkFields{sendReportGroupAddresses: test.expectReportsFor}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - } + // Receiving a report for a group we have a timer scheduled for should + // cancel our delayed report timer for the group. + mgp.handleReport(test.reportAddr) + if len(test.expectReportsFor) != 0 { + // Generic multicast protocol timers are expected to take the job mutex. + clock.Advance(maxUnsolicitedReportDelay) + if diff := mgp.check(subTest.checkFields(test.expectReportsFor)); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + } - // Should have no more messages to send. - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + // Should have no more messages to send. + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + }) } }) } @@ -454,6 +665,265 @@ func TestHandleQuery(t *testing.T) { }, } + subTests := []struct { + name string + v1Compatibility bool + checkFields func([]tcpip.Address) checkFields + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + checkFields: func(addrs []tcpip.Address) checkFields { + return checkFields{sendReportGroupAddresses: addrs} + }, + }, + { + name: "V2", + v1Compatibility: false, + checkFields: func(addrs []tcpip.Address) checkFields { + var records []mockReportV2Record + for _, addr := range addrs { + records = append(records, mockReportV2Record{ + recordType: ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode, + groupAddress: addr, + }) + } + + return checkFields{sentV2Reports: []mockReportV2{{records: records}}} + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for _, subTest := range subTests { + t.Run(subTest.name, func(t *testing.T) { + mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr3} + clock := faketime.NewManualClock() + + mgp.init(ip.GenericMulticastProtocolOptions{ + Rand: rand.New(rand.NewSource(3)), + Clock: clock, + MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, + }, subTest.v1Compatibility) + + mgp.joinGroup(addr1) + if diff := mgp.check(subTest.checkFields([]tcpip.Address{addr1})); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + mgp.joinGroup(addr2) + if diff := mgp.check(subTest.checkFields([]tcpip.Address{addr2})); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + mgp.joinGroup(addr3) + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Receiving a query should make us reschedule our delayed report timer + // to some time within the new max response delay. + mgp.handleQuery(test.queryAddr, test.maxDelay) + clock.Advance(test.maxDelay) + if diff := mgp.check(checkFields{sendReportGroupAddresses: test.expectQueriedReportsFor}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // The groups that were not affected by the query should still send a + // report after the max unsolicited report delay. + // + // If we were in V2 mode, then we would have cancelled the interface's + // state changed timer so we won't see any further reports after + // receiving a V1 query. + if subTest.v1Compatibility { + clock.Advance(maxUnsolicitedReportDelay) + if diff := mgp.check(subTest.checkFields(test.expectDelayedReportsFor)); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + } + + // Should have no more messages to send. + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + }) + } + }) + } +} + +func TestHandleQueryV2Response(t *testing.T) { + tests := []struct { + name string + queryAddr tcpip.Address + maxDelay uint16 + expectQueriedReportsFor []tcpip.Address + expectDelayedReportsFor []tcpip.Address + }{ + { + name: "Unpecified empty", + queryAddr: "", + maxDelay: 0, + expectQueriedReportsFor: []tcpip.Address{addr1, addr2}, + expectDelayedReportsFor: nil, + }, + { + name: "Unpecified any", + queryAddr: "\x00", + maxDelay: 1, + expectQueriedReportsFor: []tcpip.Address{addr1, addr2}, + expectDelayedReportsFor: nil, + }, + { + name: "Specified", + queryAddr: addr1, + maxDelay: 2, + expectQueriedReportsFor: []tcpip.Address{addr1}, + expectDelayedReportsFor: []tcpip.Address{addr2}, + }, + { + name: "Specified all-nodes", + queryAddr: addr3, + maxDelay: 3, + expectQueriedReportsFor: nil, + expectDelayedReportsFor: []tcpip.Address{addr1, addr2}, + }, + { + name: "Specified other", + queryAddr: addr4, + maxDelay: 4, + expectQueriedReportsFor: nil, + expectDelayedReportsFor: []tcpip.Address{addr1, addr2}, + }, + } + + subTests := []struct { + name string + v1Compatibility bool + checkFields func([]tcpip.Address, bool) checkFields + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + checkFields: func(addrs []tcpip.Address, _ bool) checkFields { + return checkFields{sendReportGroupAddresses: addrs} + }, + }, + { + name: "V2", + v1Compatibility: false, + checkFields: func(addrs []tcpip.Address, queryResponse bool) checkFields { + var records []mockReportV2Record + recordType := ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode + if queryResponse { + recordType = ip.MulticastGroupProtocolV2ReportRecordModeIsExclude + } + + for _, addr := range addrs { + records = append(records, mockReportV2Record{ + recordType: recordType, + groupAddress: addr, + }) + } + + return checkFields{sentV2Reports: []mockReportV2{{records: records}}} + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for _, subTest := range subTests { + t.Run(subTest.name, func(t *testing.T) { + mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr3} + clock := faketime.NewManualClock() + + mgp.init(ip.GenericMulticastProtocolOptions{ + Rand: rand.New(rand.NewSource(3)), + Clock: clock, + MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, + }, subTest.v1Compatibility) + + mgp.joinGroup(addr1) + if diff := mgp.check(subTest.checkFields([]tcpip.Address{addr1}, false /* queryResponse */)); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + mgp.joinGroup(addr2) + if diff := mgp.check(subTest.checkFields([]tcpip.Address{addr2}, false /* queryResponse */)); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + mgp.joinGroup(addr3) + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + clock.Advance(maxUnsolicitedReportDelay) + if diff := mgp.check(subTest.checkFields([]tcpip.Address{addr1, addr2}, false /* queryResponse */)); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + clock.Advance(maxUnsolicitedReportDelay) + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Receiving a query should make us reschedule our delayed report + // timer to some time within the new max response delay. + // + // Note that if we are in V1 compatbility mode, the V2 query will be + // handled as a V1 query. + mgp.handleQueryV2(test.queryAddr, test.maxDelay, header.MakeAddressIterator(len(addr1), bytes.NewBuffer(nil)), 0, 0) + if subTest.v1Compatibility { + clock.Advance(mgp.V2QueryMaxRespCodeToV1Delay(test.maxDelay)) + } else { + clock.Advance(mgp.V2QueryMaxRespCodeToV2Delay(test.maxDelay)) + } + if diff := mgp.check(subTest.checkFields(test.expectQueriedReportsFor, true /* queryResponse */)); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Should have no more messages to send. + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + }) + } + }) + } +} + +func TestV1CompatbilityModeTimer(t *testing.T) { + tests := []struct { + name string + robustnessVariable uint8 + queryInterval time.Duration + }{ + { + name: "Unspecified Robustness variable and Query interval", + robustnessVariable: 0, + queryInterval: 0, + }, + { + name: "Unspecified Robustness variable", + robustnessVariable: 0, + queryInterval: ip.DefaultQueryInterval + time.Second, + }, + { + name: "Unspecified Query interval", + robustnessVariable: ip.DefaultRobustnessVariable + 1, + queryInterval: 0, + }, + { + name: "Default Robustness variable and Query interval", + robustnessVariable: ip.DefaultRobustnessVariable, + queryInterval: ip.DefaultQueryInterval, + }, + { + name: "Specified Robustness variable and Query interval", + robustnessVariable: ip.DefaultRobustnessVariable + 1, + queryInterval: ip.DefaultQueryInterval + time.Second, + }, + } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr3} @@ -463,14 +933,272 @@ func TestHandleQuery(t *testing.T) { Rand: rand.New(rand.NewSource(3)), Clock: clock, MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, - }) + }, false /* v1Compatibiltiy */) + + v2Check := func(t *testing.T) { + t.Helper() + + mgp.joinGroup(addr1) + if diff := mgp.check(checkFields{sentV2Reports: []mockReportV2{{records: []mockReportV2Record{ + { + recordType: ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode, + groupAddress: addr1, + }, + }}}}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + if !mgp.leaveGroup(addr1) { + t.Fatalf("got mgp.leaveGroup(%s) = false, want = true", addr1) + } + if diff := mgp.check(checkFields{sentV2Reports: []mockReportV2{{records: []mockReportV2Record{ + { + recordType: ip.MulticastGroupProtocolV2ReportRecordChangeToIncludeMode, + groupAddress: addr1, + }, + }}}}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + } + v2Check(t) + + subTests := []struct { + name string + advanceTime time.Duration + }{ + { + name: "Default", + advanceTime: ip.DefaultRobustnessVariable * ip.DefaultQueryInterval, + }, + { + name: "After V2 Query", + advanceTime: func() time.Duration { + robustnessVariable := test.robustnessVariable + if robustnessVariable == 0 { + robustnessVariable = ip.DefaultRobustnessVariable + } + + queryInterval := test.queryInterval + if queryInterval == 0 { + queryInterval = ip.DefaultQueryInterval + } + + return time.Duration(robustnessVariable) * queryInterval + }(), + }, + } + + for _, subTest := range subTests { + t.Run(subTest.name, func(t *testing.T) { + mgp.handleQuery(addr3, time.Nanosecond) + v1Check := func() { + t.Helper() + mgp.joinGroup(addr1) + if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + if !mgp.leaveGroup(addr1) { + t.Fatalf("got mgp.leaveGroup(%s) = false, want = true", addr1) + } + if diff := mgp.check(checkFields{sendLeaveGroupAddresses: []tcpip.Address{addr1}}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + } + v1Check() + const minDuration = time.Duration(1) + clock.Advance(subTest.advanceTime - minDuration) + v1Check() + + clock.Advance(minDuration) + v2Check(t) + // Should update the Robustness variable and Querier's Query interval. + mgp.handleQueryV2(addr3, 0, header.MakeAddressIterator(len(addr1), bytes.NewBuffer(nil)), test.robustnessVariable, test.queryInterval) + }) + } + }) + } +} + +func TestJoinCount(t *testing.T) { + const maxUnsolicitedReportDelay = time.Second + + tests := []struct { + name string + v1Compatibility bool + checkFields func(tcpip.Address, bool) checkFields + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + checkFields: func(addr tcpip.Address, leave bool) checkFields { + if leave { + return checkFields{sendLeaveGroupAddresses: []tcpip.Address{addr}} + } + return checkFields{sendReportGroupAddresses: []tcpip.Address{addr}} + }, + }, + { + name: "V2", + v1Compatibility: false, + checkFields: func(addr tcpip.Address, leave bool) checkFields { + recordType := ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode + if leave { + recordType = ip.MulticastGroupProtocolV2ReportRecordChangeToIncludeMode + } + + return checkFields{sentV2Reports: []mockReportV2{{records: []mockReportV2Record{ + { + recordType: recordType, + groupAddress: addr, + }, + }}}} + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mgp := mockMulticastGroupProtocol{t: t} + clock := faketime.NewManualClock() + + mgp.init(ip.GenericMulticastProtocolOptions{ + Rand: rand.New(rand.NewSource(4)), + Clock: clock, + MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, + }, test.v1Compatibility) + + // Set the join count to 2 for a group. + mgp.joinGroup(addr1) + if !mgp.isLocallyJoined(addr1) { + t.Fatalf("got mgp.isLocallyJoined(%s) = false, want = true", addr1) + } + // Only the first join should trigger a report to be sent. + if diff := mgp.check(test.checkFields(addr1, false /* leave */)); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + mgp.joinGroup(addr1) + if !mgp.isLocallyJoined(addr1) { + t.Errorf("got mgp.isLocallyJoined(%s) = false, want = true", addr1) + } + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + if t.Failed() { + t.FailNow() + } + + // Group should still be considered joined after leaving once. + if !mgp.leaveGroup(addr1) { + t.Errorf("got mgp.leaveGroup(%s) = false, want = true", addr1) + } + if !mgp.isLocallyJoined(addr1) { + t.Errorf("got mgp.isLocallyJoined(%s) = false, want = true", addr1) + } + // A leave report should only be sent once the join count reaches 0. + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + if t.Failed() { + t.FailNow() + } + + // Leaving once more should actually remove us from the group. + if !mgp.leaveGroup(addr1) { + t.Errorf("got mgp.leaveGroup(%s) = false, want = true", addr1) + } + if mgp.isLocallyJoined(addr1) { + t.Errorf("got mgp.isLocallyJoined(%s) = true, want = false", addr1) + } + if diff := mgp.check(test.checkFields(addr1, true /* leave */)); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + if !test.v1Compatibility { + // V2 should still have a queued state-changed report. + clock.Advance(maxUnsolicitedReportDelay) + if diff := mgp.check(test.checkFields(addr1, true /* leave */)); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + } + if t.Failed() { + t.FailNow() + } + + // Group should no longer be joined so we should not have anything to + // leave. + if mgp.leaveGroup(addr1) { + t.Errorf("got mgp.leaveGroup(%s) = true, want = false", addr1) + } + if mgp.isLocallyJoined(addr1) { + t.Errorf("got mgp.isLocallyJoined(%s) = true, want = false", addr1) + } + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Should have no more messages to send. + // + // Generic multicast protocol timers are expected to take the job mutex. + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestMakeAllNonMemberAndInitialize(t *testing.T) { + tests := []struct { + name string + v1Compatibility bool + checkFields func([]tcpip.Address, bool) checkFields + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + checkFields: func(addrs []tcpip.Address, leave bool) checkFields { + if leave { + return checkFields{sendLeaveGroupAddresses: addrs} + } + return checkFields{sendReportGroupAddresses: addrs} + }, + }, + { + name: "V2", + v1Compatibility: false, + checkFields: func(addrs []tcpip.Address, leave bool) checkFields { + recordType := ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode + if leave { + recordType = ip.MulticastGroupProtocolV2ReportRecordChangeToIncludeMode + } + var records []mockReportV2Record + for _, addr := range addrs { + records = append(records, mockReportV2Record{ + recordType: recordType, + groupAddress: addr, + }) + } + + return checkFields{sentV2Reports: []mockReportV2{{records: records}}} + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr3} + clock := faketime.NewManualClock() + + mgp.init(ip.GenericMulticastProtocolOptions{ + Rand: rand.New(rand.NewSource(3)), + Clock: clock, + MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, + }, test.v1Compatibility) mgp.joinGroup(addr1) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { + if diff := mgp.check(test.checkFields([]tcpip.Address{addr1}, false /* leave */)); diff != "" { t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) } mgp.joinGroup(addr2) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr2}}); diff != "" { + if diff := mgp.check(test.checkFields([]tcpip.Address{addr2}, false /* leave */)); diff != "" { t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) } mgp.joinGroup(addr3) @@ -478,18 +1206,62 @@ func TestHandleQuery(t *testing.T) { t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) } - // Receiving a query should make us reschedule our delayed report timer - // to some time within the new max response delay. - mgp.handleQuery(test.queryAddr, test.maxDelay) - clock.Advance(test.maxDelay) - if diff := mgp.check(checkFields{sendReportGroupAddresses: test.expectQueriedReportsFor}); diff != "" { + // Should send the leave reports for each but still consider them locally + // joined. + mgp.makeAllNonMember() + expectedLeaveFields := test.checkFields([]tcpip.Address{addr1, addr2}, true /* leave */) + if diff := mgp.check(expectedLeaveFields); diff != "" { t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) } - // The groups that were not affected by the query should still send a - // report after the max unsolicited report delay. + // Generic multicast protocol timers are expected to take the job mutex. + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + for _, group := range []tcpip.Address{addr1, addr2, addr3} { + if !mgp.isLocallyJoined(group) { + t.Fatalf("got mgp.isLocallyJoined(%s) = false, want = true", group) + } + } + + // Should send the initial set of unsolcited V2 reports. + mgp.initializeGroups() + if diff := mgp.check(checkFields{sentV2Reports: []mockReportV2{ + { + records: []mockReportV2Record{ + { + recordType: ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode, + groupAddress: addr1, + }, + }, + }, + { + records: []mockReportV2Record{ + { + recordType: ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode, + groupAddress: addr2, + }, + }, + }, + }}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } clock.Advance(maxUnsolicitedReportDelay) - if diff := mgp.check(checkFields{sendReportGroupAddresses: test.expectDelayedReportsFor}); diff != "" { + if diff := mgp.check(checkFields{sentV2Reports: []mockReportV2{ + { + records: []mockReportV2Record{ + { + recordType: ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode, + groupAddress: addr1, + }, + { + recordType: ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode, + groupAddress: addr2, + }, + }, + }, + }}); diff != "" { t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) } @@ -502,301 +1274,238 @@ func TestHandleQuery(t *testing.T) { } } -func TestJoinCount(t *testing.T) { - mgp := mockMulticastGroupProtocol{t: t} - clock := faketime.NewManualClock() - - mgp.init(ip.GenericMulticastProtocolOptions{ - Rand: rand.New(rand.NewSource(4)), - Clock: clock, - MaxUnsolicitedReportDelay: time.Second, - }) - - // Set the join count to 2 for a group. - mgp.joinGroup(addr1) - if !mgp.isLocallyJoined(addr1) { - t.Fatalf("got mgp.isLocallyJoined(%s) = false, want = true", addr1) - } - // Only the first join should trigger a report to be sent. - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - mgp.joinGroup(addr1) - if !mgp.isLocallyJoined(addr1) { - t.Errorf("got mgp.isLocallyJoined(%s) = false, want = true", addr1) - } - if diff := mgp.check(checkFields{}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - if t.Failed() { - t.FailNow() - } - - // Group should still be considered joined after leaving once. - if !mgp.leaveGroup(addr1) { - t.Errorf("got mgp.leaveGroup(%s) = false, want = true", addr1) - } - if !mgp.isLocallyJoined(addr1) { - t.Errorf("got mgp.isLocallyJoined(%s) = false, want = true", addr1) - } - // A leave report should only be sent once the join count reaches 0. - if diff := mgp.check(checkFields{}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - if t.Failed() { - t.FailNow() - } - - // Leaving once more should actually remove us from the group. - if !mgp.leaveGroup(addr1) { - t.Errorf("got mgp.leaveGroup(%s) = false, want = true", addr1) - } - if mgp.isLocallyJoined(addr1) { - t.Errorf("got mgp.isLocallyJoined(%s) = true, want = false", addr1) - } - if diff := mgp.check(checkFields{sendLeaveGroupAddresses: []tcpip.Address{addr1}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - if t.Failed() { - t.FailNow() - } - - // Group should no longer be joined so we should not have anything to - // leave. - if mgp.leaveGroup(addr1) { - t.Errorf("got mgp.leaveGroup(%s) = true, want = false", addr1) - } - if mgp.isLocallyJoined(addr1) { - t.Errorf("got mgp.isLocallyJoined(%s) = true, want = false", addr1) - } - if diff := mgp.check(checkFields{}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - - // Should have no more messages to send. - // - // Generic multicast protocol timers are expected to take the job mutex. - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } -} - -func TestMakeAllNonMemberAndInitialize(t *testing.T) { - mgp := mockMulticastGroupProtocol{t: t, skipProtocolAddress: addr3} - clock := faketime.NewManualClock() - - mgp.init(ip.GenericMulticastProtocolOptions{ - Rand: rand.New(rand.NewSource(3)), - Clock: clock, - MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, - }) - - mgp.joinGroup(addr1) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - mgp.joinGroup(addr2) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr2}}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - mgp.joinGroup(addr3) - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - - // Should send the leave reports for each but still consider them locally - // joined. - mgp.makeAllNonMember() - if diff := mgp.check(checkFields{sendLeaveGroupAddresses: []tcpip.Address{addr1, addr2}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - // Generic multicast protocol timers are expected to take the job mutex. - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - for _, group := range []tcpip.Address{addr1, addr2, addr3} { - if !mgp.isLocallyJoined(group) { - t.Fatalf("got mgp.isLocallyJoined(%s) = false, want = true", group) - } - } - - // Should send the initial set of unsolcited reports. - mgp.initializeGroups() - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1, addr2}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - clock.Advance(maxUnsolicitedReportDelay) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1, addr2}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - - // Should have no more messages to send. - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } -} - // TestGroupStateNonMember tests that groups do not send packets when in the // non-member state, but are still considered locally joined. func TestGroupStateNonMember(t *testing.T) { - mgp := mockMulticastGroupProtocol{t: t} - clock := faketime.NewManualClock() + tests := []struct { + name string + v1Compatibility bool + checkFields func([]tcpip.Address, bool) checkFields + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + checkFields: func(addrs []tcpip.Address, leave bool) checkFields { + if leave { + return checkFields{sendLeaveGroupAddresses: addrs} + } + return checkFields{sendReportGroupAddresses: addrs} + }, + }, + { + name: "V2", + v1Compatibility: false, + checkFields: func(addrs []tcpip.Address, leave bool) checkFields { + recordType := ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode + if leave { + recordType = ip.MulticastGroupProtocolV2ReportRecordChangeToIncludeMode + } + var records []mockReportV2Record + for _, addr := range addrs { + records = append(records, mockReportV2Record{ + recordType: recordType, + groupAddress: addr, + }) + } - mgp.init(ip.GenericMulticastProtocolOptions{ - Rand: rand.New(rand.NewSource(3)), - Clock: clock, - MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, - }) - mgp.setEnabled(false) - - // Joining groups should not send any reports. - mgp.joinGroup(addr1) - if !mgp.isLocallyJoined(addr1) { - t.Fatalf("got mgp.isLocallyJoined(%s) = false, want = true", addr1) - } - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - mgp.joinGroup(addr2) - if !mgp.isLocallyJoined(addr1) { - t.Fatalf("got mgp.isLocallyJoined(%s) = false, want = true", addr2) - } - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + return checkFields{sentV2Reports: []mockReportV2{{records: records}}} + }, + }, } - // Receiving a query should not send any reports. - mgp.handleQuery(addr1, time.Nanosecond) - // Generic multicast protocol timers are expected to take the job mutex. - clock.Advance(time.Nanosecond) - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mgp := mockMulticastGroupProtocol{t: t} + clock := faketime.NewManualClock() - // Leaving groups should not send any leave messages. - if !mgp.leaveGroup(addr1) { - t.Errorf("got mgp.leaveGroup(%s) = false, want = true", addr2) - } - if mgp.isLocallyJoined(addr1) { - t.Errorf("got mgp.isLocallyJoined(%s) = true, want = false", addr2) - } - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + mgp.init(ip.GenericMulticastProtocolOptions{ + Rand: rand.New(rand.NewSource(3)), + Clock: clock, + MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, + }, test.v1Compatibility) + mgp.setEnabled(false) - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + // Joining groups should not send any reports. + mgp.joinGroup(addr1) + if !mgp.isLocallyJoined(addr1) { + t.Fatalf("got mgp.isLocallyJoined(%s) = false, want = true", addr1) + } + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + mgp.joinGroup(addr2) + if !mgp.isLocallyJoined(addr1) { + t.Fatalf("got mgp.isLocallyJoined(%s) = false, want = true", addr2) + } + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Receiving a query should not send any reports. + mgp.handleQuery(addr1, time.Nanosecond) + // Generic multicast protocol timers are expected to take the job mutex. + clock.Advance(time.Nanosecond) + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Leaving groups should not send any leave messages. + if !mgp.leaveGroup(addr1) { + t.Errorf("got mgp.leaveGroup(%s) = false, want = true", addr2) + } + if mgp.isLocallyJoined(addr1) { + t.Errorf("got mgp.isLocallyJoined(%s) = true, want = false", addr2) + } + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + }) } } func TestQueuedPackets(t *testing.T) { - clock := faketime.NewManualClock() - mgp := mockMulticastGroupProtocol{t: t} - mgp.init(ip.GenericMulticastProtocolOptions{ - Rand: rand.New(rand.NewSource(4)), - Clock: clock, - MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, - }) - - // Joining should trigger a SendReport, but mgp should report that we did not - // send the packet. - mgp.setQueuePackets(true) - mgp.joinGroup(addr1) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + tests := []struct { + name string + v1Compatibility bool + checkFields func(tcpip.Address) checkFields + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + checkFields: func(addr tcpip.Address) checkFields { + return checkFields{sendReportGroupAddresses: []tcpip.Address{addr}} + }, + }, + { + name: "V2", + v1Compatibility: false, + checkFields: func(addr tcpip.Address) checkFields { + return checkFields{sentV2Reports: []mockReportV2{{records: []mockReportV2Record{ + { + recordType: ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode, + groupAddress: addr, + }, + }}}} + }, + }, } - // The delayed report timer should have been cancelled since we did not send - // the initial report earlier. - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + clock := faketime.NewManualClock() + mgp := mockMulticastGroupProtocol{t: t} + mgp.init(ip.GenericMulticastProtocolOptions{ + Rand: rand.New(rand.NewSource(4)), + Clock: clock, + MaxUnsolicitedReportDelay: maxUnsolicitedReportDelay, + }, test.v1Compatibility) - // Mock being able to successfully send the report. - mgp.setQueuePackets(false) - mgp.sendQueuedReports() - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + // Joining should trigger a SendReport, but mgp should report that we did not + // send the packet. + mgp.setQueuePackets(true) + mgp.joinGroup(addr1) + if diff := mgp.check(test.checkFields(addr1)); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } - // The delayed report (sent after the initial report) should now be sent. - clock.Advance(maxUnsolicitedReportDelay) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + // The delayed report timer should have been cancelled since we did not send + // the initial report earlier. + clock.Advance(time.Hour) + if test.v1Compatibility { + // V1 query targetting an unjoined group should drop us into V1 + // compatibility mode without sending any packets, affecting tests. + mgp.handleQuery(addr3, 0) + } + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } - // Should not have anything else to send (we should be idle). - mgp.sendQueuedReports() - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + // Mock being able to successfully send the report. + mgp.setQueuePackets(false) + mgp.sendQueuedReports() + if diff := mgp.check(test.checkFields(addr1)); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } - // Receive a query but mock being unable to send reports again. - mgp.setQueuePackets(true) - mgp.handleQuery(addr1, time.Nanosecond) - clock.Advance(time.Nanosecond) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + // The delayed report (sent after the initial report) should now be sent. + clock.Advance(maxUnsolicitedReportDelay) + if diff := mgp.check(test.checkFields(addr1)); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } - // Mock being able to send reports again - we should have a packet queued to - // send. - mgp.setQueuePackets(false) - mgp.sendQueuedReports() - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + // Should not have anything else to send (we should be idle). + mgp.sendQueuedReports() + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } - // Should not have anything else to send. - mgp.sendQueuedReports() - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + // Receive a query but mock being unable to send reports again. + mgp.setQueuePackets(true) + mgp.handleQuery(addr1, time.Nanosecond) + clock.Advance(time.Nanosecond) + if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } - // Receive a query again, but mock being unable to send reports. - mgp.setQueuePackets(true) - mgp.handleQuery(addr1, time.Nanosecond) - clock.Advance(time.Nanosecond) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + // Mock being able to send reports again - we should have a packet queued to + // send. + mgp.setQueuePackets(false) + mgp.sendQueuedReports() + if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } - // Receiving a report should should transition us into the idle member state, - // even if we had a packet queued. We should no longer have any packets to - // send. - mgp.handleReport(addr1) - mgp.sendQueuedReports() - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } + // Should not have anything else to send. + mgp.sendQueuedReports() + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } - // When we fail to send the initial set of reports, incoming reports should - // prevent a newly joined group's reports from being sent. - mgp.setQueuePackets(true) - mgp.joinGroup(addr2) - if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr2}}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - mgp.handleReport(addr2) - // Attempting to send queued reports while still unable to send reports should - // not change the host state. - mgp.sendQueuedReports() - if diff := mgp.check(checkFields{}); diff != "" { - t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) - } - // Should not have any packets queued. - mgp.setQueuePackets(false) - mgp.sendQueuedReports() - clock.Advance(time.Hour) - if diff := mgp.check(checkFields{}); diff != "" { - t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + // Receive a query again, but mock being unable to send reports. + mgp.setQueuePackets(true) + mgp.handleQuery(addr1, time.Nanosecond) + clock.Advance(time.Nanosecond) + if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr1}}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Receiving a report should should transition us into the idle member state, + // even if we had a packet queued. We should no longer have any packets to + // send. + mgp.handleReport(addr1) + mgp.sendQueuedReports() + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // When we fail to send the initial set of reports, incoming reports should + // prevent a newly joined group's reports from being sent. + mgp.setQueuePackets(true) + mgp.joinGroup(addr2) + if diff := mgp.check(checkFields{sendReportGroupAddresses: []tcpip.Address{addr2}}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + mgp.handleReport(addr2) + // Attempting to send queued reports while still unable to send reports should + // not change the host state. + mgp.sendQueuedReports() + if diff := mgp.check(checkFields{}); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + // Should not have any packets queued. + mgp.setQueuePackets(false) + mgp.sendQueuedReports() + clock.Advance(time.Hour) + if diff := mgp.check(checkFields{}); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + }) } } diff --git a/pkg/tcpip/network/internal/testutil/BUILD b/pkg/tcpip/network/internal/testutil/BUILD index 553159ced..9bb6d698c 100644 --- a/pkg/tcpip/network/internal/testutil/BUILD +++ b/pkg/tcpip/network/internal/testutil/BUILD @@ -6,6 +6,7 @@ go_library( name = "testutil", srcs = ["testutil.go"], visibility = [ + "//pkg/tcpip/network:__pkg__", "//pkg/tcpip/network/arp:__pkg__", "//pkg/tcpip/network/internal/fragmentation:__pkg__", "//pkg/tcpip/network/ipv4:__pkg__", diff --git a/pkg/tcpip/network/internal/testutil/testutil.go b/pkg/tcpip/network/internal/testutil/testutil.go index d99901b5f..330b290f9 100644 --- a/pkg/tcpip/network/internal/testutil/testutil.go +++ b/pkg/tcpip/network/internal/testutil/testutil.go @@ -19,6 +19,7 @@ package testutil import ( "fmt" "math/rand" + "testing" "gvisor.dev/gvisor/pkg/bufferv2" "gvisor.dev/gvisor/pkg/tcpip" @@ -124,3 +125,61 @@ func MakeRandPkt(transportHeaderLength int, extraHeaderReserveLength int, viewSi } return pkt } + +func checkIGMPStats(t *testing.T, s *stack.Stack, reports, leaves, reportsV2 uint64) { + t.Helper() + + if got := s.Stats().IGMP.PacketsSent.V2MembershipReport.Value(); got != reports { + t.Errorf("got s.Stats().IGMP.PacketsSent.V2MembershipReport.Value() = %d, want = %d", got, reports) + } + if got := s.Stats().IGMP.PacketsSent.V3MembershipReport.Value(); got != reportsV2 { + t.Errorf("got s.Stats().IGMP.PacketsSent.V3MembershipReport.Value() = %d, want = %d", got, reportsV2) + } + if got := s.Stats().IGMP.PacketsSent.LeaveGroup.Value(); got != leaves { + t.Errorf("got s.Stats().IGMP.PacketsSent.LeaveGroup.Value() = %d, want = %d", got, leaves) + } +} + +// CheckIGMPv2Stats checks IGMPv2 stats. +func CheckIGMPv2Stats(t *testing.T, s *stack.Stack, reports, leaves, reportsV2 uint64) { + t.Helper() + // We still check V3 stats in V2 compatibility tests because the test may send + // V3 reports before we drop into compatibility mode. + checkIGMPStats(t, s, reports, leaves, reportsV2) +} + +// CheckIGMPv3Stats checks IGMPv3 stats. +func CheckIGMPv3Stats(t *testing.T, s *stack.Stack, reports, leaves, reportsV2 uint64) { + t.Helper() + // In IGMPv3 tests, reports/leaves are just IGMPv3 reports. + checkIGMPStats(t, s, 0 /* reports */, 0 /* leaves */, reports+leaves+reportsV2) +} + +func checkMLDStats(t *testing.T, s *stack.Stack, reports, leaves, reportsV2 uint64) { + t.Helper() + + if got := s.Stats().ICMP.V6.PacketsSent.MulticastListenerReport.Value(); got != reports { + t.Errorf("got s.Stats().ICMP.V6.PacketsSent.MulticastListenerReport.Value() = %d, want = %d", got, reports) + } + if got := s.Stats().ICMP.V6.PacketsSent.MulticastListenerReportV2.Value(); got != reportsV2 { + t.Errorf("got s.Stats().ICMP.V6.PacketsSent.MulticastListenerReportV2.Value() = %d, want = %d", got, reportsV2) + } + if got := s.Stats().ICMP.V6.PacketsSent.MulticastListenerDone.Value(); got != leaves { + t.Errorf("got s.Stats().ICMP.V6.PacketsSent.MulticastListenerDone.Value() = %d, want = %d", got, leaves) + } +} + +// CheckMLDv1Stats checks MLDv1 stats. +func CheckMLDv1Stats(t *testing.T, s *stack.Stack, reports, leaves, reportsV2 uint64) { + t.Helper() + // We still check V2 stats in V1 compatibility tests because the test may send + // V2 reports before we drop into compatibility mode. + checkMLDStats(t, s, reports, leaves, reportsV2) +} + +// CheckMLDv2Stats checks MLDv2 stats. +func CheckMLDv2Stats(t *testing.T, s *stack.Stack, reports, leaves, reportsV2 uint64) { + t.Helper() + // In MLDv2 tests, reports/leaves are just MLDv2 reports. + checkMLDStats(t, s, 0 /* reports */, 0 /* leaves */, reports+leaves+reportsV2) +} diff --git a/pkg/tcpip/network/ipv4/igmp.go b/pkg/tcpip/network/ipv4/igmp.go index bf338ea5e..ab31a8d67 100644 --- a/pkg/tcpip/network/ipv4/igmp.go +++ b/pkg/tcpip/network/ipv4/igmp.go @@ -16,6 +16,7 @@ package ipv4 import ( "fmt" + "math" "time" "gvisor.dev/gvisor/pkg/atomicbitops" @@ -136,6 +137,104 @@ func (igmp *igmpState) ShouldPerformProtocol(groupAddress tcpip.Address) bool { return groupAddress != header.IPv4AllSystems } +type igmpv3ReportBuilder struct { + igmp *igmpState + + records []header.IGMPv3ReportGroupAddressRecordSerializer +} + +// AddRecord implements ip.MulticastGroupProtocolV2ReportBuilder. +func (b *igmpv3ReportBuilder) AddRecord(genericRecordType ip.MulticastGroupProtocolV2ReportRecordType, groupAddress tcpip.Address) { + var recordType header.IGMPv3ReportRecordType + switch genericRecordType { + case ip.MulticastGroupProtocolV2ReportRecordModeIsInclude: + recordType = header.IGMPv3ReportRecordModeIsInclude + case ip.MulticastGroupProtocolV2ReportRecordModeIsExclude: + recordType = header.IGMPv3ReportRecordModeIsExclude + case ip.MulticastGroupProtocolV2ReportRecordChangeToIncludeMode: + recordType = header.IGMPv3ReportRecordChangeToIncludeMode + case ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode: + recordType = header.IGMPv3ReportRecordChangeToExcludeMode + case ip.MulticastGroupProtocolV2ReportRecordAllowNewSources: + recordType = header.IGMPv3ReportRecordAllowNewSources + case ip.MulticastGroupProtocolV2ReportRecordBlockOldSources: + recordType = header.IGMPv3ReportRecordBlockOldSources + default: + panic(fmt.Sprintf("unrecognied genericRecordType = %d", genericRecordType)) + } + + b.records = append(b.records, header.IGMPv3ReportGroupAddressRecordSerializer{ + RecordType: recordType, + GroupAddress: groupAddress, + Sources: nil, + }) +} + +// Send implements ip.MulticastGroupProtocolV2ReportBuilder. +// +// +checklocksread:b.igmp.ep.mu +func (b *igmpv3ReportBuilder) Send() (sent bool, err tcpip.Error) { + options := header.IPv4OptionsSerializer{ + &header.IPv4SerializableRouterAlertOption{}, + } + mtu := int(b.igmp.ep.MTU()) - int(options.Length()) + + allSentWithSpecifiedAddress := true + var firstErr tcpip.Error + for records := b.records; len(records) != 0; { + spaceLeft := mtu + maxRecords := 0 + + for ; maxRecords < len(records); maxRecords++ { + tmp := spaceLeft - records[maxRecords].Length() + if tmp > 0 { + spaceLeft = tmp + } else { + break + } + } + + serializer := header.IGMPv3ReportSerializer{Records: records[:maxRecords]} + records = records[maxRecords:] + + icmpView := bufferv2.NewViewSize(serializer.Length()) + serializer.SerializeInto(icmpView.AsSlice()) + if sentWithSpecifiedAddress, err := b.igmp.writePacketInner( + icmpView, + b.igmp.ep.stats.igmp.packetsSent.v3MembershipReport, + options, + header.IGMPv3RoutersAddress, + ); err != nil { + if firstErr != nil { + firstErr = nil + } + allSentWithSpecifiedAddress = false + } else if !sentWithSpecifiedAddress { + allSentWithSpecifiedAddress = false + } + } + + return allSentWithSpecifiedAddress, firstErr +} + +// NewReportV2Builder implements ip.MulticastGroupProtocol. +func (igmp *igmpState) NewReportV2Builder() ip.MulticastGroupProtocolV2ReportBuilder { + return &igmpv3ReportBuilder{igmp: igmp} +} + +// V2QueryMaxRespCodeToV2Delay implements ip.MulticastGroupProtocol. +func (*igmpState) V2QueryMaxRespCodeToV2Delay(code uint16) time.Duration { + if code > math.MaxUint8 { + panic(fmt.Sprintf("got IGMPv3 MaxRespCode = %d, want <= %d", code, math.MaxUint8)) + } + return header.IGMPv3MaximumResponseDelay(uint8(code)) +} + +// V2QueryMaxRespCodeToV1Delay implements ip.MulticastGroupProtocol. +func (*igmpState) V2QueryMaxRespCodeToV1Delay(code uint16) time.Duration { + return time.Duration(code) * time.Millisecond +} + // init sets up an igmpState struct, and is required to be called before using // a new igmpState. // @@ -206,12 +305,16 @@ func (igmp *igmpState) isPacketValidLocked(pkt stack.PacketBufferPtr, messageTyp // +checklocks:igmp.ep.mu func (igmp *igmpState) handleIGMP(pkt stack.PacketBufferPtr, hasRouterAlertOption bool) { received := igmp.ep.stats.igmp.packetsReceived - hdr, ok := pkt.Data().PullUp(header.IGMPMinimumSize) + hdr, ok := pkt.Data().PullUp(pkt.Data().Size()) if !ok { received.invalid.Increment() return } h := header.IGMP(hdr) + if len(h) < header.IGMPMinimumSize { + received.invalid.Increment() + return + } // As per RFC 1071 section 1.3, // @@ -231,7 +334,14 @@ func (igmp *igmpState) handleIGMP(pkt stack.PacketBufferPtr, hasRouterAlertOptio switch h.Type() { case header.IGMPMembershipQuery: received.membershipQuery.Increment() - if !isValid(header.IGMPQueryMinimumSize) { + if len(h) >= header.IGMPv3QueryMinimumSize { + if isValid(header.IGMPv3QueryMinimumSize) { + igmp.handleMembershipQueryV3(header.IGMPv3Query(h)) + } else { + received.invalid.Increment() + } + return + } else if !isValid(header.IGMPQueryMinimumSize) { received.invalid.Increment() return } @@ -301,6 +411,24 @@ func (igmp *igmpState) handleMembershipQuery(groupAddress tcpip.Address, maxResp igmp.genericMulticastProtocol.HandleQueryLocked(groupAddress, maxRespTime) } +// handleMembershipQueryV3 handles a membership query. +// +// +checklocks:igmp.ep.mu +func (igmp *igmpState) handleMembershipQueryV3(igmpHdr header.IGMPv3Query) { + sources, ok := igmpHdr.Sources() + if !ok { + return + } + + igmp.genericMulticastProtocol.HandleQueryV2Locked( + igmpHdr.GroupAddress(), + uint16(igmpHdr.MaximumResponseCode()), + sources, + igmpHdr.QuerierRobustnessVariable(), + igmpHdr.QuerierQueryInterval(), + ) +} + // handleMembershipReport handles a membership report. // // +checklocks:igmp.ep.mu @@ -318,9 +446,34 @@ func (igmp *igmpState) writePacket(destAddress tcpip.Address, groupAddress tcpip igmpData.SetGroupAddress(groupAddress) igmpData.SetChecksum(header.IGMPCalculateChecksum(igmpData)) + var reportType tcpip.MultiCounterStat + sentStats := igmp.ep.stats.igmp.packetsSent + switch igmpType { + case header.IGMPv1MembershipReport: + reportType = sentStats.v1MembershipReport + case header.IGMPv2MembershipReport: + reportType = sentStats.v2MembershipReport + case header.IGMPLeaveGroup: + reportType = sentStats.leaveGroup + default: + panic(fmt.Sprintf("unrecognized igmp type = %d", igmpType)) + } + + return igmp.writePacketInner( + igmpView, + reportType, + header.IPv4OptionsSerializer{ + &header.IPv4SerializableRouterAlertOption{}, + }, + destAddress, + ) +} + +// +checklocksread:igmp.ep.mu +func (igmp *igmpState) writePacketInner(buf *bufferv2.View, reportStat tcpip.MultiCounterStat, options header.IPv4OptionsSerializer, destAddress tcpip.Address) (bool, tcpip.Error) { pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ ReserveHeaderBytes: int(igmp.ep.MaxHeaderLength()), - Payload: bufferv2.MakeWithView(igmpView), + Payload: bufferv2.MakeWithView(buf), }) defer pkt.DecRef() @@ -335,9 +488,7 @@ func (igmp *igmpState) writePacket(destAddress tcpip.Address, groupAddress tcpip Protocol: header.IGMPProtocolNumber, TTL: header.IGMPTTL, TOS: stack.DefaultTOS, - }, header.IPv4OptionsSerializer{ - &header.IPv4SerializableRouterAlertOption{}, - }); err != nil { + }, options); err != nil { panic(fmt.Sprintf("failed to add IP header: %s", err)) } @@ -346,16 +497,7 @@ func (igmp *igmpState) writePacket(destAddress tcpip.Address, groupAddress tcpip sentStats.dropped.Increment() return false, err } - switch igmpType { - case header.IGMPv1MembershipReport: - sentStats.v1MembershipReport.Increment() - case header.IGMPv2MembershipReport: - sentStats.v2MembershipReport.Increment() - case header.IGMPLeaveGroup: - sentStats.leaveGroup.Increment() - default: - panic(fmt.Sprintf("unrecognized igmp type = %d", igmpType)) - } + reportStat.Increment() return true, nil } diff --git a/pkg/tcpip/network/ipv4/igmp_test.go b/pkg/tcpip/network/ipv4/igmp_test.go index 09f7f9418..372985b69 100644 --- a/pkg/tcpip/network/ipv4/igmp_test.go +++ b/pkg/tcpip/network/ipv4/igmp_test.go @@ -25,6 +25,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/faketime" "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/link/channel" + iptestutil "gvisor.dev/gvisor/pkg/tcpip/network/internal/testutil" "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" "gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/testutil" @@ -38,9 +39,10 @@ const ( ) var ( - stackAddr = testutil.MustParse4("10.0.0.1") - remoteAddr = testutil.MustParse4("10.0.0.2") - multicastAddr = testutil.MustParse4("224.0.0.3") + stackAddr = testutil.MustParse4("10.0.0.1") + remoteAddr = testutil.MustParse4("10.0.0.2") + multicastAddr = testutil.MustParse4("224.0.0.3") + unusedMulticastAddr = testutil.MustParse4("224.0.0.4") ) // validateIgmpPacket checks that a passed packet is an IPv4 IGMP packet sent @@ -59,12 +61,35 @@ func validateIgmpPacket(t *testing.T, pkt stack.PacketBufferPtr, igmpType header checker.IPv4RouterAlert(), checker.IGMP( checker.IGMPType(igmpType), - checker.IGMPMaxRespTime(header.DecisecondToDuration(maxRespTime)), + checker.IGMPMaxRespTime(header.DecisecondToDuration(uint16(maxRespTime))), checker.IGMPGroupAddress(groupAddress), ), ) } +func validateIgmpv3ReportPacket(t *testing.T, pkt stack.PacketBufferPtr, srcAddr, groupAddress tcpip.Address) { + t.Helper() + + payload := stack.PayloadSince(pkt.NetworkHeader()) + defer payload.Release() + checker.IPv4(t, payload, + checker.SrcAddr(srcAddr), + checker.DstAddr(header.IGMPv3RoutersAddress), + // TTL for an IGMP message must be 1 as per RFC 2236 section 2. + checker.TTL(1), + checker.IPv4RouterAlert(), + checker.IGMPv3Report(header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToExcludeMode, + GroupAddress: groupAddress, + Sources: nil, + }, + }, + }), + ) +} + type igmpTestContext struct { s *stack.Stack ep *channel.Endpoint @@ -157,17 +182,17 @@ func TestIGMPV1Present(t *testing.T) { t.Fatalf("JoinGroup(ipv4, nic, %s) = %s", multicastAddr, err) } - // This NIC will send an IGMPv2 report immediately, before this test can get + // This NIC will send an IGMPv3 report immediately, before this test can get // the IGMPv1 General Membership Query in. { p := e.Read() if p.IsNil() { - t.Fatal("unable to Read IGMP packet, expected V2MembershipReport") + t.Fatal("unable to Read IGMP packet, expected V3MembershipReport") } - if got := s.Stats().IGMP.PacketsSent.V2MembershipReport.Value(); got != 1 { - t.Fatalf("got V2MembershipReport messages sent = %d, want = 1", got) + if got := s.Stats().IGMP.PacketsSent.V3MembershipReport.Value(); got != 1 { + t.Fatalf("got V3MembershipReport messages sent = %d, want = 1", got) } - validateIgmpPacket(t, p, header.IGMPv2MembershipReport, 0, stackAddr, multicastAddr, multicastAddr) + validateIgmpv3ReportPacket(t, p, stackAddr, multicastAddr) p.DecRef() } if t.Failed() { @@ -176,9 +201,8 @@ func TestIGMPV1Present(t *testing.T) { // Inject an IGMPv1 General Membership Query which is identical to a standard // membership query except the Max Response Time is set to 0, which will tell - // the stack that this is a router using IGMPv1. Send it to the all systems - // group which is the only group this host belongs to. - createAndInjectIGMPPacket(e, header.IGMPMembershipQuery, 0, defaultTTL, remoteAddr, stackAddr, header.IPv4AllSystems, true /* hasRouterAlertOption */) + // the stack that this is a router using IGMPv1. + createAndInjectIGMPPacket(e, header.IGMPMembershipQuery, 0, defaultTTL, remoteAddr, stackAddr, multicastAddr, true /* hasRouterAlertOption */) if got := s.Stats().IGMP.PacketsReceived.MembershipQuery.Value(); got != 1 { t.Fatalf("got Membership Queries received = %d, want = 1", got) } @@ -219,78 +243,129 @@ func TestIGMPV1Present(t *testing.T) { if p.IsNil() { t.Fatal("unable to Read IGMP packet, expected V2MembershipReport") } - if got := s.Stats().IGMP.PacketsSent.V2MembershipReport.Value(); got != 2 { - t.Fatalf("got V2MembershipReport messages sent = %d, want = 2", got) + if got := s.Stats().IGMP.PacketsSent.V3MembershipReport.Value(); got != 2 { + t.Fatalf("got V3MembershipReport messages sent = %d, want = 2", got) } - validateIgmpPacket(t, p, header.IGMPv2MembershipReport, 0, stackAddr, multicastAddr, multicastAddr) + validateIgmpv3ReportPacket(t, p, stackAddr, multicastAddr) p.DecRef() } } func TestSendQueuedIGMPReports(t *testing.T) { - ctx := newIGMPTestContext(t, true /* igmpEnabled */) - defer ctx.cleanup() - s := ctx.s - e := ctx.ep - clock := ctx.clock + tests := []struct { + name string + v2Compatibility bool + validate func(t *testing.T, pkt stack.PacketBufferPtr, localAddress tcpip.Address, groupAddress tcpip.Address) + checkStats func(*testing.T, *stack.Stack, uint64, uint64, uint64) + }{ + { + name: "V2 Compatibility", + v2Compatibility: true, + validate: func(t *testing.T, pkt stack.PacketBufferPtr, localAddress tcpip.Address, groupAddress tcpip.Address) { + t.Helper() - // Joining a group without an assigned address should queue IGMP packets; none - // should be sent without an assigned address. - if err := s.JoinGroup(ipv4.ProtocolNumber, nicID, multicastAddr); err != nil { - t.Fatalf("JoinGroup(%d, %d, %s): %s", ipv4.ProtocolNumber, nicID, multicastAddr, err) - } - reportStat := s.Stats().IGMP.PacketsSent.V2MembershipReport - if got := reportStat.Value(); got != 0 { - t.Errorf("got reportStat.Value() = %d, want = 0", got) - } - clock.Advance(time.Hour) - if p := e.Read(); !p.IsNil() { - t.Fatalf("got unexpected packet = %#v", p) - } - - // The initial set of IGMP reports that were queued should be sent once an - // address is assigned. - protocolAddr := tcpip.ProtocolAddress{ - Protocol: ipv4.ProtocolNumber, - AddressWithPrefix: tcpip.AddressWithPrefix{ - Address: stackAddr, - PrefixLen: defaultPrefixLength, + validateIgmpPacket(t, pkt, header.IGMPv2MembershipReport, 0 /* maxRespTime */, localAddress, groupAddress, groupAddress) + }, + checkStats: iptestutil.CheckIGMPv2Stats, + }, + { + name: "V3", + v2Compatibility: false, + validate: validateIgmpv3ReportPacket, + checkStats: iptestutil.CheckIGMPv3Stats, }, } - if err := s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil { - t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, protocolAddr, err) - } - if got := reportStat.Value(); got != 1 { - t.Errorf("got reportStat.Value() = %d, want = 1", got) - } - if p := e.Read(); p.IsNil() { - t.Error("expected to send an IGMP membership report") - } else { - validateIgmpPacket(t, p, header.IGMPv2MembershipReport, 0, stackAddr, multicastAddr, multicastAddr) - p.DecRef() - } - if t.Failed() { - t.FailNow() - } - clock.Advance(ipv4.UnsolicitedReportIntervalMax) - if got := reportStat.Value(); got != 2 { - t.Errorf("got reportStat.Value() = %d, want = 2", got) - } - if p := e.Read(); p.IsNil() { - t.Error("expected to send an IGMP membership report") - } else { - validateIgmpPacket(t, p, header.IGMPv2MembershipReport, 0, stackAddr, multicastAddr, multicastAddr) - p.DecRef() - } - if t.Failed() { - t.FailNow() - } - // Should have no more packets to send after the initial set of unsolicited - // reports. - clock.Advance(time.Hour) - if p := e.Read(); !p.IsNil() { - t.Fatalf("got unexpected packet = %#v", p) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := newIGMPTestContext(t, true /* igmpEnabled */) + defer ctx.cleanup() + s := ctx.s + e := ctx.ep + clock := ctx.clock + + checkVersion := func() { + if test.v2Compatibility { + createAndInjectIGMPPacket( + e, + header.IGMPMembershipQuery, + 1, /* maxRespTime */ + header.IGMPTTL, + remoteAddr, + header.IPv4AllSystems, + unusedMulticastAddr, + true, /* hasRouterAlertOption */ + ) + } + } + protocolAddr := tcpip.ProtocolAddress{ + Protocol: ipv4.ProtocolNumber, + AddressWithPrefix: tcpip.AddressWithPrefix{ + Address: stackAddr, + PrefixLen: defaultPrefixLength, + }, + } + // Multicast traffic is not accepted unless we have an address so add an + // address and check the version which receives a multicast packet. + if err := s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil { + t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, protocolAddr, err) + } + checkVersion() + if err := s.RemoveAddress(nicID, protocolAddr.AddressWithPrefix.Address); err != nil { + t.Fatalf("RemoveAddress(%d, %s): %s", nicID, protocolAddr.AddressWithPrefix.Address, err) + } + + var reportCounter uint64 + var doneCounter uint64 + var reportV2Counter uint64 + test.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) + + // Joining a group without an assigned address should queue IGMP packets; none + // should be sent without an assigned address. + if err := s.JoinGroup(ipv4.ProtocolNumber, nicID, multicastAddr); err != nil { + t.Fatalf("JoinGroup(%d, %d, %s): %s", ipv4.ProtocolNumber, nicID, multicastAddr, err) + } + test.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) + if p := e.Read(); !p.IsNil() { + t.Fatalf("got unexpected packet = %#v", p) + } + + // The initial set of IGMP reports that were queued should be sent once an + // address is assigned. + if err := s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil { + t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, protocolAddr, err) + } + reportCounter++ + test.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Error("expected to send an IGMP membership report") + } else { + test.validate(t, p, stackAddr, multicastAddr) + p.DecRef() + } + if t.Failed() { + t.FailNow() + } + clock.Advance(ipv4.UnsolicitedReportIntervalMax) + reportCounter++ + test.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Error("expected to send an IGMP membership report") + } else { + test.validate(t, p, stackAddr, multicastAddr) + p.DecRef() + } + if t.Failed() { + t.FailNow() + } + + // Should have no more packets to send after the initial set of unsolicited + // reports. + clock.Advance(time.Hour) + if p := e.Read(); !p.IsNil() { + t.Fatalf("got unexpected packet = %#v", p) + } + }) } } diff --git a/pkg/tcpip/network/ipv4/stats.go b/pkg/tcpip/network/ipv4/stats.go index 5798cfec6..9ebef99e7 100644 --- a/pkg/tcpip/network/ipv4/stats.go +++ b/pkg/tcpip/network/ipv4/stats.go @@ -131,6 +131,7 @@ type multiCounterIGMPPacketStats struct { membershipQuery tcpip.MultiCounterStat v1MembershipReport tcpip.MultiCounterStat v2MembershipReport tcpip.MultiCounterStat + v3MembershipReport tcpip.MultiCounterStat leaveGroup tcpip.MultiCounterStat } @@ -138,6 +139,7 @@ func (m *multiCounterIGMPPacketStats) init(a, b *tcpip.IGMPPacketStats) { m.membershipQuery.Init(a.MembershipQuery, b.MembershipQuery) m.v1MembershipReport.Init(a.V1MembershipReport, b.V1MembershipReport) m.v2MembershipReport.Init(a.V2MembershipReport, b.V2MembershipReport) + m.v3MembershipReport.Init(a.V3MembershipReport, b.V3MembershipReport) m.leaveGroup.Init(a.LeaveGroup, b.LeaveGroup) } diff --git a/pkg/tcpip/network/ipv6/BUILD b/pkg/tcpip/network/ipv6/BUILD index 1be82fd2f..d7ade924d 100644 --- a/pkg/tcpip/network/ipv6/BUILD +++ b/pkg/tcpip/network/ipv6/BUILD @@ -75,6 +75,7 @@ go_test( "//pkg/tcpip/faketime", "//pkg/tcpip/header", "//pkg/tcpip/link/channel", + "//pkg/tcpip/network/internal/testutil", "//pkg/tcpip/stack", "//pkg/tcpip/testutil", ], diff --git a/pkg/tcpip/network/ipv6/icmp.go b/pkg/tcpip/network/ipv6/icmp.go index d629d7696..4ad8d2cbe 100644 --- a/pkg/tcpip/network/ipv6/icmp.go +++ b/pkg/tcpip/network/ipv6/icmp.go @@ -852,12 +852,18 @@ func (e *endpoint) handleICMP(pkt stack.PacketBufferPtr, hasFragmentHeader bool, return } - case header.ICMPv6MulticastListenerQuery, header.ICMPv6MulticastListenerReport, header.ICMPv6MulticastListenerDone: + case header.ICMPv6MulticastListenerQuery, + header.ICMPv6MulticastListenerReport, + header.ICMPv6MulticastListenerV2Report, + header.ICMPv6MulticastListenerDone: + icmpBody := h.MessageBody() switch icmpType { case header.ICMPv6MulticastListenerQuery: received.multicastListenerQuery.Increment() case header.ICMPv6MulticastListenerReport: received.multicastListenerReport.Increment() + case header.ICMPv6MulticastListenerV2Report: + received.multicastListenerReportV2.Increment() case header.ICMPv6MulticastListenerDone: received.multicastListenerDone.Increment() default: @@ -872,13 +878,17 @@ func (e *endpoint) handleICMP(pkt stack.PacketBufferPtr, hasFragmentHeader bool, switch icmpType { case header.ICMPv6MulticastListenerQuery: e.mu.Lock() - e.mu.mld.handleMulticastListenerQuery(header.MLD(h.MessageBody())) + if len(icmpBody) >= header.MLDv2QueryMinimumSize { + e.mu.mld.handleMulticastListenerQueryV2(header.MLDv2Query(icmpBody)) + } else { + e.mu.mld.handleMulticastListenerQuery(header.MLD(icmpBody)) + } e.mu.Unlock() case header.ICMPv6MulticastListenerReport: e.mu.Lock() - e.mu.mld.handleMulticastListenerReport(header.MLD(h.MessageBody())) + e.mu.mld.handleMulticastListenerReport(header.MLD(icmpBody)) e.mu.Unlock() - case header.ICMPv6MulticastListenerDone: + case header.ICMPv6MulticastListenerDone, header.ICMPv6MulticastListenerV2Report: default: panic(fmt.Sprintf("unrecognized MLD message = %d", icmpType)) } diff --git a/pkg/tcpip/network/ipv6/icmp_test.go b/pkg/tcpip/network/ipv6/icmp_test.go index 2b793aa17..815433ad0 100644 --- a/pkg/tcpip/network/ipv6/icmp_test.go +++ b/pkg/tcpip/network/ipv6/icmp_test.go @@ -348,6 +348,12 @@ func TestICMPCounts(t *testing.T) { includeRouterAlert: true, size: header.MLDMinimumSize + header.ICMPv6HeaderSize, }, + { + typ: header.ICMPv6MulticastListenerV2Report, + hopLimit: header.MLDHopLimit, + includeRouterAlert: true, + size: header.MLDv2ReportMinimumSize + header.ICMPv6HeaderSize, + }, { typ: header.ICMPv6MulticastListenerDone, hopLimit: header.MLDHopLimit, diff --git a/pkg/tcpip/network/ipv6/mld.go b/pkg/tcpip/network/ipv6/mld.go index a1e95c69f..c4645edfb 100644 --- a/pkg/tcpip/network/ipv6/mld.go +++ b/pkg/tcpip/network/ipv6/mld.go @@ -99,6 +99,103 @@ func (mld *mldState) ShouldPerformProtocol(groupAddress tcpip.Address) bool { return scope != header.IPv6Reserved0MulticastScope && scope != header.IPv6InterfaceLocalMulticastScope } +type mldv2ReportBuilder struct { + mld *mldState + + records []header.MLDv2ReportMulticastAddressRecordSerializer +} + +// AddRecord implements ip.MulticastGroupProtocolV2ReportBuilder. +func (b *mldv2ReportBuilder) AddRecord(genericRecordType ip.MulticastGroupProtocolV2ReportRecordType, groupAddress tcpip.Address) { + var recordType header.MLDv2ReportRecordType + switch genericRecordType { + case ip.MulticastGroupProtocolV2ReportRecordModeIsInclude: + recordType = header.MLDv2ReportRecordModeIsInclude + case ip.MulticastGroupProtocolV2ReportRecordModeIsExclude: + recordType = header.MLDv2ReportRecordModeIsExclude + case ip.MulticastGroupProtocolV2ReportRecordChangeToIncludeMode: + recordType = header.MLDv2ReportRecordChangeToIncludeMode + case ip.MulticastGroupProtocolV2ReportRecordChangeToExcludeMode: + recordType = header.MLDv2ReportRecordChangeToExcludeMode + case ip.MulticastGroupProtocolV2ReportRecordAllowNewSources: + recordType = header.MLDv2ReportRecordAllowNewSources + case ip.MulticastGroupProtocolV2ReportRecordBlockOldSources: + recordType = header.MLDv2ReportRecordBlockOldSources + default: + panic(fmt.Sprintf("unrecognied genericRecordType = %d", genericRecordType)) + } + + b.records = append(b.records, header.MLDv2ReportMulticastAddressRecordSerializer{ + RecordType: recordType, + MulticastAddress: groupAddress, + Sources: nil, + }) +} + +// Send implements ip.MulticastGroupProtocolV2ReportBuilder. +func (b *mldv2ReportBuilder) Send() (sent bool, err tcpip.Error) { + extensionHeaders := header.IPv6ExtHdrSerializer{ + header.IPv6SerializableHopByHopExtHdr{ + &header.IPv6RouterAlertOption{Value: header.IPv6RouterAlertMLD}, + }, + } + mtu := int(b.mld.ep.MTU()) - extensionHeaders.Length() + + allSentWithSpecifiedAddress := true + var firstErr tcpip.Error + for records := b.records; len(records) != 0; { + spaceLeft := mtu + maxRecords := 0 + + for ; maxRecords < len(records); maxRecords++ { + tmp := spaceLeft - records[maxRecords].Length() + if tmp > 0 { + spaceLeft = tmp + } else { + break + } + } + + serializer := header.MLDv2ReportSerializer{Records: records[:maxRecords]} + records = records[maxRecords:] + + icmpView := bufferv2.NewViewSize(header.ICMPv6HeaderSize + serializer.Length()) + icmp := header.ICMPv6(icmpView.AsSlice()) + serializer.SerializeInto(icmp.MessageBody()) + if sentWithSpecifiedAddress, err := b.mld.writePacketInner( + icmpView, + header.ICMPv6MulticastListenerV2Report, + b.mld.ep.stats.icmp.packetsSent.multicastListenerReportV2, + extensionHeaders, + header.MLDv2RoutersAddress, + ); err != nil { + if firstErr != nil { + firstErr = nil + } + allSentWithSpecifiedAddress = false + } else if !sentWithSpecifiedAddress { + allSentWithSpecifiedAddress = false + } + } + + return allSentWithSpecifiedAddress, firstErr +} + +// NewReportV2Builder implements ip.MulticastGroupProtocol. +func (mld *mldState) NewReportV2Builder() ip.MulticastGroupProtocolV2ReportBuilder { + return &mldv2ReportBuilder{mld: mld} +} + +// V2QueryMaxRespCodeToV2Delay implements ip.MulticastGroupProtocol. +func (*mldState) V2QueryMaxRespCodeToV2Delay(code uint16) time.Duration { + return header.MLDv2MaximumResponseDelay(code) +} + +// V2QueryMaxRespCodeToV1Delay implements ip.MulticastGroupProtocol. +func (*mldState) V2QueryMaxRespCodeToV1Delay(code uint16) time.Duration { + return time.Duration(code) * time.Millisecond +} + // init sets up an mldState struct, and is required to be called before using // a new mldState. // @@ -120,6 +217,24 @@ func (mld *mldState) handleMulticastListenerQuery(mldHdr header.MLD) { mld.genericMulticastProtocol.HandleQueryLocked(mldHdr.MulticastAddress(), mldHdr.MaximumResponseDelay()) } +// handleMulticastListenerQueryV2 handles a V2 query message. +// +// Precondition: mld.ep.mu must be locked. +func (mld *mldState) handleMulticastListenerQueryV2(mldHdr header.MLDv2Query) { + sources, ok := mldHdr.Sources() + if !ok { + return + } + + mld.genericMulticastProtocol.HandleQueryV2Locked( + mldHdr.MulticastAddress(), + mldHdr.MaximumResponseCode(), + sources, + mldHdr.QuerierRobustnessVariable(), + mldHdr.QuerierQueryInterval(), + ) +} + // handleMulticastListenerReport handles a report message. // // Precondition: mld.ep.mu must be locked. @@ -199,8 +314,26 @@ func (mld *mldState) writePacket(destAddress, groupAddress tcpip.Address, mldTyp icmpView := bufferv2.NewViewSize(header.ICMPv6HeaderSize + header.MLDMinimumSize) icmp := header.ICMPv6(icmpView.AsSlice()) - icmp.SetType(mldType) header.MLD(icmp.MessageBody()).SetMulticastAddress(groupAddress) + extensionHeaders := header.IPv6ExtHdrSerializer{ + header.IPv6SerializableHopByHopExtHdr{ + &header.IPv6RouterAlertOption{Value: header.IPv6RouterAlertMLD}, + }, + } + + return mld.writePacketInner( + icmpView, + mldType, + mldStat, + extensionHeaders, + destAddress, + ) +} + +func (mld *mldState) writePacketInner(buf *bufferv2.View, mldType header.ICMPv6Type, reportStat tcpip.MultiCounterStat, extensionHeaders header.IPv6ExtHdrSerializer, destAddress tcpip.Address) (bool, tcpip.Error) { + icmp := header.ICMPv6(buf.AsSlice()) + icmp.SetType(mldType) + // As per RFC 2710 section 3, // // All MLD messages described in this document are sent with a link-local @@ -262,15 +395,9 @@ func (mld *mldState) writePacket(destAddress, groupAddress tcpip.Address, mldTyp Dst: destAddress, })) - extensionHeaders := header.IPv6ExtHdrSerializer{ - header.IPv6SerializableHopByHopExtHdr{ - &header.IPv6RouterAlertOption{Value: header.IPv6RouterAlertMLD}, - }, - } - pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ ReserveHeaderBytes: int(mld.ep.MaxHeaderLength()) + extensionHeaders.Length(), - Payload: bufferv2.MakeWithView(icmpView), + Payload: bufferv2.MakeWithView(buf), }) defer pkt.DecRef() @@ -281,9 +408,9 @@ func (mld *mldState) writePacket(destAddress, groupAddress tcpip.Address, mldTyp panic(fmt.Sprintf("failed to add IP header: %s", err)) } if err := mld.ep.nic.WritePacketToRemote(header.EthernetAddressFromMulticastIPv6Address(destAddress), pkt); err != nil { - sentStats.dropped.Increment() + mld.ep.stats.icmp.packetsSent.dropped.Increment() return false, err } - mldStat.Increment() + reportStat.Increment() return localAddress != header.IPv6Any, nil } diff --git a/pkg/tcpip/network/ipv6/mld_test.go b/pkg/tcpip/network/ipv6/mld_test.go index 9415f50f5..06ff35afe 100644 --- a/pkg/tcpip/network/ipv6/mld_test.go +++ b/pkg/tcpip/network/ipv6/mld_test.go @@ -28,6 +28,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/faketime" "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/link/channel" + iptestutil "gvisor.dev/gvisor/pkg/tcpip/network/internal/testutil" "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" "gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/testutil" @@ -37,6 +38,7 @@ var ( linkLocalAddr = testutil.MustParse6("fe80::1") globalAddr = testutil.MustParse6("a80::1") globalMulticastAddr = testutil.MustParse6("ff05:100::2") + unusedMulticastAddr = testutil.MustParse6("ff05:100::3") linkLocalAddrSNMC = header.SolicitedNodeAddr(linkLocalAddr) globalAddrSNMC = header.SolicitedNodeAddr(globalAddr) @@ -60,6 +62,21 @@ func validateMLDPacket(t *testing.T, v *bufferv2.View, localAddress, remoteAddre ) } +func validateMLDv2ReportPacket(t *testing.T, v *bufferv2.View, localAddress tcpip.Address, report header.MLDv2ReportSerializer) { + t.Helper() + + defer v.Release() + checker.IPv6WithExtHdr(t, v, + checker.IPv6ExtHdr( + checker.IPv6HopByHopExtensionHeader(checker.IPv6RouterAlert(header.IPv6RouterAlertMLD)), + ), + checker.SrcAddr(localAddress), + checker.DstAddr(header.MLDv2RoutersAddress), + checker.TTL(header.MLDHopLimit), + checker.MLDv2Report(report), + ) +} + type mldTestContext struct { s *stack.Stack } @@ -83,44 +100,105 @@ func newMLDTestContext() mldTestContext { func TestIPv6JoinLeaveSolicitedNodeAddressPerformsMLD(t *testing.T) { const nicID = 1 - c := newMLDTestContext() - defer c.cleanup() - s := c.s + tests := []struct { + name string + v1Compatibility bool + validate func(t *testing.T, v *bufferv2.View, localAddress tcpip.Address, groupAddress tcpip.Address, leave bool) + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + validate: func(t *testing.T, v *bufferv2.View, localAddress tcpip.Address, groupAddress tcpip.Address, leave bool) { + t.Helper() - e := channel.New(1, header.IPv6MinimumMTU, "") - defer e.Close() - if err := s.CreateNIC(nicID, e); err != nil { - t.Fatalf("CreateNIC(%d, _): %s", nicID, err) + remoteAddress := groupAddress + icmpType := header.ICMPv6MulticastListenerReport + if leave { + remoteAddress = header.IPv6AllRoutersLinkLocalMulticastAddress + icmpType = header.ICMPv6MulticastListenerDone + } + + validateMLDPacket(t, v, localAddress, remoteAddress, icmpType, groupAddress) + }, + }, + { + name: "V2", + v1Compatibility: false, + validate: func(t *testing.T, v *bufferv2.View, localAddress tcpip.Address, groupAddress tcpip.Address, leave bool) { + t.Helper() + + recordType := header.MLDv2ReportRecordChangeToExcludeMode + if leave { + recordType = header.MLDv2ReportRecordChangeToIncludeMode + } + + validateMLDv2ReportPacket(t, v, localAddress, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: recordType, + MulticastAddress: groupAddress, + Sources: nil, + }, + }, + }) + }, + }, } - // The stack will join an address's solicited node multicast address when - // an address is added. An MLD report message should be sent for the - // solicited-node group. - protocolAddr := tcpip.ProtocolAddress{ - Protocol: ipv6.ProtocolNumber, - AddressWithPrefix: linkLocalAddr.WithPrefix(), - } - if err := s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil { - t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, protocolAddr, err) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - validateMLDPacket(t, stack.PayloadSince(p.NetworkHeader()), linkLocalAddr, linkLocalAddrSNMC, header.ICMPv6MulticastListenerReport, linkLocalAddrSNMC) - p.DecRef() - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + c := newMLDTestContext() + defer c.cleanup() + s := c.s - // The stack will leave an address's solicited node multicast address when - // an address is removed. An MLD done message should be sent for the - // solicited-node group. - if err := s.RemoveAddress(nicID, linkLocalAddr); err != nil { - t.Fatalf("RemoveAddress(%d, %s) = %s", nicID, linkLocalAddr, err) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a done message to be sent") - } else { - validateMLDPacket(t, stack.PayloadSince(p.NetworkHeader()), header.IPv6Any, header.IPv6AllRoutersLinkLocalMulticastAddress, header.ICMPv6MulticastListenerDone, linkLocalAddrSNMC) - p.DecRef() + e := channel.New(1, header.IPv6MinimumMTU, "") + defer e.Close() + if err := s.CreateNIC(nicID, e); err != nil { + t.Fatalf("CreateNIC(%d, _): %s", nicID, err) + } + + if test.v1Compatibility { + createAndInjectMLDPacket( + e, + header.ICMPv6MulticastListenerQuery, + header.MLDHopLimit, + linkLocalAddr, + header.IPv6Any, + true, /* withRouterAlertOption */ + header.IPv6RouterAlertMLD, + ) + } + + // The stack will join an address's solicited node multicast address when + // an address is added. An MLD report message should be sent for the + // solicited-node group. + protocolAddr := tcpip.ProtocolAddress{ + Protocol: ipv6.ProtocolNumber, + AddressWithPrefix: linkLocalAddr.WithPrefix(), + } + if err := s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil { + t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, protocolAddr, err) + } + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + test.validate(t, stack.PayloadSince(p.NetworkHeader()), linkLocalAddr, linkLocalAddrSNMC, false /* leave */) + p.DecRef() + } + + // The stack will leave an address's solicited node multicast address when + // an address is removed. An MLD done message should be sent for the + // solicited-node group. + if err := s.RemoveAddress(nicID, linkLocalAddr); err != nil { + t.Fatalf("RemoveAddress(%d, %s) = %s", nicID, linkLocalAddr, err) + } + if p := e.Read(); p.IsNil() { + t.Fatal("expected a done message to be sent") + } else { + test.validate(t, stack.PayloadSince(p.NetworkHeader()), header.IPv6Any, linkLocalAddrSNMC, true /* leave */) + p.DecRef() + } + }) } } @@ -130,6 +208,111 @@ func TestSendQueuedMLDReports(t *testing.T) { maxReports = 2 ) + getAndCheckMLDv1MulticastAddress := func(t *testing.T, seen map[tcpip.Address]bool, p stack.PacketBufferPtr) tcpip.Address { + t.Helper() + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + ipv6 := header.IPv6(payload.AsSlice()) + + ipv6HeaderIter := header.MakeIPv6PayloadIterator( + header.IPv6ExtensionHeaderIdentifier(ipv6.NextHeader()), + bufferv2.MakeWithData(ipv6.Payload()), + ) + + var transport header.IPv6RawPayloadHeader + for { + h, done, err := ipv6HeaderIter.Next() + if err != nil { + t.Fatalf("ipv6HeaderIter.Next(): %s", err) + } + if done { + t.Fatalf("ipv6HeaderIter.Next() = (%T, %t, _), want = (_, false, _)", h, done) + } + defer h.Release() + if t, ok := h.(header.IPv6RawPayloadHeader); ok { + transport = t + break + } + } + + if got := tcpip.TransportProtocolNumber(transport.Identifier); got != header.ICMPv6ProtocolNumber { + t.Fatalf("got ipv6.NextHeader() = %d, want = %d", got, header.ICMPv6ProtocolNumber) + } + icmpv6 := header.ICMPv6(transport.Buf.Flatten()) + if got := icmpv6.Type(); got != header.ICMPv6MulticastListenerReport && got != header.ICMPv6MulticastListenerDone { + t.Fatalf("got icmpv6.Type() = %d, want = %d or %d", got, header.ICMPv6MulticastListenerReport, header.ICMPv6MulticastListenerDone) + } + addr := header.MLD(icmpv6.MessageBody()).MulticastAddress() + s, ok := seen[addr] + if !ok { + t.Fatalf("unexpectedly got a packet for group %s", addr) + } + if s { + t.Fatalf("already saw packet for group %s", addr) + } + seen[addr] = true + return addr + } + + getAndCheckMLDv2MulticastAddress := func(t *testing.T, seen map[tcpip.Address]bool, p stack.PacketBufferPtr) tcpip.Address { + t.Helper() + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + ipv6 := header.IPv6(payload.AsSlice()) + + ipv6HeaderIter := header.MakeIPv6PayloadIterator( + header.IPv6ExtensionHeaderIdentifier(ipv6.NextHeader()), + bufferv2.MakeWithData(ipv6.Payload()), + ) + + var transport header.IPv6RawPayloadHeader + for { + h, done, err := ipv6HeaderIter.Next() + if err != nil { + t.Fatalf("ipv6HeaderIter.Next(): %s", err) + } + if done { + t.Fatalf("ipv6HeaderIter.Next() = (%T, %t, _), want = (_, false, _)", h, done) + } + defer h.Release() + if t, ok := h.(header.IPv6RawPayloadHeader); ok { + transport = t + break + } + } + + if got := tcpip.TransportProtocolNumber(transport.Identifier); got != header.ICMPv6ProtocolNumber { + t.Fatalf("got ipv6.NextHeader() = %d, want = %d", got, header.ICMPv6ProtocolNumber) + } + icmpv6 := header.ICMPv6(transport.Buf.Flatten()) + if got := icmpv6.Type(); got != header.ICMPv6MulticastListenerV2Report { + t.Fatalf("got icmpv6.Type() = %d, want = %d", got, header.ICMPv6MulticastListenerV2Report) + } + + report := header.MLDv2Report(icmpv6.MessageBody()) + records := report.MulticastAddressRecords() + record, res := records.Next() + if res != header.MLDv2ReportMulticastAddressRecordIteratorNextOk { + t.Fatalf("got records.Next() = %d, want = %d", res, header.MLDv2ReportMulticastAddressRecordIteratorNextOk) + } + addr := record.MulticastAddress() + + s, ok := seen[addr] + if !ok { + t.Fatalf("unexpectedly got a packet for group %s", addr) + } + if s { + t.Fatalf("already saw packet for group %s", addr) + } + seen[addr] = true + + if _, res := records.Next(); res != header.MLDv2ReportMulticastAddressRecordIteratorNextDone { + t.Errorf("got records.Next() = %d, want = %d", res, header.MLDv2ReportMulticastAddressRecordIteratorNextDone) + } + + return addr + } + tests := []struct { name string dadTransmits uint8 @@ -143,7 +326,58 @@ func TestSendQueuedMLDReports(t *testing.T) { { name: "DAD Enabled", dadTransmits: 1, - retransmitTimer: time.Second, + retransmitTimer: ipv6.UnsolicitedReportIntervalMax + time.Second, + }, + } + + subTests := []struct { + name string + v1Compatibility bool + validate func(t *testing.T, v *bufferv2.View, localAddress tcpip.Address, groupAddress tcpip.Address, leave bool) + checkStats func(*testing.T, *stack.Stack, uint64, uint64, uint64) + getAndCheckGroupAddress func(*testing.T, map[tcpip.Address]bool, stack.PacketBufferPtr) tcpip.Address + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + validate: func(t *testing.T, v *bufferv2.View, localAddress tcpip.Address, groupAddress tcpip.Address, leave bool) { + t.Helper() + + remoteAddress := groupAddress + icmpType := header.ICMPv6MulticastListenerReport + if leave { + remoteAddress = header.IPv6AllRoutersLinkLocalMulticastAddress + icmpType = header.ICMPv6MulticastListenerDone + } + + validateMLDPacket(t, v, localAddress, remoteAddress, icmpType, groupAddress) + }, + checkStats: iptestutil.CheckMLDv1Stats, + getAndCheckGroupAddress: getAndCheckMLDv1MulticastAddress, + }, + { + name: "V2", + v1Compatibility: false, + validate: func(t *testing.T, v *bufferv2.View, localAddress tcpip.Address, groupAddress tcpip.Address, leave bool) { + t.Helper() + + recordType := header.MLDv2ReportRecordChangeToExcludeMode + if leave { + recordType = header.MLDv2ReportRecordChangeToIncludeMode + } + + validateMLDv2ReportPacket(t, v, localAddress, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: recordType, + MulticastAddress: groupAddress, + Sources: nil, + }, + }, + }) + }, + checkStats: iptestutil.CheckMLDv2Stats, + getAndCheckGroupAddress: getAndCheckMLDv2MulticastAddress, }, } @@ -161,206 +395,215 @@ func TestSendQueuedMLDReports(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - dadResolutionTime := test.retransmitTimer * time.Duration(test.dadTransmits) - clock := faketime.NewManualClock() - var secureRNG bytes.Reader - secureRNG.Reset(secureRNGBytes[:]) - s := stack.New(stack.Options{ - SecureRNG: &secureRNG, - RandSource: rand.NewSource(time.Now().UnixNano()), - NetworkProtocols: []stack.NetworkProtocolFactory{ipv6.NewProtocolWithOptions(ipv6.Options{ - DADConfigs: stack.DADConfigurations{ - DupAddrDetectTransmits: test.dadTransmits, - RetransmitTimer: test.retransmitTimer, - }, - MLD: ipv6.MLDOptions{ - Enabled: true, - }, - })}, - Clock: clock, - }) + for _, subTest := range subTests { + t.Run(subTest.name, func(t *testing.T) { + dadResolutionTime := test.retransmitTimer * time.Duration(test.dadTransmits) + clock := faketime.NewManualClock() + var secureRNG bytes.Reader + secureRNG.Reset(secureRNGBytes[:]) + s := stack.New(stack.Options{ + SecureRNG: &secureRNG, + RandSource: rand.NewSource(time.Now().UnixNano()), + NetworkProtocols: []stack.NetworkProtocolFactory{ipv6.NewProtocolWithOptions(ipv6.Options{ + DADConfigs: stack.DADConfigurations{ + DupAddrDetectTransmits: test.dadTransmits, + RetransmitTimer: test.retransmitTimer, + }, + MLD: ipv6.MLDOptions{ + Enabled: true, + }, + })}, + Clock: clock, + }) - // Allow space for an extra packet so we can observe packets that were - // unexpectedly sent. - e := channel.New(maxReports+int(test.dadTransmits)+1 /* extra */, header.IPv6MinimumMTU, "") - if err := s.CreateNIC(nicID, e); err != nil { - t.Fatalf("CreateNIC(%d, _): %s", nicID, err) - } - - defer func() { - s.Close() - s.Wait() - e.Close() - }() - - resolveDAD := func(addr, snmc tcpip.Address) { - clock.Advance(dadResolutionTime) - if p := e.Read(); p.IsNil() { - t.Fatal("expected DAD packet") - } else { - payload := stack.PayloadSince(p.NetworkHeader()) - defer payload.Release() - checker.IPv6(t, payload, - checker.SrcAddr(header.IPv6Any), - checker.DstAddr(snmc), - checker.TTL(header.NDPHopLimit), - checker.NDPNS( - checker.NDPNSTargetAddress(addr), - checker.NDPNSOptions([]header.NDPOption{header.NDPNonceOption(nonce[:])}), - )) - p.DecRef() - } - } - - var reportCounter uint64 - reportStat := s.Stats().ICMP.V6.PacketsSent.MulticastListenerReport - if got := reportStat.Value(); got != reportCounter { - t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter) - } - var doneCounter uint64 - doneStat := s.Stats().ICMP.V6.PacketsSent.MulticastListenerDone - if got := doneStat.Value(); got != doneCounter { - t.Errorf("got doneStat.Value() = %d, want = %d", got, doneCounter) - } - - // Joining a group without an assigned address should send an MLD report - // with the unspecified address. - if err := s.JoinGroup(ipv6.ProtocolNumber, nicID, globalMulticastAddr); err != nil { - t.Fatalf("JoinGroup(%d, %d, %s): %s", ipv6.ProtocolNumber, nicID, globalMulticastAddr, err) - } - reportCounter++ - if got := reportStat.Value(); got != reportCounter { - t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Errorf("expected MLD report for %s", globalMulticastAddr) - } else { - validateMLDPacket(t, stack.PayloadSince(p.NetworkHeader()), header.IPv6Any, globalMulticastAddr, header.ICMPv6MulticastListenerReport, globalMulticastAddr) - p.DecRef() - } - clock.Advance(time.Hour) - if p := e.Read(); !p.IsNil() { - t.Errorf("got unexpected packet = %#v", p) - p.DecRef() - } - if t.Failed() { - t.FailNow() - } - - // Adding a global address should not send reports for the already joined - // group since we should only send queued reports when a link-local - // address is assigned. - // - // Note, we will still expect to send a report for the global address's - // solicited node address from the unspecified address as per RFC 3590 - // section 4. - properties := stack.AddressProperties{PEB: stack.FirstPrimaryEndpoint} - globalProtocolAddr := tcpip.ProtocolAddress{ - Protocol: ipv6.ProtocolNumber, - AddressWithPrefix: globalAddr.WithPrefix(), - } - if err := s.AddProtocolAddress(nicID, globalProtocolAddr, properties); err != nil { - t.Fatalf("AddProtocolAddress(%d, %+v, %+v): %s", nicID, globalProtocolAddr, properties, err) - } - reportCounter++ - if got := reportStat.Value(); got != reportCounter { - t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Errorf("expected MLD report for %s", globalAddrSNMC) - } else { - validateMLDPacket(t, stack.PayloadSince(p.NetworkHeader()), header.IPv6Any, globalAddrSNMC, header.ICMPv6MulticastListenerReport, globalAddrSNMC) - p.DecRef() - } - if dadResolutionTime != 0 { - // Reports should not be sent when the address resolves. - resolveDAD(globalAddr, globalAddrSNMC) - if got := reportStat.Value(); got != reportCounter { - t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter) - } - } - // Leave the group since we don't care about the global address's - // solicited node multicast group membership. - if err := s.LeaveGroup(ipv6.ProtocolNumber, nicID, globalAddrSNMC); err != nil { - t.Fatalf("LeaveGroup(%d, %d, %s): %s", ipv6.ProtocolNumber, nicID, globalAddrSNMC, err) - } - if got := doneStat.Value(); got != doneCounter { - t.Errorf("got doneStat.Value() = %d, want = %d", got, doneCounter) - } - if p := e.Read(); !p.IsNil() { - t.Errorf("got unexpected packet = %#v", p) - p.DecRef() - } - if t.Failed() { - t.FailNow() - } - - // Adding a link-local address should send a report for its solicited node - // address and globalMulticastAddr. - linkLocalProtocolAddr := tcpip.ProtocolAddress{ - Protocol: ipv6.ProtocolNumber, - AddressWithPrefix: linkLocalAddr.WithPrefix(), - } - if err := s.AddProtocolAddress(nicID, linkLocalProtocolAddr, stack.AddressProperties{}); err != nil { - t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, linkLocalProtocolAddr, err) - } - if dadResolutionTime != 0 { - reportCounter++ - if got := reportStat.Value(); got != reportCounter { - t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Errorf("expected MLD report for %s", linkLocalAddrSNMC) - } else { - validateMLDPacket(t, stack.PayloadSince(p.NetworkHeader()), header.IPv6Any, linkLocalAddrSNMC, header.ICMPv6MulticastListenerReport, linkLocalAddrSNMC) - p.DecRef() - } - resolveDAD(linkLocalAddr, linkLocalAddrSNMC) - } - - // We expect two batches of reports to be sent (1 batch when the - // link-local address is assigned, and another after the maximum - // unsolicited report interval. - for i := 0; i < 2; i++ { - // We expect reports to be sent (one for globalMulticastAddr and another - // for linkLocalAddrSNMC). - reportCounter += maxReports - if got := reportStat.Value(); got != reportCounter { - t.Errorf("got reportStat.Value() = %d, want = %d", got, reportCounter) - } - - addrs := map[tcpip.Address]bool{ - globalMulticastAddr: false, - linkLocalAddrSNMC: false, - } - for range addrs { - p := e.Read() - if p.IsNil() { - t.Fatalf("expected MLD report for %s and %s; addrs = %#v", globalMulticastAddr, linkLocalAddrSNMC, addrs) + // Allow space for an extra packet so we can observe packets that were + // unexpectedly sent. + e := channel.New(maxReports+int(test.dadTransmits)+1 /* extra */, header.IPv6MinimumMTU, "") + if err := s.CreateNIC(nicID, e); err != nil { + t.Fatalf("CreateNIC(%d, _): %s", nicID, err) } - v := stack.PayloadSince(p.NetworkHeader()) - defer v.Release() - addr := header.IPv6(v.AsSlice()).DestinationAddress() - if seen, ok := addrs[addr]; !ok { - t.Fatalf("got unexpected packet destined to %s", addr) - } else if seen { - t.Fatalf("got another packet destined to %s", addr) + defer func() { + s.Close() + s.Wait() + e.Close() + }() + + resolveDAD := func(addr, snmc tcpip.Address) { + t.Helper() + clock.Advance(dadResolutionTime) + if p := e.Read(); p.IsNil() { + t.Fatal("expected DAD packet") + } else { + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + checker.IPv6(t, payload, + checker.SrcAddr(header.IPv6Any), + checker.DstAddr(snmc), + checker.TTL(header.NDPHopLimit), + checker.NDPNS( + checker.NDPNSTargetAddress(addr), + checker.NDPNSOptions([]header.NDPOption{header.NDPNonceOption(nonce[:])}), + )) + p.DecRef() + } } - addrs[addr] = true - validateMLDPacket(t, v.Clone(), linkLocalAddr, addr, header.ICMPv6MulticastListenerReport, addr) - p.DecRef() + checkVersion := func() { + if subTest.v1Compatibility { + createAndInjectMLDPacket( + e, + header.ICMPv6MulticastListenerQuery, + header.MLDHopLimit, + linkLocalAddr, + unusedMulticastAddr, + true, /* withRouterAlertOption */ + header.IPv6RouterAlertMLD, + ) + } + } + checkVersion() - clock.Advance(ipv6.UnsolicitedReportIntervalMax) - } - } + var reportCounter uint64 + var doneCounter uint64 + var reportV2Counter uint64 + subTest.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) - // Should not send any more reports. - clock.Advance(time.Hour) - if p := e.Read(); !p.IsNil() { - t.Errorf("got unexpected packet = %#v", p) - p.DecRef() + // Joining a group without an assigned address should send an MLD report + // with the unspecified address. + if err := s.JoinGroup(ipv6.ProtocolNumber, nicID, globalMulticastAddr); err != nil { + t.Fatalf("JoinGroup(%d, %d, %s): %s", ipv6.ProtocolNumber, nicID, globalMulticastAddr, err) + } + reportCounter++ + subTest.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Errorf("expected MLD report for %s", globalMulticastAddr) + } else { + subTest.validate(t, stack.PayloadSince(p.NetworkHeader()), header.IPv6Any, globalMulticastAddr, false /* leave */) + p.DecRef() + } + clock.Advance(time.Hour) + checkVersion() + if p := e.Read(); !p.IsNil() { + t.Errorf("got unexpected packet = %#v", p) + p.DecRef() + } + if t.Failed() { + t.FailNow() + } + + // Adding a global address should not send reports for the already joined + // group since we should only send queued reports when a link-local + // address is assigned. + // + // Note, we will still expect to send a report for the global address's + // solicited node address from the unspecified address as per RFC 3590 + // section 4. + properties := stack.AddressProperties{PEB: stack.FirstPrimaryEndpoint} + globalProtocolAddr := tcpip.ProtocolAddress{ + Protocol: ipv6.ProtocolNumber, + AddressWithPrefix: globalAddr.WithPrefix(), + } + if err := s.AddProtocolAddress(nicID, globalProtocolAddr, properties); err != nil { + t.Fatalf("AddProtocolAddress(%d, %+v, %+v): %s", nicID, globalProtocolAddr, properties, err) + } + reportCounter++ + subTest.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Errorf("expected MLD report for %s", globalAddrSNMC) + } else { + subTest.validate(t, stack.PayloadSince(p.NetworkHeader()), header.IPv6Any, globalAddrSNMC, false /* leave */) + p.DecRef() + } + if dadResolutionTime != 0 { + // Reports should not be sent when the address resolves. + resolveDAD(globalAddr, globalAddrSNMC) + subTest.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) + } + // Leave the group since we don't care about the global address's + // solicited node multicast group membership. + if err := s.LeaveGroup(ipv6.ProtocolNumber, nicID, globalAddrSNMC); err != nil { + t.Fatalf("LeaveGroup(%d, %d, %s): %s", ipv6.ProtocolNumber, nicID, globalAddrSNMC, err) + } + if !subTest.v1Compatibility { + doneCounter++ + subTest.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Errorf("expected MLD report for %s", globalAddrSNMC) + } else { + subTest.validate(t, stack.PayloadSince(p.NetworkHeader()), header.IPv6Any, globalAddrSNMC, true /* leave */) + p.DecRef() + } + } + subTest.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) + if p := e.Read(); !p.IsNil() { + t.Errorf("got unexpected packet = %#v", p) + p.DecRef() + } + if t.Failed() { + t.FailNow() + } + + // Adding a link-local address should send a report for its solicited node + // address and globalMulticastAddr. + linkLocalProtocolAddr := tcpip.ProtocolAddress{ + Protocol: ipv6.ProtocolNumber, + AddressWithPrefix: linkLocalAddr.WithPrefix(), + } + if err := s.AddProtocolAddress(nicID, linkLocalProtocolAddr, stack.AddressProperties{}); err != nil { + t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, linkLocalProtocolAddr, err) + } + if dadResolutionTime != 0 { + reportCounter++ + if p := e.Read(); p.IsNil() { + t.Errorf("expected MLD report for %s", linkLocalAddrSNMC) + } else { + subTest.validate(t, stack.PayloadSince(p.NetworkHeader()), header.IPv6Any, linkLocalAddrSNMC, false /* leave */) + p.DecRef() + } + resolveDAD(linkLocalAddr, linkLocalAddrSNMC) + } + + // We expect two batches of reports to be sent (1 batch when the + // link-local address is assigned, and another after the maximum + // unsolicited report interval. + for i := 0; i < 2; i++ { + // We expect reports to be sent (one for globalMulticastAddr and another + // for linkLocalAddrSNMC). + reportCounter += maxReports + subTest.checkStats(t, s, reportCounter, doneCounter, reportV2Counter) + + addrs := map[tcpip.Address]bool{ + globalMulticastAddr: false, + linkLocalAddrSNMC: false, + } + for range addrs { + p := e.Read() + if p.IsNil() { + t.Fatalf("expected MLD report for %s and %s; addrs = %#v", globalMulticastAddr, linkLocalAddrSNMC, addrs) + } + + subTest.validate( + t, + stack.PayloadSince(p.NetworkHeader()), + linkLocalAddr, + subTest.getAndCheckGroupAddress(t, addrs, p), + false, /* leave */ + ) + + p.DecRef() + + clock.Advance(ipv6.UnsolicitedReportIntervalMax) + } + } + + // Should not send any more reports. + clock.Advance(time.Hour) + if p := e.Read(); !p.IsNil() { + t.Errorf("got unexpected packet = %#v", p) + p.DecRef() + } + }) } }) } @@ -368,7 +611,7 @@ func TestSendQueuedMLDReports(t *testing.T) { // createAndInjectMLDPacket creates and injects an MLD packet with the // specified fields. -func createAndInjectMLDPacket(e *channel.Endpoint, mldType header.ICMPv6Type, hopLimit uint8, srcAddress tcpip.Address, withRouterAlertOption bool, routerAlertValue header.IPv6RouterAlertValue) { +func createAndInjectMLDPacket(e *channel.Endpoint, mldType header.ICMPv6Type, hopLimit uint8, srcAddress, groupAddress tcpip.Address, withRouterAlertOption bool, routerAlertValue header.IPv6RouterAlertValue) { var extensionHeaders header.IPv6ExtHdrSerializer if withRouterAlertOption { extensionHeaders = header.IPv6ExtHdrSerializer{ @@ -396,7 +639,7 @@ func createAndInjectMLDPacket(e *channel.Endpoint, mldType header.ICMPv6Type, ho icmp.SetType(mldType) mld := header.MLD(icmp.MessageBody()) mld.SetMaximumResponseDelay(0) - mld.SetMulticastAddress(header.IPv6Any) + mld.SetMulticastAddress(groupAddress) icmp.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmp, Src: srcAddress, @@ -497,7 +740,7 @@ func TestMLDPacketValidation(t *testing.T) { if got := stats.IP.PacketsDelivered.Value(); got != 0 { t.Fatalf("got stats.IP.PacketsDelivered.Value() = %d, want = 0", got) } - createAndInjectMLDPacket(e, test.messageType, test.hopLimit, test.srcAddr, test.includeRouterAlertOption, test.routerAlertValue) + createAndInjectMLDPacket(e, test.messageType, test.hopLimit, test.srcAddr, header.IPv6Any, test.includeRouterAlertOption, test.routerAlertValue) // We always expect the packet to pass IP validation. if got := stats.IP.PacketsDelivered.Value(); got != 1 { t.Fatalf("got stats.IP.PacketsDelivered.Value() = %d, want = 1", got) @@ -608,55 +851,102 @@ func TestMLDSkipProtocol(t *testing.T) { }, } + subTests := []struct { + name string + v1Compatibility bool + validate func(t *testing.T, v *bufferv2.View, localAddress tcpip.Address, groupAddress tcpip.Address) + }{ + { + name: "V1 Compatibility", + v1Compatibility: true, + validate: func(t *testing.T, v *bufferv2.View, localAddress tcpip.Address, groupAddress tcpip.Address) { + t.Helper() + validateMLDPacket(t, v, localAddress, groupAddress, header.ICMPv6MulticastListenerReport, groupAddress) + }, + }, + { + name: "V2", + v1Compatibility: false, + validate: func(t *testing.T, v *bufferv2.View, localAddress tcpip.Address, groupAddress tcpip.Address) { + t.Helper() + validateMLDv2ReportPacket(t, v, localAddress, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToExcludeMode, + MulticastAddress: groupAddress, + Sources: nil, + }, + }, + }) + }, + }, + } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - c := newMLDTestContext() - s := c.s + for _, subTest := range subTests { + t.Run(subTest.name, func(t *testing.T) { + c := newMLDTestContext() + s := c.s - e := channel.New(1, header.IPv6MinimumMTU, "") - if err := s.CreateNIC(nicID, e); err != nil { - t.Fatalf("CreateNIC(%d, _): %s", nicID, err) - } + e := channel.New(1, header.IPv6MinimumMTU, "") + if err := s.CreateNIC(nicID, e); err != nil { + t.Fatalf("CreateNIC(%d, _): %s", nicID, err) + } - defer e.Close() - defer c.cleanup() + defer e.Close() + defer c.cleanup() - protocolAddr := tcpip.ProtocolAddress{ - Protocol: ipv6.ProtocolNumber, - AddressWithPrefix: linkLocalAddr.WithPrefix(), - } - if err := s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil { - t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, protocolAddr, err) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - validateMLDPacket(t, stack.PayloadSince(p.NetworkHeader()), linkLocalAddr, linkLocalAddrSNMC, header.ICMPv6MulticastListenerReport, linkLocalAddrSNMC) - p.DecRef() - } + if subTest.v1Compatibility { + createAndInjectMLDPacket( + e, + header.ICMPv6MulticastListenerQuery, + header.MLDHopLimit, + linkLocalAddr, + header.IPv6Any, + true, /* withRouterAlertOption */ + header.IPv6RouterAlertMLD, + ) + } - if err := s.JoinGroup(ipv6.ProtocolNumber, nicID, test.group); err != nil { - t.Fatalf("s.JoinGroup(%d, %d, %s): %s", ipv6.ProtocolNumber, nicID, test.group, err) - } - if isInGroup, err := s.IsInGroup(nicID, test.group); err != nil { - t.Fatalf("IsInGroup(%d, %s): %s", nicID, test.group, err) - } else if !isInGroup { - t.Fatalf("got IsInGroup(%d, %s) = false, want = true", nicID, test.group) - } + protocolAddr := tcpip.ProtocolAddress{ + Protocol: ipv6.ProtocolNumber, + AddressWithPrefix: linkLocalAddr.WithPrefix(), + } + if err := s.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil { + t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", nicID, protocolAddr, err) + } + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + subTest.validate(t, stack.PayloadSince(p.NetworkHeader()), linkLocalAddr, linkLocalAddrSNMC) + p.DecRef() + } - if !test.expectReport { - if p := e.Read(); !p.IsNil() { - t.Fatalf("got e.Read() = (%#v, true), want = (_, false)", p) - } + if err := s.JoinGroup(ipv6.ProtocolNumber, nicID, test.group); err != nil { + t.Fatalf("s.JoinGroup(%d, %d, %s): %s", ipv6.ProtocolNumber, nicID, test.group, err) + } + if isInGroup, err := s.IsInGroup(nicID, test.group); err != nil { + t.Fatalf("IsInGroup(%d, %s): %s", nicID, test.group, err) + } else if !isInGroup { + t.Fatalf("got IsInGroup(%d, %s) = false, want = true", nicID, test.group) + } - return - } + if !test.expectReport { + if p := e.Read(); !p.IsNil() { + t.Fatalf("got e.Read() = (%#v, true), want = (_, false)", p) + } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - validateMLDPacket(t, stack.PayloadSince(p.NetworkHeader()), linkLocalAddr, test.group, header.ICMPv6MulticastListenerReport, test.group) - p.DecRef() + return + } + + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + subTest.validate(t, stack.PayloadSince(p.NetworkHeader()), linkLocalAddr, test.group) + p.DecRef() + } + }) } }) } diff --git a/pkg/tcpip/network/ipv6/stats.go b/pkg/tcpip/network/ipv6/stats.go index 2f18f60e8..8a7ae91a3 100644 --- a/pkg/tcpip/network/ipv6/stats.go +++ b/pkg/tcpip/network/ipv6/stats.go @@ -52,20 +52,21 @@ type sharedStats struct { // LINT.IfChange(multiCounterICMPv6PacketStats) type multiCounterICMPv6PacketStats struct { - echoRequest tcpip.MultiCounterStat - echoReply tcpip.MultiCounterStat - dstUnreachable tcpip.MultiCounterStat - packetTooBig tcpip.MultiCounterStat - timeExceeded tcpip.MultiCounterStat - paramProblem tcpip.MultiCounterStat - routerSolicit tcpip.MultiCounterStat - routerAdvert tcpip.MultiCounterStat - neighborSolicit tcpip.MultiCounterStat - neighborAdvert tcpip.MultiCounterStat - redirectMsg tcpip.MultiCounterStat - multicastListenerQuery tcpip.MultiCounterStat - multicastListenerReport tcpip.MultiCounterStat - multicastListenerDone tcpip.MultiCounterStat + echoRequest tcpip.MultiCounterStat + echoReply tcpip.MultiCounterStat + dstUnreachable tcpip.MultiCounterStat + packetTooBig tcpip.MultiCounterStat + timeExceeded tcpip.MultiCounterStat + paramProblem tcpip.MultiCounterStat + routerSolicit tcpip.MultiCounterStat + routerAdvert tcpip.MultiCounterStat + neighborSolicit tcpip.MultiCounterStat + neighborAdvert tcpip.MultiCounterStat + redirectMsg tcpip.MultiCounterStat + multicastListenerQuery tcpip.MultiCounterStat + multicastListenerReport tcpip.MultiCounterStat + multicastListenerReportV2 tcpip.MultiCounterStat + multicastListenerDone tcpip.MultiCounterStat } func (m *multiCounterICMPv6PacketStats) init(a, b *tcpip.ICMPv6PacketStats) { @@ -82,6 +83,7 @@ func (m *multiCounterICMPv6PacketStats) init(a, b *tcpip.ICMPv6PacketStats) { m.redirectMsg.Init(a.RedirectMsg, b.RedirectMsg) m.multicastListenerQuery.Init(a.MulticastListenerQuery, b.MulticastListenerQuery) m.multicastListenerReport.Init(a.MulticastListenerReport, b.MulticastListenerReport) + m.multicastListenerReportV2.Init(a.MulticastListenerReportV2, b.MulticastListenerReportV2) m.multicastListenerDone.Init(a.MulticastListenerDone, b.MulticastListenerDone) } diff --git a/pkg/tcpip/network/multicast_group_test.go b/pkg/tcpip/network/multicast_group_test.go index 9e5900580..a5c6f9494 100644 --- a/pkg/tcpip/network/multicast_group_test.go +++ b/pkg/tcpip/network/multicast_group_test.go @@ -28,6 +28,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/link/channel" "gvisor.dev/gvisor/pkg/tcpip/link/loopback" + iptestutil "gvisor.dev/gvisor/pkg/tcpip/network/internal/testutil" "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" "gvisor.dev/gvisor/pkg/tcpip/stack" @@ -100,6 +101,23 @@ func validateMLDPacket(t *testing.T, p stack.PacketBufferPtr, remoteAddress tcpi ) } +func validateMLDv2ReportPacket(t *testing.T, p stack.PacketBufferPtr, report header.MLDv2ReportSerializer) { + t.Helper() + + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + + checker.IPv6WithExtHdr(t, payload, + checker.IPv6ExtHdr( + checker.IPv6HopByHopExtensionHeader(checker.IPv6RouterAlert(header.IPv6RouterAlertMLD)), + ), + checker.SrcAddr(linkLocalIPv6Addr1), + checker.DstAddr(header.MLDv2RoutersAddress), + checker.TTL(header.MLDHopLimit), + checker.MLDv2Report(report), + ) +} + // validateIGMPPacket checks that a passed PacketInfo is an IPv4 IGMP packet // sent to the provided address with the passed fields set. func validateIGMPPacket(t *testing.T, p stack.PacketBufferPtr, remoteAddress tcpip.Address, igmpType uint8, maxRespTime byte, groupAddress tcpip.Address) { @@ -115,12 +133,26 @@ func validateIGMPPacket(t *testing.T, p stack.PacketBufferPtr, remoteAddress tcp checker.IPv4RouterAlert(), checker.IGMP( checker.IGMPType(header.IGMPType(igmpType)), - checker.IGMPMaxRespTime(header.DecisecondToDuration(maxRespTime)), + checker.IGMPMaxRespTime(header.DecisecondToDuration(uint16(maxRespTime))), checker.IGMPGroupAddress(groupAddress), ), ) } +func validateIGMPv3ReportPacket(t *testing.T, p stack.PacketBufferPtr, report header.IGMPv3ReportSerializer) { + t.Helper() + + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + checker.IPv4(t, payload, + checker.SrcAddr(stackIPv4Addr), + checker.DstAddr(header.IGMPv3RoutersAddress), + checker.TTL(header.IGMPTTL), + checker.IPv4RouterAlert(), + checker.IGMPv3Report(report), + ) +} + type multicastTestContext struct { s *stack.Stack e *channel.Endpoint @@ -198,19 +230,25 @@ func createStackWithLinkEndpoint(t *testing.T, v4, mgpEnabled bool, e stack.Link // To not interfere with tests, checkInitialIPv6Groups will leave the added // address's solicited node multicast group so that the tests can all assume // the NIC has not joined any IPv6 groups. -func checkInitialIPv6Groups(t *testing.T, e *channel.Endpoint, s *stack.Stack, clock *faketime.ManualClock) (reportCounter uint64, leaveCounter uint64) { +func checkInitialIPv6Groups(t *testing.T, e *channel.Endpoint, s *stack.Stack, clock *faketime.ManualClock) uint64 { t.Helper() - stats := s.Stats().ICMP.V6.PacketsSent + var reportCounter uint64 reportCounter++ - if got := stats.MulticastListenerReport.Value(); got != reportCounter { - t.Errorf("got stats.MulticastListenerReport.Value() = %d, want = %d", got, reportCounter) - } + iptestutil.CheckMLDv2Stats(t, s, 0, 0, reportCounter) if p := e.Read(); p.IsNil() { t.Fatal("expected a report message to be sent") } else { - validateMLDPacket(t, p, ipv6AddrSNMC, mldReport, 0, ipv6AddrSNMC) + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToExcludeMode, + MulticastAddress: ipv6AddrSNMC, + Sources: nil, + }, + }, + }) p.DecRef() } @@ -219,15 +257,25 @@ func checkInitialIPv6Groups(t *testing.T, e *channel.Endpoint, s *stack.Stack, c if err := s.LeaveGroup(ipv6.ProtocolNumber, nicID, ipv6AddrSNMC); err != nil { t.Fatalf("LeaveGroup(%d, %d, %s): %s", ipv6.ProtocolNumber, nicID, ipv6AddrSNMC, err) } - leaveCounter++ - if got := stats.MulticastListenerDone.Value(); got != leaveCounter { - t.Errorf("got stats.MulticastListenerDone.Value() = %d, want = %d", got, leaveCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - validateMLDPacket(t, p, header.IPv6AllRoutersLinkLocalMulticastAddress, mldDone, 0, ipv6AddrSNMC) - p.DecRef() + for i := 0; i < 2; i++ { + reportCounter++ + iptestutil.CheckMLDv2Stats(t, s, 0, 0, reportCounter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToIncludeMode, + MulticastAddress: ipv6AddrSNMC, + Sources: nil, + }, + }, + }) + p.DecRef() + } + + clock.Advance(ipv6.UnsolicitedReportIntervalMax) } // Should not send any more packets. @@ -236,16 +284,16 @@ func checkInitialIPv6Groups(t *testing.T, e *channel.Endpoint, s *stack.Stack, c t.Fatalf("sent unexpected packet = %#v", p) } - return reportCounter, leaveCounter + return reportCounter } // createAndInjectIGMPPacket creates and injects an IGMP packet with the // specified fields. -func createAndInjectIGMPPacket(e *channel.Endpoint, igmpType byte, maxRespTime byte, groupAddress tcpip.Address) { +func createAndInjectIGMPPacket(e *channel.Endpoint, igmpType byte, maxRespTime byte, groupAddress tcpip.Address, extraLength int) { options := header.IPv4OptionsSerializer{ &header.IPv4SerializableRouterAlertOption{}, } - buf := make([]byte, header.IPv4MinimumSize+int(options.Length())+header.IGMPQueryMinimumSize) + buf := make([]byte, header.IPv4MinimumSize+int(options.Length())+header.IGMPQueryMinimumSize+extraLength) ip := header.IPv4(buf) ip.Encode(&header.IPv4Fields{ TotalLength: uint16(len(buf)), @@ -272,7 +320,7 @@ func createAndInjectIGMPPacket(e *channel.Endpoint, igmpType byte, maxRespTime b // createAndInjectMLDPacket creates and injects an MLD packet with the // specified fields. -func createAndInjectMLDPacket(e *channel.Endpoint, mldType uint8, maxRespDelay byte, groupAddress tcpip.Address) { +func createAndInjectMLDPacket(e *channel.Endpoint, mldType uint8, maxRespDelay byte, groupAddress tcpip.Address, extraLength int) { extensionHeaders := header.IPv6ExtHdrSerializer{ header.IPv6SerializableHopByHopExtHdr{ &header.IPv6RouterAlertOption{Value: header.IPv6RouterAlertMLD}, @@ -280,7 +328,7 @@ func createAndInjectMLDPacket(e *channel.Endpoint, mldType uint8, maxRespDelay b } extensionHeadersLength := extensionHeaders.Length() - payloadLength := extensionHeadersLength + header.ICMPv6HeaderSize + header.MLDMinimumSize + payloadLength := extensionHeadersLength + header.ICMPv6HeaderSize + header.MLDMinimumSize + extraLength buf := make([]byte, header.IPv6MinimumSize+payloadLength) ip := header.IPv6(buf) @@ -333,7 +381,7 @@ func TestMGPDisabled(t *testing.T) { return s.Stats().IGMP.PacketsReceived.MembershipQuery }, rxQuery: func(e *channel.Endpoint) { - createAndInjectIGMPPacket(e, igmpMembershipQuery, unsolicitedIGMPReportIntervalMaxTenthSec, header.IPv4Any) + createAndInjectIGMPPacket(e, igmpMembershipQuery, unsolicitedIGMPReportIntervalMaxTenthSec, header.IPv4Any, 0 /* extraLength */) }, }, { @@ -347,7 +395,7 @@ func TestMGPDisabled(t *testing.T) { return s.Stats().ICMP.V6.PacketsReceived.MulticastListenerQuery }, rxQuery: func(e *channel.Endpoint) { - createAndInjectMLDPacket(e, mldQuery, 0, header.IPv6Any) + createAndInjectMLDPacket(e, mldQuery, 0, header.IPv6Any, 0 /* extraLength */) }, }, } @@ -405,7 +453,7 @@ func TestMGPReceiveCounters(t *testing.T) { maxRespTime byte groupAddress tcpip.Address statCounter func(*stack.Stack) *tcpip.StatCounter - rxMGPkt func(*channel.Endpoint, byte, byte, tcpip.Address) + rxMGPkt func(*channel.Endpoint, byte, byte, tcpip.Address, int) }{ { name: "IGMP Membership Query", @@ -484,7 +532,7 @@ func TestMGPReceiveCounters(t *testing.T) { ctx := newMulticastTestContext(t, len(test.groupAddress) == header.IPv4AddressSize /* v4 */, true /* mgpEnabled */) defer ctx.cleanup() - test.rxMGPkt(ctx.e, test.headerType, test.maxRespTime, test.groupAddress) + test.rxMGPkt(ctx.e, test.headerType, test.maxRespTime, test.groupAddress, 0 /* extraLength */) if got := test.statCounter(ctx.s).Value(); got != 1 { t.Fatalf("got %s received = %d, want = 1", test.name, got) } @@ -495,31 +543,62 @@ func TestMGPReceiveCounters(t *testing.T) { // TestMGPJoinGroup tests that when explicitly joining a multicast group, the // stack schedules and sends correct Membership Reports. func TestMGPJoinGroup(t *testing.T) { + type subTest struct { + name string + enterVersion func(e *channel.Endpoint) + validateReport func(*testing.T, stack.PacketBufferPtr) + checkStats func(*testing.T, *stack.Stack, uint64, uint64, uint64) + } + tests := []struct { name string protoNum tcpip.NetworkProtocolNumber multicastAddr tcpip.Address maxUnsolicitedResponseDelay time.Duration - sentReportStat func(*stack.Stack) *tcpip.StatCounter receivedQueryStat func(*stack.Stack) *tcpip.StatCounter - validateReport func(*testing.T, stack.PacketBufferPtr) - checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) (uint64, uint64) + checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) uint64 + subTests []subTest }{ { name: "IGMP", protoNum: ipv4.ProtocolNumber, multicastAddr: ipv4MulticastAddr1, maxUnsolicitedResponseDelay: ipv4.UnsolicitedReportIntervalMax, - sentReportStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().IGMP.PacketsSent.V2MembershipReport - }, receivedQueryStat: func(s *stack.Stack) *tcpip.StatCounter { return s.Stats().IGMP.PacketsReceived.MembershipQuery }, - validateReport: func(t *testing.T, p stack.PacketBufferPtr) { - t.Helper() + subTests: []subTest{ + { + name: "V2", + enterVersion: func(e *channel.Endpoint) { + // V2 query for unrelated group. + createAndInjectIGMPPacket(e, igmpMembershipQuery, 1, ipv4MulticastAddr3, 0 /* extraLength */) + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() - validateIGMPPacket(t, p, ipv4MulticastAddr1, igmpv2MembershipReport, 0, ipv4MulticastAddr1) + validateIGMPPacket(t, p, ipv4MulticastAddr1, igmpv2MembershipReport, 0, ipv4MulticastAddr1) + }, + checkStats: iptestutil.CheckIGMPv2Stats, + }, + { + name: "V3", + enterVersion: func(*channel.Endpoint) {}, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToExcludeMode, + GroupAddress: ipv4MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + checkStats: iptestutil.CheckIGMPv3Stats, + }, }, }, { @@ -527,74 +606,102 @@ func TestMGPJoinGroup(t *testing.T) { protoNum: ipv6.ProtocolNumber, multicastAddr: ipv6MulticastAddr1, maxUnsolicitedResponseDelay: ipv6.UnsolicitedReportIntervalMax, - sentReportStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().ICMP.V6.PacketsSent.MulticastListenerReport - }, receivedQueryStat: func(s *stack.Stack) *tcpip.StatCounter { return s.Stats().ICMP.V6.PacketsReceived.MulticastListenerQuery }, - validateReport: func(t *testing.T, p stack.PacketBufferPtr) { - t.Helper() - - validateMLDPacket(t, p, ipv6MulticastAddr1, mldReport, 0, ipv6MulticastAddr1) - }, checkInitialGroups: checkInitialIPv6Groups, + subTests: []subTest{ + { + name: "V1", + enterVersion: func(e *channel.Endpoint) { + // V1 query for unrelated group. + createAndInjectMLDPacket(e, mldQuery, 0, ipv6MulticastAddr3, 0 /* extraLength */) + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateMLDPacket(t, p, ipv6MulticastAddr1, mldReport, 0, ipv6MulticastAddr1) + }, + checkStats: iptestutil.CheckMLDv1Stats, + }, + { + name: "V2", + enterVersion: func(*channel.Endpoint) {}, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToExcludeMode, + MulticastAddress: ipv6MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + checkStats: iptestutil.CheckMLDv2Stats, + }, + }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) - defer ctx.cleanup() - s, e, clock := ctx.s, ctx.e, ctx.clock + for _, subTest := range test.subTests { + t.Run(subTest.name, func(t *testing.T) { + ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) + defer ctx.cleanup() + s, e, clock := ctx.s, ctx.e, ctx.clock - var reportCounter uint64 - if test.checkInitialGroups != nil { - reportCounter, _ = test.checkInitialGroups(t, e, s, clock) - } + var reportCounter uint64 + var leaveCounter uint64 + var reportV2Counter uint64 + if test.checkInitialGroups != nil { + reportV2Counter = test.checkInitialGroups(t, e, s, clock) + } - // Test joining a specific address explicitly and verify a Report is sent - // immediately. - if err := s.JoinGroup(test.protoNum, nicID, test.multicastAddr); err != nil { - t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, test.multicastAddr, err) - } - reportCounter++ - sentReportStat := test.sentReportStat(s) - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("got sentReportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - test.validateReport(t, p) - p.DecRef() - } - if t.Failed() { - t.FailNow() - } + subTest.enterVersion(e) - // Verify the second report is sent by the maximum unsolicited response - // interval. - p := e.Read() - if !p.IsNil() { - t.Fatalf("sent unexpected packet, expected report only after advancing the clock = %#v", p) - } - clock.Advance(test.maxUnsolicitedResponseDelay) - reportCounter++ - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("got sentReportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - test.validateReport(t, p) - p.DecRef() - } + // Test joining a specific address explicitly and verify a Report is sent + // immediately. + if err := s.JoinGroup(test.protoNum, nicID, test.multicastAddr); err != nil { + t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, test.multicastAddr, err) + } + reportCounter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + subTest.validateReport(t, p) + p.DecRef() + } + if t.Failed() { + t.FailNow() + } - // Should not send any more packets. - clock.Advance(time.Hour) - if p := e.Read(); !p.IsNil() { - t.Fatalf("sent unexpected packet = %#v", p) + // Verify the second report is sent by the maximum unsolicited response + // interval. + p := e.Read() + if !p.IsNil() { + t.Fatalf("sent unexpected packet, expected report only after advancing the clock = %#v", p) + } + clock.Advance(test.maxUnsolicitedResponseDelay) + reportCounter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + subTest.validateReport(t, p) + p.DecRef() + } + + // Should not send any more packets. + clock.Advance(time.Hour) + if p := e.Read(); !p.IsNil() { + t.Fatalf("sent unexpected packet = %#v", p) + } + }) } }) } @@ -603,109 +710,198 @@ func TestMGPJoinGroup(t *testing.T) { // TestMGPLeaveGroup tests that when leaving a previously joined multicast // group the stack sends a leave/done message. func TestMGPLeaveGroup(t *testing.T) { + type subTest struct { + name string + enterVersion func(e *channel.Endpoint) + validateReport func(*testing.T, stack.PacketBufferPtr) + validateLeave func(*testing.T, stack.PacketBufferPtr) + leaveCount uint8 + checkStats func(*testing.T, *stack.Stack, uint64, uint64, uint64) + } + tests := []struct { - name string - protoNum tcpip.NetworkProtocolNumber - multicastAddr tcpip.Address - sentReportStat func(*stack.Stack) *tcpip.StatCounter - sentLeaveStat func(*stack.Stack) *tcpip.StatCounter - validateReport func(*testing.T, stack.PacketBufferPtr) - validateLeave func(*testing.T, stack.PacketBufferPtr) - checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) (uint64, uint64) + name string + protoNum tcpip.NetworkProtocolNumber + multicastAddr tcpip.Address + maxUnsolicitedResponseDelay time.Duration + checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) uint64 + subTests []subTest }{ { - name: "IGMP", - protoNum: ipv4.ProtocolNumber, - multicastAddr: ipv4MulticastAddr1, - sentReportStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().IGMP.PacketsSent.V2MembershipReport - }, - sentLeaveStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().IGMP.PacketsSent.LeaveGroup - }, - validateReport: func(t *testing.T, p stack.PacketBufferPtr) { - t.Helper() + name: "IGMP", + protoNum: ipv4.ProtocolNumber, + multicastAddr: ipv4MulticastAddr1, + maxUnsolicitedResponseDelay: ipv4.UnsolicitedReportIntervalMax, + subTests: []subTest{ + { + name: "V2", + enterVersion: func(e *channel.Endpoint) { + // V2 query for unrelated group. + createAndInjectIGMPPacket(e, igmpMembershipQuery, 1, ipv4MulticastAddr3, 0 /* extraLength */) + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() - validateIGMPPacket(t, p, ipv4MulticastAddr1, igmpv2MembershipReport, 0, ipv4MulticastAddr1) - }, - validateLeave: func(t *testing.T, p stack.PacketBufferPtr) { - t.Helper() + validateIGMPPacket(t, p, ipv4MulticastAddr1, igmpv2MembershipReport, 0, ipv4MulticastAddr1) + }, + validateLeave: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() - validateIGMPPacket(t, p, header.IPv4AllRoutersGroup, igmpLeaveGroup, 0, ipv4MulticastAddr1) + validateIGMPPacket(t, p, header.IPv4AllRoutersGroup, igmpLeaveGroup, 0, ipv4MulticastAddr1) + }, + leaveCount: 1, + checkStats: iptestutil.CheckIGMPv2Stats, + }, + { + name: "V3", + enterVersion: func(*channel.Endpoint) {}, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToExcludeMode, + GroupAddress: ipv4MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + validateLeave: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToIncludeMode, + GroupAddress: ipv4MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + leaveCount: 2, + checkStats: iptestutil.CheckIGMPv3Stats, + }, }, }, { - name: "MLD", - protoNum: ipv6.ProtocolNumber, - multicastAddr: ipv6MulticastAddr1, - sentReportStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().ICMP.V6.PacketsSent.MulticastListenerReport - }, - sentLeaveStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().ICMP.V6.PacketsSent.MulticastListenerDone - }, - validateReport: func(t *testing.T, p stack.PacketBufferPtr) { - t.Helper() + name: "MLD", + protoNum: ipv6.ProtocolNumber, + multicastAddr: ipv6MulticastAddr1, + maxUnsolicitedResponseDelay: ipv6.UnsolicitedReportIntervalMax, + checkInitialGroups: checkInitialIPv6Groups, + subTests: []subTest{ + { + name: "V1", + enterVersion: func(e *channel.Endpoint) { + // V1 query for unrelated group. + createAndInjectMLDPacket(e, mldQuery, 0, ipv6MulticastAddr3, 0 /* extraLength */) + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() - validateMLDPacket(t, p, ipv6MulticastAddr1, mldReport, 0, ipv6MulticastAddr1) - }, - validateLeave: func(t *testing.T, p stack.PacketBufferPtr) { - t.Helper() + validateMLDPacket(t, p, ipv6MulticastAddr1, mldReport, 0, ipv6MulticastAddr1) + }, + validateLeave: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() - validateMLDPacket(t, p, header.IPv6AllRoutersLinkLocalMulticastAddress, mldDone, 0, ipv6MulticastAddr1) + validateMLDPacket(t, p, header.IPv6AllRoutersLinkLocalMulticastAddress, mldDone, 0, ipv6MulticastAddr1) + }, + leaveCount: 1, + checkStats: iptestutil.CheckMLDv1Stats, + }, + { + name: "V2", + enterVersion: func(*channel.Endpoint) {}, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToExcludeMode, + MulticastAddress: ipv6MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + validateLeave: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToIncludeMode, + MulticastAddress: ipv6MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + leaveCount: 2, + checkStats: iptestutil.CheckMLDv2Stats, + }, }, - checkInitialGroups: checkInitialIPv6Groups, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) - defer ctx.cleanup() - s, e, clock := ctx.s, ctx.e, ctx.clock + for _, subTest := range test.subTests { + t.Run(subTest.name, func(t *testing.T) { + ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) + defer ctx.cleanup() + s, e, clock := ctx.s, ctx.e, ctx.clock - var reportCounter uint64 - var leaveCounter uint64 - if test.checkInitialGroups != nil { - reportCounter, leaveCounter = test.checkInitialGroups(t, e, s, clock) - } + var reportCounter uint64 + var leaveCounter uint64 + var reportV2Counter uint64 + if test.checkInitialGroups != nil { + reportV2Counter = test.checkInitialGroups(t, e, s, clock) + } - if err := s.JoinGroup(test.protoNum, nicID, test.multicastAddr); err != nil { - t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, test.multicastAddr, err) - } - reportCounter++ - if got := test.sentReportStat(s).Value(); got != reportCounter { - t.Errorf("got sentReportStat(_).Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - test.validateReport(t, p) - p.DecRef() - } - if t.Failed() { - t.FailNow() - } + subTest.enterVersion(e) - // Leaving the group should trigger an leave/done message to be sent. - if err := s.LeaveGroup(test.protoNum, nicID, test.multicastAddr); err != nil { - t.Fatalf("LeaveGroup(%d, nic, %s): %s", test.protoNum, test.multicastAddr, err) - } - leaveCounter++ - if got := test.sentLeaveStat(s).Value(); got != leaveCounter { - t.Fatalf("got sentLeaveStat(_).Value() = %d, want = %d", got, leaveCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a leave message to be sent") - } else { - test.validateLeave(t, p) - p.DecRef() - } + if err := s.JoinGroup(test.protoNum, nicID, test.multicastAddr); err != nil { + t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, test.multicastAddr, err) + } + reportCounter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + subTest.validateReport(t, p) + p.DecRef() + } + if t.Failed() { + t.FailNow() + } - // Should not send any more packets. - clock.Advance(time.Hour) - if p := e.Read(); !p.IsNil() { - t.Fatalf("sent unexpected packet = %#v", p) + // Leaving the group should trigger an leave/done message to be sent. + if err := s.LeaveGroup(test.protoNum, nicID, test.multicastAddr); err != nil { + t.Fatalf("LeaveGroup(%d, nic, %s): %s", test.protoNum, test.multicastAddr, err) + } + for i := subTest.leaveCount; i > 0; i-- { + leaveCounter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a leave message to be sent") + } else { + subTest.validateLeave(t, p) + p.DecRef() + } + clock.Advance(test.maxUnsolicitedResponseDelay) + } + + // Should not send any more packets. + clock.Advance(time.Hour) + if p := e.Read(); !p.IsNil() { + t.Fatalf("sent unexpected packet = %#v", p) + } + }) } }) } @@ -714,68 +910,140 @@ func TestMGPLeaveGroup(t *testing.T) { // TestMGPQueryMessages tests that a report is sent in response to query // messages. func TestMGPQueryMessages(t *testing.T) { + type subTest struct { + name string + enterVersion func(e *channel.Endpoint) + validateReport func(*testing.T, stack.PacketBufferPtr, bool) + checkStats func(*testing.T, *stack.Stack, uint64, uint64, uint64) + rxQuery func(*channel.Endpoint, uint8, tcpip.Address) + } + tests := []struct { name string protoNum tcpip.NetworkProtocolNumber multicastAddr tcpip.Address maxUnsolicitedResponseDelay time.Duration - sentReportStat func(*stack.Stack) *tcpip.StatCounter receivedQueryStat func(*stack.Stack) *tcpip.StatCounter - rxQuery func(*channel.Endpoint, uint8, tcpip.Address) - validateReport func(*testing.T, stack.PacketBufferPtr) - maxRespTimeToDuration func(uint8) time.Duration - checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) (uint64, uint64) + maxRespTimeToDuration func(uint16) time.Duration + checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) uint64 + subTests []subTest }{ { name: "IGMP", protoNum: ipv4.ProtocolNumber, multicastAddr: ipv4MulticastAddr1, maxUnsolicitedResponseDelay: ipv4.UnsolicitedReportIntervalMax, - sentReportStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().IGMP.PacketsSent.V2MembershipReport - }, receivedQueryStat: func(s *stack.Stack) *tcpip.StatCounter { return s.Stats().IGMP.PacketsReceived.MembershipQuery }, - rxQuery: func(e *channel.Endpoint, maxRespTime uint8, groupAddress tcpip.Address) { - createAndInjectIGMPPacket(e, igmpMembershipQuery, maxRespTime, groupAddress) - }, - validateReport: func(t *testing.T, p stack.PacketBufferPtr) { - t.Helper() - - validateIGMPPacket(t, p, ipv4MulticastAddr1, igmpv2MembershipReport, 0, ipv4MulticastAddr1) - }, maxRespTimeToDuration: header.DecisecondToDuration, + subTests: []subTest{ + { + name: "V2", + enterVersion: func(e *channel.Endpoint) { + // V2 query for unrelated group. + createAndInjectIGMPPacket(e, igmpMembershipQuery, 1, ipv4MulticastAddr3, 0 /* extraLength */) + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr, _ bool) { + t.Helper() + + validateIGMPPacket(t, p, ipv4MulticastAddr1, igmpv2MembershipReport, 0, ipv4MulticastAddr1) + }, + rxQuery: func(e *channel.Endpoint, maxRespTime uint8, groupAddress tcpip.Address) { + createAndInjectIGMPPacket(e, igmpMembershipQuery, maxRespTime, groupAddress, 0 /* extraLength */) + }, + checkStats: iptestutil.CheckIGMPv2Stats, + }, + { + name: "V3", + enterVersion: func(*channel.Endpoint) {}, + validateReport: func(t *testing.T, p stack.PacketBufferPtr, queryResponse bool) { + t.Helper() + + recordType := header.IGMPv3ReportRecordChangeToExcludeMode + if queryResponse { + recordType = header.IGMPv3ReportRecordModeIsExclude + } + + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: recordType, + GroupAddress: ipv4MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + rxQuery: func(e *channel.Endpoint, maxRespTime uint8, groupAddress tcpip.Address) { + createAndInjectIGMPPacket(e, igmpMembershipQuery, maxRespTime, groupAddress, header.IGMPv3QueryMinimumSize-header.IGMPQueryMinimumSize /* extraLength */) + }, + checkStats: iptestutil.CheckIGMPv3Stats, + }, + }, }, { name: "MLD", protoNum: ipv6.ProtocolNumber, multicastAddr: ipv6MulticastAddr1, maxUnsolicitedResponseDelay: ipv6.UnsolicitedReportIntervalMax, - sentReportStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().ICMP.V6.PacketsSent.MulticastListenerReport - }, receivedQueryStat: func(s *stack.Stack) *tcpip.StatCounter { return s.Stats().ICMP.V6.PacketsReceived.MulticastListenerQuery }, - rxQuery: func(e *channel.Endpoint, maxRespTime uint8, groupAddress tcpip.Address) { - createAndInjectMLDPacket(e, mldQuery, maxRespTime, groupAddress) - }, - validateReport: func(t *testing.T, p stack.PacketBufferPtr) { - t.Helper() - - validateMLDPacket(t, p, ipv6MulticastAddr1, mldReport, 0, ipv6MulticastAddr1) - }, - maxRespTimeToDuration: func(d uint8) time.Duration { + maxRespTimeToDuration: func(d uint16) time.Duration { return time.Duration(d) * time.Millisecond }, checkInitialGroups: checkInitialIPv6Groups, + subTests: []subTest{ + { + name: "V1", + enterVersion: func(e *channel.Endpoint) { + // V1 query for unrelated group. + createAndInjectMLDPacket(e, mldQuery, 0, ipv6MulticastAddr3, 0 /* extraLength */) + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr, _ bool) { + t.Helper() + + validateMLDPacket(t, p, ipv6MulticastAddr1, mldReport, 0, ipv6MulticastAddr1) + }, + rxQuery: func(e *channel.Endpoint, maxRespTime uint8, groupAddress tcpip.Address) { + createAndInjectMLDPacket(e, mldQuery, maxRespTime, groupAddress, 0 /* extraLength */) + }, + checkStats: iptestutil.CheckMLDv1Stats, + }, + { + name: "V2", + enterVersion: func(*channel.Endpoint) {}, + validateReport: func(t *testing.T, p stack.PacketBufferPtr, queryResponse bool) { + t.Helper() + + recordType := header.MLDv2ReportRecordChangeToExcludeMode + if queryResponse { + recordType = header.MLDv2ReportRecordModeIsExclude + } + + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: recordType, + MulticastAddress: ipv6MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + rxQuery: func(e *channel.Endpoint, maxRespTime uint8, groupAddress tcpip.Address) { + createAndInjectMLDPacket(e, mldQuery, maxRespTime, groupAddress, header.MLDv2QueryMinimumSize-header.MLDMinimumSize /* extraLength */) + }, + checkStats: iptestutil.CheckMLDv2Stats, + }, + }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - subTests := []struct { + addrTests := []struct { name string multicastAddr tcpip.Address expectReport bool @@ -801,72 +1069,74 @@ func TestMGPQueryMessages(t *testing.T) { }, } - for _, subTest := range subTests { - t.Run(subTest.name, func(t *testing.T) { - ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) - defer ctx.cleanup() - s, e, clock := ctx.s, ctx.e, ctx.clock + for _, addrTest := range addrTests { + t.Run(addrTest.name, func(t *testing.T) { + for _, subTest := range test.subTests { + t.Run(subTest.name, func(t *testing.T) { + ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) + defer ctx.cleanup() + s, e, clock := ctx.s, ctx.e, ctx.clock - var reportCounter uint64 - if test.checkInitialGroups != nil { - reportCounter, _ = test.checkInitialGroups(t, e, s, clock) - } + var reportCounter uint64 + var leaveCounter uint64 + var reportV2Counter uint64 + if test.checkInitialGroups != nil { + reportV2Counter = test.checkInitialGroups(t, e, s, clock) + } - if err := s.JoinGroup(test.protoNum, nicID, test.multicastAddr); err != nil { - t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, test.multicastAddr, err) - } - sentReportStat := test.sentReportStat(s) - for i := 0; i < maxUnsolicitedReports; i++ { - sentReportStat := test.sentReportStat(s) - reportCounter++ - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("(i=%d) got sentReportStat.Value() = %d, want = %d", i, got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatalf("expected %d-th report message to be sent", i) - } else { - test.validateReport(t, p) - p.DecRef() - } - clock.Advance(test.maxUnsolicitedResponseDelay) - } - if t.Failed() { - t.FailNow() - } + subTest.enterVersion(e) - // Should not send any more packets until a query. - clock.Advance(time.Hour) - if p := e.Read(); !p.IsNil() { - t.Fatalf("sent unexpected packet = %#v", p) - } + if err := s.JoinGroup(test.protoNum, nicID, test.multicastAddr); err != nil { + t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, test.multicastAddr, err) + } + for i := 0; i < maxUnsolicitedReports; i++ { + reportCounter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatalf("expected %d-th report message to be sent", i) + } else { + subTest.validateReport(t, p, false /* queryResponse */) + p.DecRef() + } + clock.Advance(test.maxUnsolicitedResponseDelay) + } + if t.Failed() { + t.FailNow() + } - // Receive a query message which should trigger a report to be sent at - // some time before the maximum response time if the report is - // targeted at the host. - const maxRespTime = 100 - test.rxQuery(e, maxRespTime, subTest.multicastAddr) - if p := e.Read(); !p.IsNil() { - t.Fatalf("sent unexpected packet = %#v", p) - } + // Should not send any more packets until a query. + clock.Advance(time.Hour) + if p := e.Read(); !p.IsNil() { + t.Fatalf("sent unexpected packet = %#v", p) + } - if subTest.expectReport { - clock.Advance(test.maxRespTimeToDuration(maxRespTime)) - reportCounter++ - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("got sentReportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - test.validateReport(t, p) - p.DecRef() - } - } + // Receive a query message which should trigger a report to be sent at + // some time before the maximum response time if the report is + // targeted at the host. + const maxRespTime = 100 + subTest.rxQuery(e, maxRespTime, addrTest.multicastAddr) + if p := e.Read(); !p.IsNil() { + t.Fatalf("sent unexpected packet = %#v", p) + } - // Should not send any more packets. - clock.Advance(time.Hour) - if p := e.Read(); !p.IsNil() { - t.Fatalf("sent unexpected packet = %#v", p) + if addrTest.expectReport { + clock.Advance(test.maxRespTimeToDuration(maxRespTime)) + reportCounter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + subTest.validateReport(t, p, true /* queryResponse */) + p.DecRef() + } + } + + // Should not send any more packets. + clock.Advance(time.Hour) + if p := e.Read(); !p.IsNil() { + t.Fatalf("sent unexpected packet = %#v", p) + } + }) } }) } @@ -877,126 +1147,383 @@ func TestMGPQueryMessages(t *testing.T) { // TestMGPQueryMessages tests that no further reports or leave/done messages // are sent after receiving a report. func TestMGPReportMessages(t *testing.T) { + type subTest struct { + name string + enterVersion func(e *channel.Endpoint) + validateReport func(*testing.T, stack.PacketBufferPtr) + validateLeave func(*testing.T, stack.PacketBufferPtr) + leaveCount uint8 + checkStats func(*testing.T, *stack.Stack, uint64, uint64, uint64) + } + tests := []struct { - name string - protoNum tcpip.NetworkProtocolNumber - multicastAddr tcpip.Address - sentReportStat func(*stack.Stack) *tcpip.StatCounter - sentLeaveStat func(*stack.Stack) *tcpip.StatCounter - rxReport func(*channel.Endpoint) - validateReport func(*testing.T, stack.PacketBufferPtr) - maxRespTimeToDuration func(uint8) time.Duration - checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) (uint64, uint64) + name string + protoNum tcpip.NetworkProtocolNumber + multicastAddr tcpip.Address + maxUnsolicitedResponseDelay time.Duration + rxReport func(*channel.Endpoint) + checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) uint64 + subTests []subTest }{ { name: "IGMP", protoNum: ipv4.ProtocolNumber, multicastAddr: ipv4MulticastAddr1, - sentReportStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().IGMP.PacketsSent.V2MembershipReport - }, - sentLeaveStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().IGMP.PacketsSent.LeaveGroup - }, rxReport: func(e *channel.Endpoint) { - createAndInjectIGMPPacket(e, igmpv2MembershipReport, 0, ipv4MulticastAddr1) + createAndInjectIGMPPacket(e, igmpv2MembershipReport, 0, ipv4MulticastAddr1, 0 /* extraLength */) }, - validateReport: func(t *testing.T, p stack.PacketBufferPtr) { - t.Helper() + maxUnsolicitedResponseDelay: ipv4.UnsolicitedReportIntervalMax, + subTests: []subTest{ + { + name: "V2", + enterVersion: func(e *channel.Endpoint) { + // V2 query for unrelated group. + createAndInjectIGMPPacket(e, igmpMembershipQuery, 1, ipv4MulticastAddr3, 0 /* extraLength */) + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() - validateIGMPPacket(t, p, ipv4MulticastAddr1, igmpv2MembershipReport, 0, ipv4MulticastAddr1) + validateIGMPPacket(t, p, ipv4MulticastAddr1, igmpv2MembershipReport, 0, ipv4MulticastAddr1) + }, + leaveCount: 0, + checkStats: iptestutil.CheckIGMPv2Stats, + }, + { + name: "V3", + enterVersion: func(*channel.Endpoint) {}, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToExcludeMode, + GroupAddress: ipv4MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + validateLeave: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToIncludeMode, + GroupAddress: ipv4MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + leaveCount: 2, + checkStats: iptestutil.CheckIGMPv3Stats, + }, }, - maxRespTimeToDuration: header.DecisecondToDuration, }, { name: "MLD", protoNum: ipv6.ProtocolNumber, multicastAddr: ipv6MulticastAddr1, - sentReportStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().ICMP.V6.PacketsSent.MulticastListenerReport - }, - sentLeaveStat: func(s *stack.Stack) *tcpip.StatCounter { - return s.Stats().ICMP.V6.PacketsSent.MulticastListenerDone - }, rxReport: func(e *channel.Endpoint) { - createAndInjectMLDPacket(e, mldReport, 0, ipv6MulticastAddr1) + createAndInjectMLDPacket(e, mldReport, 0, ipv6MulticastAddr1, 0 /* extraLength */) }, - validateReport: func(t *testing.T, p stack.PacketBufferPtr) { - t.Helper() + maxUnsolicitedResponseDelay: ipv6.UnsolicitedReportIntervalMax, + checkInitialGroups: checkInitialIPv6Groups, + subTests: []subTest{ + { + name: "V1", + enterVersion: func(e *channel.Endpoint) { + // V1 query for unrelated group. + createAndInjectMLDPacket(e, mldQuery, 0, ipv6MulticastAddr3, 0 /* extraLength */) + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() - validateMLDPacket(t, p, ipv6MulticastAddr1, mldReport, 0, ipv6MulticastAddr1) + validateMLDPacket(t, p, ipv6MulticastAddr1, mldReport, 0, ipv6MulticastAddr1) + }, + leaveCount: 0, + checkStats: iptestutil.CheckMLDv1Stats, + }, + { + name: "V2", + enterVersion: func(*channel.Endpoint) {}, + validateReport: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToExcludeMode, + MulticastAddress: ipv6MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + validateLeave: func(t *testing.T, p stack.PacketBufferPtr) { + t.Helper() + + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToIncludeMode, + MulticastAddress: ipv6MulticastAddr1, + Sources: nil, + }, + }, + }) + }, + leaveCount: 2, + checkStats: iptestutil.CheckMLDv2Stats, + }, }, - maxRespTimeToDuration: func(d uint8) time.Duration { - return time.Duration(d) * time.Millisecond - }, - checkInitialGroups: checkInitialIPv6Groups, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) - defer ctx.cleanup() - s, e, clock := ctx.s, ctx.e, ctx.clock + for _, subTest := range test.subTests { + t.Run(subTest.name, func(t *testing.T) { + ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) + defer ctx.cleanup() + s, e, clock := ctx.s, ctx.e, ctx.clock - var reportCounter uint64 - var leaveCounter uint64 - if test.checkInitialGroups != nil { - reportCounter, leaveCounter = test.checkInitialGroups(t, e, s, clock) - } + var reportCounter uint64 + var leaveCounter uint64 + var reportV2Counter uint64 + if test.checkInitialGroups != nil { + reportV2Counter = test.checkInitialGroups(t, e, s, clock) + } - if err := s.JoinGroup(test.protoNum, nicID, test.multicastAddr); err != nil { - t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, test.multicastAddr, err) - } - sentReportStat := test.sentReportStat(s) - reportCounter++ - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("got sentReportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - test.validateReport(t, p) - p.DecRef() - } - if t.Failed() { - t.FailNow() - } + subTest.enterVersion(e) - // Receiving a report for a group we joined should cancel any further - // reports. - test.rxReport(e) - clock.Advance(time.Hour) - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("got sentReportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); !p.IsNil() { - t.Errorf("sent unexpected packet = %#v", p) - } - if t.Failed() { - t.FailNow() - } + if err := s.JoinGroup(test.protoNum, nicID, test.multicastAddr); err != nil { + t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, test.multicastAddr, err) + } + reportCounter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + subTest.validateReport(t, p) + p.DecRef() + } + if t.Failed() { + t.FailNow() + } - // Leaving a group after getting a report should not send a leave/done - // message. - if err := s.LeaveGroup(test.protoNum, nicID, test.multicastAddr); err != nil { - t.Fatalf("LeaveGroup(%d, nic, %s): %s", test.protoNum, test.multicastAddr, err) - } - clock.Advance(time.Hour) - if got := test.sentLeaveStat(s).Value(); got != leaveCounter { - t.Fatalf("got sentLeaveStat(_).Value() = %d, want = %d", got, leaveCounter) - } + // Receiving a report for a group we joined should cancel any further + // reports. + test.rxReport(e) + clock.Advance(time.Hour) + subTest.enterVersion(e) + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); !p.IsNil() { + t.Errorf("sent unexpected packet = %#v", p) + } + if t.Failed() { + t.FailNow() + } - // Should not send any more packets. - clock.Advance(time.Hour) - if p := e.Read(); !p.IsNil() { - t.Fatalf("sent unexpected packet = %#v", p) + // Leaving a group after getting a report should not send a leave/done + // message. + if err := s.LeaveGroup(test.protoNum, nicID, test.multicastAddr); err != nil { + t.Fatalf("LeaveGroup(%d, nic, %s): %s", test.protoNum, test.multicastAddr, err) + } + for i := subTest.leaveCount; i > 0; i-- { + leaveCounter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a leave message to be sent") + } else { + subTest.validateLeave(t, p) + p.DecRef() + } + clock.Advance(test.maxUnsolicitedResponseDelay) + } + + // Should not send any more packets. + clock.Advance(time.Hour) + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); !p.IsNil() { + t.Fatalf("sent unexpected packet = %#v", p) + } + }) } }) } } func TestMGPWithNICLifecycle(t *testing.T) { + type subTest struct { + name string + enterVersion func(e *channel.Endpoint) + validateReport func(*testing.T, stack.PacketBufferPtr, tcpip.Address) + validateLeave func(*testing.T, stack.PacketBufferPtr, tcpip.Address) + checkStats func(*testing.T, *stack.Stack, uint64, uint64, uint64) + getAndCheckGroupAddress func(*testing.T, map[tcpip.Address]bool, stack.PacketBufferPtr) tcpip.Address + } + + getAndCheckIGMPv2GroupAddress := func(t *testing.T, seen map[tcpip.Address]bool, p stack.PacketBufferPtr) tcpip.Address { + t.Helper() + + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + ipv4 := header.IPv4(payload.AsSlice()) + if got := tcpip.TransportProtocolNumber(ipv4.Protocol()); got != header.IGMPProtocolNumber { + t.Fatalf("got ipv4.Protocol() = %d, want = %d", got, header.IGMPProtocolNumber) + } + addr := header.IGMP(ipv4.Payload()).GroupAddress() + s, ok := seen[addr] + if !ok { + t.Fatalf("unexpectedly got a packet for group %s", addr) + } + if s { + t.Fatalf("already saw packet for group %s", addr) + } + seen[addr] = true + return addr + } + + getAndCheckIGMPv3GroupAddress := func(t *testing.T, seen map[tcpip.Address]bool, p stack.PacketBufferPtr) tcpip.Address { + t.Helper() + + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + ipv4 := header.IPv4(payload.AsSlice()) + if got := tcpip.TransportProtocolNumber(ipv4.Protocol()); got != header.IGMPProtocolNumber { + t.Fatalf("got ipv4.Protocol() = %d, want = %d", got, header.IGMPProtocolNumber) + } + report := header.IGMPv3Report(ipv4.Payload()) + records := report.GroupAddressRecords() + record, res := records.Next() + if res != header.IGMPv3ReportGroupAddressRecordIteratorNextOk { + t.Fatalf("got records.Next() = %d, want = %d", res, header.IGMPv3ReportGroupAddressRecordIteratorNextOk) + } + addr := record.GroupAddress() + + if s, ok := seen[addr]; !ok { + t.Fatalf("unexpectedly got a packet for group %s", addr) + } else if s { + t.Fatalf("already saw packet for group %s", addr) + } + seen[addr] = true + + if _, res := records.Next(); res != header.IGMPv3ReportGroupAddressRecordIteratorNextDone { + t.Errorf("got records.Next() = %d, want = %d", res, header.IGMPv3ReportGroupAddressRecordIteratorNextDone) + } + + return addr + } + + getAndCheckMLDv1MulticastAddress := func(t *testing.T, seen map[tcpip.Address]bool, p stack.PacketBufferPtr) tcpip.Address { + t.Helper() + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + ipv6 := header.IPv6(payload.AsSlice()) + + ipv6HeaderIter := header.MakeIPv6PayloadIterator( + header.IPv6ExtensionHeaderIdentifier(ipv6.NextHeader()), + bufferv2.MakeWithData(ipv6.Payload()), + ) + + var transport header.IPv6RawPayloadHeader + for { + h, done, err := ipv6HeaderIter.Next() + if err != nil { + t.Fatalf("ipv6HeaderIter.Next(): %s", err) + } + if done { + t.Fatalf("ipv6HeaderIter.Next() = (%T, %t, _), want = (_, false, _)", h, done) + } + defer h.Release() + if t, ok := h.(header.IPv6RawPayloadHeader); ok { + transport = t + break + } + } + + if got := tcpip.TransportProtocolNumber(transport.Identifier); got != header.ICMPv6ProtocolNumber { + t.Fatalf("got ipv6.NextHeader() = %d, want = %d", got, header.ICMPv6ProtocolNumber) + } + icmpv6 := header.ICMPv6(transport.Buf.Flatten()) + if got := icmpv6.Type(); got != header.ICMPv6MulticastListenerReport && got != header.ICMPv6MulticastListenerDone { + t.Fatalf("got icmpv6.Type() = %d, want = %d or %d", got, header.ICMPv6MulticastListenerReport, header.ICMPv6MulticastListenerDone) + } + addr := header.MLD(icmpv6.MessageBody()).MulticastAddress() + s, ok := seen[addr] + if !ok { + t.Fatalf("unexpectedly got a packet for group %s", addr) + } + if s { + t.Fatalf("already saw packet for group %s", addr) + } + seen[addr] = true + return addr + } + + getAndCheckMLDv2MulticastAddress := func(t *testing.T, seen map[tcpip.Address]bool, p stack.PacketBufferPtr) tcpip.Address { + t.Helper() + + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + ipv6 := header.IPv6(payload.AsSlice()) + + ipv6HeaderIter := header.MakeIPv6PayloadIterator( + header.IPv6ExtensionHeaderIdentifier(ipv6.NextHeader()), + bufferv2.MakeWithData(ipv6.Payload()), + ) + + var transport header.IPv6RawPayloadHeader + for { + h, done, err := ipv6HeaderIter.Next() + if err != nil { + t.Fatalf("ipv6HeaderIter.Next(): %s", err) + } + if done { + t.Fatalf("ipv6HeaderIter.Next() = (%T, %t, _), want = (_, false, _)", h, done) + } + defer h.Release() + if t, ok := h.(header.IPv6RawPayloadHeader); ok { + transport = t + break + } + } + + if got := tcpip.TransportProtocolNumber(transport.Identifier); got != header.ICMPv6ProtocolNumber { + t.Fatalf("got ipv6.NextHeader() = %d, want = %d", got, header.ICMPv6ProtocolNumber) + } + icmpv6 := header.ICMPv6(transport.Buf.Flatten()) + if got := icmpv6.Type(); got != header.ICMPv6MulticastListenerV2Report { + t.Fatalf("got icmpv6.Type() = %d, want = %d", got, header.ICMPv6MulticastListenerV2Report) + } + + report := header.MLDv2Report(icmpv6.MessageBody()) + records := report.MulticastAddressRecords() + record, res := records.Next() + if res != header.MLDv2ReportMulticastAddressRecordIteratorNextOk { + t.Fatalf("got records.Next() = %d, want = %d", res, header.MLDv2ReportMulticastAddressRecordIteratorNextOk) + } + addr := record.MulticastAddress() + + s, ok := seen[addr] + if !ok { + t.Fatalf("unexpectedly got a packet for group %s", addr) + } + if s { + t.Fatalf("already saw packet for group %s", addr) + } + seen[addr] = true + + if _, res := records.Next(); res != header.MLDv2ReportMulticastAddressRecordIteratorNextDone { + t.Errorf("got records.Next() = %d, want = %d", res, header.MLDv2ReportMulticastAddressRecordIteratorNextDone) + } + + return addr + } + tests := []struct { name string protoNum tcpip.NetworkProtocolNumber @@ -1008,7 +1535,9 @@ func TestMGPWithNICLifecycle(t *testing.T) { validateReport func(*testing.T, stack.PacketBufferPtr, tcpip.Address) validateLeave func(*testing.T, stack.PacketBufferPtr, tcpip.Address) getAndCheckGroupAddress func(*testing.T, map[tcpip.Address]bool, stack.PacketBufferPtr) tcpip.Address - checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) (uint64, uint64) + checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) uint64 + checkStats func(*testing.T, *stack.Stack, uint64, uint64, uint64) + subTests []subTest }{ { name: "IGMP", @@ -1025,32 +1554,83 @@ func TestMGPWithNICLifecycle(t *testing.T) { validateReport: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { t.Helper() - validateIGMPPacket(t, p, addr, igmpv2MembershipReport, 0, addr) + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToExcludeMode, + GroupAddress: addr, + Sources: nil, + }, + }, + }) }, validateLeave: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { t.Helper() - validateIGMPPacket(t, p, header.IPv4AllRoutersGroup, igmpLeaveGroup, 0, addr) + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToIncludeMode, + GroupAddress: addr, + Sources: nil, + }, + }, + }) }, - getAndCheckGroupAddress: func(t *testing.T, seen map[tcpip.Address]bool, p stack.PacketBufferPtr) tcpip.Address { - t.Helper() + getAndCheckGroupAddress: getAndCheckIGMPv3GroupAddress, + checkStats: iptestutil.CheckIGMPv3Stats, + subTests: []subTest{ + { + name: "V2", + enterVersion: func(e *channel.Endpoint) { + // V2 query for unrelated group. + createAndInjectIGMPPacket(e, igmpMembershipQuery, 1, ipv4MulticastAddr3, 0 /* extraLength */) + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { + t.Helper() - payload := stack.PayloadSince(p.NetworkHeader()) - defer payload.Release() - ipv4 := header.IPv4(payload.AsSlice()) - if got := tcpip.TransportProtocolNumber(ipv4.Protocol()); got != header.IGMPProtocolNumber { - t.Fatalf("got ipv4.Protocol() = %d, want = %d", got, header.IGMPProtocolNumber) - } - addr := header.IGMP(ipv4.Payload()).GroupAddress() - s, ok := seen[addr] - if !ok { - t.Fatalf("unexpectedly got a packet for group %s", addr) - } - if s { - t.Fatalf("already saw packet for group %s", addr) - } - seen[addr] = true - return addr + validateIGMPPacket(t, p, addr, igmpv2MembershipReport, 0, addr) + }, + validateLeave: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { + t.Helper() + + validateIGMPPacket(t, p, header.IPv4AllRoutersGroup, igmpLeaveGroup, 0, addr) + }, + checkStats: iptestutil.CheckIGMPv2Stats, + getAndCheckGroupAddress: getAndCheckIGMPv2GroupAddress, + }, + { + name: "V3", + enterVersion: func(*channel.Endpoint) {}, + validateReport: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { + t.Helper() + + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToExcludeMode, + GroupAddress: addr, + Sources: nil, + }, + }, + }) + }, + validateLeave: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { + t.Helper() + + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToIncludeMode, + GroupAddress: addr, + Sources: nil, + }, + }, + }) + }, + checkStats: iptestutil.CheckIGMPv3Stats, + getAndCheckGroupAddress: getAndCheckIGMPv3GroupAddress, + }, }, }, { @@ -1068,220 +1648,235 @@ func TestMGPWithNICLifecycle(t *testing.T) { validateReport: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { t.Helper() - validateMLDPacket(t, p, addr, mldReport, 0, addr) + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToExcludeMode, + MulticastAddress: addr, + Sources: nil, + }, + }, + }) }, validateLeave: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { t.Helper() - validateMLDPacket(t, p, header.IPv6AllRoutersLinkLocalMulticastAddress, mldDone, 0, addr) + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToIncludeMode, + MulticastAddress: addr, + Sources: nil, + }, + }, + }) }, - getAndCheckGroupAddress: func(t *testing.T, seen map[tcpip.Address]bool, p stack.PacketBufferPtr) tcpip.Address { - t.Helper() - payload := stack.PayloadSince(p.NetworkHeader()) - defer payload.Release() - ipv6 := header.IPv6(payload.AsSlice()) + getAndCheckGroupAddress: getAndCheckMLDv2MulticastAddress, + checkInitialGroups: checkInitialIPv6Groups, + checkStats: iptestutil.CheckMLDv2Stats, + subTests: []subTest{ + { + name: "V1", + enterVersion: func(e *channel.Endpoint) { + // V1 query for unrelated group. + createAndInjectMLDPacket(e, mldQuery, 0, ipv6MulticastAddr3, 0 /* extraLength */) + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { + t.Helper() - ipv6HeaderIter := header.MakeIPv6PayloadIterator( - header.IPv6ExtensionHeaderIdentifier(ipv6.NextHeader()), - bufferv2.MakeWithData(ipv6.Payload()), - ) + validateMLDPacket(t, p, addr, mldReport, 0, addr) + }, + validateLeave: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { + t.Helper() - var transport header.IPv6RawPayloadHeader - for { - h, done, err := ipv6HeaderIter.Next() - if err != nil { - t.Fatalf("ipv6HeaderIter.Next(): %s", err) - } - if done { - t.Fatalf("ipv6HeaderIter.Next() = (%T, %t, _), want = (_, false, _)", h, done) - } - defer h.Release() - if t, ok := h.(header.IPv6RawPayloadHeader); ok { - transport = t - break - } - } + validateMLDPacket(t, p, header.IPv6AllRoutersLinkLocalMulticastAddress, mldDone, 0, addr) + }, + checkStats: iptestutil.CheckMLDv1Stats, + getAndCheckGroupAddress: getAndCheckMLDv1MulticastAddress, + }, + { + name: "V2", + enterVersion: func(*channel.Endpoint) {}, + validateReport: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { + t.Helper() - if got := tcpip.TransportProtocolNumber(transport.Identifier); got != header.ICMPv6ProtocolNumber { - t.Fatalf("got ipv6.NextHeader() = %d, want = %d", got, header.ICMPv6ProtocolNumber) - } - icmpv6 := header.ICMPv6(transport.Buf.Flatten()) - if got := icmpv6.Type(); got != header.ICMPv6MulticastListenerReport && got != header.ICMPv6MulticastListenerDone { - t.Fatalf("got icmpv6.Type() = %d, want = %d or %d", got, header.ICMPv6MulticastListenerReport, header.ICMPv6MulticastListenerDone) - } - addr := header.MLD(icmpv6.MessageBody()).MulticastAddress() - s, ok := seen[addr] - if !ok { - t.Fatalf("unexpectedly got a packet for group %s", addr) - } - if s { - t.Fatalf("already saw packet for group %s", addr) - } - seen[addr] = true - return addr + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToExcludeMode, + MulticastAddress: addr, + Sources: nil, + }, + }, + }) + }, + validateLeave: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { + t.Helper() + + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToIncludeMode, + MulticastAddress: addr, + Sources: nil, + }, + }, + }) + }, + checkStats: iptestutil.CheckMLDv2Stats, + getAndCheckGroupAddress: getAndCheckMLDv2MulticastAddress, + }, }, - checkInitialGroups: checkInitialIPv6Groups, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) - defer ctx.cleanup() - s, e, clock := ctx.s, ctx.e, ctx.clock + for _, subTest := range test.subTests { + t.Run(subTest.name, func(t *testing.T) { + ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) + defer ctx.cleanup() + s, e, clock := ctx.s, ctx.e, ctx.clock - var reportCounter uint64 - var leaveCounter uint64 - if test.checkInitialGroups != nil { - reportCounter, leaveCounter = test.checkInitialGroups(t, e, s, clock) - } - - sentReportStat := test.sentReportStat(s) - for _, a := range test.multicastAddrs { - if err := s.JoinGroup(test.protoNum, nicID, a); err != nil { - t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, a, err) - } - reportCounter++ - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("got sentReportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatalf("expected a report message to be sent for %s", a) - } else { - test.validateReport(t, p, a) - p.DecRef() - } - } - if t.Failed() { - t.FailNow() - } - - // Leave messages should be sent for the joined groups when the NIC is - // disabled. - if err := s.DisableNIC(nicID); err != nil { - t.Fatalf("DisableNIC(%d): %s", nicID, err) - } - sentLeaveStat := test.sentLeaveStat(s) - leaveCounter += uint64(len(test.multicastAddrs)) - if got := sentLeaveStat.Value(); got != leaveCounter { - t.Errorf("got sentLeaveStat.Value() = %d, want = %d", got, leaveCounter) - } - { - seen := make(map[tcpip.Address]bool) - for _, a := range test.multicastAddrs { - seen[a] = false - } - - for i := range test.multicastAddrs { - p := e.Read() - if p.IsNil() { - t.Fatalf("expected (%d-th) leave message to be sent", i) + var reportCounter uint64 + var leaveCounter uint64 + var reportV2Counter uint64 + if test.checkInitialGroups != nil { + reportV2Counter = test.checkInitialGroups(t, e, s, clock) } - test.validateLeave(t, p, test.getAndCheckGroupAddress(t, seen, p)) - p.DecRef() - } - } - if t.Failed() { - t.FailNow() - } + subTest.enterVersion(e) - // Reports should be sent for the joined groups when the NIC is enabled. - if err := s.EnableNIC(nicID); err != nil { - t.Fatalf("EnableNIC(%d): %s", nicID, err) - } - reportCounter += uint64(len(test.multicastAddrs)) - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("got sentReportStat.Value() = %d, want = %d", got, reportCounter) - } - { - seen := make(map[tcpip.Address]bool) - for _, a := range test.multicastAddrs { - seen[a] = false - } - - for i := range test.multicastAddrs { - p := e.Read() - if p.IsNil() { - t.Fatalf("expected (%d-th) report message to be sent", i) + for _, a := range test.multicastAddrs { + if err := s.JoinGroup(test.protoNum, nicID, a); err != nil { + t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, a, err) + } + reportCounter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatalf("expected a report message to be sent for %s", a) + } else { + subTest.validateReport(t, p, a) + p.DecRef() + } + } + if t.Failed() { + t.FailNow() } - test.validateReport(t, p, test.getAndCheckGroupAddress(t, seen, p)) - p.DecRef() - } - } - if t.Failed() { - t.FailNow() - } + // Leave messages should be sent for the joined groups when the NIC is + // disabled. + if err := s.DisableNIC(nicID); err != nil { + t.Fatalf("DisableNIC(%d): %s", nicID, err) + } + leaveCounter += uint64(len(test.multicastAddrs)) + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + { + seen := make(map[tcpip.Address]bool) + for _, a := range test.multicastAddrs { + seen[a] = false + } - // Joining/leaving a group while disabled should not send any messages. - if err := s.DisableNIC(nicID); err != nil { - t.Fatalf("DisableNIC(%d): %s", nicID, err) - } - leaveCounter += uint64(len(test.multicastAddrs)) - if got := sentLeaveStat.Value(); got != leaveCounter { - t.Errorf("got sentLeaveStat.Value() = %d, want = %d", got, leaveCounter) - } - for i := range test.multicastAddrs { - if p := e.Read(); p.IsNil() { - t.Fatalf("expected (%d-th) leave message to be sent", i) - } else { - p.DecRef() - } - } - for _, a := range test.multicastAddrs { - if err := s.LeaveGroup(test.protoNum, nicID, a); err != nil { - t.Fatalf("LeaveGroup(%d, nic, %s): %s", test.protoNum, a, err) - } - if got := sentLeaveStat.Value(); got != leaveCounter { - t.Errorf("got sentLeaveStat.Value() = %d, want = %d", got, leaveCounter) - } - if p := e.Read(); !p.IsNil() { - t.Fatalf("leaving group %s on disabled NIC sent unexpected packet = %#v", a, p) - } - } - if err := s.JoinGroup(test.protoNum, nicID, test.finalMulticastAddr); err != nil { - t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, test.finalMulticastAddr, err) - } - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("got sentReportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); !p.IsNil() { - t.Fatalf("joining group %s on disabled NIC sent unexpected packet = %#v", test.finalMulticastAddr, p) - } + for i := range test.multicastAddrs { + p := e.Read() + if p.IsNil() { + t.Fatalf("expected (%d-th) leave message to be sent", i) + } - // A report should only be sent for the group we last joined after - // enabling the NIC since the original groups were all left. - if err := s.EnableNIC(nicID); err != nil { - t.Fatalf("EnableNIC(%d): %s", nicID, err) - } - reportCounter++ - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("got sentReportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - test.validateReport(t, p, test.finalMulticastAddr) - p.DecRef() - } + subTest.validateLeave(t, p, subTest.getAndCheckGroupAddress(t, seen, p)) + p.DecRef() + } + } + if t.Failed() { + t.FailNow() + } - clock.Advance(test.maxUnsolicitedResponseDelay) - reportCounter++ - if got := sentReportStat.Value(); got != reportCounter { - t.Errorf("got sentReportStat.Value() = %d, want = %d", got, reportCounter) - } - if p := e.Read(); p.IsNil() { - t.Fatal("expected a report message to be sent") - } else { - test.validateReport(t, p, test.finalMulticastAddr) - p.DecRef() - } + // Reports should be sent for the joined groups when the NIC is enabled. + if err := s.EnableNIC(nicID); err != nil { + t.Fatalf("EnableNIC(%d): %s", nicID, err) + } + reportV2Counter += uint64(len(test.multicastAddrs)) + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + { + seen := make(map[tcpip.Address]bool) + for _, a := range test.multicastAddrs { + seen[a] = false + } - // Should not send any more packets. - clock.Advance(time.Hour) - if p := e.Read(); !p.IsNil() { - t.Fatalf("sent unexpected packet = %#v", p) + for i := range test.multicastAddrs { + p := e.Read() + if p.IsNil() { + t.Fatalf("expected (%d-th) report message to be sent", i) + } + + test.validateReport(t, p, test.getAndCheckGroupAddress(t, seen, p)) + p.DecRef() + } + } + if t.Failed() { + t.FailNow() + } + + // Joining/leaving a group while disabled should not send any messages. + if err := s.DisableNIC(nicID); err != nil { + t.Fatalf("DisableNIC(%d): %s", nicID, err) + } + reportV2Counter += uint64(len(test.multicastAddrs)) + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + for i := range test.multicastAddrs { + if p := e.Read(); p.IsNil() { + t.Fatalf("expected (%d-th) leave message to be sent", i) + } else { + p.DecRef() + } + } + for _, a := range test.multicastAddrs { + if err := s.LeaveGroup(test.protoNum, nicID, a); err != nil { + t.Fatalf("LeaveGroup(%d, nic, %s): %s", test.protoNum, a, err) + } + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); !p.IsNil() { + t.Fatalf("leaving group %s on disabled NIC sent unexpected packet = %#v", a, p) + } + } + if err := s.JoinGroup(test.protoNum, nicID, test.finalMulticastAddr); err != nil { + t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, test.finalMulticastAddr, err) + } + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); !p.IsNil() { + t.Fatalf("joining group %s on disabled NIC sent unexpected packet = %#v", test.finalMulticastAddr, p) + } + + // A report should only be sent for the group we last joined after + // enabling the NIC since the original groups were all left. + if err := s.EnableNIC(nicID); err != nil { + t.Fatalf("EnableNIC(%d): %s", nicID, err) + } + reportV2Counter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + test.validateReport(t, p, test.finalMulticastAddr) + p.DecRef() + } + + clock.Advance(test.maxUnsolicitedResponseDelay) + reportV2Counter++ + subTest.checkStats(t, s, reportCounter, leaveCounter, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + test.validateReport(t, p, test.finalMulticastAddr) + p.DecRef() + } + + // Should not send any more packets. + clock.Advance(time.Hour) + if p := e.Read(); !p.IsNil() { + t.Fatalf("sent unexpected packet = %#v", p) + } + }) } }) } @@ -1345,3 +1940,287 @@ func TestMGPDisabledOnLoopback(t *testing.T) { }) } } + +func TestMGPCoalescedQueryResponseRecords(t *testing.T) { + const ( + extraGroups = 1 + igmpv3MLDv2ReportRecordHeaderLen = 4 + ) + + type subTest struct { + name string + enterVersion func(e *channel.Endpoint) + validateReport func(*testing.T, stack.PacketBufferPtr) + checkStats func(*testing.T, *stack.Stack, uint64, uint64, uint64) + } + + genAddr := func(bytes []byte, i uint16) tcpip.Address { + bytes[len(bytes)-1] = byte(i & 0xFF) + bytes[len(bytes)-2] = byte(i >> 8) + return tcpip.Address(bytes[:]) + } + + calcMaxRecordsPerMessage := func(hdrLen, recordLen uint16) uint16 { + return (header.IPv6MinimumMTU - hdrLen) / recordLen + } + + tests := []struct { + name string + protoNum tcpip.NetworkProtocolNumber + maxUnsolicitedResponseDelay time.Duration + receivedQueryStat func(*stack.Stack) *tcpip.StatCounter + checkInitialGroups func(*testing.T, *channel.Endpoint, *stack.Stack, *faketime.ManualClock) uint64 + validateReport func(*testing.T, stack.PacketBufferPtr, tcpip.Address) + checkStats func(*testing.T, *stack.Stack, uint64) + genAddr func(uint16) tcpip.Address + maxRecordsPerMessage uint16 + rxQuery func(*channel.Endpoint, uint8) + validateReportWithMultipleRecords func(*testing.T, map[tcpip.Address]bool, stack.PacketBufferPtr, uint16) + }{ + { + name: "IGMP", + protoNum: ipv4.ProtocolNumber, + maxUnsolicitedResponseDelay: ipv4.UnsolicitedReportIntervalMax, + receivedQueryStat: func(s *stack.Stack) *tcpip.StatCounter { + return s.Stats().IGMP.PacketsReceived.MembershipQuery + }, + validateReport: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { + t.Helper() + + validateIGMPv3ReportPacket(t, p, header.IGMPv3ReportSerializer{ + Records: []header.IGMPv3ReportGroupAddressRecordSerializer{ + { + RecordType: header.IGMPv3ReportRecordChangeToExcludeMode, + GroupAddress: addr, + Sources: nil, + }, + }, + }) + }, + checkStats: func(t *testing.T, s *stack.Stack, reports uint64) { + t.Helper() + iptestutil.CheckIGMPv3Stats(t, s, 0, 0, reports) + }, + genAddr: func(i uint16) tcpip.Address { + bytes := [header.IPv4AddressSize]byte{224, 1, 0, 0} + return genAddr(bytes[:], i) + }, + maxRecordsPerMessage: calcMaxRecordsPerMessage(header.IPv4MinimumSize+8 /* size of IGMPv3 report header */, igmpv3MLDv2ReportRecordHeaderLen+header.IPv4AddressSize), + rxQuery: func(e *channel.Endpoint, maxRespTime uint8) { + createAndInjectIGMPPacket(e, igmpMembershipQuery, maxRespTime, header.IPv4Any, header.IGMPv3QueryMinimumSize-header.IGMPQueryMinimumSize /* extraLength */) + }, + validateReportWithMultipleRecords: func(t *testing.T, seen map[tcpip.Address]bool, p stack.PacketBufferPtr, expectedRecords uint16) { + t.Helper() + + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + ipv4 := header.IPv4(payload.AsSlice()) + if got := tcpip.TransportProtocolNumber(ipv4.Protocol()); got != header.IGMPProtocolNumber { + t.Fatalf("got ipv4.Protocol() = %d, want = %d", got, header.IGMPProtocolNumber) + } + report := header.IGMPv3Report(ipv4.Payload()) + records := report.GroupAddressRecords() + for recordsCount := uint16(0); ; recordsCount++ { + record, res := records.Next() + switch res { + case header.IGMPv3ReportGroupAddressRecordIteratorNextOk: + case header.IGMPv3ReportGroupAddressRecordIteratorNextDone: + if recordsCount != expectedRecords { + t.Errorf("got recordsCount = %d, want = %d", recordsCount, expectedRecords) + } + return + default: + t.Fatalf("records.Next(): %d", res) + } + + if res != header.IGMPv3ReportGroupAddressRecordIteratorNextOk { + t.Fatalf("got records.Next() = %d, want = %d", res, header.IGMPv3ReportGroupAddressRecordIteratorNextOk) + } + addr := record.GroupAddress() + + if s, ok := seen[addr]; !ok { + t.Fatalf("unexpectedly got a packet for group %s", addr) + } else if s { + t.Fatalf("already saw packet for group %s", addr) + } + seen[addr] = true + } + }, + }, + { + name: "MLD", + protoNum: ipv6.ProtocolNumber, + maxUnsolicitedResponseDelay: ipv6.UnsolicitedReportIntervalMax, + receivedQueryStat: func(s *stack.Stack) *tcpip.StatCounter { + return s.Stats().ICMP.V6.PacketsReceived.MulticastListenerQuery + }, + checkInitialGroups: checkInitialIPv6Groups, + validateReport: func(t *testing.T, p stack.PacketBufferPtr, addr tcpip.Address) { + t.Helper() + + validateMLDv2ReportPacket(t, p, header.MLDv2ReportSerializer{ + Records: []header.MLDv2ReportMulticastAddressRecordSerializer{ + { + RecordType: header.MLDv2ReportRecordChangeToExcludeMode, + MulticastAddress: addr, + Sources: nil, + }, + }, + }) + }, + checkStats: func(t *testing.T, s *stack.Stack, reports uint64) { + t.Helper() + iptestutil.CheckMLDv2Stats(t, s, 0, 0, reports) + }, + genAddr: func(i uint16) tcpip.Address { + bytes := [header.IPv6AddressSize]byte{0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0} + return genAddr(bytes[:], i) + }, + maxRecordsPerMessage: calcMaxRecordsPerMessage(header.IPv6MinimumSize+8 /* size of MLDv2 report header */, igmpv3MLDv2ReportRecordHeaderLen+header.IPv6AddressSize), + rxQuery: func(e *channel.Endpoint, maxRespTime uint8) { + createAndInjectMLDPacket(e, mldQuery, maxRespTime, header.IPv6Any, header.MLDv2QueryMinimumSize-header.MLDMinimumSize /* extraLength */) + }, + validateReportWithMultipleRecords: func(t *testing.T, seen map[tcpip.Address]bool, p stack.PacketBufferPtr, expectedRecords uint16) { + t.Helper() + + payload := stack.PayloadSince(p.NetworkHeader()) + defer payload.Release() + ipv6 := header.IPv6(payload.AsSlice()) + + ipv6HeaderIter := header.MakeIPv6PayloadIterator( + header.IPv6ExtensionHeaderIdentifier(ipv6.NextHeader()), + bufferv2.MakeWithData(ipv6.Payload()), + ) + + var transport header.IPv6RawPayloadHeader + for { + h, done, err := ipv6HeaderIter.Next() + if err != nil { + t.Fatalf("ipv6HeaderIter.Next(): %s", err) + } + if done { + t.Fatalf("ipv6HeaderIter.Next() = (%T, %t, _), want = (_, false, _)", h, done) + } + defer h.Release() + if t, ok := h.(header.IPv6RawPayloadHeader); ok { + transport = t + break + } + } + + if got := tcpip.TransportProtocolNumber(transport.Identifier); got != header.ICMPv6ProtocolNumber { + t.Fatalf("got ipv6.NextHeader() = %d, want = %d", got, header.ICMPv6ProtocolNumber) + } + icmpv6 := header.ICMPv6(transport.Buf.Flatten()) + if got := icmpv6.Type(); got != header.ICMPv6MulticastListenerV2Report { + t.Fatalf("got icmpv6.Type() = %d, want = %d", got, header.ICMPv6MulticastListenerV2Report) + } + + report := header.MLDv2Report(icmpv6.MessageBody()) + records := report.MulticastAddressRecords() + for recordsCount := uint16(0); ; recordsCount++ { + record, res := records.Next() + switch res { + case header.MLDv2ReportMulticastAddressRecordIteratorNextOk: + case header.MLDv2ReportMulticastAddressRecordIteratorNextDone: + if recordsCount != expectedRecords { + t.Errorf("got recordsCount = %d, want = %d", recordsCount, expectedRecords) + } + return + default: + t.Fatalf("records.Next(): %d", res) + } + + addr := record.MulticastAddress() + + s, ok := seen[addr] + if !ok { + t.Fatalf("unexpectedly got a packet for group %s", addr) + } + if s { + t.Fatalf("already saw packet for group %s", addr) + } + seen[addr] = true + } + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := newMulticastTestContext(t, test.protoNum == ipv4.ProtocolNumber /* v4 */, true /* mgpEnabled */) + defer ctx.cleanup() + s, e, clock := ctx.s, ctx.e, ctx.clock + + var reportV2Counter uint64 + if test.checkInitialGroups != nil { + reportV2Counter = test.checkInitialGroups(t, e, s, clock) + } + + seen := make(map[tcpip.Address]bool) + for i := uint16(0); i < test.maxRecordsPerMessage+extraGroups; i++ { + addr := test.genAddr(i) + seen[addr] = false + + if err := s.JoinGroup(test.protoNum, nicID, addr); err != nil { + t.Fatalf("JoinGroup(%d, %d, %s): %s", test.protoNum, nicID, addr, err) + } + reportV2Counter++ + test.checkStats(t, s, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + test.validateReport(t, p, addr) + p.DecRef() + } + if t.Failed() { + t.FailNow() + } + + // Verify the second report is sent by the maximum unsolicited response + // interval. + p := e.Read() + if !p.IsNil() { + t.Fatalf("sent unexpected packet, expected report only after advancing the clock = %#v", p) + } + clock.Advance(test.maxUnsolicitedResponseDelay) + reportV2Counter++ + test.checkStats(t, s, reportV2Counter) + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + test.validateReport(t, p, addr) + p.DecRef() + } + } + + // Should not send any more packets. + clock.Advance(time.Hour) + if p := e.Read(); !p.IsNil() { + t.Fatalf("sent unexpected packet = %#v", p) + } + test.checkStats(t, s, reportV2Counter) + + // Receive a query which should send a few reports which together hold + // records for all the groups we joined. + test.rxQuery(e, 1) + clock.Advance(time.Second) + reportV2Counter += 2 + test.checkStats(t, s, reportV2Counter) + for _, expectedRecords := range []uint16{test.maxRecordsPerMessage, extraGroups} { + if p := e.Read(); p.IsNil() { + t.Fatal("expected a report message to be sent") + } else { + test.validateReportWithMultipleRecords(t, seen, p, expectedRecords) + p.DecRef() + } + } + + for addr, seen := range seen { + if !seen { + t.Errorf("got seen[%s] = false, want = true", addr) + } + } + }) + } +} diff --git a/pkg/tcpip/tcpip.go b/pkg/tcpip/tcpip.go index e4c8d666b..abe31d7d9 100644 --- a/pkg/tcpip/tcpip.go +++ b/pkg/tcpip/tcpip.go @@ -1563,6 +1563,10 @@ type ICMPv6PacketStats struct { // counted. MulticastListenerReport *StatCounter + // MulticastListenerReportV2 is the number of Multicast Listener Report + // messages counted. + MulticastListenerReportV2 *StatCounter + // MulticastListenerDone is the number of Multicast Listener Done messages // counted. MulticastListenerDone *StatCounter @@ -1643,6 +1647,10 @@ type IGMPPacketStats struct { // counted. V2MembershipReport *StatCounter + // V3MembershipReport is the number of Version 3 Membership Report messages + // counted. + V3MembershipReport *StatCounter + // LeaveGroup is the number of Leave Group messages counted. LeaveGroup *StatCounter diff --git a/pkg/tcpip/tests/integration/link_resolution_test.go b/pkg/tcpip/tests/integration/link_resolution_test.go index 887227f7c..53dfe2700 100644 --- a/pkg/tcpip/tests/integration/link_resolution_test.go +++ b/pkg/tcpip/tests/integration/link_resolution_test.go @@ -1761,7 +1761,19 @@ func TestUpdateCachedNeighborEntry(t *testing.T) { host2NICID = 4 ) stackOpts := stack.Options{ - NetworkProtocols: []stack.NetworkProtocolFactory{arp.NewProtocol, ipv4.NewProtocol, ipv6.NewProtocol}, + NetworkProtocols: []stack.NetworkProtocolFactory{ + arp.NewProtocol, + ipv4.NewProtocolWithOptions(ipv4.Options{ + IGMP: ipv4.IGMPOptions{ + Enabled: false, + }, + }), + ipv6.NewProtocolWithOptions(ipv6.Options{ + MLD: ipv6.MLDOptions{ + Enabled: false, + }, + }), + }, TransportProtocols: []stack.TransportProtocolFactory{udp.NewProtocol}, }