tailscale/net/routetable/routetable_bsd.go

294 lines
6.9 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin || freebsd
package routetable
import (
"bufio"
"fmt"
"net"
"net/netip"
"runtime"
"sort"
"strings"
"syscall"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
)
type RouteEntryBSD struct {
// GatewayInterface is the name of the interface specified as a gateway
// for this route, if any.
GatewayInterface string
// GatewayIdx is the index of the interface specified as a gateway for
// this route, if any.
GatewayIdx int
// GatewayAddr is the link-layer address of the gateway for this route,
// if any.
GatewayAddr string
// Flags contains a string representation of common flags for this
// route.
Flags []string
// RawFlags contains the raw flags that were returned by the operating
// system for this route.
RawFlags int
}
// Format implements the fmt.Formatter interface.
func (r RouteEntryBSD) Format(f fmt.State, verb rune) {
logger.ArgWriter(func(w *bufio.Writer) {
var pstart bool
pr := func(format string, args ...any) {
if pstart {
fmt.Fprintf(w, ", "+format, args...)
} else {
fmt.Fprintf(w, format, args...)
pstart = true
}
}
w.WriteString("{")
if r.GatewayInterface != "" {
pr("GatewayInterface: %s", r.GatewayInterface)
}
if r.GatewayIdx > 0 {
pr("GatewayIdx: %d", r.GatewayIdx)
}
if r.GatewayAddr != "" {
pr("GatewayAddr: %s", r.GatewayAddr)
}
pr("Flags: %v", r.Flags)
unknownFlags := r.RawFlags
for fv := range flags {
if r.RawFlags&fv == fv {
unknownFlags &= ^fv
}
}
if unknownFlags != 0 {
pr("UnknownFlags: %x ", unknownFlags)
}
w.WriteString("}")
}).Format(f, verb)
}
// ipFromRMAddr returns a netip.Addr converted from one of the
// route.Inet{4,6}Addr types.
func ipFromRMAddr(ifs map[int]netmon.Interface, addr any) netip.Addr {
switch v := addr.(type) {
case *route.Inet4Addr:
return netip.AddrFrom4(v.IP)
case *route.Inet6Addr:
ip := netip.AddrFrom16(v.IP)
if v.ZoneID != 0 {
if iif, ok := ifs[v.ZoneID]; ok {
ip = ip.WithZone(iif.Name)
} else {
ip = ip.WithZone(fmt.Sprint(v.ZoneID))
}
}
return ip
}
return netip.Addr{}
}
// populateGateway populates gateway fields on a RouteEntry/RouteEntryBSD.
func populateGateway(re *RouteEntry, reSys *RouteEntryBSD, ifs map[int]netmon.Interface, addr any) {
// If the address type has a valid IP, use that.
if ip := ipFromRMAddr(ifs, addr); ip.IsValid() {
re.Gateway = ip
return
}
switch v := addr.(type) {
case *route.LinkAddr:
reSys.GatewayIdx = v.Index
if iif, ok := ifs[v.Index]; ok {
reSys.GatewayInterface = iif.Name
}
var sb strings.Builder
for i, x := range v.Addr {
if i != 0 {
sb.WriteByte(':')
}
fmt.Fprintf(&sb, "%02x", x)
}
reSys.GatewayAddr = sb.String()
}
}
// populateDestination populates the 'Dst' field on a RouteEntry based on the
// RouteMessage's destination and netmask fields.
func populateDestination(re *RouteEntry, ifs map[int]netmon.Interface, rm *route.RouteMessage) {
dst := rm.Addrs[unix.RTAX_DST]
if dst == nil {
return
}
ip := ipFromRMAddr(ifs, dst)
if !ip.IsValid() {
return
}
if ip.Is4() {
re.Family = 4
} else {
re.Family = 6
}
re.Dst = RouteDestination{
Prefix: netip.PrefixFrom(ip, 32), // default if nothing more specific
}
// If the RTF_HOST flag is set, then this is a host route and there's
// no netmask in this RouteMessage.
if rm.Flags&unix.RTF_HOST != 0 {
return
}
// As above if there's no netmask in the list of addrs
if len(rm.Addrs) < unix.RTAX_NETMASK || rm.Addrs[unix.RTAX_NETMASK] == nil {
return
}
nm := ipFromRMAddr(ifs, rm.Addrs[unix.RTAX_NETMASK])
if !ip.IsValid() {
return
}
// Count the number of bits in the netmask IP and use that to make our prefix.
ones, _ /* bits */ := net.IPMask(nm.AsSlice()).Size()
// Print this ourselves instead of using netip.Prefix so that we don't
// lose the zone (since netip.Prefix strips that).
//
// NOTE(andrew): this doesn't print the same values as the 'netstat' tool
// for some addresses on macOS, and I have no idea why. Specifically,
// 'netstat -rn' will show something like:
// ff00::/8 ::1 UmCI lo0
//
// But we will get:
// destination=ff00::/40 [...]
//
// The netmask that we get back from FetchRIB has 32 more bits in it
// than netstat prints, but only for multicast routes.
//
// For consistency's sake, we're going to do the same here so that we
// get the same values as netstat returns.
if runtime.GOOS == "darwin" && ip.Is6() && ip.IsMulticast() && ones > 32 {
ones -= 32
}
re.Dst = RouteDestination{
Prefix: netip.PrefixFrom(ip, ones),
Zone: ip.Zone(),
}
}
// routeEntryFromMsg returns a RouteEntry from a single route.Message
// returned by the operating system.
func routeEntryFromMsg(ifsByIdx map[int]netmon.Interface, msg route.Message) (RouteEntry, bool) {
rm, ok := msg.(*route.RouteMessage)
if !ok {
return RouteEntry{}, false
}
// Ignore things that we don't understand
if rm.Version < 3 || rm.Version > 5 {
return RouteEntry{}, false
}
if rm.Type != rmExpectedType {
return RouteEntry{}, false
}
if len(rm.Addrs) < unix.RTAX_GATEWAY {
return RouteEntry{}, false
}
if rm.Flags&skipFlags != 0 {
return RouteEntry{}, false
}
reSys := RouteEntryBSD{
RawFlags: rm.Flags,
}
for fv, fs := range flags {
if rm.Flags&fv == fv {
reSys.Flags = append(reSys.Flags, fs)
}
}
sort.Strings(reSys.Flags)
re := RouteEntry{}
hasFlag := func(f int) bool { return rm.Flags&f != 0 }
switch {
case hasFlag(unix.RTF_LOCAL):
re.Type = RouteTypeLocal
case hasFlag(unix.RTF_BROADCAST):
re.Type = RouteTypeBroadcast
case hasFlag(unix.RTF_MULTICAST):
re.Type = RouteTypeMulticast
// From the manpage: "host entry (net otherwise)"
case !hasFlag(unix.RTF_HOST):
re.Type = RouteTypeUnicast
default:
re.Type = RouteTypeOther
}
populateDestination(&re, ifsByIdx, rm)
if unix.RTAX_GATEWAY < len(rm.Addrs) {
populateGateway(&re, &reSys, ifsByIdx, rm.Addrs[unix.RTAX_GATEWAY])
}
if outif, ok := ifsByIdx[rm.Index]; ok {
re.Interface = outif.Name
}
re.Sys = reSys
return re, true
}
// Get returns route entries from the system route table, limited to at most
// 'max' results.
func Get(max int) ([]RouteEntry, error) {
// Fetching the list of interfaces can race with fetching our route
// table, but we do it anyway since it's helpful for debugging.
ifs, err := netmon.GetInterfaceList()
if err != nil {
return nil, err
}
ifsByIdx := make(map[int]netmon.Interface)
for _, iif := range ifs {
ifsByIdx[iif.Index] = iif
}
rib, err := route.FetchRIB(syscall.AF_UNSPEC, ribType, 0)
if err != nil {
return nil, err
}
msgs, err := route.ParseRIB(parseType, rib)
if err != nil {
return nil, err
}
var ret []RouteEntry
for _, m := range msgs {
re, ok := routeEntryFromMsg(ifsByIdx, m)
if ok {
ret = append(ret, re)
if len(ret) == max {
break
}
}
}
return ret, nil
}