blob: c543230ec7bddcd31ba3ac381835fc52eaea5b93 [file] [log] [blame]
// Copyright 2018 Google Inc.
//
// 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"
"sync"
"time"
"github.com/google/netstack/sleep"
"github.com/google/netstack/tcpip"
)
const linkAddrCacheSize = 512 // max cache entries
// linkAddrCache is a fixed-sized cache mapping IP addresses to link addresses.
//
// The entries are stored in a ring buffer, oldest entry replaced first.
//
// This struct is safe for concurrent use.
type linkAddrCache struct {
// ageLimit is how long a cache entry is valid for.
ageLimit time.Duration
// resolutionTimeout is the amount of time to wait for a link request to
// resolve an address.
resolutionTimeout time.Duration
// resolutionAttempts is the number of times an address is attempted to be
// resolved before failing.
resolutionAttempts int
mu sync.Mutex
cache map[tcpip.FullAddress]*linkAddrEntry
next int // array index of next available entry
entries [linkAddrCacheSize]linkAddrEntry
}
// entryState controls the state of a single entry in the cache.
type entryState int
const (
// incomplete means that there is an outstanding request to resolve the
// address. This is the initial state.
incomplete entryState = iota
// ready means that the address has been resolved and can be used.
ready
// failed means that address resolution timed out and the address
// could not be resolved.
failed
// expired means that the cache entry has expired and the address must be
// resolved again.
expired
)
// String implements Stringer.
func (s entryState) String() string {
switch s {
case incomplete:
return "incomplete"
case ready:
return "ready"
case failed:
return "failed"
case expired:
return "expired"
default:
return fmt.Sprintf("invalid entryState: %d", s)
}
}
// A linkAddrEntry is an entry in the linkAddrCache.
// This struct is thread-compatible.
type linkAddrEntry struct {
addr tcpip.FullAddress
linkAddr tcpip.LinkAddress
expiration time.Time
s entryState
// wakers is a set of waiters for address resolution result. Anytime
// state transitions out of 'incomplete' these waiters are notified.
wakers map[*sleep.Waker]struct{}
cancel chan struct{}
}
func (e *linkAddrEntry) state() entryState {
if e.s != expired && time.Now().After(e.expiration) {
// Force the transition to ensure waiters are notified.
e.changeState(expired)
}
return e.s
}
func (e *linkAddrEntry) changeState(ns entryState) {
if e.s == ns {
return
}
// Validate state transition.
switch e.s {
case incomplete:
// All transitions are valid.
case ready, failed:
if ns != expired {
panic(fmt.Sprintf("invalid state transition from %v to %v", e.s, ns))
}
case expired:
// Terminal state.
panic(fmt.Sprintf("invalid state transition from %v to %v", e.s, ns))
default:
panic(fmt.Sprintf("invalid state: %v", e.s))
}
// Notify whoever is waiting on address resolution when transitioning
// out of 'incomplete'.
if e.s == incomplete {
for w := range e.wakers {
w.Assert()
}
e.wakers = nil
}
e.s = ns
}
func (e *linkAddrEntry) addWaker(w *sleep.Waker) {
e.wakers[w] = struct{}{}
}
func (e *linkAddrEntry) removeWaker(w *sleep.Waker) {
delete(e.wakers, w)
}
// add adds a k -> v mapping to the cache.
func (c *linkAddrCache) add(k tcpip.FullAddress, v tcpip.LinkAddress) {
c.mu.Lock()
defer c.mu.Unlock()
entry := c.cache[k]
if entry != nil {
s := entry.state()
if s != expired && entry.linkAddr == v {
// Disregard repeated calls.
return
}
// Check if entry is waiting for address resolution.
if s == incomplete {
entry.linkAddr = v
} else {
// Otherwise create a new entry to replace it.
entry = c.makeAndAddEntry(k, v)
}
} else {
entry = c.makeAndAddEntry(k, v)
}
entry.changeState(ready)
}
// makeAndAddEntry is a helper function to create and add a new
// entry to the cache map and evict older entry as needed.
func (c *linkAddrCache) makeAndAddEntry(k tcpip.FullAddress, v tcpip.LinkAddress) *linkAddrEntry {
// Take over the next entry.
entry := &c.entries[c.next]
if c.cache[entry.addr] == entry {
delete(c.cache, entry.addr)
}
// Mark the soon-to-be-replaced entry as expired, just in case there is
// someone waiting for address resolution on it.
entry.changeState(expired)
if entry.cancel != nil {
entry.cancel <- struct{}{}
}
*entry = linkAddrEntry{
addr: k,
linkAddr: v,
expiration: time.Now().Add(c.ageLimit),
wakers: make(map[*sleep.Waker]struct{}),
cancel: make(chan struct{}, 1),
}
c.cache[k] = entry
c.next++
if c.next == len(c.entries) {
c.next = 0
}
return entry
}
// get reports any known link address for k.
func (c *linkAddrCache) get(k tcpip.FullAddress, linkRes LinkAddressResolver, localAddr tcpip.Address, linkEP LinkEndpoint, waker *sleep.Waker) (tcpip.LinkAddress, *tcpip.Error) {
if linkRes != nil {
if addr, ok := linkRes.ResolveStaticAddress(k.Addr); ok {
return addr, nil
}
}
c.mu.Lock()
entry := c.cache[k]
if entry == nil || entry.state() == expired {
c.mu.Unlock()
if linkRes == nil {
return "", tcpip.ErrNoLinkAddress
}
c.startAddressResolution(k, linkRes, localAddr, linkEP, waker)
return "", tcpip.ErrWouldBlock
}
defer c.mu.Unlock()
switch s := entry.state(); s {
case expired:
// It's possible that entry expired between state() call above and here
// in that case it's safe to consider it ready.
fallthrough
case ready:
return entry.linkAddr, nil
case failed:
return "", tcpip.ErrNoLinkAddress
case incomplete:
// Address resolution is still in progress.
entry.addWaker(waker)
return "", tcpip.ErrWouldBlock
default:
panic(fmt.Sprintf("invalid cache entry state: %d", s))
}
}
// removeWaker removes a waker previously added through get().
func (c *linkAddrCache) removeWaker(k tcpip.FullAddress, waker *sleep.Waker) {
c.mu.Lock()
defer c.mu.Unlock()
if entry := c.cache[k]; entry != nil {
entry.removeWaker(waker)
}
}
func (c *linkAddrCache) startAddressResolution(k tcpip.FullAddress, linkRes LinkAddressResolver, localAddr tcpip.Address, linkEP LinkEndpoint, waker *sleep.Waker) {
c.mu.Lock()
defer c.mu.Unlock()
// Look up again with lock held to ensure entry wasn't added by someone else.
if e := c.cache[k]; e != nil && e.state() != expired {
return
}
// Add 'incomplete' entry in the cache to mark that resolution is in progress.
e := c.makeAndAddEntry(k, "")
e.addWaker(waker)
go func() {
for i := 0; ; i++ {
// Send link request, then wait for the timeout limit and check
// whether the request succeeded.
linkRes.LinkAddressRequest(k.Addr, localAddr, linkEP)
c.mu.Lock()
cancel := e.cancel
c.mu.Unlock()
select {
case <-time.After(c.resolutionTimeout):
if stop := c.checkLinkRequest(k, i); stop {
return
}
case <-cancel:
return
}
}
}()
}
// checkLinkRequest checks whether previous attempt to resolve address has succeeded
// and mark the entry accordingly, e.g. ready, failed, etc. Return true if request
// can stop, false if another request should be sent.
func (c *linkAddrCache) checkLinkRequest(k tcpip.FullAddress, attempt int) bool {
c.mu.Lock()
defer c.mu.Unlock()
entry, ok := c.cache[k]
if !ok {
// Entry was evicted from the cache.
return true
}
switch s := entry.state(); s {
case ready, failed, expired:
// Entry was made ready by resolver or failed. Either way we're done.
return true
case incomplete:
if attempt+1 >= c.resolutionAttempts {
// Max number of retries reached, mark entry as failed.
entry.changeState(failed)
return true
}
// No response yet, need to send another ARP request.
return false
default:
panic(fmt.Sprintf("invalid cache entry state: %d", s))
}
}
func newLinkAddrCache(ageLimit, resolutionTimeout time.Duration, resolutionAttempts int) *linkAddrCache {
return &linkAddrCache{
ageLimit: ageLimit,
resolutionTimeout: resolutionTimeout,
resolutionAttempts: resolutionAttempts,
cache: make(map[tcpip.FullAddress]*linkAddrEntry, linkAddrCacheSize),
}
}