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>
		
			
				
	
	
		
			434 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			434 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| //go:build darwin || freebsd
 | |
| 
 | |
| package routetable
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"net"
 | |
| 	"net/netip"
 | |
| 	"reflect"
 | |
| 	"runtime"
 | |
| 	"testing"
 | |
| 
 | |
| 	"golang.org/x/net/route"
 | |
| 	"golang.org/x/sys/unix"
 | |
| 	"tailscale.com/net/netmon"
 | |
| )
 | |
| 
 | |
| func TestRouteEntryFromMsg(t *testing.T) {
 | |
| 	ifs := map[int]netmon.Interface{
 | |
| 		1: {
 | |
| 			Interface: &net.Interface{
 | |
| 				Name: "iface0",
 | |
| 			},
 | |
| 		},
 | |
| 		2: {
 | |
| 			Interface: &net.Interface{
 | |
| 				Name: "tailscale0",
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	ip4 := func(s string) *route.Inet4Addr {
 | |
| 		ip := netip.MustParseAddr(s)
 | |
| 		return &route.Inet4Addr{IP: ip.As4()}
 | |
| 	}
 | |
| 	ip6 := func(s string) *route.Inet6Addr {
 | |
| 		ip := netip.MustParseAddr(s)
 | |
| 		return &route.Inet6Addr{IP: ip.As16()}
 | |
| 	}
 | |
| 	ip6zone := func(s string, idx int) *route.Inet6Addr {
 | |
| 		ip := netip.MustParseAddr(s)
 | |
| 		return &route.Inet6Addr{IP: ip.As16(), ZoneID: idx}
 | |
| 	}
 | |
| 	link := func(idx int, addr string) *route.LinkAddr {
 | |
| 		if _, found := ifs[idx]; !found {
 | |
| 			panic("index not found")
 | |
| 		}
 | |
| 
 | |
| 		ret := &route.LinkAddr{
 | |
| 			Index: idx,
 | |
| 		}
 | |
| 		if addr != "" {
 | |
| 			ret.Addr = make([]byte, 6)
 | |
| 			fmt.Sscanf(addr, "%02x:%02x:%02x:%02x:%02x:%02x",
 | |
| 				&ret.Addr[0],
 | |
| 				&ret.Addr[1],
 | |
| 				&ret.Addr[2],
 | |
| 				&ret.Addr[3],
 | |
| 				&ret.Addr[4],
 | |
| 				&ret.Addr[5],
 | |
| 			)
 | |
| 		}
 | |
| 		return ret
 | |
| 	}
 | |
| 
 | |
| 	type testCase struct {
 | |
| 		name string
 | |
| 		msg  *route.RouteMessage
 | |
| 		want RouteEntry
 | |
| 		fail bool
 | |
| 	}
 | |
| 
 | |
| 	testCases := []testCase{
 | |
| 		{
 | |
| 			name: "BasicIPv4",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip4("1.2.3.4"),       // dst
 | |
| 					ip4("1.2.3.1"),       // gateway
 | |
| 					ip4("255.255.255.0"), // netmask
 | |
| 				},
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family:  4,
 | |
| 				Type:    RouteTypeUnicast,
 | |
| 				Dst:     RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
 | |
| 				Gateway: netip.MustParseAddr("1.2.3.1"),
 | |
| 				Sys:     RouteEntryBSD{},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "BasicIPv6",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip6("fd7a:115c:a1e0::"), // dst
 | |
| 					ip6("1234::"),           // gateway
 | |
| 					ip6("ffff:ffff:ffff::"), // netmask
 | |
| 				},
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family:  6,
 | |
| 				Type:    RouteTypeUnicast,
 | |
| 				Dst:     RouteDestination{Prefix: netip.MustParsePrefix("fd7a:115c:a1e0::/48")},
 | |
| 				Gateway: netip.MustParseAddr("1234::"),
 | |
| 				Sys:     RouteEntryBSD{},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "IPv6WithZone",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip6zone("fe80::", 2),         // dst
 | |
| 					ip6("1234::"),                // gateway
 | |
| 					ip6("ffff:ffff:ffff:ffff::"), // netmask
 | |
| 				},
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family:  6,
 | |
| 				Type:    RouteTypeUnicast, // TODO
 | |
| 				Dst:     RouteDestination{Prefix: netip.MustParsePrefix("fe80::/64"), Zone: "tailscale0"},
 | |
| 				Gateway: netip.MustParseAddr("1234::"),
 | |
| 				Sys:     RouteEntryBSD{},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "IPv6WithUnknownZone",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip6zone("fe80::", 4),         // dst
 | |
| 					ip6("1234::"),                // gateway
 | |
| 					ip6("ffff:ffff:ffff:ffff::"), // netmask
 | |
| 				},
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family:  6,
 | |
| 				Type:    RouteTypeUnicast, // TODO
 | |
| 				Dst:     RouteDestination{Prefix: netip.MustParsePrefix("fe80::/64"), Zone: "4"},
 | |
| 				Gateway: netip.MustParseAddr("1234::"),
 | |
| 				Sys:     RouteEntryBSD{},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "DefaultIPv4",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip4("0.0.0.0"), // dst
 | |
| 					ip4("1.2.3.4"), // gateway
 | |
| 					ip4("0.0.0.0"), // netmask
 | |
| 				},
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family:  4,
 | |
| 				Type:    RouteTypeUnicast,
 | |
| 				Dst:     defaultRouteIPv4,
 | |
| 				Gateway: netip.MustParseAddr("1.2.3.4"),
 | |
| 				Sys:     RouteEntryBSD{},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "DefaultIPv6",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip6("0::"),    // dst
 | |
| 					ip6("1234::"), // gateway
 | |
| 					ip6("0::"),    // netmask
 | |
| 				},
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family:  6,
 | |
