// 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) } }