From 732e98985546958fdab8ae3d49e36f17ae89f71c Mon Sep 17 00:00:00 2001 From: Ghanan Gowripalan Date: Tue, 24 Nov 2020 11:48:09 -0800 Subject: Extract IGMPv2 core state machine The IGMPv2 core state machine can be shared with MLDv1 since they are almost identical, ignoring specific addresses, constants and packets. Bug #4682, #4861 PiperOrigin-RevId: 344102615 --- pkg/tcpip/network/ipv4/BUILD | 1 + pkg/tcpip/network/ipv4/igmp.go | 248 ++++++++++++----------------------------- 2 files changed, 72 insertions(+), 177 deletions(-) (limited to 'pkg/tcpip/network/ipv4') diff --git a/pkg/tcpip/network/ipv4/BUILD b/pkg/tcpip/network/ipv4/BUILD index 68b1ea1cd..32f53f217 100644 --- a/pkg/tcpip/network/ipv4/BUILD +++ b/pkg/tcpip/network/ipv4/BUILD @@ -18,6 +18,7 @@ go_library( "//pkg/tcpip/header/parse", "//pkg/tcpip/network/fragmentation", "//pkg/tcpip/network/hash", + "//pkg/tcpip/network/ip", "//pkg/tcpip/stack", ], ) diff --git a/pkg/tcpip/network/ipv4/igmp.go b/pkg/tcpip/network/ipv4/igmp.go index 18fe2fd2f..c9bf117de 100644 --- a/pkg/tcpip/network/ipv4/igmp.go +++ b/pkg/tcpip/network/ipv4/igmp.go @@ -17,11 +17,13 @@ package ipv4 import ( "fmt" "sync" + "sync/atomic" "time" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/buffer" "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/network/ip" "gvisor.dev/gvisor/pkg/tcpip/stack" ) @@ -29,7 +31,7 @@ const ( // igmpV1PresentDefault is the initial state for igmpV1Present in the // igmpState. As per RFC 2236 Page 9 says "No IGMPv1 Router Present ... is // the initial state." - igmpV1PresentDefault = false + igmpV1PresentDefault = 0 // v1RouterPresentTimeout from RFC 2236 Section 8.11, Page 18 // See note on igmpState.igmpV1Present for more detail. @@ -49,6 +51,8 @@ const ( UnsolicitedReportIntervalMax = 10 * time.Second ) +var _ ip.MulticastGroupProtocol = (*igmpState)(nil) + // igmpState is the per-interface IGMP state. // // igmpState.init() MUST be called after creating an IGMP state. @@ -56,22 +60,23 @@ type igmpState struct { // The IPv4 endpoint this igmpState is for. ep *endpoint + // igmpV1Present is for maintaining compatibility with IGMPv1 Routers, from + // RFC 2236 Section 4 Page 6: "The IGMPv1 router expects Version 1 + // Membership Reports in response to its Queries, and will not pay + // attention to Version 2 Membership Reports. Therefore, a state variable + // MUST be kept for each interface, describing whether the multicast + // Querier on that interface is running IGMPv1 or IGMPv2. This variable + // MUST be based upon whether or not an IGMPv1 query was heard in the last + // [Version 1 Router Present Timeout] seconds". + // + // Must be accessed with atomic operations. Holds a value of 1 when true, 0 + // when false. + igmpV1Present uint32 + mu struct { sync.RWMutex - // memberships contains the map of host groups to their state, timer, and - // flag info. - memberships map[tcpip.Address]membershipInfo - - // igmpV1Present is for maintaining compatibility with IGMPv1 Routers, from - // RFC 2236 Section 4 Page 6: "The IGMPv1 router expects Version 1 - // Membership Reports in response to its Queries, and will not pay - // attention to Version 2 Membership Reports. Therefore, a state variable - // MUST be kept for each interface, describing whether the multicast - // Querier on that interface is running IGMPv1 or IGMPv2. This variable - // MUST be based upon whether or not an IGMPv1 query was heard in the last - // [Version 1 Router Present Timeout] seconds" - igmpV1Present bool + genericMulticastProtocol ip.GenericMulticastProtocolState // igmpV1Job is scheduled when this interface receives an IGMPv1 style // message, upon expiration the igmpV1Present flag is cleared. @@ -80,43 +85,26 @@ type igmpState struct { } } -// membershipInfo holds the IGMPv2 state for a particular multicast address. -type membershipInfo struct { - // state contains the current IGMP state for this member. - state hostState - - // lastToSendReport is true if this was "the last host to send a report from - // this group." - // RFC 2236, Section 6, Page 9. This is used to track whether or not there - // are other hosts on this subnet that belong to this group - RFC 2236 - // Section 3, Page 5. - lastToSendReport bool - - // delayedReportJob is used to delay sending responses to IGMP messages in - // order to reduce duplicate reports from multiple hosts on the interface. - // Must not be nil. - delayedReportJob *tcpip.Job +// SendReport implements ip.MulticastGroupProtocol. +func (igmp *igmpState) SendReport(groupAddress tcpip.Address) *tcpip.Error { + igmpType := header.IGMPv2MembershipReport + if igmp.v1Present() { + igmpType = header.IGMPv1MembershipReport + } + return igmp.writePacket(groupAddress, groupAddress, igmpType) } -type hostState int - -// From RFC 2236, Section 6, Page 7. -const ( - // "'Non-Member' state, when the host does not belong to the group on - // the interface. This is the initial state for all memberships on - // all network interfaces; it requires no storage in the host." - _ hostState = iota - - // delayingMember is the "'Delaying Member' state, when the host belongs to - // the group on the interface and has a report delay timer running for that - // membership." - delayingMember - - // idleMember is the "Idle Member" state, when the host belongs to the group - // on the interface and does not have a report delay timer running for that - // membership. - idleMember -) +// SendLeave implements ip.MulticastGroupProtocol. +func (igmp *igmpState) SendLeave(groupAddress tcpip.Address) *tcpip.Error { + // As per RFC 2236 Section 6, Page 8: "If the interface state says the + // Querier is running IGMPv1, this action SHOULD be skipped. If the flag + // saying we were the last host to report is cleared, this action MAY be + // skipped." + if igmp.v1Present() { + return nil + } + return igmp.writePacket(header.IPv4AllRoutersGroup, groupAddress, header.IGMPLeaveGroup) +} // init sets up an igmpState struct, and is required to be called before using // a new igmpState. @@ -124,10 +112,10 @@ func (igmp *igmpState) init(ep *endpoint) { igmp.mu.Lock() defer igmp.mu.Unlock() igmp.ep = ep - igmp.mu.memberships = make(map[tcpip.Address]membershipInfo) - igmp.mu.igmpV1Present = igmpV1PresentDefault + igmp.mu.genericMulticastProtocol.Init(ep.protocol.stack.Rand(), ep.protocol.stack.Clock(), igmp, UnsolicitedReportIntervalMax) + igmp.igmpV1Present = igmpV1PresentDefault igmp.mu.igmpV1Job = igmp.ep.protocol.stack.NewJob(&igmp.mu, func() { - igmp.mu.igmpV1Present = false + igmp.setV1Present(false) }) } @@ -188,6 +176,18 @@ func (igmp *igmpState) handleIGMP(pkt *stack.PacketBuffer) { } } +func (igmp *igmpState) v1Present() bool { + return atomic.LoadUint32(&igmp.igmpV1Present) == 1 +} + +func (igmp *igmpState) setV1Present(v bool) { + if v { + atomic.StoreUint32(&igmp.igmpV1Present, 1) + } else { + atomic.StoreUint32(&igmp.igmpV1Present, 0) + } +} + func (igmp *igmpState) handleMembershipQuery(groupAddress tcpip.Address, maxRespTime time.Duration) { igmp.mu.Lock() defer igmp.mu.Unlock() @@ -198,56 +198,22 @@ func (igmp *igmpState) handleMembershipQuery(groupAddress tcpip.Address, maxResp if maxRespTime == 0 { igmp.mu.igmpV1Job.Cancel() igmp.mu.igmpV1Job.Schedule(v1RouterPresentTimeout) - igmp.mu.igmpV1Present = true + igmp.setV1Present(true) maxRespTime = v1MaxRespTime } - // IPv4Any is the General Query Address. - if groupAddress == header.IPv4Any { - for membershipAddress, info := range igmp.mu.memberships { - igmp.setDelayTimerForAddressRLocked(membershipAddress, &info, maxRespTime) - igmp.mu.memberships[membershipAddress] = info - } - } else if info, ok := igmp.mu.memberships[groupAddress]; ok { - igmp.setDelayTimerForAddressRLocked(groupAddress, &info, maxRespTime) - igmp.mu.memberships[groupAddress] = info - } -} - -// setDelayTimerForAddressRLocked modifies the passed info only and does not -// modify IGMP state directly. -// -// Precondition: igmp.mu MUST be read locked. -func (igmp *igmpState) setDelayTimerForAddressRLocked(groupAddress tcpip.Address, info *membershipInfo, maxRespTime time.Duration) { - if info.state == delayingMember { - // As per RFC 2236 Section 3, page 3: "If a timer for the group is already - // running, it is reset to the random value only if the requested Max - // Response Time is less than the remaining value of the running timer. - // TODO: Reset the timer if time remaining is greater than maxRespTime. - return - } - info.state = delayingMember - info.delayedReportJob.Cancel() - info.delayedReportJob.Schedule(igmp.calculateDelayTimerDuration(maxRespTime)) + igmp.mu.genericMulticastProtocol.HandleQuery(groupAddress, maxRespTime) } func (igmp *igmpState) handleMembershipReport(groupAddress tcpip.Address) { igmp.mu.Lock() defer igmp.mu.Unlock() - - // As per RFC 2236 Section 3, pages 3-4: "If the host receives another host's - // Report (version 1 or 2) while it has a timer running, it stops its timer - // for the specified group and does not send a Report" - if info, ok := igmp.mu.memberships[groupAddress]; ok { - info.delayedReportJob.Cancel() - info.lastToSendReport = false - igmp.mu.memberships[groupAddress] = info - } + igmp.mu.genericMulticastProtocol.HandleReport(groupAddress) } // writePacket assembles and sends an IGMP packet with the provided fields, // incrementing the provided stat counter on success. -func (igmp *igmpState) writePacket(destAddress tcpip.Address, groupAddress tcpip.Address, igmpType header.IGMPType) { +func (igmp *igmpState) writePacket(destAddress tcpip.Address, groupAddress tcpip.Address, igmpType header.IGMPType) *tcpip.Error { igmpData := header.IGMP(buffer.NewView(header.IGMPReportMinimumSize)) igmpData.SetType(igmpType) igmpData.SetGroupAddress(groupAddress) @@ -275,56 +241,19 @@ func (igmp *igmpState) writePacket(destAddress tcpip.Address, groupAddress tcpip sent := igmp.ep.protocol.stack.Stats().IGMP.PacketsSent if err := igmp.ep.nic.WritePacketToRemote(header.EthernetAddressFromMulticastIPv4Address(destAddress), nil /* gso */, header.IPv4ProtocolNumber, pkt); err != nil { sent.Dropped.Increment() - } else { - switch igmpType { - case header.IGMPv1MembershipReport: - sent.V1MembershipReport.Increment() - case header.IGMPv2MembershipReport: - sent.V2MembershipReport.Increment() - case header.IGMPLeaveGroup: - sent.LeaveGroup.Increment() - default: - panic(fmt.Sprintf("unrecognized igmp type = %d", igmpType)) - } + return err } -} - -// sendReport sends a Host Membership Report in response to a query or after -// this host joins a new group on this interface. -// -// Precondition: igmp.mu MUST be locked. -func (igmp *igmpState) sendReportLocked(groupAddress tcpip.Address) { - igmpType := header.IGMPv2MembershipReport - if igmp.mu.igmpV1Present { - igmpType = header.IGMPv1MembershipReport - } - igmp.writePacket(groupAddress, groupAddress, igmpType) - - // Update the state of the membership for this group. If the group no longer - // exists, do nothing since this report must have been a race with a remove - // or is in the process of being added. - info, ok := igmp.mu.memberships[groupAddress] - if !ok { - return - } - info.state = idleMember - info.lastToSendReport = true - igmp.mu.memberships[groupAddress] = info -} - -// sendLeave sends a Leave Group report to the IPv4 All Routers Group. -// -// Precondition: igmp.mu MUST be read locked. -func (igmp *igmpState) sendLeaveRLocked(groupAddress tcpip.Address) { - // As per RFC 2236 Section 6, Page 8: "If the interface state says the - // Querier is running IGMPv1, this action SHOULD be skipped. If the flag - // saying we were the last host to report is cleared, this action MAY be - // skipped." - if igmp.mu.igmpV1Present || !igmp.mu.memberships[groupAddress].lastToSendReport { - return + switch igmpType { + case header.IGMPv1MembershipReport: + sent.V1MembershipReport.Increment() + case header.IGMPv2MembershipReport: + sent.V2MembershipReport.Increment() + case header.IGMPLeaveGroup: + sent.LeaveGroup.Increment() + default: + panic(fmt.Sprintf("unrecognized igmp type = %d", igmpType)) } - - igmp.writePacket(header.IPv4AllRoutersGroup, groupAddress, header.IGMPLeaveGroup) + return nil } // joinGroup handles adding a new group to the membership map, setting up the @@ -336,28 +265,11 @@ func (igmp *igmpState) sendLeaveRLocked(groupAddress tcpip.Address) { func (igmp *igmpState) joinGroup(groupAddress tcpip.Address) *tcpip.Error { igmp.mu.Lock() defer igmp.mu.Unlock() - if _, ok := igmp.mu.memberships[groupAddress]; ok { - // The group already exists in the membership map. - return tcpip.ErrDuplicateAddress - } - info := membershipInfo{ - // There isn't a Job scheduled currently, so it's just idle. - state: idleMember, - // Joining a group immediately sends a report. - lastToSendReport: true, - delayedReportJob: igmp.ep.protocol.stack.NewJob(&igmp.mu, func() { - igmp.sendReportLocked(groupAddress) - }), + // JoinGroup returns false if we have already joined the group. + if !igmp.mu.genericMulticastProtocol.JoinGroup(groupAddress) { + return tcpip.ErrDuplicateAddress } - - // As per RFC 2236 Section 3, Page 5: "When a host joins a multicast group, - // it should immediately transmit an unsolicited Version 2 Membership Report - // for that group" ... "it is recommended that it be repeated" - igmp.sendReportLocked(groupAddress) - igmp.setDelayTimerForAddressRLocked(groupAddress, &info, UnsolicitedReportIntervalMax) - igmp.mu.memberships[groupAddress] = info - return nil } @@ -370,23 +282,5 @@ func (igmp *igmpState) joinGroup(groupAddress tcpip.Address) *tcpip.Error { func (igmp *igmpState) leaveGroup(groupAddress tcpip.Address) { igmp.mu.Lock() defer igmp.mu.Unlock() - info, ok := igmp.mu.memberships[groupAddress] - if !ok { - return - } - - // Clean up the state of the group before sending the leave message and - // removing it from the map. - info.delayedReportJob.Cancel() - info.state = idleMember - igmp.mu.memberships[groupAddress] = info - - igmp.sendLeaveRLocked(groupAddress) - delete(igmp.mu.memberships, groupAddress) -} - -// RFC 2236 Section 3, Page 3: The response time is set to a "random value... -// selected from the range (0, Max Response Time]". -func (igmp *igmpState) calculateDelayTimerDuration(maxRespTime time.Duration) time.Duration { - return time.Duration(igmp.ep.protocol.stack.Rand().Int63n(int64(maxRespTime))) + igmp.mu.genericMulticastProtocol.LeaveGroup(groupAddress) } -- cgit v1.2.3