blob: db616bb826417e4cbfb4f482d12705d37bec9e06 [file] [log] [blame]
// Copyright The Monogon Project Authors.
// SPDX-License-Identifier: Apache-2.0
package callback
import (
"fmt"
"math"
"net"
"os"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
"source.monogon.dev/osbase/net/dhcp4c"
)
func trivialLeaseFromNet(ipnet net.IPNet) *dhcp4c.Lease {
opts := make(dhcpv4.Options)
opts.Update(dhcpv4.OptSubnetMask(ipnet.Mask))
return &dhcp4c.Lease{
AssignedIP: ipnet.IP,
ExpiresAt: time.Now().Add(1 * time.Second),
Options: opts,
}
}
var (
testNet1 = net.IPNet{IP: net.IP{10, 0, 1, 2}, Mask: net.CIDRMask(24, 32)}
testNet1Broadcast = net.IP{10, 0, 1, 255}
testNet1Router = net.IP{10, 0, 1, 1}
testNet2 = net.IPNet{IP: net.IP{10, 0, 2, 2}, Mask: net.CIDRMask(24, 32)}
testNet2Broadcast = net.IP{10, 0, 2, 255}
testNet2Router = net.IP{10, 0, 2, 1}
mainRoutingTable = 254 // Linux automatically puts all routes into this table unless specified
)
func TestAssignedIPCallback(t *testing.T) {
if os.Getenv("IN_KTEST") != "true" {
t.Skip("Not in ktest")
}
var tests = []struct {
name string
initialAddrs []netlink.Addr
newLease *dhcp4c.Lease
expectedAddrs []netlink.Addr
}{
// Lifetimes are necessary, otherwise the Kernel sets the
// IFA_F_PERMANENT flag behind our back.
{
name: "RemoveOldIPs",
initialAddrs: []netlink.Addr{{IPNet: &testNet1, ValidLft: 60}, {IPNet: &testNet2, ValidLft: 60}},
newLease: nil,
expectedAddrs: nil,
},
{
name: "IgnoresPermanentIPs",
initialAddrs: []netlink.Addr{{IPNet: &testNet1, Flags: unix.IFA_F_PERMANENT}, {IPNet: &testNet2, ValidLft: 60}},
newLease: trivialLeaseFromNet(testNet2),
expectedAddrs: []netlink.Addr{
{IPNet: &testNet1, Flags: unix.IFA_F_PERMANENT, ValidLft: math.MaxUint32, PreferedLft: math.MaxUint32, Broadcast: testNet1Broadcast},
{IPNet: &testNet2, ValidLft: 1, PreferedLft: 1, Broadcast: testNet2Broadcast},
},
},
{
name: "AssignsNewIP",
initialAddrs: []netlink.Addr{},
newLease: trivialLeaseFromNet(testNet2),
expectedAddrs: []netlink.Addr{
{IPNet: &testNet2, ValidLft: 1, PreferedLft: 1, Broadcast: testNet2Broadcast},
},
},
{
name: "UpdatesIP",
initialAddrs: []netlink.Addr{},
newLease: trivialLeaseFromNet(testNet1),
expectedAddrs: []netlink.Addr{
{IPNet: &testNet1, ValidLft: 1, PreferedLft: 1, Broadcast: testNet1Broadcast},
},
},
{
name: "RemovesIPOnRelease",
initialAddrs: []netlink.Addr{{IPNet: &testNet1, ValidLft: 60, PreferedLft: 60}},
newLease: nil,
expectedAddrs: nil,
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
testLink := &netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: fmt.Sprintf("aipcb-test-%d", i),
Flags: unix.IFF_UP,
},
}
if err := netlink.LinkAdd(testLink); err != nil {
t.Fatalf("test cannot set up network interface: %v", err)
}
defer netlink.LinkDel(testLink)
for _, addr := range test.initialAddrs {
if err := netlink.AddrAdd(testLink, &addr); err != nil {
t.Fatalf("test cannot set up initial addrs: %v", err)
}
}
// Associate dynamically-generated interface name for later comparison
for i := range test.expectedAddrs {
test.expectedAddrs[i].Label = testLink.Name
test.expectedAddrs[i].LinkIndex = testLink.Index
}
cb := ManageIP(testLink)
if err := cb(test.newLease); err != nil {
t.Fatalf("callback returned an error: %v", err)
}
addrs, err := netlink.AddrList(testLink, netlink.FAMILY_V4)
if err != nil {
t.Fatalf("test cannot read back addrs from interface: %v", err)
}
if diff := cmp.Diff(test.expectedAddrs, addrs); diff != "" {
t.Errorf("Wrong IPs on interface (-want +got):\n%s", diff)
}
})
}
}
func leaseAddRouter(lease *dhcp4c.Lease, router net.IP) *dhcp4c.Lease {
lease.Options.Update(dhcpv4.OptRouter(router))
return lease
}
func leaseAddClasslessRoutes(lease *dhcp4c.Lease, routes ...*dhcpv4.Route) *dhcp4c.Lease {
lease.Options.Update(dhcpv4.OptClasslessStaticRoute(routes...))
return lease
}
func mustParseCIDR(cidr string) *net.IPNet {
_, n, err := net.ParseCIDR(cidr)
if err != nil {
panic(err)
}
// Equality checks don't know about net.IP's canonicalization rules.
if n.IP.To4() != nil {
n.IP = n.IP.To4()
}
return n
}
func TestDefaultRouteCallback(t *testing.T) {
if os.Getenv("IN_KTEST") != "true" {
t.Skip("Not in ktest")
}
// testRoute is only used as a route destination and not configured on any
// interface.
testRoute := net.IPNet{IP: net.IP{10, 0, 3, 0}, Mask: net.CIDRMask(24, 32)}
// A test interface is set up for each test and assigned testNet1 and
// testNet2 so that testNet1Router and testNet2Router are valid gateways
// for routes in this environment. A LinkIndex of -1 is replaced by the
// correct link index for this test interface at runtime for both
// initialRoutes and expectedRoutes.
var tests = []struct {
name string
initialRoutes []netlink.Route
newLease *dhcp4c.Lease
expectedRoutes []netlink.Route
}{
{
name: "AddsDefaultRoute",
initialRoutes: []netlink.Route{},
newLease: leaseAddRouter(trivialLeaseFromNet(testNet1), testNet1Router),
expectedRoutes: []netlink.Route{{
Protocol: unix.RTPROT_DHCP,
Dst: mustParseCIDR("0.0.0.0/0"),
Family: unix.AF_INET,
Gw: testNet1Router,
Src: testNet1.IP,
Table: mainRoutingTable,
LinkIndex: -1, // Filled in dynamically with test interface
Type: unix.RTN_UNICAST,
}},
},
{
name: "IgnoresLeasesWithoutRouter",
initialRoutes: []netlink.Route{},
newLease: trivialLeaseFromNet(testNet1),
expectedRoutes: nil,
},
{
name: "RemovesUnrelatedOldRoutes",
initialRoutes: []netlink.Route{{
Dst: &testRoute,
Family: unix.AF_INET,
LinkIndex: -1, // Filled in dynamically with test interface
Protocol: unix.RTPROT_DHCP,
Gw: testNet2Router,
Scope: netlink.SCOPE_UNIVERSE,
}},
newLease: nil,
expectedRoutes: nil,
},
{
name: "IgnoresNonDHCPRoutes",
initialRoutes: []netlink.Route{{
Dst: &testRoute,
Family: unix.AF_INET,
LinkIndex: -1, // Filled in dynamically with test interface
Protocol: unix.RTPROT_BIRD,
Gw: testNet2Router,
}},
newLease: nil,
expectedRoutes: []netlink.Route{{
Protocol: unix.RTPROT_BIRD,
Dst: &testRoute,
Family: unix.AF_INET,
Gw: testNet2Router,
Table: mainRoutingTable,
LinkIndex: -1, // Filled in dynamically with test interface
Type: unix.RTN_UNICAST,
}},
},
{
name: "RemovesRoute",
initialRoutes: []netlink.Route{{
Dst: nil,
Family: unix.AF_INET,
LinkIndex: -1, // Filled in dynamically with test interface
Protocol: unix.RTPROT_DHCP,
Gw: testNet2Router,
}},
newLease: nil,
expectedRoutes: nil,
},
{
name: "UpdatesRoute",
initialRoutes: []netlink.Route{{
Dst: nil,
Family: unix.AF_INET,
LinkIndex: -1, // Filled in dynamically with test interface
Protocol: unix.RTPROT_DHCP,
Src: testNet1.IP,
Gw: testNet1Router,
}},
newLease: leaseAddRouter(trivialLeaseFromNet(testNet2), testNet2Router),
expectedRoutes: []netlink.Route{{
Protocol: unix.RTPROT_DHCP,
Dst: mustParseCIDR("0.0.0.0/0"),
Family: unix.AF_INET,
Gw: testNet2Router,
Src: testNet2.IP,
Table: mainRoutingTable,
LinkIndex: -1, // Filled in dynamically with test interface
Type: unix.RTN_UNICAST,
}},
},
{
name: "AddsClasslessStaticRoutes",
initialRoutes: []netlink.Route{},
newLease: leaseAddClasslessRoutes(
// Router should be ignored
leaseAddRouter(trivialLeaseFromNet(testNet1), testNet1Router),
// P2P/foreign gateway route
&dhcpv4.Route{Dest: mustParseCIDR("192.168.42.1/32"), Router: net.IPv4zero},
// Standard route over foreign gateway set up by previous route
&dhcpv4.Route{Dest: mustParseCIDR("0.0.0.0/0"), Router: net.IPv4(192, 168, 42, 1)},
),
expectedRoutes: []netlink.Route{{
Protocol: unix.RTPROT_DHCP,
Dst: mustParseCIDR("0.0.0.0/0"),
Family: unix.AF_INET,
Gw: net.IPv4(192, 168, 42, 1).To4(), // Equal() doesn't know about canonicalization
Src: testNet1.IP,
Table: mainRoutingTable,
LinkIndex: -1, // Filled in dynamically with test interface
Type: unix.RTN_UNICAST,
}, {
Protocol: unix.RTPROT_DHCP,
Dst: mustParseCIDR("192.168.42.1/32"),
Family: unix.AF_INET,
Gw: nil,
Src: testNet1.IP,
Table: mainRoutingTable,
LinkIndex: -1, // Filled in dynamically with test interface
Type: unix.RTN_UNICAST,
Scope: unix.RT_SCOPE_LINK,
}},
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
testLink := &netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: fmt.Sprintf("drcb-test-%d", i),
Flags: unix.IFF_UP,
},
}
if err := netlink.LinkAdd(testLink); err != nil {
t.Fatalf("test cannot set up network interface: %v", err)
}
defer func() { // Clean up after each test
routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, &netlink.Route{}, 0)
if err == nil {
for _, route := range routes {
netlink.RouteDel(&route)
}
}
}()
defer netlink.LinkDel(testLink)
if err := netlink.AddrAdd(testLink, &netlink.Addr{
IPNet: &testNet1,
}); err != nil {
t.Fatalf("test cannot set up test addrs: %v", err)
}
if err := netlink.AddrAdd(testLink, &netlink.Addr{
IPNet: &testNet2,
}); err != nil {
t.Fatalf("test cannot set up test addrs: %v", err)
}
for _, route := range test.initialRoutes {
if route.LinkIndex == -1 {
route.LinkIndex = testLink.Index
}
if err := netlink.RouteAdd(&route); err != nil {
t.Fatalf("test cannot set up initial routes: %v", err)
}
}
for i := range test.expectedRoutes {
if test.expectedRoutes[i].LinkIndex == -1 {
test.expectedRoutes[i].LinkIndex = testLink.Index
}
}
cb := ManageRoutes(testLink)
if err := cb(test.newLease); err != nil {
t.Fatalf("callback returned an error: %v", err)
}
routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, &netlink.Route{}, 0)
if err != nil {
t.Fatalf("test cannot read back routes: %v", err)
}
var notKernelRoutes []netlink.Route
for _, route := range routes {
if route.Protocol != unix.RTPROT_KERNEL { // Filter kernel-managed routes
notKernelRoutes = append(notKernelRoutes, route)
}
}
if diff := cmp.Diff(test.expectedRoutes, notKernelRoutes); diff != "" {
t.Errorf("Expected route mismatch (-want +got):\n%s", diff)
}
})
}
}