// 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 stack import ( "fmt" "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/tcpip" ) const neighborCacheSize = 512 // max entries per interface // NeighborStats holds metrics for the neighbor table. type NeighborStats struct { // UnreachableEntryLookups counts the number of lookups performed on an // entry in Unreachable state. UnreachableEntryLookups *tcpip.StatCounter } // neighborCache maps IP addresses to link addresses. It uses the Least // Recently Used (LRU) eviction strategy to implement a bounded cache for // dynamically acquired entries. It contains the state machine and configuration // for running Neighbor Unreachability Detection (NUD). // // There are two types of entries in the neighbor cache: // 1. Dynamic entries are discovered automatically by neighbor discovery // protocols (e.g. ARP, NDP). These protocols will attempt to reconfirm // reachability with the device once the entry's state becomes Stale. // 2. Static entries are explicitly added by a user and have no expiration. // Their state is always Static. The amount of static entries stored in the // cache is unbounded. type neighborCache struct { nic *nic state *NUDState linkRes LinkAddressResolver mu struct { sync.RWMutex cache map[tcpip.Address]*neighborEntry dynamic struct { lru neighborEntryList // count tracks the amount of dynamic entries in the cache. This is // needed since static entries do not count towards the LRU cache // eviction strategy. count uint16 } } } // getOrCreateEntry retrieves a cache entry associated with addr. The // returned entry is always refreshed in the cache (it is reachable via the // map, and its place is bumped in LRU). // // If a matching entry exists in the cache, it is returned. If no matching // entry exists and the cache is full, an existing entry is evicted via LRU, // reset to state incomplete, and returned. If no matching entry exists and the // cache is not full, a new entry with state incomplete is allocated and // returned. func (n *neighborCache) getOrCreateEntry(remoteAddr tcpip.Address) *neighborEntry { n.mu.Lock() defer n.mu.Unlock() if entry, ok := n.mu.cache[remoteAddr]; ok { entry.mu.RLock() if entry.mu.neigh.State != Static { n.mu.dynamic.lru.Remove(entry) n.mu.dynamic.lru.PushFront(entry) } entry.mu.RUnlock() return entry } // The entry that needs to be created must be dynamic since all static // entries are directly added to the cache via addStaticEntry. entry := newNeighborEntry(n, remoteAddr, n.state) if n.mu.dynamic.count == neighborCacheSize { e := n.mu.dynamic.lru.Back() e.mu.Lock() delete(n.mu.cache, e.mu.neigh.Addr) n.mu.dynamic.lru.Remove(e) n.mu.dynamic.count-- e.removeLocked() e.mu.Unlock() } n.mu.cache[remoteAddr] = entry n.mu.dynamic.lru.PushFront(entry) n.mu.dynamic.count++ return entry } // entry looks up neighbor information matching the remote address, and returns // it if readily available. // // Returns ErrWouldBlock if the link address is not readily available, along // with a notification channel for the caller to block on. Triggers address // resolution asynchronously. // // If onResolve is provided, it will be called either immediately, if resolution // is not required, or when address resolution is complete, with the resolved // link address and whether resolution succeeded. After any callbacks have been // called, the returned notification channel is closed. // // NB: if a callback is provided, it should not call into the neighbor cache. // // If specified, the local address must be an address local to the interface the // neighbor cache belongs to. The local address is the source address of a // packet prompting NUD/link address resolution. // // TODO(gvisor.dev/issue/5151): Don't return the neighbor entry. func (n *neighborCache) entry(remoteAddr, localAddr tcpip.Address, onResolve func(LinkResolutionResult)) (NeighborEntry, <-chan struct{}, tcpip.Error) { entry := n.getOrCreateEntry(remoteAddr) entry.mu.Lock() defer entry.mu.Unlock() switch s := entry.mu.neigh.State; s { case Stale: entry.handlePacketQueuedLocked(localAddr) fallthrough case Reachable, Static, Delay, Probe: // As per RFC 4861 section 7.3.3: // "Neighbor Unreachability Detection operates in parallel with the sending // of packets to a neighbor. While reasserting a neighbor's reachability, // a node continues sending packets to that neighbor using the cached // link-layer address." if onResolve != nil { onResolve(LinkResolutionResult{LinkAddress: entry.mu.neigh.LinkAddr, Success: true}) } return entry.mu.neigh, nil, nil case Unknown, Incomplete, Unreachable: if onResolve != nil { entry.mu.onResolve = append(entry.mu.onResolve, onResolve) } if entry.mu.done == nil { // Address resolution needs to be initiated. entry.mu.done = make(chan struct{}) } entry.handlePacketQueuedLocked(localAddr) return entry.mu.neigh, entry.mu.done, &tcpip.ErrWouldBlock{} default: panic(fmt.Sprintf("Invalid cache entry state: %s", s)) } } // entries returns all entries in the neighbor cache. func (n *neighborCache) entries() []NeighborEntry { n.mu.RLock() defer n.mu.RUnlock() entries := make([]NeighborEntry, 0, len(n.mu.cache)) for _, entry := range n.mu.cache { entry.mu.RLock() entries = append(entries, entry.mu.neigh) entry.mu.RUnlock() } return entries } // addStaticEntry adds a static entry to the neighbor cache, mapping an IP // address to a link address. If a dynamic entry exists in the neighbor cache // with the same address, it will be replaced with this static entry. If a // static entry exists with the same address but different link address, it // will be updated with the new link address. If a static entry exists with the // same address and link address, nothing will happen. func (n *neighborCache) addStaticEntry(addr tcpip.Address, linkAddr tcpip.LinkAddress) { n.mu.Lock() defer n.mu.Unlock() if entry, ok := n.mu.cache[addr]; ok { entry.mu.Lock() if entry.mu.neigh.State != Static { // Dynamic entry found with the same address. n.mu.dynamic.lru.Remove(entry) n.mu.dynamic.count-- } else if entry.mu.neigh.LinkAddr == linkAddr { // Static entry found with the same address and link address. entry.mu.Unlock() return } else { // Static entry found with the same address but different link address. entry.mu.neigh.LinkAddr = linkAddr entry.dispatchChangeEventLocked() entry.mu.Unlock() return } entry.removeLocked() entry.mu.Unlock() } entry := newStaticNeighborEntry(n, addr, linkAddr, n.state) n.mu.cache[addr] = entry entry.mu.Lock() defer entry.mu.Unlock() entry.dispatchAddEventLocked() } // removeEntry removes a dynamic or static entry by address from the neighbor // cache. Returns true if the entry was found and deleted. func (n *neighborCache) removeEntry(addr tcpip.Address) bool { n.mu.Lock() defer n.mu.Unlock() entry, ok := n.mu.cache[addr] if !ok { return false } entry.mu.Lock() defer entry.mu.Unlock() if entry.mu.neigh.State != Static { n.mu.dynamic.lru.Remove(entry) n.mu.dynamic.count-- } entry.removeLocked() delete(n.mu.cache, entry.mu.neigh.Addr) return true } // clear removes all dynamic and static entries from the neighbor cache. func (n *neighborCache) clear() { n.mu.Lock() defer n.mu.Unlock() for _, entry := range n.mu.cache { entry.mu.Lock() entry.removeLocked() entry.mu.Unlock() } n.mu.dynamic.lru = neighborEntryList{} n.mu.cache = make(map[tcpip.Address]*neighborEntry) n.mu.dynamic.count = 0 } // config returns the NUD configuration. func (n *neighborCache) config() NUDConfigurations { return n.state.Config() } // setConfig changes the NUD configuration. // // If config contains invalid NUD configuration values, it will be fixed to // use default values for the erroneous values. func (n *neighborCache) setConfig(config NUDConfigurations) { config.resetInvalidFields() n.state.SetConfig(config) } // handleProbe handles a neighbor probe as defined by RFC 4861 section 7.2.3. // // Validation of the probe is expected to be handled by the caller. func (n *neighborCache) handleProbe(remoteAddr tcpip.Address, remoteLinkAddr tcpip.LinkAddress) { entry := n.getOrCreateEntry(remoteAddr) entry.mu.Lock() entry.handleProbeLocked(remoteLinkAddr) entry.mu.Unlock() } // handleConfirmation handles a neighbor confirmation as defined by // RFC 4861 section 7.2.5. // // Validation of the confirmation is expected to be handled by the caller. func (n *neighborCache) handleConfirmation(addr tcpip.Address, linkAddr tcpip.LinkAddress, flags ReachabilityConfirmationFlags) { n.mu.RLock() entry, ok := n.mu.cache[addr] n.mu.RUnlock() if ok { entry.mu.Lock() entry.handleConfirmationLocked(linkAddr, flags) entry.mu.Unlock() } // The confirmation SHOULD be silently discarded if the recipient did not // initiate any communication with the target. This is indicated if there is // no matching entry for the remote address. } // handleUpperLevelConfirmation processes a confirmation of reachablity from // some protocol that operates at a layer above the IP/link layer. func (n *neighborCache) handleUpperLevelConfirmation(addr tcpip.Address) { n.mu.RLock() entry, ok := n.mu.cache[addr] n.mu.RUnlock() if ok { entry.mu.Lock() entry.handleUpperLevelConfirmationLocked() entry.mu.Unlock() } } func (n *neighborCache) init(nic *nic, r LinkAddressResolver) { *n = neighborCache{ nic: nic, state: NewNUDState(nic.stack.nudConfigs, nic.stack.randomGenerator), linkRes: r, } n.mu.Lock() n.mu.cache = make(map[tcpip.Address]*neighborEntry, neighborCacheSize) n.mu.Unlock() }