| 				Type:    RouteTypeUnicast,
 | |
| 				Dst:     defaultRouteIPv6,
 | |
| 				Gateway: netip.MustParseAddr("1234::"),
 | |
| 				Sys:     RouteEntryBSD{},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "ShortAddrs",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip4("1.2.3.4"), // dst
 | |
| 				},
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family: 4,
 | |
| 				Type:   RouteTypeUnicast,
 | |
| 				Dst:    RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/32")},
 | |
| 				Sys:    RouteEntryBSD{},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "TailscaleIPv4",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip4("100.64.0.0"), // dst
 | |
| 					link(2, ""),
 | |
| 					ip4("255.192.0.0"), // netmask
 | |
| 				},
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family: 4,
 | |
| 				Type:   RouteTypeUnicast,
 | |
| 				Dst:    RouteDestination{Prefix: netip.MustParsePrefix("100.64.0.0/10")},
 | |
| 				Sys: RouteEntryBSD{
 | |
| 					GatewayInterface: "tailscale0",
 | |
| 					GatewayIdx:       2,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Flags",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip4("1.2.3.4"),       // dst
 | |
| 					ip4("1.2.3.1"),       // gateway
 | |
| 					ip4("255.255.255.0"), // netmask
 | |
| 				},
 | |
| 				Flags: unix.RTF_STATIC | unix.RTF_GATEWAY | unix.RTF_UP,
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family:  4,
 | |
| 				Type:    RouteTypeUnicast,
 | |
| 				Dst:     RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
 | |
| 				Gateway: netip.MustParseAddr("1.2.3.1"),
 | |
| 				Sys: RouteEntryBSD{
 | |
| 					Flags:    []string{"gateway", "static", "up"},
 | |
| 					RawFlags: unix.RTF_STATIC | unix.RTF_GATEWAY | unix.RTF_UP,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "SkipNoAddrs",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs:   []route.Addr{},
 | |
| 			},
 | |
| 			fail: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "SkipBadVersion",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 1,
 | |
| 			},
 | |
| 			fail: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "SkipBadType",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType + 1,
 | |
| 			},
 | |
| 			fail: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "OutputIface",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Index:   1,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip4("1.2.3.4"), // dst
 | |
| 				},
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family:    4,
 | |
| 				Type:      RouteTypeUnicast,
 | |
| 				Dst:       RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/32")},
 | |
| 				Interface: "iface0",
 | |
| 				Sys:       RouteEntryBSD{},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "GatewayMAC",
 | |
| 			msg: &route.RouteMessage{
 | |
| 				Version: 3,
 | |
| 				Type:    rmExpectedType,
 | |
| 				Addrs: []route.Addr{
 | |
| 					ip4("100.64.0.0"), // dst
 | |
| 					link(1, "01:02:03:04:05:06"),
 | |
| 					ip4("255.192.0.0"), // netmask
 | |
| 				},
 | |
| 			},
 | |
