Files
Nayana BidariandgVisor bot 90266aa28a Netstack S/R: Mark all the structs in netstack as savable.
Marks the structs in netstack as savable. This does not change or break any
existing behavior as the netstack itself is not savable yet.

PiperOrigin-RevId: 635943481
2024-05-21 15:19:07 -07:00

447 lines
14 KiB
Go

// Copyright 2022 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 multicast contains utilities for supporting multicast routing.
package multicast
import (
"errors"
"fmt"
"sync"
"time"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
// RouteTable represents a multicast routing table.
//
// +stateify savable
type RouteTable struct {
// Internally, installed and pending routes are stored and locked separately
// A couple of reasons for structuring the table this way:
//
// 1. We can avoid write locking installed routes when pending packets are
// being queued. In other words, the happy path of reading installed
// routes doesn't require an exclusive lock.
// 2. The cleanup process for expired routes only needs to operate on pending
// routes. Like above, a write lock on the installed routes can be
// avoided.
// 3. This structure is similar to the Linux implementation:
// https://github.com/torvalds/linux/blob/cffb2b72d3e/include/linux/mroute_base.h#L250
// The installedMu lock should typically be acquired before the pendingMu
// lock. This ensures that installed routes can continue to be read even when
// the pending routes are write locked.
installedMu sync.RWMutex `state:"nosave"`
// Maintaining pointers ensures that the installed routes are exclusively
// locked only when a route is being installed.
// +checklocks:installedMu
installedRoutes map[stack.UnicastSourceAndMulticastDestination]*InstalledRoute
pendingMu sync.RWMutex `state:"nosave"`
// +checklocks:pendingMu
pendingRoutes map[stack.UnicastSourceAndMulticastDestination]PendingRoute
// cleanupPendingRoutesTimer is a timer that triggers a routine to remove
// pending routes that are expired.
// +checklocks:pendingMu
cleanupPendingRoutesTimer tcpip.Timer
// +checklocks:pendingMu
isCleanupRoutineRunning bool
config Config
}
var (
// ErrNoBufferSpace indicates that no buffer space is available in the
// pending route packet queue.
ErrNoBufferSpace = errors.New("unable to queue packet, no buffer space available")
// ErrMissingClock indicates that a clock was not provided as part of the
// Config, but is required.
ErrMissingClock = errors.New("clock must not be nil")
// ErrAlreadyInitialized indicates that RouteTable.Init was already invoked.
ErrAlreadyInitialized = errors.New("table is already initialized")
)
// InstalledRoute represents a route that is in the installed state.
//
// If a route is in the installed state, then it may be used to forward
// multicast packets.
//
// +stateify savable
type InstalledRoute struct {
stack.MulticastRoute
lastUsedTimestampMu sync.RWMutex `state:"nosave"`
// +checklocks:lastUsedTimestampMu
lastUsedTimestamp tcpip.MonotonicTime
}
// LastUsedTimestamp returns a monotonic timestamp that corresponds to the last
// time the route was used or updated.
func (r *InstalledRoute) LastUsedTimestamp() tcpip.MonotonicTime {
r.lastUsedTimestampMu.RLock()
defer r.lastUsedTimestampMu.RUnlock()
return r.lastUsedTimestamp
}
// SetLastUsedTimestamp sets the time that the route was last used.
//
// The timestamp is only updated if it occurs after the currently set
// timestamp. Callers should invoke this anytime the route is used to forward a
// packet.
func (r *InstalledRoute) SetLastUsedTimestamp(monotonicTime tcpip.MonotonicTime) {
r.lastUsedTimestampMu.Lock()
defer r.lastUsedTimestampMu.Unlock()
if monotonicTime.After(r.lastUsedTimestamp) {
r.lastUsedTimestamp = monotonicTime
}
}
// PendingRoute represents a route that is in the "pending" state.
//
// A route is in the pending state if an installed route does not yet exist
// for the entry. For such routes, packets are added to an expiring queue until
// a route is installed.
//
// +stateify savable
type PendingRoute struct {
packets []*stack.PacketBuffer
// expiration is the timestamp at which the pending route should be expired.
//
// If this value is before the current time, then this pending route will
// be dropped.
expiration tcpip.MonotonicTime
}
func (p *PendingRoute) releasePackets() {
for _, pkt := range p.packets {
pkt.DecRef()
}
}
func (p *PendingRoute) isExpired(currentTime tcpip.MonotonicTime) bool {
return currentTime.After(p.expiration)
}
const (
// DefaultMaxPendingQueueSize corresponds to the number of elements that can
// be in the packet queue for a pending route.
//
// Matches the Linux default queue size:
// https://github.com/torvalds/linux/blob/26291c54e11/net/ipv6/ip6mr.c#L1186
DefaultMaxPendingQueueSize uint8 = 3
// DefaultPendingRouteExpiration is the default maximum lifetime of a pending
// route.
//
// Matches the Linux default:
// https://github.com/torvalds/linux/blob/26291c54e11/net/ipv6/ip6mr.c#L991
DefaultPendingRouteExpiration time.Duration = 10 * time.Second
// DefaultCleanupInterval is the default frequency of the routine that
// expires pending routes.
//
// Matches the Linux default:
// https://github.com/torvalds/linux/blob/26291c54e11/net/ipv6/ip6mr.c#L793
DefaultCleanupInterval time.Duration = 10 * time.Second
)
// Config represents the options for configuring a RouteTable.
//
// +stateify savable
type Config struct {
// MaxPendingQueueSize corresponds to the maximum number of queued packets
// for a pending route.
//
// If the caller attempts to queue a packet and the queue already contains
// MaxPendingQueueSize elements, then the packet will be rejected and should
// not be forwarded.
MaxPendingQueueSize uint8
// Clock represents the clock that should be used to obtain the current time.
//
// This field is required and must have a non-nil value.
Clock tcpip.Clock
}
// DefaultConfig returns the default configuration for the table.
func DefaultConfig(clock tcpip.Clock) Config {
return Config{
MaxPendingQueueSize: DefaultMaxPendingQueueSize,
Clock: clock,
}
}
// Init initializes the RouteTable with the provided config.
//
// An error is returned if the config is not valid.
//
// Must be called before any other function on the table.
func (r *RouteTable) Init(config Config) error {
r.installedMu.Lock()
defer r.installedMu.Unlock()
r.pendingMu.Lock()
defer r.pendingMu.Unlock()
if r.installedRoutes != nil {
return ErrAlreadyInitialized
}
if config.Clock == nil {
return ErrMissingClock
}
r.config = config
r.installedRoutes = make(map[stack.UnicastSourceAndMulticastDestination]*InstalledRoute)
r.pendingRoutes = make(map[stack.UnicastSourceAndMulticastDestination]PendingRoute)
return nil
}
// Close cleans up resources held by the table.
//
// Calling this will stop the cleanup routine and release any packets owned by
// the table.
func (r *RouteTable) Close() {
r.pendingMu.Lock()
defer r.pendingMu.Unlock()
if r.cleanupPendingRoutesTimer != nil {
r.cleanupPendingRoutesTimer.Stop()
}
for key, route := range r.pendingRoutes {
delete(r.pendingRoutes, key)
route.releasePackets()
}
}
// maybeStopCleanupRoutine stops the pending routes cleanup routine if no
// pending routes exist.
//
// Returns true if the timer is not running. Otherwise, returns false.
//
// +checklocks:r.pendingMu
func (r *RouteTable) maybeStopCleanupRoutineLocked() bool {
if !r.isCleanupRoutineRunning {
return true
}
if len(r.pendingRoutes) == 0 {
r.cleanupPendingRoutesTimer.Stop()
r.isCleanupRoutineRunning = false
return true
}
return false
}
func (r *RouteTable) cleanupPendingRoutes() {
currentTime := r.config.Clock.NowMonotonic()
r.pendingMu.Lock()
defer r.pendingMu.Unlock()
for key, route := range r.pendingRoutes {
if route.isExpired(currentTime) {
delete(r.pendingRoutes, key)
route.releasePackets()
}
}
if stopped := r.maybeStopCleanupRoutineLocked(); !stopped {
r.cleanupPendingRoutesTimer.Reset(DefaultCleanupInterval)
}
}
func (r *RouteTable) newPendingRoute() PendingRoute {
return PendingRoute{
packets: make([]*stack.PacketBuffer, 0, r.config.MaxPendingQueueSize),
expiration: r.config.Clock.NowMonotonic().Add(DefaultPendingRouteExpiration),
}
}
// NewInstalledRoute instantiates an installed route for the table.
func (r *RouteTable) NewInstalledRoute(route stack.MulticastRoute) *InstalledRoute {
return &InstalledRoute{
MulticastRoute: route,
lastUsedTimestamp: r.config.Clock.NowMonotonic(),
}
}
// GetRouteResult represents the result of calling GetRouteOrInsertPending.
type GetRouteResult struct {
// GetRouteResultState signals the result of calling GetRouteOrInsertPending.
GetRouteResultState GetRouteResultState
// InstalledRoute represents the existing installed route. This field will
// only be populated if the GetRouteResultState is InstalledRouteFound.
InstalledRoute *InstalledRoute
}
// GetRouteResultState signals the result of calling GetRouteOrInsertPending.
type GetRouteResultState uint8
const (
// InstalledRouteFound indicates that an InstalledRoute was found.
InstalledRouteFound GetRouteResultState = iota
// PacketQueuedInPendingRoute indicates that the packet was queued in an
// existing pending route.
PacketQueuedInPendingRoute
// NoRouteFoundAndPendingInserted indicates that no route was found and that
// a pending route was newly inserted into the RouteTable.
NoRouteFoundAndPendingInserted
)
func (e GetRouteResultState) String() string {
switch e {
case InstalledRouteFound:
return "InstalledRouteFound"
case PacketQueuedInPendingRoute:
return "PacketQueuedInPendingRoute"
case NoRouteFoundAndPendingInserted:
return "NoRouteFoundAndPendingInserted"
default:
return fmt.Sprintf("%d", uint8(e))
}
}
// GetRouteOrInsertPending attempts to fetch the installed route that matches
// the provided key.
//
// If no matching installed route is found, then the pkt is cloned and queued
// in a pending route. The GetRouteResult.GetRouteResultState will indicate
// whether the pkt was queued in a new pending route or an existing one.
//
// If the relevant pending route queue is at max capacity, then returns false.
// Otherwise, returns true.
func (r *RouteTable) GetRouteOrInsertPending(key stack.UnicastSourceAndMulticastDestination, pkt *stack.PacketBuffer) (GetRouteResult, bool) {
r.installedMu.RLock()
defer r.installedMu.RUnlock()
if route, ok := r.installedRoutes[key]; ok {
return GetRouteResult{GetRouteResultState: InstalledRouteFound, InstalledRoute: route}, true
}
r.pendingMu.Lock()
defer r.pendingMu.Unlock()
pendingRoute, getRouteResultState := r.getOrCreatePendingRouteRLocked(key)
if len(pendingRoute.packets) >= int(r.config.MaxPendingQueueSize) {
// The incoming packet is rejected if the pending queue is already at max
// capacity. This behavior matches the Linux implementation:
// https://github.com/torvalds/linux/blob/ae085d7f936/net/ipv4/ipmr.c#L1147
return GetRouteResult{}, false
}
pendingRoute.packets = append(pendingRoute.packets, pkt.Clone())
r.pendingRoutes[key] = pendingRoute
if !r.isCleanupRoutineRunning {
// The cleanup routine isn't running, but should be. Start it.
if r.cleanupPendingRoutesTimer == nil {
r.cleanupPendingRoutesTimer = r.config.Clock.AfterFunc(DefaultCleanupInterval, r.cleanupPendingRoutes)
} else {
r.cleanupPendingRoutesTimer.Reset(DefaultCleanupInterval)
}
r.isCleanupRoutineRunning = true
}
return GetRouteResult{GetRouteResultState: getRouteResultState, InstalledRoute: nil}, true
}
// +checklocks:r.pendingMu
func (r *RouteTable) getOrCreatePendingRouteRLocked(key stack.UnicastSourceAndMulticastDestination) (PendingRoute, GetRouteResultState) {
if pendingRoute, ok := r.pendingRoutes[key]; ok {
return pendingRoute, PacketQueuedInPendingRoute
}
return r.newPendingRoute(), NoRouteFoundAndPendingInserted
}
// AddInstalledRoute adds the provided route to the table.
//
// Packets that were queued while the route was in the pending state are
// returned. The caller assumes ownership of these packets and is responsible
// for forwarding and releasing them. If an installed route already exists for
// the provided key, then it is overwritten.
func (r *RouteTable) AddInstalledRoute(key stack.UnicastSourceAndMulticastDestination, route *InstalledRoute) []*stack.PacketBuffer {
r.installedMu.Lock()
defer r.installedMu.Unlock()
r.installedRoutes[key] = route
r.pendingMu.Lock()
pendingRoute, ok := r.pendingRoutes[key]
delete(r.pendingRoutes, key)
// No need to reset the timer here. The cleanup routine is responsible for
// doing so.
_ = r.maybeStopCleanupRoutineLocked()
r.pendingMu.Unlock()
// Ignore the pending route if it is expired. It may be in this state since
// the cleanup process is only run periodically.
if !ok || pendingRoute.isExpired(r.config.Clock.NowMonotonic()) {
pendingRoute.releasePackets()
return nil
}
return pendingRoute.packets
}
// RemoveInstalledRoute deletes any installed route that matches the provided
// key.
//
// Returns true if a route was removed. Otherwise returns false.
func (r *RouteTable) RemoveInstalledRoute(key stack.UnicastSourceAndMulticastDestination) bool {
r.installedMu.Lock()
defer r.installedMu.Unlock()
if _, ok := r.installedRoutes[key]; ok {
delete(r.installedRoutes, key)
return true
}
return false
}
// RemoveAllInstalledRoutes removes all installed routes from the table.
func (r *RouteTable) RemoveAllInstalledRoutes() {
r.installedMu.Lock()
defer r.installedMu.Unlock()
for key := range r.installedRoutes {
delete(r.installedRoutes, key)
}
}
// GetLastUsedTimestamp returns a monotonic timestamp that represents the last
// time the route that matches the provided key was used or updated.
//
// Returns true if a matching route was found. Otherwise returns false.
func (r *RouteTable) GetLastUsedTimestamp(key stack.UnicastSourceAndMulticastDestination) (tcpip.MonotonicTime, bool) {
r.installedMu.RLock()
defer r.installedMu.RUnlock()
if route, ok := r.installedRoutes[key]; ok {
return route.LastUsedTimestamp(), true
}
return tcpip.MonotonicTime{}, false
}