Files
RoutingTableToWg/cmd/app/convert_test.go
T

156 lines
6.1 KiB
Go

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