| 			want: RouteEntry{
 | |
| 				Family: 4,
 | |
| 				Type:   RouteTypeUnicast,
 | |
| 				Dst:    RouteDestination{Prefix: netip.MustParsePrefix("100.64.0.0/10")},
 | |
| 				Sys: RouteEntryBSD{
 | |
| 					GatewayAddr:      "01:02:03:04:05:06",
 | |
| 					GatewayInterface: "iface0",
 | |
| 					GatewayIdx:       1,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	if runtime.GOOS == "darwin" {
 | |
| 		testCases = append(testCases,
 | |
| 			testCase{
 | |
| 				name: "SkipFlags",
 | |
| 				msg: &route.RouteMessage{
 | |
| 					Version: 3,
 | |
| 					Type:    rmExpectedType,
 | |
| 					Addrs: []route.Addr{
 | |
| 						ip4("1.2.3.4"),       // dst
 | |
| 						ip4("1.2.3.1"),       // gateway
 | |
| 						ip4("255.255.255.0"), // netmask
 | |
| 					},
 | |
| 					Flags: unix.RTF_UP | skipFlags,
 | |
| 				},
 | |
| 				fail: true,
 | |
| 			},
 | |
| 			testCase{
 | |
| 				name: "NetmaskAdjust",
 | |
| 				msg: &route.RouteMessage{
 | |
| 					Version: 3,
 | |
| 					Type:    rmExpectedType,
 | |
| 					Flags:   unix.RTF_MULTICAST,
 | |
| 					Addrs: []route.Addr{
 | |
| 						ip6("ff00::"),           // dst
 | |
| 						ip6("1234::"),           // gateway
 | |
| 						ip6("ffff:ffff:ff00::"), // netmask
 | |
| 					},
 | |
| 				},
 | |
| 				want: RouteEntry{
 | |
| 					Family:  6,
 | |
| 					Type:    RouteTypeMulticast,
 | |
| 					Dst:     RouteDestination{Prefix: netip.MustParsePrefix("ff00::/8")},
 | |
| 					Gateway: netip.MustParseAddr("1234::"),
 | |
| 					Sys: RouteEntryBSD{
 | |
| 						Flags:    []string{"multicast"},
 | |
| 						RawFlags: unix.RTF_MULTICAST,
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		)
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range testCases {
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 			re, ok := routeEntryFromMsg(ifs, tc.msg)
 | |
| 			if wantOk := !tc.fail; ok != wantOk {
 | |
| 				t.Fatalf("ok = %v; want %v", ok, wantOk)
 | |
| 			}
 | |
| 
 | |
| 			if !reflect.DeepEqual(re, tc.want) {
 | |
| 				t.Fatalf("RouteEntry mismatch:\n got: %+v\nwant: %+v", re, tc.want)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRouteEntryFormatting(t *testing.T) {
 | |
| 	testCases := []struct {
 | |
| 		re   RouteEntry
 | |
| 		want string
 | |
| 	}{
 | |
| 		{
 | |
| 			re: RouteEntry{
 | |
| 				Family:    4,
 | |
| 				Type:      RouteTypeUnicast,
 | |
| 				Dst:       RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.0/24")},
 | |
| 				Interface: "en0",
 | |
| 				Sys: RouteEntryBSD{
 | |
| 					GatewayInterface: "en0",
 | |
| 					Flags:            []string{"static", "up"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: `{Family: IPv4, Dst: 1.2.3.0/24, Interface: en0, Sys: {GatewayInterface: en0, Flags: [static up]}}`,
 | |
| 		},
 | |
| 		{
 | |
| 			re: RouteEntry{
 | |
| 				Family:    6,
 | |
| 				Type:      RouteTypeUnicast,
 | |
| 				Dst:       RouteDestination{Prefix: netip.MustParsePrefix("fd7a:115c:a1e0::/24")},
 | |
| 				Interface: "en0",
 | |
| 				Sys: RouteEntryBSD{
 | |
| 					GatewayIdx: 3,
 | |
| 					Flags:      []string{"static", "up"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: `{Family: IPv6, Dst: fd7a:115c:a1e0::/24, Interface: en0, Sys: {GatewayIdx: 3, Flags: [static up]}}`,
 | |
| 		},
 | |
| 	}
 | |
| 	for _, tc := range testCases {
 | |
| 		t.Run("", func(t *testing.T) {
 | |
| 			got := fmt.Sprint(tc.re)
 | |
| 			if got != tc.want {
 | |
| 				t.Fatalf("RouteEntry.String() mismatch\n got: %q\nwant: %q", got, tc.want)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestGetRouteTable(t *testing.T) {
 | |
| 	routes, err := Get(1000)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	// Basic assertion: we have at least one 'default' route
 | |
| 	var (
 | |
| 		hasDefault bool
 | |
| 	)
 | |
| 	for _, route := range routes {
 | |
| 		if route.Dst == defaultRouteIPv4 || route.Dst == defaultRouteIPv6 {
 | |
| 			hasDefault = true
 | |
| 		}
 | |
| 	}
 | |
| 	if !hasDefault {
 | |
| 		t.Errorf("expected at least one default route; routes=%v", routes)
 | |
| 	}
 | |
| }
 |