blob: 2f1c6bb2100736dcb3c82465f05903e4e5a38ffc [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package routes
import (
"errors"
"fmt"
"net"
"sort"
"strings"
syslog "go.fuchsia.dev/fuchsia/src/lib/syslog/go"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/fidlconv"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/routetypes"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/sync"
"go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/util"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
const tag = "routes"
var (
ErrNoSuchRoute = errors.New("no such route")
ErrNoSuchNIC = errors.New("no such NIC")
)
type ExtendedRouteTable []routetypes.ExtendedRoute
func (rt ExtendedRouteTable) String() string {
var out strings.Builder
for _, r := range rt {
fmt.Fprintf(&out, "%s\n", &r)
}
return out.String()
}
type RoutingTableChange = routetypes.RoutingTableChange
type SendRoutingTableChangeCb func(RoutingTableChange)
// RoutingChangeSender queues up pending changes to the RouteTable, and
// sends these changes to clients of the `fuchsia.net.routes.Watcher` protocols
// once the events changes are committed into gVisor.
type routingChangeSender struct {
// A buffer containing accumulated changes to the routing table that have
// not yet been sent into `RoutingChangesChan`.
pendingChanges []RoutingTableChange
// A channel to forward changes in routing state to the clients of
// fuchsia.net.routes Watcher protocols.
onChangeCb SendRoutingTableChangeCb
}
// queueChange queues a RoutingTableChange in the RoutingChangeSender.
func (s *routingChangeSender) queueChange(c RoutingTableChange) {
if s.onChangeCb != nil {
s.pendingChanges = append(s.pendingChanges, c)
}
}
// flushChanges drains the pendingChanges by sending each RoutingTableChange
// into the RoutingChangesChan.
func (s *routingChangeSender) flushChanges() {
if s.onChangeCb == nil {
return
}
for _, c := range s.pendingChanges {
s.onChangeCb(c)
}
s.pendingChanges = nil
}
// RouteTable implements a sorted list of extended routes that is used to build
// the Netstack lib route table.
type RouteTable struct {
sync.Mutex
routes ExtendedRouteTable
sender routingChangeSender
}
func NewRouteTableWithOnChangeCallback(cb SendRoutingTableChangeCb) RouteTable {
return RouteTable{
sender: routingChangeSender{
onChangeCb: cb,
},
}
}
// For debugging.
func (rt *RouteTable) dumpLocked() {
if rt == nil {
syslog.VLogTf(syslog.TraceVerbosity, tag, "Current Route Table:<nil>")
} else {
syslog.VLogTf(syslog.TraceVerbosity, tag, "Current Route Table:\n%s", rt.routes)
}
}
// HasDefaultRoutes returns whether an interface has default IPv4/IPv6 routes.
func (rt *RouteTable) HasDefaultRouteLocked(nicid tcpip.NICID) (bool, bool) {
var v4, v6 bool
for _, er := range rt.routes {
if er.Route.NIC == nicid && er.Enabled {
if er.Route.Destination.Equal(header.IPv4EmptySubnet) {
v4 = true
} else if er.Route.Destination.Equal(header.IPv6EmptySubnet) {
v6 = true
}
}
}
return v4, v6
}
// For testing.
func (rt *RouteTable) Set(r []routetypes.ExtendedRoute) {
rt.Lock()
defer rt.Unlock()
rt.routes = append([]routetypes.ExtendedRoute(nil), r...)
}
type AddResult struct {
NewlyAddedToTable bool
NewlyAddedToSet bool
}
func (rt *RouteTable) AddRouteLocked(route tcpip.Route, prf routetypes.Preference, metric routetypes.Metric, tracksInterface, dynamic, enabled, replaceMatchingGvisorRoutes bool, addingSet *routetypes.RouteSetId) AddResult {
syslog.VLogTf(syslog.DebugVerbosity, tag, "RouteTable:Adding route %s with prf=%d metric=%d, tracksInterface=%t, dynamic=%t, enabled=%t, replaceMatchingGvisorRoutes=%t", route, prf, metric, tracksInterface, dynamic, enabled, replaceMatchingGvisorRoutes)
type foldResult int
const (
neitherPresentInTableNorInSet foldResult = iota
presentInTableButNotInSet
presentInTableAndInSet
)
newEr := routetypes.ExtendedRoute{
Route: route,
Prf: prf,
Metric: metric,
MetricTracksInterface: tracksInterface,
Dynamic: dynamic,
Enabled: enabled,
OwningSets: map[*routetypes.RouteSetId]struct{}{
addingSet: {},
},
}
incomingKey, err := fidlconv.ToRouteComparisonKey(newEr)
if err != nil {
_ = syslog.ErrorTf(tag, "RouteTable:conversion of route-to-be-added %#v to comparison key failed: %s", newEr, err)
return AddResult{
NewlyAddedToTable: false,
NewlyAddedToSet: false,
}
}
result := foldMapRoutesLocked[foldResult](
rt,
func(er *routetypes.ExtendedRoute) (bool, foldResult) {
existingKey, err := fidlconv.ToRouteComparisonKey(*er)
if err != nil {
panic(fmt.Sprintf("existing route in table %#v is invalid and thus cannot be converted to a comparison key: %s", *er, err))
}
if er.Route == route && replaceMatchingGvisorRoutes {
// Remove the route from the table, because we're going to re-add it.
return false, neitherPresentInTableNorInSet
} else if existingKey == incomingKey {
// Routes match. Make sure that the route is only treated as dynamic
// if all instances of it are dynamic.
er.Dynamic = er.Dynamic && newEr.Dynamic
if !addingSet.IsGlobal() {
if er.IsMemberOfRouteSet(addingSet) {
return true, presentInTableAndInSet
} else {
er.OwningSets[addingSet] = struct{}{}
return true, presentInTableButNotInSet
}
} else {
// If the route set is global, then being present in the table is
// equivalent to being present in the set.
return true, presentInTableAndInSet
}
} else {
// This route is unrelated, so we keep it.
return true, neitherPresentInTableNorInSet
}
},
func(a, b foldResult) foldResult {
// If either foldResult indicates the route already exists in the table,
// keep that one.
if a != neitherPresentInTableNorInSet && b != neitherPresentInTableNorInSet {
panic(fmt.Sprintf("duplicate routing table entries in %s", rt.routes))
}
if a != neitherPresentInTableNorInSet {
return a
}
return b
},
neitherPresentInTableNorInSet,
)
switch result {
case presentInTableButNotInSet:
return AddResult{
NewlyAddedToTable: false,
NewlyAddedToSet: true,
}
case presentInTableAndInSet:
return AddResult{
NewlyAddedToTable: false,
NewlyAddedToSet: false,
}
case neitherPresentInTableNorInSet:
default:
panic(fmt.Sprintf("unknown foldResult value: %d", result))
}
// Find the target position for the new route in the table so it remains
// sorted.
targetIdx := sort.Search(len(rt.routes), func(i int) bool {
return Less(&newEr, &rt.routes[i])
})
// Extend the table by adding the new route at the end, then move it into its
// proper place.
rt.routes = append(rt.routes, newEr)
if targetIdx < len(rt.routes)-1 {
copy(rt.routes[targetIdx+1:], rt.routes[targetIdx:])
rt.routes[targetIdx] = newEr
}
rt.dumpLocked()
rt.sender.queueChange(RoutingTableChange{
Change: routetypes.RouteAdded,
Route: newEr,
})
return AddResult{
NewlyAddedToTable: true,
NewlyAddedToSet: true,
}
}
// AddRoute inserts the given route to the table in a sorted fashion. If the
// route already exists, it simply updates that route's preference, metric,
// dynamic, and enabled fields.
func (rt *RouteTable) AddRoute(route tcpip.Route, prf routetypes.Preference, metric routetypes.Metric, tracksInterface, dynamic, enabled bool, addingSet *routetypes.RouteSetId) AddResult {
rt.Lock()
defer rt.Unlock()
return rt.AddRouteLocked(route, prf, metric, tracksInterface, dynamic, enabled, true /* replaceMatchingGvisorRoutes */, addingSet)
}
func (rt *RouteTable) DelRouteLocked(route tcpip.Route, deletingSet *routetypes.RouteSetId) []routetypes.ExtendedRoute {
return rt.delRouteLocked(route, deletingSet)
}
func (rt *RouteTable) delRouteLocked(route tcpip.Route, deletingSet *routetypes.RouteSetId) []routetypes.ExtendedRoute {
syslog.VLogTf(syslog.DebugVerbosity, tag, "RouteTable:Deleting route %s", route)
routesDeleted := foldMapRoutesLocked[[]routetypes.ExtendedRoute](
rt,
func(er *routetypes.ExtendedRoute) (bool, []routetypes.ExtendedRoute) {
if er.Route.Destination == route.Destination && er.Route.NIC == route.NIC {
deletingSetMatches := !deletingSet.IsGlobal() && er.IsMemberOfRouteSet(deletingSet)
// Match any route if Gateway is empty.
if route.Gateway.Len() == 0 ||
er.Route.Gateway == route.Gateway {
if deletingSetMatches {
delete(er.OwningSets, deletingSet)
}
if deletingSet.IsGlobal() || deletingSetMatches && len(er.OwningSets) == 0 {
return false, []routetypes.ExtendedRoute{*er}
}
}
}
return true, nil
},
func(a, b []routetypes.ExtendedRoute) []routetypes.ExtendedRoute {
return append(a, b...)
},
nil,
)
return routesDeleted
}
// foldMapRoutesLocked runs f on every route in the RouteTable. If f returns
// false for a given route, the route is removed; otherwise, the route is
// preserved (including modifications to the pointed-to route).
//
// The second return value of f is aggregated for each route using agg, and
// foldMapRoutesLocked returns the aggregate value.
func foldMapRoutesLocked[T any](rt *RouteTable, f func(er *routetypes.ExtendedRoute) (bool, T), agg func(a T, b T) T, init T) T {
var routesDeleted []routetypes.ExtendedRoute
oldTable := rt.routes
rt.routes = oldTable[:0]
if cap(oldTable) > 2*len(oldTable) {
// Remove excess route table capacity instead of reusing old capacity.
rt.routes = make([]routetypes.ExtendedRoute, 0, len(oldTable))
}
for _, er := range oldTable {
keepEr, meta := f(&er)
init = agg(meta, init)
if keepEr {
rt.routes = append(rt.routes, er)
} else {
routesDeleted = append(routesDeleted, er)
}
}
// Zero out unused entries in the routes slice so that they can be garbage collected.
deadRoutes := rt.routes[len(rt.routes):cap(rt.routes)]
for i := range deadRoutes {
deadRoutes[i] = routetypes.ExtendedRoute{}
}
rt.dumpLocked()
for _, er := range routesDeleted {
rt.sender.queueChange(RoutingTableChange{
Change: routetypes.RouteRemoved,
Route: er,
})
}
return init
}
type DelResult struct {
NewlyRemovedFromTable bool
NewlyRemovedFromSet bool
}
// DelRouteExactMatchLocked deletes a route from the routing table if there is
// an extended route with the same tcpip.Route, preference, and metric.
func (rt *RouteTable) DelRouteExactMatchLocked(route tcpip.Route, prf routetypes.Preference, metric routetypes.Metric, tracksInterface bool, set *routetypes.RouteSetId) DelResult {
syslog.DebugTf(tag, "RouteTable:Deleting route %s requiring exact match for prf=%d metric=%d tracksInterface=%t", route, prf, metric, tracksInterface)
delEr := routetypes.ExtendedRoute{
Route: route,
Prf: prf,
Metric: metric,
MetricTracksInterface: tracksInterface,
}
incomingKey, err := fidlconv.ToRouteComparisonKey(delEr)
if err != nil {
_ = syslog.ErrorTf(tag, "RouteTable:conversion of route-to-be-deleted %#v to comparison key failed: %s", delEr, err)
return DelResult{
NewlyRemovedFromTable: false,
NewlyRemovedFromSet: false,
}
}
delResult := foldMapRoutesLocked[DelResult](
rt,
func(er *routetypes.ExtendedRoute) (bool, DelResult) {
existingKey, err := fidlconv.ToRouteComparisonKey(*er)
if err != nil {
panic(fmt.Sprintf("existing route in table %#v is invalid and thus cannot be converted to a comparison key: %s", *er, err))
}
keepInTable := true
removedFromSet := false
if existingKey == incomingKey {
if er.IsMemberOfRouteSet(set) || set.IsGlobal() {
// The global route set is intentionally never in the owning
// sets for a route.
if !set.IsGlobal() {
delete(er.OwningSets, set)
}
removedFromSet = true
if len(er.OwningSets) == 0 || set.IsGlobal() {
keepInTable = false
}
}
}
return keepInTable, DelResult{
NewlyRemovedFromTable: !keepInTable,
NewlyRemovedFromSet: removedFromSet,
}
},
func(a, b DelResult) DelResult {
return DelResult{
NewlyRemovedFromTable: a.NewlyRemovedFromTable || b.NewlyRemovedFromTable,
NewlyRemovedFromSet: a.NewlyRemovedFromSet || b.NewlyRemovedFromSet,
}
},
DelResult{},
)
if !delResult.NewlyRemovedFromSet {
_ = syslog.DebugTf(tag, "did not find exact match for route=%#v prf=%#v metric=%#v tracksInterface=%#v set=%#v; routes: %#v", route, prf, metric, tracksInterface, set, rt)
}
return delResult
}
// DelRoute removes matching routes from the route table, returning them.
func (rt *RouteTable) DelRoute(route tcpip.Route, deletingSet *routetypes.RouteSetId) []routetypes.ExtendedRoute {
rt.Lock()
defer rt.Unlock()
return rt.DelRouteLocked(route, deletingSet)
}
// DelRouteSetLocked closes a user routeSet and returns any routes deleted from the route table as a
// result. If the routeSet is global, no changes are made.
func (rt *RouteTable) DelRouteSetLocked(routeSet *routetypes.RouteSetId) []routetypes.ExtendedRoute {
if routeSet.IsGlobal() {
return nil
}
routesDeleted := foldMapRoutesLocked[[]routetypes.ExtendedRoute](
rt,
func(er *routetypes.ExtendedRoute) (bool, []routetypes.ExtendedRoute) {
if er.IsMemberOfRouteSet(routeSet) {
delete(er.OwningSets, routeSet)
if len(er.OwningSets) == 0 {
return false, []routetypes.ExtendedRoute{*er}
}
}
_ = syslog.DebugTf(tag, "DelRouteSetLocked(%#v) did not delete %s with OwningSets %#v", routeSet, er, er.OwningSets)
return true, nil
},
func(a, b []routetypes.ExtendedRoute) []routetypes.ExtendedRoute {
return append(a, b...)
},
nil,
)
_ = syslog.DebugTf(tag, "DelRouteSetLocked(%#v) deleted %d routes", routeSet, len(routesDeleted))
return routesDeleted
}
// GetExtendedRouteTable returns a copy of the current extended route table.
func (rt *RouteTable) GetExtendedRouteTable() ExtendedRouteTable {
rt.Lock()
defer rt.Unlock()
rt.dumpLocked()
return append([]routetypes.ExtendedRoute(nil), rt.routes...)
}
// UpdateStack updates stack with the current route table.
func (rt *RouteTable) UpdateStackLocked(stack *stack.Stack, onUpdateSucceeded func()) {
t := make([]tcpip.Route, 0, len(rt.routes))
for _, er := range rt.routes {
if er.Enabled {
t = append(t, er.Route)
}
}
stack.SetRouteTable(t)
_ = syslog.VLogTf(syslog.DebugVerbosity, tag, "UpdateStack route table: %+v", t)
onUpdateSucceeded()
// Notify clients of `fuchsia.net.routes/Watcher` of the changes.
rt.sender.flushChanges()
}
// UpdateStack updates stack with the current route table.
func (rt *RouteTable) UpdateStack(stack *stack.Stack, onUpdateSucceeded func()) {
rt.Lock()
defer rt.Unlock()
rt.UpdateStackLocked(stack, onUpdateSucceeded)
}
func (rt *RouteTable) UpdateRoutesByInterfaceLocked(nicid tcpip.NICID, action routetypes.Action) {
syslog.VLogTf(syslog.DebugVerbosity, tag, "RouteTable:Update route table for routes to nic-%d with action:%d", nicid, action)
oldTable := rt.routes
rt.routes = oldTable[:0]
if cap(oldTable) > 2*len(oldTable) {
// Remove excess route table capacity instead of reusing old capacity.
rt.routes = make([]routetypes.ExtendedRoute, 0, len(oldTable))
}
for _, er := range oldTable {
if er.Route.NIC == nicid {
switch action {
case routetypes.ActionDeleteAll:
rt.sender.queueChange(RoutingTableChange{
Change: routetypes.RouteRemoved,
Route: er,
})
continue // delete
case routetypes.ActionDeleteDynamic:
if er.Dynamic {
rt.sender.queueChange(RoutingTableChange{
Change: routetypes.RouteRemoved,
Route: er,
})
continue // delete
}
case routetypes.ActionDisableStatic:
if !er.Dynamic {
er.Enabled = false
}
case routetypes.ActionEnableStatic:
if !er.Dynamic {
er.Enabled = true
}
}
}
// Keep.
rt.routes = append(rt.routes, er)
}
// Zero out unused entries in the routes slice so that they can be garbage collected.
deadRoutes := rt.routes[len(rt.routes):cap(rt.routes)]
for i := range deadRoutes {
deadRoutes[i] = routetypes.ExtendedRoute{}
}
rt.sortRouteTableLocked()
rt.dumpLocked()
}
// UpdateRoutesByInterface applies an action to the routes pointing to an interface.
func (rt *RouteTable) UpdateRoutesByInterface(nicid tcpip.NICID, action routetypes.Action) {
rt.Lock()
defer rt.Unlock()
rt.UpdateRoutesByInterfaceLocked(nicid, action)
}
// FindNIC returns the NIC-ID that the given address is routed on. This requires
// an exact route match, i.e. no default route.
func (rt *RouteTable) FindNIC(addr tcpip.Address) (tcpip.NICID, error) {
rt.Lock()
defer rt.Unlock()
for _, er := range rt.routes {
// Ignore default routes.
if util.IsAny(er.Route.Destination.ID()) {
continue
}
if er.Match(addr) && er.Route.NIC > 0 {
return er.Route.NIC, nil
}
}
return 0, ErrNoSuchNIC
}
// GetNICsWithDefaultRoutesLocked returns the set of NICs that have default
// IPv4 routes.
func (rt *RouteTable) GetNICsWithDefaultV4RoutesLocked() map[tcpip.NICID]struct{} {
set := make(map[tcpip.NICID]struct{})
for _, er := range rt.routes {
if er.Route.Destination.Equal(header.IPv4EmptySubnet) {
set[er.Route.NIC] = struct{}{}
}
}
return set
}
// GetNICsWithDefaultRoutesLocked returns the set of NICs that have default
// IPv6 routes.
func (rt *RouteTable) GetNICsWithDefaultV6RoutesLocked() map[tcpip.NICID]struct{} {
set := make(map[tcpip.NICID]struct{})
for _, er := range rt.routes {
if er.Route.Destination.Equal(header.IPv6EmptySubnet) {
set[er.Route.NIC] = struct{}{}
}
}
return set
}
func (rt *RouteTable) sortRouteTableLocked() {
sort.SliceStable(rt.routes, func(i, j int) bool {
return Less(&rt.routes[i], &rt.routes[j])
})
}
// Less compares two routes and returns which one should appear earlier in the
// route table.
func Less(ei, ej *routetypes.ExtendedRoute) bool {
ri, rj := ei.Route, ej.Route
riDest, rjDest := ri.Destination.ID(), rj.Destination.ID()
// Loopback routes before non-loopback ones.
// (as a workaround for github.com/google/gvisor/issues/1169).
if riIsLoop, rjIsLoop := net.IP(riDest.AsSlice()).IsLoopback(), net.IP(rjDest.AsSlice()).IsLoopback(); riIsLoop != rjIsLoop {
return riIsLoop
}
// Non-default before default one.
if riAny, rjAny := util.IsAny(riDest), util.IsAny(rjDest); riAny != rjAny {
return !riAny
}
// IPv4 before IPv6 (arbitrary choice).
if riLen, rjLen := riDest.Len(), rjDest.Len(); riLen != rjLen {
return riLen == header.IPv4AddressSize
}
// Longer prefix wins.
if riPrefix, rjPrefix := ri.Destination.Prefix(), rj.Destination.Prefix(); riPrefix != rjPrefix {
return riPrefix > rjPrefix
}
// On-link wins.
if riOnLink, rjOnLink := ri.Gateway.Len() == 0, rj.Gateway.Len() == 0; riOnLink != rjOnLink {
return riOnLink
}
// Higher preference wins.
if ei.Prf != ej.Prf {
return ei.Prf > ej.Prf
}
// Lower metrics wins.
if ei.Metric != ej.Metric {
return ei.Metric < ej.Metric
}
// Everything that matters is the same. At this point we still need a
// deterministic way to tie-break. First go by destination IPs (lower wins),
// finally use the NIC.
riDestAsSlice := riDest.AsSlice()
rjDestAsSlice := rjDest.AsSlice()
for i := 0; i < riDest.Len(); i++ {
if riDestAsSlice[i] != rjDestAsSlice[i] {
return riDestAsSlice[i] < rjDestAsSlice[i]
}
}
// Same prefix and destination IPs (e.g. loopback IPs), use NIC as final
// tie-breaker.
return ri.NIC < rj.NIC
}