blob: db616bb826417e4cbfb4f482d12705d37bec9e06 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Lorenz Brun9601f262020-12-09 19:44:41 +01002// SPDX-License-Identifier: Apache-2.0
Lorenz Brun9601f262020-12-09 19:44:41 +01003
4package callback
5
6import (
7 "fmt"
8 "math"
9 "net"
10 "os"
11 "testing"
12 "time"
13
Lorenz Brun153c9c12025-01-07 17:44:45 +010014 "github.com/google/go-cmp/cmp"
Lorenz Brun9601f262020-12-09 19:44:41 +010015 "github.com/insomniacslk/dhcp/dhcpv4"
Lorenz Brun9601f262020-12-09 19:44:41 +010016 "github.com/vishvananda/netlink"
17 "golang.org/x/sys/unix"
Serge Bazanski96043bc2021-10-05 12:10:13 +020018
Jan Schär07a39e22025-09-04 11:16:59 +020019 "source.monogon.dev/osbase/net/dhcp4c"
Lorenz Brun9601f262020-12-09 19:44:41 +010020)
21
22func trivialLeaseFromNet(ipnet net.IPNet) *dhcp4c.Lease {
23 opts := make(dhcpv4.Options)
24 opts.Update(dhcpv4.OptSubnetMask(ipnet.Mask))
25 return &dhcp4c.Lease{
26 AssignedIP: ipnet.IP,
27 ExpiresAt: time.Now().Add(1 * time.Second),
28 Options: opts,
29 }
30}
31
32var (
33 testNet1 = net.IPNet{IP: net.IP{10, 0, 1, 2}, Mask: net.CIDRMask(24, 32)}
34 testNet1Broadcast = net.IP{10, 0, 1, 255}
35 testNet1Router = net.IP{10, 0, 1, 1}
36 testNet2 = net.IPNet{IP: net.IP{10, 0, 2, 2}, Mask: net.CIDRMask(24, 32)}
37 testNet2Broadcast = net.IP{10, 0, 2, 255}
38 testNet2Router = net.IP{10, 0, 2, 1}
39 mainRoutingTable = 254 // Linux automatically puts all routes into this table unless specified
40)
41
42func TestAssignedIPCallback(t *testing.T) {
43 if os.Getenv("IN_KTEST") != "true" {
44 t.Skip("Not in ktest")
45 }
46
47 var tests = []struct {
Jan Schärba404a62024-07-11 10:46:27 +020048 name string
49 initialAddrs []netlink.Addr
50 newLease *dhcp4c.Lease
51 expectedAddrs []netlink.Addr
Lorenz Brun9601f262020-12-09 19:44:41 +010052 }{
Serge Bazanski216fe7b2021-05-21 18:36:16 +020053 // Lifetimes are necessary, otherwise the Kernel sets the
54 // IFA_F_PERMANENT flag behind our back.
55 {
Lorenz Brun9601f262020-12-09 19:44:41 +010056 name: "RemoveOldIPs",
57 initialAddrs: []netlink.Addr{{IPNet: &testNet1, ValidLft: 60}, {IPNet: &testNet2, ValidLft: 60}},
Lorenz Brun9601f262020-12-09 19:44:41 +010058 newLease: nil,
59 expectedAddrs: nil,
60 },
61 {
62 name: "IgnoresPermanentIPs",
63 initialAddrs: []netlink.Addr{{IPNet: &testNet1, Flags: unix.IFA_F_PERMANENT}, {IPNet: &testNet2, ValidLft: 60}},
Lorenz Brun9601f262020-12-09 19:44:41 +010064 newLease: trivialLeaseFromNet(testNet2),
65 expectedAddrs: []netlink.Addr{
66 {IPNet: &testNet1, Flags: unix.IFA_F_PERMANENT, ValidLft: math.MaxUint32, PreferedLft: math.MaxUint32, Broadcast: testNet1Broadcast},
67 {IPNet: &testNet2, ValidLft: 1, PreferedLft: 1, Broadcast: testNet2Broadcast},
68 },
69 },
70 {
71 name: "AssignsNewIP",
72 initialAddrs: []netlink.Addr{},
Lorenz Brun9601f262020-12-09 19:44:41 +010073 newLease: trivialLeaseFromNet(testNet2),
74 expectedAddrs: []netlink.Addr{
75 {IPNet: &testNet2, ValidLft: 1, PreferedLft: 1, Broadcast: testNet2Broadcast},
76 },
77 },
78 {
79 name: "UpdatesIP",
80 initialAddrs: []netlink.Addr{},
Lorenz Brun9601f262020-12-09 19:44:41 +010081 newLease: trivialLeaseFromNet(testNet1),
82 expectedAddrs: []netlink.Addr{
83 {IPNet: &testNet1, ValidLft: 1, PreferedLft: 1, Broadcast: testNet1Broadcast},
84 },
85 },
86 {
87 name: "RemovesIPOnRelease",
88 initialAddrs: []netlink.Addr{{IPNet: &testNet1, ValidLft: 60, PreferedLft: 60}},
Lorenz Brun9601f262020-12-09 19:44:41 +010089 newLease: nil,
90 expectedAddrs: nil,
91 },
92 }
93 for i, test := range tests {
94 t.Run(test.name, func(t *testing.T) {
95 testLink := &netlink.Dummy{
96 LinkAttrs: netlink.LinkAttrs{
97 Name: fmt.Sprintf("aipcb-test-%d", i),
98 Flags: unix.IFF_UP,
99 },
100 }
101 if err := netlink.LinkAdd(testLink); err != nil {
102 t.Fatalf("test cannot set up network interface: %v", err)
103 }
104 defer netlink.LinkDel(testLink)
105 for _, addr := range test.initialAddrs {
106 if err := netlink.AddrAdd(testLink, &addr); err != nil {
107 t.Fatalf("test cannot set up initial addrs: %v", err)
108 }
109 }
110 // Associate dynamically-generated interface name for later comparison
111 for i := range test.expectedAddrs {
112 test.expectedAddrs[i].Label = testLink.Name
Lorenz Brund13c1c62022-03-30 19:58:58 +0200113 test.expectedAddrs[i].LinkIndex = testLink.Index
Lorenz Brun9601f262020-12-09 19:44:41 +0100114 }
115 cb := ManageIP(testLink)
Jan Schärba404a62024-07-11 10:46:27 +0200116 if err := cb(test.newLease); err != nil {
Lorenz Brun9601f262020-12-09 19:44:41 +0100117 t.Fatalf("callback returned an error: %v", err)
118 }
119 addrs, err := netlink.AddrList(testLink, netlink.FAMILY_V4)
120 if err != nil {
121 t.Fatalf("test cannot read back addrs from interface: %v", err)
122 }
Lorenz Brun153c9c12025-01-07 17:44:45 +0100123 if diff := cmp.Diff(test.expectedAddrs, addrs); diff != "" {
124 t.Errorf("Wrong IPs on interface (-want +got):\n%s", diff)
125 }
Lorenz Brun9601f262020-12-09 19:44:41 +0100126 })
127 }
128}
129
130func leaseAddRouter(lease *dhcp4c.Lease, router net.IP) *dhcp4c.Lease {
131 lease.Options.Update(dhcpv4.OptRouter(router))
132 return lease
133}
134
Lorenz Brunfdb73222021-12-13 05:19:25 +0100135func leaseAddClasslessRoutes(lease *dhcp4c.Lease, routes ...*dhcpv4.Route) *dhcp4c.Lease {
136 lease.Options.Update(dhcpv4.OptClasslessStaticRoute(routes...))
137 return lease
138}
139
140func mustParseCIDR(cidr string) *net.IPNet {
141 _, n, err := net.ParseCIDR(cidr)
142 if err != nil {
143 panic(err)
144 }
145 // Equality checks don't know about net.IP's canonicalization rules.
146 if n.IP.To4() != nil {
147 n.IP = n.IP.To4()
148 }
149 return n
150}
151
Lorenz Brun9601f262020-12-09 19:44:41 +0100152func TestDefaultRouteCallback(t *testing.T) {
153 if os.Getenv("IN_KTEST") != "true" {
154 t.Skip("Not in ktest")
155 }
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200156 // testRoute is only used as a route destination and not configured on any
157 // interface.
Lorenz Brun9601f262020-12-09 19:44:41 +0100158 testRoute := net.IPNet{IP: net.IP{10, 0, 3, 0}, Mask: net.CIDRMask(24, 32)}
159
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200160 // A test interface is set up for each test and assigned testNet1 and
161 // testNet2 so that testNet1Router and testNet2Router are valid gateways
162 // for routes in this environment. A LinkIndex of -1 is replaced by the
163 // correct link index for this test interface at runtime for both
164 // initialRoutes and expectedRoutes.
Lorenz Brun9601f262020-12-09 19:44:41 +0100165 var tests = []struct {
Jan Schärba404a62024-07-11 10:46:27 +0200166 name string
167 initialRoutes []netlink.Route
168 newLease *dhcp4c.Lease
169 expectedRoutes []netlink.Route
Lorenz Brun9601f262020-12-09 19:44:41 +0100170 }{
171 {
172 name: "AddsDefaultRoute",
173 initialRoutes: []netlink.Route{},
Lorenz Brun9601f262020-12-09 19:44:41 +0100174 newLease: leaseAddRouter(trivialLeaseFromNet(testNet1), testNet1Router),
175 expectedRoutes: []netlink.Route{{
Lorenz Brun153c9c12025-01-07 17:44:45 +0100176 Protocol: unix.RTPROT_DHCP,
177 Dst: mustParseCIDR("0.0.0.0/0"),
Mateusz Zalegaf220b292023-01-31 16:52:53 +0000178 Family: unix.AF_INET,
Lorenz Brun9601f262020-12-09 19:44:41 +0100179 Gw: testNet1Router,
180 Src: testNet1.IP,
181 Table: mainRoutingTable,
182 LinkIndex: -1, // Filled in dynamically with test interface
183 Type: unix.RTN_UNICAST,
184 }},
185 },
186 {
187 name: "IgnoresLeasesWithoutRouter",
188 initialRoutes: []netlink.Route{},
Lorenz Brun9601f262020-12-09 19:44:41 +0100189 newLease: trivialLeaseFromNet(testNet1),
190 expectedRoutes: nil,
191 },
192 {
193 name: "RemovesUnrelatedOldRoutes",
194 initialRoutes: []netlink.Route{{
195 Dst: &testRoute,
Mateusz Zalegaf220b292023-01-31 16:52:53 +0000196 Family: unix.AF_INET,
Lorenz Brun9601f262020-12-09 19:44:41 +0100197 LinkIndex: -1, // Filled in dynamically with test interface
198 Protocol: unix.RTPROT_DHCP,
199 Gw: testNet2Router,
200 Scope: netlink.SCOPE_UNIVERSE,
201 }},
Lorenz Brun9601f262020-12-09 19:44:41 +0100202 newLease: nil,
203 expectedRoutes: nil,
204 },
205 {
206 name: "IgnoresNonDHCPRoutes",
207 initialRoutes: []netlink.Route{{
208 Dst: &testRoute,
Mateusz Zalegaf220b292023-01-31 16:52:53 +0000209 Family: unix.AF_INET,
Lorenz Brun9601f262020-12-09 19:44:41 +0100210 LinkIndex: -1, // Filled in dynamically with test interface
211 Protocol: unix.RTPROT_BIRD,
212 Gw: testNet2Router,
213 }},
Lorenz Brun9601f262020-12-09 19:44:41 +0100214 newLease: nil,
215 expectedRoutes: []netlink.Route{{
216 Protocol: unix.RTPROT_BIRD,
217 Dst: &testRoute,
Mateusz Zalegaf220b292023-01-31 16:52:53 +0000218 Family: unix.AF_INET,
Lorenz Brun9601f262020-12-09 19:44:41 +0100219 Gw: testNet2Router,
220 Table: mainRoutingTable,
221 LinkIndex: -1, // Filled in dynamically with test interface
222 Type: unix.RTN_UNICAST,
223 }},
224 },
225 {
226 name: "RemovesRoute",
227 initialRoutes: []netlink.Route{{
228 Dst: nil,
Mateusz Zalegaf220b292023-01-31 16:52:53 +0000229 Family: unix.AF_INET,
Lorenz Brun9601f262020-12-09 19:44:41 +0100230 LinkIndex: -1, // Filled in dynamically with test interface
231 Protocol: unix.RTPROT_DHCP,
232 Gw: testNet2Router,
233 }},
Lorenz Brun9601f262020-12-09 19:44:41 +0100234 newLease: nil,
235 expectedRoutes: nil,
236 },
237 {
238 name: "UpdatesRoute",
239 initialRoutes: []netlink.Route{{
240 Dst: nil,
Mateusz Zalegaf220b292023-01-31 16:52:53 +0000241 Family: unix.AF_INET,
Lorenz Brun9601f262020-12-09 19:44:41 +0100242 LinkIndex: -1, // Filled in dynamically with test interface
243 Protocol: unix.RTPROT_DHCP,
244 Src: testNet1.IP,
245 Gw: testNet1Router,
246 }},
Lorenz Brun9601f262020-12-09 19:44:41 +0100247 newLease: leaseAddRouter(trivialLeaseFromNet(testNet2), testNet2Router),
248 expectedRoutes: []netlink.Route{{
249 Protocol: unix.RTPROT_DHCP,
Lorenz Brun153c9c12025-01-07 17:44:45 +0100250 Dst: mustParseCIDR("0.0.0.0/0"),
Mateusz Zalegaf220b292023-01-31 16:52:53 +0000251 Family: unix.AF_INET,
Lorenz Brun9601f262020-12-09 19:44:41 +0100252 Gw: testNet2Router,
253 Src: testNet2.IP,
254 Table: mainRoutingTable,
255 LinkIndex: -1, // Filled in dynamically with test interface
256 Type: unix.RTN_UNICAST,
257 }},
258 },
Lorenz Brunfdb73222021-12-13 05:19:25 +0100259 {
260 name: "AddsClasslessStaticRoutes",
261 initialRoutes: []netlink.Route{},
Lorenz Brunfdb73222021-12-13 05:19:25 +0100262 newLease: leaseAddClasslessRoutes(
263 // Router should be ignored
264 leaseAddRouter(trivialLeaseFromNet(testNet1), testNet1Router),
265 // P2P/foreign gateway route
266 &dhcpv4.Route{Dest: mustParseCIDR("192.168.42.1/32"), Router: net.IPv4zero},
267 // Standard route over foreign gateway set up by previous route
268 &dhcpv4.Route{Dest: mustParseCIDR("0.0.0.0/0"), Router: net.IPv4(192, 168, 42, 1)},
269 ),
270 expectedRoutes: []netlink.Route{{
271 Protocol: unix.RTPROT_DHCP,
Lorenz Brun153c9c12025-01-07 17:44:45 +0100272 Dst: mustParseCIDR("0.0.0.0/0"),
Mateusz Zalegaf220b292023-01-31 16:52:53 +0000273 Family: unix.AF_INET,
Lorenz Brunfdb73222021-12-13 05:19:25 +0100274 Gw: net.IPv4(192, 168, 42, 1).To4(), // Equal() doesn't know about canonicalization
275 Src: testNet1.IP,
276 Table: mainRoutingTable,
277 LinkIndex: -1, // Filled in dynamically with test interface
278 Type: unix.RTN_UNICAST,
279 }, {
280 Protocol: unix.RTPROT_DHCP,
281 Dst: mustParseCIDR("192.168.42.1/32"),
Mateusz Zalegaf220b292023-01-31 16:52:53 +0000282 Family: unix.AF_INET,
Lorenz Brunfdb73222021-12-13 05:19:25 +0100283 Gw: nil,
284 Src: testNet1.IP,
285 Table: mainRoutingTable,
286 LinkIndex: -1, // Filled in dynamically with test interface
287 Type: unix.RTN_UNICAST,
288 Scope: unix.RT_SCOPE_LINK,
289 }},
290 },
Lorenz Brun9601f262020-12-09 19:44:41 +0100291 }
292 for i, test := range tests {
293 t.Run(test.name, func(t *testing.T) {
294 testLink := &netlink.Dummy{
295 LinkAttrs: netlink.LinkAttrs{
296 Name: fmt.Sprintf("drcb-test-%d", i),
297 Flags: unix.IFF_UP,
298 },
299 }
300 if err := netlink.LinkAdd(testLink); err != nil {
301 t.Fatalf("test cannot set up network interface: %v", err)
302 }
303 defer func() { // Clean up after each test
304 routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, &netlink.Route{}, 0)
305 if err == nil {
306 for _, route := range routes {
307 netlink.RouteDel(&route)
308 }
309 }
310 }()
311 defer netlink.LinkDel(testLink)
312 if err := netlink.AddrAdd(testLink, &netlink.Addr{
313 IPNet: &testNet1,
314 }); err != nil {
315 t.Fatalf("test cannot set up test addrs: %v", err)
316 }
317 if err := netlink.AddrAdd(testLink, &netlink.Addr{
318 IPNet: &testNet2,
319 }); err != nil {
320 t.Fatalf("test cannot set up test addrs: %v", err)
321 }
322 for _, route := range test.initialRoutes {
323 if route.LinkIndex == -1 {
324 route.LinkIndex = testLink.Index
325 }
326 if err := netlink.RouteAdd(&route); err != nil {
327 t.Fatalf("test cannot set up initial routes: %v", err)
328 }
329 }
330 for i := range test.expectedRoutes {
331 if test.expectedRoutes[i].LinkIndex == -1 {
332 test.expectedRoutes[i].LinkIndex = testLink.Index
333 }
334 }
335
Lorenz Brunfdb73222021-12-13 05:19:25 +0100336 cb := ManageRoutes(testLink)
Jan Schärba404a62024-07-11 10:46:27 +0200337 if err := cb(test.newLease); err != nil {
Lorenz Brun9601f262020-12-09 19:44:41 +0100338 t.Fatalf("callback returned an error: %v", err)
339 }
340 routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, &netlink.Route{}, 0)
341 if err != nil {
342 t.Fatalf("test cannot read back routes: %v", err)
343 }
344 var notKernelRoutes []netlink.Route
345 for _, route := range routes {
346 if route.Protocol != unix.RTPROT_KERNEL { // Filter kernel-managed routes
347 notKernelRoutes = append(notKernelRoutes, route)
348 }
349 }
Lorenz Brun153c9c12025-01-07 17:44:45 +0100350 if diff := cmp.Diff(test.expectedRoutes, notKernelRoutes); diff != "" {
351 t.Errorf("Expected route mismatch (-want +got):\n%s", diff)
352 }
Lorenz Brun9601f262020-12-09 19:44:41 +0100353 })
354 }
355}