blob: 483a8b0739033d9526e738d37a69210c4c5bfc6d [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.
// +build !build_with_native_toolchain
package routes
import (
"errors"
"fmt"
"net"
"sort"
"strings"
"sync"
syslog "go.fuchsia.dev/fuchsia/src/lib/syslog/go"
"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"
)
type Action uint32
const (
ActionDeleteAll Action = iota
ActionDeleteDynamic
ActionDisableStatic
ActionEnableStatic
)
const tag = "routes"
var (
ErrNoSuchRoute = errors.New("no such route")
ErrNoSuchNIC = errors.New("no such NIC")
)
// Metric is the metric used for sorting the route table. It acts as a
// priority with a lower value being better.
type Metric uint32
// ExtendedRoute is a single route that contains the standard tcpip.Route plus
// additional attributes.
type ExtendedRoute struct {
// Route used to build the route table to be fed into the
// gvisor.dev/gvisor/pkg lib.
Route tcpip.Route
// Metric acts as a tie-breaker when comparing otherwise identical routes.
Metric Metric
// MetricTracksInterface is true when the metric tracks the metric of the
// interface for this route. This means when the interface metric changes, so
// will this route's metric. If false, the metric is static and only changed
// explicitly by API.
MetricTracksInterface bool
// Dynamic marks a route as being obtained via DHCP. Such routes are removed
// from the table when the interface goes down, vs. just being disabled.
Dynamic bool
// Enabled marks a route as inactive, i.e., its interface is down and packets
// must not use this route.
// Disabled routes are omitted when building the route table for the
// Netstack lib.
// This flag is used with non-dynamic routes (i.e., statically added routes)
// to keep them in the table while their interface is down.
Enabled bool
}
// Match matches the given address against this route.
func (er *ExtendedRoute) Match(addr tcpip.Address) bool {
return er.Route.Destination.Contains(addr)
}
func (er *ExtendedRoute) String() string {
var out strings.Builder
fmt.Fprintf(&out, "%s", er.Route)
if er.MetricTracksInterface {
fmt.Fprintf(&out, " metric[if] %d", er.Metric)
} else {
fmt.Fprintf(&out, " metric[static] %d", er.Metric)
}
if er.Dynamic {
fmt.Fprintf(&out, " (dynamic)")
} else {
fmt.Fprintf(&out, " (static)")
}
if !er.Enabled {
fmt.Fprintf(&out, " (disabled)")
}
return out.String()
}
type ExtendedRouteTable []ExtendedRoute
func (rt ExtendedRouteTable) String() string {
var out strings.Builder
for _, r := range rt {
fmt.Fprintf(&out, "%s\n", &r)
}
return out.String()
}
// RouteTable implements a sorted list of extended routes that is used to build
// the Netstack lib route table.
type RouteTable struct {
mu struct {
sync.Mutex
routes ExtendedRouteTable
}
}
// 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.mu.routes)
}
}
// For testing.
func (rt *RouteTable) Set(r []ExtendedRoute) {
rt.mu.Lock()
defer rt.mu.Unlock()
rt.mu.routes = append([]ExtendedRoute(nil), r...)
}
// AddRoute inserts the given route to the table in a sorted fashion. If the
// route already exists, it simply updates that route's metric, dynamic and
// enabled fields.
func (rt *RouteTable) AddRoute(route tcpip.Route, metric Metric, tracksInterface bool, dynamic bool, enabled bool) {
syslog.VLogTf(syslog.DebugVerbosity, tag, "RouteTable:Adding route %s with metric:%d, trackIf=%t, dynamic=%t, enabled=%t", route, metric, tracksInterface, dynamic, enabled)
rt.mu.Lock()
defer rt.mu.Unlock()
// First check if the route already exists, and remove it.
for i, er := range rt.mu.routes {
if er.Route == route {
rt.mu.routes = append(rt.mu.routes[:i], rt.mu.routes[i+1:]...)
break
}
}
newEr := ExtendedRoute{
Route: route,
Metric: metric,
MetricTracksInterface: tracksInterface,
Dynamic: dynamic,
Enabled: enabled,
}
// Find the target position for the new route in the table so it remains
// sorted. Initialized to point to the end of the table.
targetIdx := len(rt.mu.routes)
for i, er := range rt.mu.routes {
if Less(&newEr, &er) {
targetIdx = i
break
}
}
// Extend the table by adding the new route at the end, then move it into its
// proper place.
rt.mu.routes = append(rt.mu.routes, newEr)
if targetIdx < len(rt.mu.routes)-1 {
copy(rt.mu.routes[targetIdx+1:], rt.mu.routes[targetIdx:])
rt.mu.routes[targetIdx] = newEr
}
rt.dumpLocked()
}
// DelRoute removes the given route from the route table.
func (rt *RouteTable) DelRoute(route tcpip.Route) error {
syslog.VLogTf(syslog.DebugVerbosity, tag, "RouteTable:Deleting route %s", route)
rt.mu.Lock()
defer rt.mu.Unlock()
routeDeleted := false
oldTable := rt.mu.routes
rt.mu.routes = oldTable[:0]
for _, er := range oldTable {
// Match all fields that are non-zero.
if er.Route.Destination == route.Destination {
if route.NIC == 0 || route.NIC == er.Route.NIC {
if len(route.Gateway) == 0 || route.Gateway == er.Route.Gateway {
routeDeleted = true
continue
}
}
}
// Not matched, remains in the route table.
rt.mu.routes = append(rt.mu.routes, er)
}
if !routeDeleted {
return ErrNoSuchRoute
}
rt.dumpLocked()
return nil
}
// GetExtendedRouteTable returns a copy of the current extended route table.
func (rt *RouteTable) GetExtendedRouteTable() ExtendedRouteTable {
rt.mu.Lock()
defer rt.mu.Unlock()
rt.dumpLocked()
return append([]ExtendedRoute(nil), rt.mu.routes...)
}
// UpdateStack updates stack with the current route table.
func (rt *RouteTable) UpdateStack(stack *stack.Stack) {
rt.mu.Lock()
t := make([]tcpip.Route, 0, len(rt.mu.routes))
for _, er := range rt.mu.routes {
if er.Enabled {
t = append(t, er.Route)
}
}
stack.SetRouteTable(t)
rt.mu.Unlock()
_ = syslog.VLogTf(syslog.DebugVerbosity, tag, "UpdateStack route table: %+v", t)
}
// UpdateMetricByInterface changes the metric for all routes that track a
// given interface.
func (rt *RouteTable) UpdateMetricByInterface(nicid tcpip.NICID, metric Metric) {
syslog.VLogf(syslog.DebugVerbosity, "RouteTable:Update route table on nic-%d metric change to %d", nicid, metric)
rt.mu.Lock()
defer rt.mu.Unlock()
for i, er := range rt.mu.routes {
if er.Route.NIC == nicid && er.MetricTracksInterface {
rt.mu.routes[i].Metric = metric
}
}
rt.sortRouteTableLocked()
rt.dumpLocked()
}
// UpdateRoutesByInterface applies an action to the routes pointing to an interface.
func (rt *RouteTable) UpdateRoutesByInterface(nicid tcpip.NICID, action Action) {
syslog.VLogTf(syslog.DebugVerbosity, tag, "RouteTable:Update route table for routes to nic-%d with action:%d", nicid, action)
rt.mu.Lock()
defer rt.mu.Unlock()
oldTable := rt.mu.routes
rt.mu.routes = oldTable[:0]
for _, er := range oldTable {
if er.Route.NIC == nicid {
switch action {
case ActionDeleteAll:
continue // delete
case ActionDeleteDynamic:
if er.Dynamic {
continue // delete
}
case ActionDisableStatic:
if !er.Dynamic {
er.Enabled = false
}
case ActionEnableStatic:
if !er.Dynamic {
er.Enabled = true
}
}
}
// Keep.
rt.mu.routes = append(rt.mu.routes, er)
}
rt.sortRouteTableLocked()
rt.dumpLocked()
}
// 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.mu.Lock()
defer rt.mu.Unlock()
for _, er := range rt.mu.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
}
func (rt *RouteTable) sortRouteTableLocked() {
sort.SliceStable(rt.mu.routes, func(i, j int) bool {
return Less(&rt.mu.routes[i], &rt.mu.routes[j])
})
}
// Less compares two routes and returns which one should appear earlier in the
// route table.
func Less(ei, ej *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).IsLoopback(), net.IP(rjDest).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 := len(riDest), len(rjDest); riLen != rjLen {
return riLen == header.IPv4AddressSize
}
// Longer prefix wins.
if riPrefix, rjPrefix := ri.Destination.Prefix(), rj.Destination.Prefix(); riPrefix != rjPrefix {
return riPrefix > rjPrefix
}
// 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.
for i := 0; i < len(riDest); i++ {
if riDest[i] != rjDest[i] {
return riDest[i] < rjDest[i]
}
}
// Same prefix and destination IPs (e.g. loopback IPs), use NIC as final
// tie-breaker.
return ri.NIC < rj.NIC
}