package main import ( "net" "net/netip" "slices" "testing" "git.ruekov.eu/ruakij/routingtabletowg/lib/reconcile" "github.com/vishvananda/netlink" "golang.org/x/sys/unix" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) func TestToRoutes(t *testing.T) { _, v4Net, _ := net.ParseCIDR("203.0.113.0/24") _, v6Net, _ := net.ParseCIDR("2001:db8::/32") list := []netlink.Route{ {Dst: v4Net, Gw: net.ParseIP("10.0.0.1"), Priority: 5, Table: 254, Family: unix.AF_INET}, {Dst: v6Net, Gw: net.ParseIP("fd00::1"), Priority: 1024, Family: unix.AF_INET6}, // default routes have no Dst, the family comes from the route and not from the gateway {Gw: net.ParseIP("10.0.0.1"), Family: unix.AF_INET}, {Gw: net.ParseIP("fd00::1"), Family: unix.AF_INET6}, // gateway as 16 bytes {Gw: net.ParseIP("10.0.0.2").To16(), Family: unix.AF_INET}, // no gateway {Dst: v4Net, Family: unix.AF_INET}, {Family: unix.AF_INET}, // no destination to tell {Gw: net.ParseIP("10.0.0.1")}, } got, skipped := toRoutes(list) if len(skipped) != 0 { t.Errorf("skipped %v", skipped) } want := []reconcile.Route{ {Dst: netip.MustParsePrefix("203.0.113.0/24"), Gw: netip.MustParseAddr("10.0.0.1"), Metric: 5, Table: 254}, {Dst: netip.MustParsePrefix("2001:db8::/32"), Gw: netip.MustParseAddr("fd00::1"), Metric: 1024}, {Dst: netip.MustParsePrefix("0.0.0.0/0"), Gw: netip.MustParseAddr("10.0.0.1")}, {Dst: netip.MustParsePrefix("::/0"), Gw: netip.MustParseAddr("fd00::1")}, {Dst: netip.MustParsePrefix("0.0.0.0/0"), Gw: netip.MustParseAddr("10.0.0.2")}, } if !slices.Equal(got, want) { t.Errorf("got %v\nwant %v", got, want) } } // wgctrl would send an IPv4-mapped IPv6 destination as IPv4 with a mask too long for it, // which the kernel rejects for the whole call. func TestToRoutesSkipsUnwritableDestinations(t *testing.T) { _, mapped, _ := net.ParseCIDR("::ffff:10.0.0.0/104") _, mappedRange, _ := net.ParseCIDR("::ffff:0:0/96") _, short, _ := net.ParseCIDR("::ffff:0:0/80") // nothing of the mapped form is left in it _, plain, _ := net.ParseCIDR("2001:db8::/32") list := []netlink.Route{ {Dst: mapped, Gw: net.ParseIP("fd00::1"), Family: unix.AF_INET6}, {Dst: mappedRange, Gw: net.ParseIP("fd00::1"), Family: unix.AF_INET6}, {Dst: short, Gw: net.ParseIP("fd00::1"), Family: unix.AF_INET6}, {Dst: plain, Gw: net.ParseIP("fd00::1"), Family: unix.AF_INET6}, // without gateway it is not translated anyway, so there is nothing to report {Dst: mapped, Family: unix.AF_INET6}, } routes, skipped := toRoutes(list) if len(routes) != 2 || routes[0].Dst != netip.MustParsePrefix("::/80") || routes[1].Dst != netip.MustParsePrefix("2001:db8::/32") { t.Errorf("routes = %v", routes) } wantSkipped := []netip.Prefix{netip.MustParsePrefix("::ffff:10.0.0.0/104"), netip.MustParsePrefix("::ffff:0.0.0.0/96")} if !slices.Equal(skipped, wantSkipped) { t.Errorf("skipped = %v, want %v", skipped, wantSkipped) } } // wgctrl reports IPv4 prefixes with 4 byte IPs and masks, so they must equal the synthesized default route. func TestToPeersMatchesDefaultRoute(t *testing.T) { device := &wgtypes.Device{Peers: []wgtypes.Peer{{ PublicKey: wgtypes.Key{1}, AllowedIPs: []net.IPNet{ {IP: net.IP{0, 0, 0, 0}, Mask: net.CIDRMask(0, 32)}, {IP: net.IPv6zero, Mask: net.CIDRMask(0, 128)}, }, }}} peers := toPeers(device) want := []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")} if len(peers) != 1 || !slices.Equal(peers[0].AllowedIPs, want) { t.Errorf("got %v, want %v", peers, want) } routes, _ := toRoutes([]netlink.Route{{Gw: net.ParseIP("10.0.0.1"), Family: unix.AF_INET}}) if len(routes) != 1 || !slices.Contains(peers[0].AllowedIPs, routes[0].Dst) { t.Errorf("default route %v not found in %v", routes, peers[0].AllowedIPs) } } // A prefix that is IPv6 for WireGuard must not turn into an IPv4 one on the way back. func TestRemovalKeepsOtherEntriesAsTheyAre(t *testing.T) { mapped := net.IPNet{IP: net.ParseIP("::ffff:0:0"), Mask: net.CIDRMask(96, 128)} v4Default := net.IPNet{IP: net.IP{0, 0, 0, 0}, Mask: net.CIDRMask(0, 32)} other := net.IPNet{IP: net.IP{10, 1, 2, 0}, Mask: net.CIDRMask(24, 32)} list := []net.IPNet{mapped, v4Default, other} device := &wgtypes.Device{Peers: []wgtypes.Peer{{PublicKey: wgtypes.Key{1}, AllowedIPs: list}}} wantPrefixes := []netip.Prefix{netip.MustParsePrefix("::ffff:0.0.0.0/96"), netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("10.1.2.0/24")} if got := toPeers(device)[0].AllowedIPs; !slices.Equal(got, wantPrefixes) { t.Fatalf("got %v, want %v", got, wantPrefixes) } got := withoutPrefixes(list, []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("203.0.113.0/24")}) want := []net.IPNet{mapped, other} if len(got) != len(want) { t.Fatalf("got %v, want %v", got, want) } for i := range want { if !got[i].IP.Equal(want[i].IP) || len(got[i].IP) != len(want[i].IP) || !slices.Equal(got[i].Mask, want[i].Mask) { t.Errorf("entry %d: got %v, want %v", i, got[i], want[i]) } } if len(list) != 3 { t.Errorf("input was modified: %v", list) } } func TestRouteFilter(t *testing.T) { tests := []struct { name string options FilterOptions wantTable int wantProto bool protoValue netlink.RouteProtocol }{ {name: "all", options: FilterOptions{Table: -1, Protocol: -1}}, {name: "table", options: FilterOptions{Table: 100, Protocol: -1}, wantTable: 100}, {name: "protocol", options: FilterOptions{Table: -1, Protocol: 12}, wantProto: true, protoValue: 12}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter, mask := tt.options.routeFilter(7) if mask&netlink.RT_FILTER_OIF == 0 || mask&netlink.RT_FILTER_TABLE == 0 || filter.LinkIndex != 7 { t.Errorf("link and table must always be filtered: %v %b", filter, mask) } if filter.Table != tt.wantTable { t.Errorf("table = %d, want %d", filter.Table, tt.wantTable) } if (mask&netlink.RT_FILTER_PROTOCOL != 0) != tt.wantProto || filter.Protocol != tt.protoValue { t.Errorf("protocol filter = %v/%d, want %v/%d", mask&netlink.RT_FILTER_PROTOCOL != 0, filter.Protocol, tt.wantProto, tt.protoValue) } }) } }