diff options
author | Ghanan Gowripalan <ghanan@google.com> | 2020-11-24 11:48:09 -0800 |
---|---|---|
committer | gVisor bot <gvisor-bot@google.com> | 2020-11-24 11:50:00 -0800 |
commit | 732e98985546958fdab8ae3d49e36f17ae89f71c (patch) | |
tree | 16efeda1f25304990fad6676fa50a509d0b9f84a | |
parent | e5fd23c18d1fd602603babd8297dabf679c336aa (diff) |
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
-rw-r--r-- | pkg/tcpip/network/ip/BUILD | 25 | ||||
-rw-r--r-- | pkg/tcpip/network/ip/generic_multicast_protocol.go | 346 | ||||
-rw-r--r-- | pkg/tcpip/network/ip/generic_multicast_protocol_test.go | 294 | ||||
-rw-r--r-- | pkg/tcpip/network/ipv4/BUILD | 1 | ||||
-rw-r--r-- | pkg/tcpip/network/ipv4/igmp.go | 248 | ||||
-rw-r--r-- | pkg/tcpip/tcpip.go | 10 | ||||
-rw-r--r-- | pkg/tcpip/tcpip_test.go | 44 |
7 files changed, 791 insertions, 177 deletions
diff --git a/pkg/tcpip/network/ip/BUILD b/pkg/tcpip/network/ip/BUILD new file mode 100644 index 000000000..6ca200b48 --- /dev/null +++ b/pkg/tcpip/network/ip/BUILD @@ -0,0 +1,25 @@ +load("//tools:defs.bzl", "go_library", "go_test") + +package(licenses = ["notice"]) + +go_library( + name = "ip", + srcs = ["generic_multicast_protocol.go"], + visibility = ["//visibility:public"], + deps = [ + "//pkg/sync", + "//pkg/tcpip", + ], +) + +go_test( + name = "ip_test", + size = "small", + srcs = ["generic_multicast_protocol_test.go"], + deps = [ + ":ip", + "//pkg/tcpip", + "//pkg/tcpip/faketime", + "@com_github_google_go_cmp//cmp:go_default_library", + ], +) diff --git a/pkg/tcpip/network/ip/generic_multicast_protocol.go b/pkg/tcpip/network/ip/generic_multicast_protocol.go new file mode 100644 index 000000000..3113f4bbe --- /dev/null +++ b/pkg/tcpip/network/ip/generic_multicast_protocol.go @@ -0,0 +1,346 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package ip holds IPv4/IPv6 common utilities. +package ip + +import ( + "fmt" + "math/rand" + "time" + + "gvisor.dev/gvisor/pkg/sync" + "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. +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." + // + // 'Non-Listener' is the MLDv1 term used to describe this state. + _ 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." + // + // 'Delaying Listener' is the MLDv1 term used to describe this state. + 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. + // + // 'Idle Listener' is the MLDv1 term used to describe this state. + idleMember +) + +// multicastGroupState holds the Generic Multicast Protocol state for a +// multicast group. +type multicastGroupState struct { + // state contains the host's state for the group. + state hostState + + // 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 + // members of the group. + // + // Defined in RFC 2236 section 6 page 9 for IGMPv2 and RFC 2710 section 5 page + // 8 for MLDv1. + lastToSendReport bool + + // delayedReportJob is used to delay sending responses to membership report + // messages in order to reduce duplicate reports from multiple hosts on the + // interface. + // + // Must not be nil. + delayedReportJob *tcpip.Job +} + +// MulticastGroupProtocol is a multicast group protocol whose core state machine +// can be represented by GenericMulticastProtocolState. +type MulticastGroupProtocol interface { + // SendReport sends a multicast report for the specified group address. + SendReport(groupAddress tcpip.Address) *tcpip.Error + + // SendLeave sends a multicast leave for the specified group address. + SendLeave(groupAddress tcpip.Address) *tcpip.Error +} + +// GenericMulticastProtocolState is the per interface generic multicast protocol +// state. +// +// There is actually no protocol named "Generic Multicast Protocol". Instead, +// the term used to refer to a generic multicast protocol that applies to both +// IPv4 and IPv6. Specifically, Generic Multicast Protocol is the core state +// machine of IGMPv2 as defined by RFC 2236 and MLDv1 as defined by RFC 2710. +// +// GenericMulticastProtocolState.Init MUST be called before calling any of +// the methods on GenericMulticastProtocolState. +type GenericMulticastProtocolState struct { + rand *rand.Rand + clock tcpip.Clock + protocol MulticastGroupProtocol + maxUnsolicitedReportDelay time.Duration + + mu struct { + sync.Mutex + + // memberships holds group addresses and their associated state. + memberships map[tcpip.Address]multicastGroupState + } +} + +// Init initializes the Generic Multicast Protocol state. +// +// maxUnsolicitedReportDelay is the maximum time between sending unsolicited +// reports after joining a group. +func (g *GenericMulticastProtocolState) Init(rand *rand.Rand, clock tcpip.Clock, protocol MulticastGroupProtocol, maxUnsolicitedReportDelay time.Duration) { + g.mu.Lock() + defer g.mu.Unlock() + g.rand = rand + g.clock = clock + g.protocol = protocol + g.maxUnsolicitedReportDelay = maxUnsolicitedReportDelay + g.mu.memberships = make(map[tcpip.Address]multicastGroupState) +} + +// JoinGroup handles joining a new group. +// +// Returns false if the group has already been joined. +func (g *GenericMulticastProtocolState) JoinGroup(groupAddress tcpip.Address) bool { + g.mu.Lock() + defer g.mu.Unlock() + + if _, ok := g.mu.memberships[groupAddress]; ok { + // The group has already been joined. + return false + } + + info := multicastGroupState{ + // There isn't a job scheduled currently, so it's just idle. + state: idleMember, + // Joining a group immediately sends a report. + lastToSendReport: true, + delayedReportJob: tcpip.NewJob(g.clock, &g.mu, func() { + info, ok := g.mu.memberships[groupAddress] + if !ok { + panic(fmt.Sprintf("expected to find group state for group = %s", groupAddress)) + } + + info.lastToSendReport = g.protocol.SendReport(groupAddress) == nil + info.state = idleMember + g.mu.memberships[groupAddress] = info + }), + } + + // As per RFC 2236 section 3 page 5 (for IGMPv2), + // + // 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". + // + // As per RFC 2710 section 4 page 6 (for MLDv1), + // + // 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]. + // + // TODO(gvisor.dev/issue/4901): Support a configurable number of initial + // unsolicited reports. + info.lastToSendReport = g.protocol.SendReport(groupAddress) == nil + g.setDelayTimerForAddressRLocked(groupAddress, &info, g.maxUnsolicitedReportDelay) + g.mu.memberships[groupAddress] = info + return true +} + +// LeaveGroup handles leaving the group. +func (g *GenericMulticastProtocolState) LeaveGroup(groupAddress tcpip.Address) { + g.mu.Lock() + defer g.mu.Unlock() + + info, ok := g.mu.memberships[groupAddress] + if !ok { + return + } + + info.delayedReportJob.Cancel() + delete(g.mu.memberships, groupAddress) + if info.lastToSendReport { + // Okay to ignore the error here as if packet write failed, the multicast + // routers will eventually drop our membership anyways. If the interface is + // being disabled or removed, the generic multicast protocol's should be + // cleared eventually. + // + // As per RFC 2236 section 3 page 5 (for IGMPv2), + // + // When a router receives a Report, it adds the group being reported to + // the list of multicast group memberships on the network on which it + // received the Report and sets the timer for the membership to the + // [Group Membership Interval]. Repeated Reports refresh the timer. If + // no Reports are received for a particular group before this timer has + // expired, the router assumes that the group has no local members and + // that it need not forward remotely-originated multicasts for that + // group onto the attached network. + // + // As per RFC 2710 section 4 page 5 (for MLDv1), + // + // When a router receives a Report from a link, if the reported address + // is not already present in the router's list of multicast address + // having listeners on that link, the reported address is added to the + // list, its timer is set to [Multicast Listener Interval], and its + // appearance is made known to the router's multicast routing component. + // If a Report is received for a multicast address that is already + // present in the router's list, the timer for that address is reset to + // [Multicast Listener Interval]. If an address's timer expires, it is + // assumed that there are no longer any listeners for that address + // present on the link, so it is deleted from the list and its + // disappearance is made known to the multicast routing component. + // + // The requirement to send a leave message is also optional (it MAY be + // skipped): + // + // As per RFC 2236 section 6 page 8 (for IGMPv2), + // + // "send leave" for the group on the interface. 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. The Leave Message is sent to + // the ALL-ROUTERS group (224.0.0.2). + // + // As per RFC 2710 section 5 page 8 (for MLDv1), + // + // "send done" for the address on the interface. If the flag saying + // we were the last node to report is cleared, this action MAY be + // skipped. The Done message is sent to the link-scope all-routers + // address (FF02::2). + _ = g.protocol.SendLeave(groupAddress) + } +} + +// HandleQuery handles a query message with the specified maximum response time. +// +// If the group address is unspecified, then reports will be scheduled for all +// joined groups. +// +// Report(s) will be scheduled to be sent after a random duration between 0 and +// the maximum response time. +func (g *GenericMulticastProtocolState) HandleQuery(groupAddress tcpip.Address, maxResponseTime time.Duration) { + g.mu.Lock() + defer g.mu.Unlock() + + // As per RFC 2236 section 2.4 (for IGMPv2), + // + // In a Membership Query message, the group address field is set to zero + // when sending a General Query, and set to the group address being + // queried when sending a Group-Specific Query. + // + // As per RFC 2710 section 3.6 (for MLDv1), + // + // In a Query message, the Multicast Address field is set to zero when + // sending a General Query, and set to a specific IPv6 multicast address + // when sending a Multicast-Address-Specific Query. + if groupAddress.Unspecified() { + // This is a general query as the group address is unspecified. + for groupAddress, info := range g.mu.memberships { + g.setDelayTimerForAddressRLocked(groupAddress, &info, maxResponseTime) + g.mu.memberships[groupAddress] = info + } + } else if info, ok := g.mu.memberships[groupAddress]; ok { + g.setDelayTimerForAddressRLocked(groupAddress, &info, maxResponseTime) + g.mu.memberships[groupAddress] = info + } +} + +// HandleReport handles a report message. +// +// If the report is for a joined group, any active delayed report will be +// cancelled and the host state for the group transitions to idle. +func (g *GenericMulticastProtocolState) HandleReport(groupAddress tcpip.Address) { + g.mu.Lock() + defer g.mu.Unlock() + + // As per RFC 2236 section 3 pages 3-4 (for IGMPv2), + // + // 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 + // + // As per RFC 2710 section 4 page 6 (for MLDv1), + // + // If a node receives another node's Report from an interface for a + // 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.mu.memberships[groupAddress]; ok { + info.delayedReportJob.Cancel() + info.lastToSendReport = false + info.state = idleMember + g.mu.memberships[groupAddress] = info + } +} + +// setDelayTimerForAddressRLocked sets timer to send a delay report. +// +// Precondition: g.mu MUST be read locked. +func (g *GenericMulticastProtocolState) setDelayTimerForAddressRLocked(groupAddress tcpip.Address, info *multicastGroupState, maxResponseTime time.Duration) { + // 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 + // value only if the requested Max Response Time is less than the remaining + // value of the running timer. + // + // As per RFC 2710 section 4 page 5 (for MLDv1), + // + // If a timer for any address is already running, it is reset to the new + // random value only if the requested Maximum Response Delay is less than + // the remaining value of the running timer. + if info.state == delayingMember { + // TODO: Reset the timer if time remaining is greater than maxResponseTime. + return + } + info.state = delayingMember + info.delayedReportJob.Cancel() + info.delayedReportJob.Schedule(g.calculateDelayTimerDuration(maxResponseTime)) +} + +// calculateDelayTimerDuration returns a random time between (0, maxRespTime]. +func (g *GenericMulticastProtocolState) calculateDelayTimerDuration(maxRespTime time.Duration) time.Duration { + // As per RFC 2236 section 3 page 3 (for IGMPv2), + // + // When a host receives a Group-Specific Query, it sets a delay timer to a + // random value selected from the range (0, Max Response Time]... + // + // As per RFC 2710 section 4 page 6 (for MLDv1), + // + // When a node receives a Multicast-Address-Specific Query, if it is + // listening to the queried Multicast Address on the interface from + // which the Query was received, it sets a delay timer for that address + // to a random value selected from the range [0, Maximum Response Delay], + // as above. + if maxRespTime == 0 { + return 0 + } + return time.Duration(g.rand.Int63n(int64(maxRespTime))) +} diff --git a/pkg/tcpip/network/ip/generic_multicast_protocol_test.go b/pkg/tcpip/network/ip/generic_multicast_protocol_test.go new file mode 100644 index 000000000..eb48c0d51 --- /dev/null +++ b/pkg/tcpip/network/ip/generic_multicast_protocol_test.go @@ -0,0 +1,294 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/faketime" + "gvisor.dev/gvisor/pkg/tcpip/network/ip" +) + +const ( + addr1 = tcpip.Address("\x01") + addr2 = tcpip.Address("\x02") + addr3 = tcpip.Address("\x03") +) + +var _ ip.MulticastGroupProtocol = (*mockMulticastGroupProtocol)(nil) + +type mockMulticastGroupProtocol struct { + sendReportGroupAddrCount map[tcpip.Address]int + sendLeaveGroupAddr tcpip.Address +} + +func (m *mockMulticastGroupProtocol) init() { + m.sendReportGroupAddrCount = make(map[tcpip.Address]int) + m.sendLeaveGroupAddr = "" +} + +func (m *mockMulticastGroupProtocol) SendReport(groupAddress tcpip.Address) *tcpip.Error { + m.sendReportGroupAddrCount[groupAddress]++ + return nil +} + +func (m *mockMulticastGroupProtocol) SendLeave(groupAddress tcpip.Address) *tcpip.Error { + m.sendLeaveGroupAddr = groupAddress + return nil +} + +func checkProtocol(mgp *mockMulticastGroupProtocol, sendReportGroupAddresses []tcpip.Address, sendLeaveGroupAddr tcpip.Address) string { + sendReportGroupAddressesMap := make(map[tcpip.Address]int) + for _, a := range sendReportGroupAddresses { + sendReportGroupAddressesMap[a] = 1 + } + + diff := cmp.Diff(mockMulticastGroupProtocol{ + sendReportGroupAddrCount: sendReportGroupAddressesMap, + sendLeaveGroupAddr: sendLeaveGroupAddr, + }, *mgp, cmp.AllowUnexported(mockMulticastGroupProtocol{})) + mgp.init() + return diff +} + +func TestJoinGroup(t *testing.T) { + const maxUnsolicitedReportDelay = time.Second + + var g ip.GenericMulticastProtocolState + var mgp mockMulticastGroupProtocol + mgp.init() + clock := faketime.NewManualClock() + g.Init(rand.New(rand.NewSource(0)), clock, &mgp, maxUnsolicitedReportDelay) + + // Joining a group should send a report immediately and another after + // a random interval between 0 and the maximum unsolicited report delay. + if !g.JoinGroup(addr1) { + t.Errorf("got g.JoinGroup(%s) = false, want = true", addr1) + } + if diff := checkProtocol(&mgp, []tcpip.Address{addr1} /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + clock.Advance(maxUnsolicitedReportDelay) + if diff := checkProtocol(&mgp, []tcpip.Address{addr1} /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Should have no more messages to send. + clock.Advance(time.Hour) + if diff := checkProtocol(&mgp, nil /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } +} + +func TestLeaveGroup(t *testing.T) { + const maxUnsolicitedReportDelay = time.Second + + var g ip.GenericMulticastProtocolState + var mgp mockMulticastGroupProtocol + mgp.init() + clock := faketime.NewManualClock() + g.Init(rand.New(rand.NewSource(1)), clock, &mgp, maxUnsolicitedReportDelay) + + if !g.JoinGroup(addr1) { + t.Fatalf("got g.JoinGroup(%s) = false, want = true", addr1) + } + if diff := checkProtocol(&mgp, []tcpip.Address{addr1} /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Leaving a group should send a leave report immediately and cancel any + // delayed reports. + g.LeaveGroup(addr1) + if diff := checkProtocol(&mgp, nil /* sendReportGroupAddresses */, addr1 /* sendLeaveGroupAddr */); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Should have no more messages to send. + clock.Advance(time.Hour) + if diff := checkProtocol(&mgp, nil /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } +} + +func TestHandleReport(t *testing.T) { + const maxUnsolicitedReportDelay = time.Second + + tests := []struct { + name string + reportAddr tcpip.Address + expectReportsFor []tcpip.Address + }{ + { + name: "Unpecified empty", + reportAddr: "", + expectReportsFor: []tcpip.Address{addr1, addr2}, + }, + { + name: "Unpecified any", + reportAddr: "\x00", + expectReportsFor: []tcpip.Address{addr1, addr2}, + }, + { + name: "Specified", + reportAddr: addr1, + expectReportsFor: []tcpip.Address{addr2}, + }, + { + name: "Specified other", + reportAddr: addr3, + expectReportsFor: []tcpip.Address{addr1, addr2}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var g ip.GenericMulticastProtocolState + var mgp mockMulticastGroupProtocol + mgp.init() + clock := faketime.NewManualClock() + g.Init(rand.New(rand.NewSource(2)), clock, &mgp, maxUnsolicitedReportDelay) + + if !g.JoinGroup(addr1) { + t.Fatalf("got g.JoinGroup(%s) = false, want = true", addr1) + } + if diff := checkProtocol(&mgp, []tcpip.Address{addr1} /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + if !g.JoinGroup(addr2) { + t.Fatalf("got g.JoinGroup(%s) = false, want = true", addr2) + } + if diff := checkProtocol(&mgp, []tcpip.Address{addr2} /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); 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. + g.HandleReport(test.reportAddr) + if len(test.expectReportsFor) != 0 { + clock.Advance(maxUnsolicitedReportDelay) + if diff := checkProtocol(&mgp, test.expectReportsFor /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + } + + // Should have no more messages to send. + clock.Advance(time.Hour) + if diff := checkProtocol(&mgp, nil /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestHandleQuery(t *testing.T) { + const maxUnsolicitedReportDelay = time.Second + + tests := []struct { + name string + queryAddr tcpip.Address + maxDelay time.Duration + expectReportsFor []tcpip.Address + }{ + { + name: "Unpecified empty", + queryAddr: "", + maxDelay: 0, + expectReportsFor: []tcpip.Address{addr1, addr2}, + }, + { + name: "Unpecified any", + queryAddr: "\x00", + maxDelay: 1, + expectReportsFor: []tcpip.Address{addr1, addr2}, + }, + { + name: "Specified", + queryAddr: addr1, + maxDelay: 2, + expectReportsFor: []tcpip.Address{addr1}, + }, + { + name: "Specified other", + queryAddr: addr3, + maxDelay: 3, + expectReportsFor: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var g ip.GenericMulticastProtocolState + var mgp mockMulticastGroupProtocol + mgp.init() + clock := faketime.NewManualClock() + g.Init(rand.New(rand.NewSource(3)), clock, &mgp, maxUnsolicitedReportDelay) + + if !g.JoinGroup(addr1) { + t.Fatalf("got g.JoinGroup(%s) = false, want = true", addr1) + } + if diff := checkProtocol(&mgp, []tcpip.Address{addr1} /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + if !g.JoinGroup(addr2) { + t.Fatalf("got g.JoinGroup(%s) = false, want = true", addr2) + } + if diff := checkProtocol(&mgp, []tcpip.Address{addr2} /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + clock.Advance(maxUnsolicitedReportDelay) + if diff := checkProtocol(&mgp, []tcpip.Address{addr1, addr2} /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Fatalf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + + // Receiving a query should make us schedule a new delayed report if it + // is a query directed at us or a general query. + g.HandleQuery(test.queryAddr, test.maxDelay) + if len(test.expectReportsFor) != 0 { + clock.Advance(test.maxDelay) + if diff := checkProtocol(&mgp, test.expectReportsFor /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + } + + // Should have no more messages to send. + clock.Advance(time.Hour) + if diff := checkProtocol(&mgp, nil /* sendReportGroupAddresses */, "" /* sendLeaveGroupAddr */); diff != "" { + t.Errorf("mockMulticastGroupProtocol mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestDoubleJoinGroup(t *testing.T) { + var g ip.GenericMulticastProtocolState + var mgp mockMulticastGroupProtocol + mgp.init() + clock := faketime.NewManualClock() + g.Init(rand.New(rand.NewSource(4)), clock, &mgp, time.Second) + + if !g.JoinGroup(addr1) { + t.Fatalf("got g.JoinGroup(%s) = false, want = true", addr1) + } + + // Joining the same group twice should fail. + if g.JoinGroup(addr1) { + t.Errorf("got g.JoinGroup(%s) = true, want = false", addr1) + } +} 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) } diff --git a/pkg/tcpip/tcpip.go b/pkg/tcpip/tcpip.go index 3c7c5c0a8..40f6e8aa9 100644 --- a/pkg/tcpip/tcpip.go +++ b/pkg/tcpip/tcpip.go @@ -247,6 +247,16 @@ func (a Address) WithPrefix() AddressWithPrefix { } } +// Unspecified returns true if the address is unspecified. +func (a Address) Unspecified() bool { + for _, b := range a { + if b != 0 { + return false + } + } + return true +} + // AddressMask is a bitmask for an address. type AddressMask string diff --git a/pkg/tcpip/tcpip_test.go b/pkg/tcpip/tcpip_test.go index 1c8e2bc34..c461da137 100644 --- a/pkg/tcpip/tcpip_test.go +++ b/pkg/tcpip/tcpip_test.go @@ -226,3 +226,47 @@ func TestAddressWithPrefixSubnet(t *testing.T) { } } } + +func TestAddressUnspecified(t *testing.T) { + tests := []struct { + addr Address + unspecified bool + }{ + { + addr: "", + unspecified: true, + }, + { + addr: "\x00", + unspecified: true, + }, + { + addr: "\x01", + unspecified: false, + }, + { + addr: "\x00\x00", + unspecified: true, + }, + { + addr: "\x01\x00", + unspecified: false, + }, + { + addr: "\x00\x01", + unspecified: false, + }, + { + addr: "\x01\x01", + unspecified: false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("addr=%s", test.addr), func(t *testing.T) { + if got := test.addr.Unspecified(); got != test.unspecified { + t.Fatalf("got addr.Unspecified() = %t, want = %t", got, test.unspecified) + } + }) + } +} |