mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	In prep for most of the package funcs in net/interfaces to become methods in a long-lived netmon.Monitor that can cache things. (Many of the funcs are very heavy to call regularly, whereas the long-lived netmon.Monitor can subscribe to things from the OS and remember answers to questions it's asked regularly later) Updates tailscale/corp#10910 Updates tailscale/corp#18960 Updates #7967 Updates #3299 Change-Id: Ie4e8dedb70136af2d611b990b865a822cd1797e5 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			294 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			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
 | |
| }
 |