mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Drop state labels in IGMP/MLD
This change removes the explicit `state`/`hostState` fields from the generic multicast host state and instead uses other readily available or other newly introduced fields that more explicitly describe what actions are pending for a host's group membership. 1) `delayedReportJobFiresAt` already lets us know if we are in the "delaying" member state/have a timer scheduled. 2) `transmissionLeft` lets us know if we have pending reports to send. This prepares us to use the same state machine to support both MLDv1/IGMPv2 and MLDv2/IGMPv3. Note that MLDv2/IGMPv3 do not use the same "state machine" as MLDv1/IGMPv2. Specifically, states like "Non-Listener/Member", "Delaying Listener/Member" and "Idle Listener/Member" do not exist in MLDv2/IGMPv3. PiperOrigin-RevId: 495386469
This commit is contained in:
committed by
gVisor bot
parent
92e25882bc
commit
f68e7dbd32
@@ -23,93 +23,61 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
)
|
||||
|
||||
// hostState is the state a host may be in for a multicast group.
|
||||
type hostState int
|
||||
|
||||
// The states below are generic across IGMPv2 (RFC 2236 section 6) and MLDv1
|
||||
// (RFC 2710 section 5). Even though the states are generic across both IGMPv2
|
||||
// and MLDv1, IGMPv2 terminology will be used.
|
||||
//
|
||||
// ______________receive query______________
|
||||
// | |
|
||||
// | _____send or receive report_____ |
|
||||
// | | | |
|
||||
// V | V |
|
||||
// +-------+ +-----------+ +------------+ +-------------------+ +--------+ |
|
||||
// | Non-M | | Pending-M | | Delaying-M | | Queued Delaying-M | | Idle-M | -
|
||||
// +-------+ +-----------+ +------------+ +-------------------+ +--------+
|
||||
// | ^ | ^ | ^ | ^
|
||||
// | | | | | | | |
|
||||
// ---------- ------- ---------- -------------
|
||||
// initialize new send inital fail to send send or receive
|
||||
// group membership report delayed report report
|
||||
//
|
||||
// Not shown in the diagram above, but any state may transition into the non
|
||||
// member state when a group is left.
|
||||
const (
|
||||
// nonMember is the "'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."
|
||||
// As per RFC 2236 section 3,
|
||||
//
|
||||
// 'Non-Listener' is the MLDv1 term used to describe this state.
|
||||
// When a host joins a multicast group, it should immediately transmit
|
||||
// an unsolicited Version 2 Membership Report for that group, in case it
|
||||
// is the first member of that group on the network. To cover the
|
||||
// possibility of the initial Membership Report being lost or damaged,
|
||||
// it is recommended that it be repeated once or twice after short
|
||||
// delays [Unsolicited Report Interval]. (A simple way to accomplish
|
||||
// this is to send the initial Version 2 Membership Report and then act
|
||||
// as if a Group-Specific Query was received for that group, and set a
|
||||
// timer appropriately).
|
||||
//
|
||||
// This state is used to keep track of groups that have been joined locally,
|
||||
// but without advertising the membership to the network.
|
||||
nonMember hostState = iota
|
||||
// As per RFC 2710 section 4,
|
||||
//
|
||||
// When a node starts listening to a multicast address on an interface,
|
||||
// it should immediately transmit an unsolicited Report for that address
|
||||
// on that interface, in case it is the first listener on the link. To
|
||||
// cover the possibility of the initial Report being lost or damaged, it
|
||||
// is recommended that it be repeated once or twice after short delays
|
||||
// [Unsolicited Report Interval]. (A simple way to accomplish this is
|
||||
// to send the initial Report and then act as if a Multicast-Address-
|
||||
// Specific Query was received for that address, and set a timer
|
||||
// appropriately).
|
||||
unsolicitedTransmissionCount = 2
|
||||
|
||||
// pendingMember is a newly joined member that is waiting to successfully send
|
||||
// the initial set of reports.
|
||||
// Responses to queries may be delayed, but we only send a response to a
|
||||
// query once. A response to a query can be handled by any pending
|
||||
// unsolicited transmission count, but we should send at least one report
|
||||
// after sending a query.
|
||||
//
|
||||
// This is not an RFC defined state; it is an implementation specific state to
|
||||
// track that the initial report needs to be sent.
|
||||
// As per RFC 2236 section 3,
|
||||
//
|
||||
// MAY NOT transition to the idle member state from this state.
|
||||
pendingMember
|
||||
|
||||
// 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."
|
||||
// When a host receives a General Query, it sets delay timers for each
|
||||
// group (excluding the all-systems group) of which it is a member on
|
||||
// the interface from which it received the query.
|
||||
//
|
||||
// 'Delaying Listener' is the MLDv1 term used to describe this state.
|
||||
delayingMember
|
||||
|
||||
// queuedDelayingMember is a delayingMember that failed to send a report after
|
||||
// its delayed report timer fired. Hosts in this state are waiting to attempt
|
||||
// retransmission of the delayed report.
|
||||
// As per RFC 2710 section 4,
|
||||
//
|
||||
// This is not an RFC defined state; it is an implementation specific state to
|
||||
// track that the delayed report needs to be sent.
|
||||
//
|
||||
// May transition to idle member if a report is received for a group.
|
||||
queuedDelayingMember
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 'Idle Listener' is the MLDv1 term used to describe this state.
|
||||
idleMember
|
||||
// When a node receives a General Query, it sets a delay timer for each
|
||||
// multicast address to which it is listening on the interface from
|
||||
// which it received the Query, EXCLUDING the link-scope all-nodes
|
||||
// address and any multicast addresses of scope 0 (reserved) or 1
|
||||
// (node-local).
|
||||
minQueryResponseTransmissionCount = 1
|
||||
)
|
||||
|
||||
func (s hostState) isDelayingMember() bool {
|
||||
switch s {
|
||||
case nonMember, pendingMember, idleMember:
|
||||
return false
|
||||
case delayingMember, queuedDelayingMember:
|
||||
return true
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized host state = %d", s))
|
||||
}
|
||||
}
|
||||
|
||||
// multicastGroupState holds the Generic Multicast Protocol state for a
|
||||
// multicast group.
|
||||
type multicastGroupState struct {
|
||||
// joins is the number of times the group has been joined.
|
||||
joins uint64
|
||||
|
||||
// state holds the host's state for the group.
|
||||
state hostState
|
||||
// transmissionLeft is the number of transmissions left to send.
|
||||
transmissionLeft uint
|
||||
|
||||
// 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
|
||||
@@ -135,6 +103,7 @@ type multicastGroupState struct {
|
||||
func (m *multicastGroupState) cancelDelayedReportJob() {
|
||||
m.delayedReportJob.Cancel()
|
||||
m.delayedReportJobFiresAt = time.Time{}
|
||||
m.transmissionLeft = 0
|
||||
}
|
||||
|
||||
// GenericMulticastProtocolOptions holds options for the generic multicast
|
||||
@@ -281,20 +250,10 @@ func (g *GenericMulticastProtocolState) InitializeGroupsLocked() {
|
||||
// Precondition: g.protocolMU must be locked.
|
||||
func (g *GenericMulticastProtocolState) SendQueuedReportsLocked() {
|
||||
for groupAddress, info := range g.memberships {
|
||||
switch info.state {
|
||||
case nonMember, delayingMember, idleMember:
|
||||
case pendingMember:
|
||||
// pendingMembers failed to send their initial unsolicited report so try
|
||||
// to send the report and queue the extra unsolicited reports.
|
||||
g.maybeSendInitialReportLocked(groupAddress, &info)
|
||||
case queuedDelayingMember:
|
||||
// queuedDelayingMembers failed to send their delayed reports so try to
|
||||
// send the report and transition them to the idle state.
|
||||
g.maybeSendDelayedReportLocked(groupAddress, &info)
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized host state = %d", info.state))
|
||||
if info.delayedReportJobFiresAt.IsZero() {
|
||||
g.maybeSendReportLocked(groupAddress, &info)
|
||||
g.memberships[groupAddress] = info
|
||||
}
|
||||
g.memberships[groupAddress] = info
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,9 +270,7 @@ func (g *GenericMulticastProtocolState) JoinGroupLocked(groupAddress tcpip.Addre
|
||||
|
||||
info := multicastGroupState{
|
||||
// Since we just joined the group, its count is 1.
|
||||
joins: 1,
|
||||
// The state will be updated below, if required.
|
||||
state: nonMember,
|
||||
joins: 1,
|
||||
lastToSendReport: false,
|
||||
delayedReportJob: tcpip.NewJob(g.opts.Clock, g.protocolMU, func() {
|
||||
if !g.opts.Protocol.Enabled() {
|
||||
@@ -325,7 +282,8 @@ func (g *GenericMulticastProtocolState) JoinGroupLocked(groupAddress tcpip.Addre
|
||||
panic(fmt.Sprintf("expected to find group state for group = %s", groupAddress))
|
||||
}
|
||||
|
||||
g.maybeSendDelayedReportLocked(groupAddress, &info)
|
||||
info.delayedReportJobFiresAt = time.Time{}
|
||||
g.maybeSendReportLocked(groupAddress, &info)
|
||||
g.memberships[groupAddress] = info
|
||||
}),
|
||||
}
|
||||
@@ -400,11 +358,11 @@ func (g *GenericMulticastProtocolState) HandleQueryLocked(groupAddress tcpip.Add
|
||||
if groupAddress.Unspecified() {
|
||||
// This is a general query as the group address is unspecified.
|
||||
for groupAddress, info := range g.memberships {
|
||||
g.setDelayTimerForAddressRLocked(groupAddress, &info, maxResponseTime)
|
||||
g.setDelayTimerForAddressLocked(groupAddress, &info, maxResponseTime)
|
||||
g.memberships[groupAddress] = info
|
||||
}
|
||||
} else if info, ok := g.memberships[groupAddress]; ok {
|
||||
g.setDelayTimerForAddressRLocked(groupAddress, &info, maxResponseTime)
|
||||
g.setDelayTimerForAddressLocked(groupAddress, &info, maxResponseTime)
|
||||
g.memberships[groupAddress] = info
|
||||
}
|
||||
}
|
||||
@@ -432,10 +390,9 @@ func (g *GenericMulticastProtocolState) HandleReportLocked(groupAddress tcpip.Ad
|
||||
// multicast address while it has a timer running for that same address
|
||||
// on that interface, it stops its timer and does not send a Report for
|
||||
// that address, thus suppressing duplicate reports on the link.
|
||||
if info, ok := g.memberships[groupAddress]; ok && info.state.isDelayingMember() {
|
||||
if info, ok := g.memberships[groupAddress]; ok {
|
||||
info.cancelDelayedReportJob()
|
||||
info.lastToSendReport = false
|
||||
info.state = idleMember
|
||||
g.memberships[groupAddress] = info
|
||||
}
|
||||
}
|
||||
@@ -444,30 +401,23 @@ func (g *GenericMulticastProtocolState) HandleReportLocked(groupAddress tcpip.Ad
|
||||
//
|
||||
// Precondition: g.protocolMU must be locked.
|
||||
func (g *GenericMulticastProtocolState) initializeNewMemberLocked(groupAddress tcpip.Address, info *multicastGroupState) {
|
||||
if info.state != nonMember {
|
||||
panic(fmt.Sprintf("host must be in non-member state to be initialized; group = %s, state = %d", groupAddress, info.state))
|
||||
}
|
||||
|
||||
info.lastToSendReport = false
|
||||
|
||||
if !g.opts.Protocol.ShouldPerformProtocol(groupAddress) {
|
||||
info.state = idleMember
|
||||
return
|
||||
if g.shouldPerformForGroup(groupAddress) {
|
||||
info.transmissionLeft = unsolicitedTransmissionCount
|
||||
g.maybeSendReportLocked(groupAddress, info)
|
||||
}
|
||||
|
||||
info.state = pendingMember
|
||||
g.maybeSendInitialReportLocked(groupAddress, info)
|
||||
}
|
||||
|
||||
// maybeSendInitialReportLocked attempts to start transmission of the initial
|
||||
// set of reports after newly joining a group.
|
||||
//
|
||||
// Host must be in pending member state.
|
||||
func (g *GenericMulticastProtocolState) shouldPerformForGroup(groupAddress tcpip.Address) bool {
|
||||
return g.opts.Protocol.ShouldPerformProtocol(groupAddress) && g.opts.Protocol.Enabled()
|
||||
}
|
||||
|
||||
// maybeSendReportLocked attempts to send a report for a group.
|
||||
//
|
||||
// Precondition: g.protocolMU must be locked.
|
||||
func (g *GenericMulticastProtocolState) maybeSendInitialReportLocked(groupAddress tcpip.Address, info *multicastGroupState) {
|
||||
if info.state != pendingMember {
|
||||
panic(fmt.Sprintf("host must be in pending member state to send initial reports; group = %s, state = %d", groupAddress, info.state))
|
||||
func (g *GenericMulticastProtocolState) maybeSendReportLocked(groupAddress tcpip.Address, info *multicastGroupState) {
|
||||
if info.transmissionLeft == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// As per RFC 2236 section 3 page 5 (for IGMPv2),
|
||||
@@ -490,36 +440,17 @@ func (g *GenericMulticastProtocolState) maybeSendInitialReportLocked(groupAddres
|
||||
sent, err := g.opts.Protocol.SendReport(groupAddress)
|
||||
if err == nil && sent {
|
||||
info.lastToSendReport = true
|
||||
g.setDelayTimerForAddressRLocked(groupAddress, info, g.opts.MaxUnsolicitedReportDelay)
|
||||
}
|
||||
}
|
||||
|
||||
// maybeSendDelayedReportLocked attempts to send the delayed report.
|
||||
//
|
||||
// Host must be in pending, delaying or queued delaying member state.
|
||||
//
|
||||
// Precondition: g.protocolMU must be locked.
|
||||
func (g *GenericMulticastProtocolState) maybeSendDelayedReportLocked(groupAddress tcpip.Address, info *multicastGroupState) {
|
||||
if !info.state.isDelayingMember() {
|
||||
panic(fmt.Sprintf("host must be in delaying or queued delaying member state to send delayed reports; group = %s, state = %d", groupAddress, info.state))
|
||||
}
|
||||
|
||||
sent, err := g.opts.Protocol.SendReport(groupAddress)
|
||||
if err == nil && sent {
|
||||
info.lastToSendReport = true
|
||||
info.state = idleMember
|
||||
} else {
|
||||
info.state = queuedDelayingMember
|
||||
info.transmissionLeft--
|
||||
if info.transmissionLeft > 0 {
|
||||
g.setDelayTimerForAddressLocked(groupAddress, info, g.opts.MaxUnsolicitedReportDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// maybeSendLeave attempts to send a leave message.
|
||||
func (g *GenericMulticastProtocolState) maybeSendLeave(groupAddress tcpip.Address, lastToSendReport bool) {
|
||||
if !g.opts.Protocol.Enabled() || !lastToSendReport {
|
||||
return
|
||||
}
|
||||
|
||||
if !g.opts.Protocol.ShouldPerformProtocol(groupAddress) {
|
||||
if !g.shouldPerformForGroup(groupAddress) || !lastToSendReport {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -578,31 +509,30 @@ func (g *GenericMulticastProtocolState) maybeSendLeave(groupAddress tcpip.Addres
|
||||
//
|
||||
// Precondition: g.protocolMU must be locked.
|
||||
func (g *GenericMulticastProtocolState) transitionToNonMemberLocked(groupAddress tcpip.Address, info *multicastGroupState) {
|
||||
if info.state == nonMember {
|
||||
if !g.shouldPerformForGroup(groupAddress) {
|
||||
return
|
||||
}
|
||||
|
||||
info.cancelDelayedReportJob()
|
||||
g.maybeSendLeave(groupAddress, info.lastToSendReport)
|
||||
info.lastToSendReport = false
|
||||
info.state = nonMember
|
||||
}
|
||||
|
||||
// setDelayTimerForAddressRLocked sets timer to send a delay report.
|
||||
// setDelayTimerForAddressLocked sets timer to send a delayed report.
|
||||
//
|
||||
// Precondition: g.protocolMU MUST be read locked.
|
||||
func (g *GenericMulticastProtocolState) setDelayTimerForAddressRLocked(groupAddress tcpip.Address, info *multicastGroupState, maxResponseTime time.Duration) {
|
||||
if info.state == nonMember {
|
||||
// Precondition: g.protocolMU MUST be locked.
|
||||
func (g *GenericMulticastProtocolState) setDelayTimerForAddressLocked(groupAddress tcpip.Address, info *multicastGroupState, maxResponseTime time.Duration) {
|
||||
if !g.shouldPerformForGroup(groupAddress) {
|
||||
return
|
||||
}
|
||||
|
||||
if !g.opts.Protocol.ShouldPerformProtocol(groupAddress) {
|
||||
return
|
||||
if info.transmissionLeft < minQueryResponseTransmissionCount {
|
||||
info.transmissionLeft = minQueryResponseTransmissionCount
|
||||
}
|
||||
|
||||
// As per RFC 2236 section 3 page 3 (for IGMPv2),
|
||||
//
|
||||
// If a timer for the group is already unning, it is reset to the random
|
||||
// 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.
|
||||
//
|
||||
@@ -612,21 +542,14 @@ func (g *GenericMulticastProtocolState) setDelayTimerForAddressRLocked(groupAddr
|
||||
// random value only if the requested Maximum Response Delay is less than
|
||||
// the remaining value of the running timer.
|
||||
now := g.opts.Clock.Now()
|
||||
if info.state == delayingMember {
|
||||
if info.delayedReportJobFiresAt.IsZero() {
|
||||
panic(fmt.Sprintf("delayed report unscheduled while in the delaying member state; group = %s", groupAddress))
|
||||
}
|
||||
|
||||
if info.delayedReportJobFiresAt.Sub(now) <= maxResponseTime {
|
||||
// The timer is scheduled to fire before the maximum response time so we
|
||||
// leave our timer as is.
|
||||
return
|
||||
}
|
||||
if !info.delayedReportJobFiresAt.IsZero() && info.delayedReportJobFiresAt.Sub(now) <= maxResponseTime {
|
||||
// The timer is scheduled to fire before the maximum response time so we
|
||||
// leave our timer as is.
|
||||
return
|
||||
}
|
||||
|
||||
info.state = delayingMember
|
||||
info.cancelDelayedReportJob()
|
||||
maxResponseTime = g.calculateDelayTimerDuration(maxResponseTime)
|
||||
info.delayedReportJob.Cancel()
|
||||
info.delayedReportJob.Schedule(maxResponseTime)
|
||||
info.delayedReportJobFiresAt = now.Add(maxResponseTime)
|
||||
}
|
||||
|
||||
@@ -774,7 +774,7 @@ func TestQueuedPackets(t *testing.T) {
|
||||
}
|
||||
|
||||
// When we fail to send the initial set of reports, incoming reports should
|
||||
// not affect a newly joined group's reports from being sent.
|
||||
// prevent a newly joined group's reports from being sent.
|
||||
mgp.setQueuePackets(true)
|
||||
mgp.joinGroup(addr2)
|
||||
if diff := mgp.check([]tcpip.Address{addr2} /* sendReportGroupAddresses */, nil /* sendLeaveGroupAddresses */); diff != "" {
|
||||
@@ -784,25 +784,14 @@ func TestQueuedPackets(t *testing.T) {
|
||||
// Attempting to send queued reports while still unable to send reports should
|
||||
// not change the host state.
|
||||
mgp.sendQueuedReports()
|
||||
if diff := mgp.check([]tcpip.Address{addr2} /* sendReportGroupAddresses */, nil /* sendLeaveGroupAddresses */); 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([]tcpip.Address{addr2} /* sendReportGroupAddresses */, nil /* sendLeaveGroupAddresses */); 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([]tcpip.Address{addr2} /* sendReportGroupAddresses */, nil /* sendLeaveGroupAddresses */); diff != "" {
|
||||
t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// Should not have anything else to send.
|
||||
mgp.sendQueuedReports()
|
||||
clock.Advance(time.Hour)
|
||||
if diff := mgp.check(nil /* sendReportGroupAddresses */, nil /* sendLeaveGroupAddresses */); 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(nil /* sendReportGroupAddresses */, nil /* sendLeaveGroupAddresses */); diff != "" {
|
||||
t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user