github.com/cilium/cilium@v1.16.2/pkg/datapath/tables/node_address_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package tables 5 6 import ( 7 "context" 8 "math/rand/v2" 9 "net" 10 "net/netip" 11 "slices" 12 "sort" 13 "strings" 14 "testing" 15 16 "github.com/cilium/hive/cell" 17 "github.com/cilium/hive/hivetest" 18 "github.com/cilium/statedb" 19 "github.com/spf13/pflag" 20 "github.com/stretchr/testify/assert" 21 "github.com/stretchr/testify/require" 22 23 "github.com/cilium/cilium/pkg/defaults" 24 "github.com/cilium/cilium/pkg/hive" 25 "github.com/cilium/cilium/pkg/ip" 26 "github.com/cilium/cilium/pkg/node" 27 "github.com/cilium/cilium/pkg/option" 28 ) 29 30 func TestNodeAddressConfig(t *testing.T) { 31 var cfg NodeAddressConfig 32 newHive := func() *hive.Hive { 33 return hive.New( 34 cell.Config(NodeAddressConfig{}), 35 cell.Invoke(func(c NodeAddressConfig) { cfg = c }), 36 ) 37 } 38 testCases := [][]string{ 39 nil, // Empty 40 {"1.2.3.0/24"}, // IPv4 41 {"1.2.0.0/16", "fe80::/64"}, // IPv4 & IPv6 42 } 43 44 for _, testCase := range testCases { 45 flags := pflag.NewFlagSet("", pflag.ContinueOnError) 46 h := newHive() 47 h.RegisterFlags(flags) 48 flags.Set("nodeport-addresses", strings.Join(testCase, ",")) 49 tlog := hivetest.Logger(t) 50 if assert.NoError(t, h.Start(tlog, context.TODO()), "Start") { 51 assert.NoError(t, h.Stop(tlog, context.TODO()), "Stop") 52 require.Len(t, cfg.NodePortAddresses, len(testCase)) 53 for i := range testCase { 54 assert.Equal(t, testCase[i], cfg.NodePortAddresses[i].String()) 55 } 56 } 57 } 58 } 59 60 var ( 61 testNodeIPv4 = netip.MustParseAddr("172.16.0.1") 62 testNodeIPv6 = netip.MustParseAddr("2222::1") 63 ciliumHostIP = net.ParseIP("9.9.9.9") 64 ciliumHostIPLinkScoped = net.ParseIP("9.9.9.8") 65 ) 66 67 var nodeAddressTests = []struct { 68 name string 69 addrs []DeviceAddress // Addresses to add to the "test" device 70 wantAddrs []net.IP 71 wantPrimary []net.IP 72 wantNodePort []net.IP 73 }{ 74 { 75 name: "ipv4 simple", 76 addrs: []DeviceAddress{ 77 { 78 Addr: netip.MustParseAddr("10.0.0.1"), 79 Scope: RT_SCOPE_SITE, 80 }, 81 }, 82 wantAddrs: []net.IP{ 83 ciliumHostIP, 84 ciliumHostIPLinkScoped, 85 net.ParseIP("10.0.0.1"), 86 }, 87 wantPrimary: []net.IP{ 88 ciliumHostIP, 89 net.ParseIP("10.0.0.1"), 90 }, 91 wantNodePort: []net.IP{ 92 net.ParseIP("10.0.0.1"), 93 }, 94 }, 95 { 96 name: "ipv6 simple", 97 addrs: []DeviceAddress{ 98 { 99 Addr: netip.MustParseAddr("2001:db8::1"), 100 Scope: RT_SCOPE_SITE, 101 }, 102 }, 103 wantAddrs: []net.IP{ 104 ciliumHostIP, 105 ciliumHostIPLinkScoped, 106 net.ParseIP("2001:db8::1"), 107 }, 108 wantPrimary: []net.IP{ 109 ciliumHostIP, 110 net.ParseIP("2001:db8::1"), 111 }, 112 wantNodePort: []net.IP{ 113 net.ParseIP("2001:db8::1"), 114 }, 115 }, 116 { 117 name: "v4/v6 mix", 118 addrs: []DeviceAddress{ 119 { 120 Addr: netip.MustParseAddr("10.0.0.1"), 121 Scope: RT_SCOPE_SITE, 122 }, 123 { 124 Addr: netip.MustParseAddr("2001:db8::1"), 125 Scope: RT_SCOPE_UNIVERSE, 126 }, 127 }, 128 129 wantAddrs: []net.IP{ 130 ciliumHostIP, 131 ciliumHostIPLinkScoped, 132 net.ParseIP("2001:db8::1"), 133 net.ParseIP("10.0.0.1"), 134 }, 135 wantPrimary: []net.IP{ 136 ciliumHostIP, 137 net.ParseIP("2001:db8::1"), 138 net.ParseIP("10.0.0.1"), 139 }, 140 wantNodePort: []net.IP{ 141 net.ParseIP("10.0.0.1"), 142 net.ParseIP("2001:db8::1"), 143 }, 144 }, 145 { 146 147 name: "skip-out-of-scope-addrs", 148 addrs: []DeviceAddress{ 149 { 150 Addr: netip.MustParseAddr("10.0.1.1"), 151 Scope: RT_SCOPE_UNIVERSE, 152 }, 153 { 154 Addr: netip.MustParseAddr("10.0.2.2"), 155 Scope: RT_SCOPE_LINK, 156 }, 157 { 158 Addr: netip.MustParseAddr("10.0.3.3"), 159 Secondary: true, 160 Scope: RT_SCOPE_HOST, 161 }, 162 }, 163 164 // The default AddressMaxScope is set to LINK-1, so addresses with 165 // scope LINK or above are ignored (except for cilium_host addresses) 166 wantAddrs: []net.IP{ 167 ciliumHostIP, 168 ciliumHostIPLinkScoped, 169 net.ParseIP("10.0.1.1"), 170 }, 171 172 wantPrimary: []net.IP{ 173 ciliumHostIP, 174 net.ParseIP("10.0.1.1"), 175 }, 176 177 wantNodePort: []net.IP{ 178 net.ParseIP("10.0.1.1"), 179 }, 180 }, 181 182 { 183 name: "multiple", 184 addrs: []DeviceAddress{ 185 { 186 Addr: netip.MustParseAddr("10.0.0.1"), 187 Scope: RT_SCOPE_UNIVERSE, 188 }, 189 { 190 Addr: netip.MustParseAddr("10.0.0.2"), 191 Scope: RT_SCOPE_UNIVERSE, 192 Secondary: true, 193 }, 194 { 195 Addr: netip.MustParseAddr("1.1.1.1"), 196 Scope: RT_SCOPE_UNIVERSE, 197 }, 198 }, 199 200 wantAddrs: []net.IP{ 201 ciliumHostIP, 202 ciliumHostIPLinkScoped, 203 net.ParseIP("10.0.0.1"), 204 net.ParseIP("10.0.0.2"), 205 net.ParseIP("1.1.1.1"), 206 }, 207 208 wantPrimary: []net.IP{ 209 ciliumHostIP, 210 net.ParseIP("1.1.1.1"), 211 }, 212 213 wantNodePort: []net.IP{ 214 net.ParseIP("10.0.0.1"), 215 }, 216 }, 217 { 218 name: "ipv6 multiple", 219 addrs: []DeviceAddress{ 220 { // Second public address 221 Addr: netip.MustParseAddr("2600:beef::2"), 222 Scope: RT_SCOPE_SITE, 223 }, 224 { // First public address 225 Addr: netip.MustParseAddr("2600:beef::3"), 226 Scope: RT_SCOPE_UNIVERSE, 227 }, 228 229 { // First private address (preferred for NodePort) 230 Addr: netip.MustParseAddr("2001:db8::1"), 231 Scope: RT_SCOPE_UNIVERSE, 232 }, 233 }, 234 235 wantAddrs: []net.IP{ 236 ciliumHostIP, 237 ciliumHostIPLinkScoped, 238 net.ParseIP("2001:db8::1"), 239 net.ParseIP("2600:beef::2"), 240 net.ParseIP("2600:beef::3"), 241 }, 242 243 wantPrimary: []net.IP{ 244 ciliumHostIP, 245 net.ParseIP("2600:beef::3"), 246 }, 247 248 wantNodePort: []net.IP{ 249 net.ParseIP("2001:db8::1"), 250 }, 251 }, 252 253 { 254 name: "node IP preferred", 255 addrs: []DeviceAddress{ 256 { 257 Addr: netip.MustParseAddr("10.0.0.1"), 258 Scope: RT_SCOPE_UNIVERSE, 259 }, 260 { 261 Addr: netip.MustParseAddr("1.1.1.1"), 262 Scope: RT_SCOPE_UNIVERSE, 263 }, 264 { 265 Addr: testNodeIPv4, 266 Scope: RT_SCOPE_UNIVERSE, 267 }, 268 { 269 Addr: netip.MustParseAddr("2001:db8::1"), 270 Scope: RT_SCOPE_UNIVERSE, 271 }, 272 { 273 Addr: testNodeIPv6, 274 Scope: RT_SCOPE_UNIVERSE, 275 }, 276 }, 277 278 wantAddrs: []net.IP{ 279 ciliumHostIP, 280 ciliumHostIPLinkScoped, 281 net.ParseIP("10.0.0.1"), 282 net.ParseIP("1.1.1.1"), 283 net.ParseIP("2001:db8::1"), 284 testNodeIPv4.AsSlice(), 285 testNodeIPv6.AsSlice(), 286 }, 287 288 wantPrimary: []net.IP{ 289 ciliumHostIP, 290 testNodeIPv4.AsSlice(), 291 testNodeIPv6.AsSlice(), 292 }, 293 294 wantNodePort: []net.IP{ 295 testNodeIPv4.AsSlice(), 296 testNodeIPv6.AsSlice(), 297 }, 298 }, 299 } 300 301 func TestNodeAddress(t *testing.T) { 302 t.Parallel() 303 304 // Use a shared fixture so that we're dealing with an evolving set of addresses 305 // for the device. 306 db, devices, nodeAddrs, _ := fixture(t, defaults.AddressScopeMax, nil) 307 308 _, watch := nodeAddrs.AllWatch(db.ReadTxn()) 309 txn := db.WriteTxn(devices) 310 devices.Insert(txn, &Device{ 311 Index: 2, 312 Name: "cilium_host", 313 Flags: net.FlagUp, 314 Addrs: []DeviceAddress{ 315 {Addr: ip.MustAddrFromIP(ciliumHostIP), Scope: RT_SCOPE_UNIVERSE}, 316 {Addr: ip.MustAddrFromIP(ciliumHostIPLinkScoped), Scope: RT_SCOPE_LINK}, 317 }, 318 Selected: false, 319 }) 320 devices.Insert(txn, &Device{ 321 Index: 1, 322 Name: "lo", 323 Flags: net.FlagUp | net.FlagLoopback, 324 Addrs: []DeviceAddress{ 325 {Addr: netip.MustParseAddr("127.0.0.1"), Scope: RT_SCOPE_HOST}, 326 {Addr: netip.MustParseAddr("::1"), Scope: RT_SCOPE_HOST}, 327 }, 328 Selected: false, 329 }) 330 txn.Commit() 331 332 // Wait for cilium_host addresses to be processed. 333 <-watch 334 iter := nodeAddrs.All(db.ReadTxn()) 335 addrs := statedb.Collect(statedb.Map(iter, func(n NodeAddress) string { return n.String() })) 336 assert.Equal(t, addrs, 337 []string{"::1 (*)", "9.9.9.8 (cilium_host)", "9.9.9.9 (cilium_host)", "127.0.0.1 (*)"}, 338 "unexpected initial node addresses") 339 340 for _, tt := range nodeAddressTests { 341 t.Run(tt.name, func(t *testing.T) { 342 343 txn := db.WriteTxn(devices) 344 _, watch := nodeAddrs.AllWatch(txn) 345 346 shuffleSlice(tt.addrs) // For extra bit of randomness 347 devices.Insert(txn, 348 &Device{ 349 Index: 3, 350 Name: "test", 351 Selected: true, 352 Flags: net.FlagUp, 353 Addrs: tt.addrs, 354 }) 355 356 txn.Commit() 357 <-watch // wait for propagation 358 359 iter := nodeAddrs.All(db.ReadTxn()) 360 addrs := statedb.Collect(iter) 361 local := []string{} 362 nodePort := []string{} 363 primary := []string{} 364 for _, addr := range addrs { 365 if addr.DeviceName == WildcardDeviceName { 366 continue 367 } 368 local = append(local, addr.Addr.String()) 369 if addr.NodePort { 370 nodePort = append(nodePort, addr.Addr.String()) 371 } 372 if addr.Primary { 373 primary = append(primary, addr.Addr.String()) 374 } 375 } 376 assert.ElementsMatch(t, local, ipStrings(tt.wantAddrs), "Addresses do not match") 377 assert.ElementsMatch(t, nodePort, ipStrings(tt.wantNodePort), "NodePort addresses do not match") 378 assert.ElementsMatch(t, primary, ipStrings(tt.wantPrimary), "Primary addresses do not match") 379 assertOnePrimaryPerDevice(t, addrs) 380 381 }) 382 } 383 384 // Delete the devices and check that node addresses is cleaned up. 385 _, watch = nodeAddrs.AllWatch(db.ReadTxn()) 386 txn = db.WriteTxn(devices) 387 devices.Delete(txn, &Device{Index: 1}) 388 devices.Delete(txn, &Device{Index: 2}) 389 devices.Delete(txn, &Device{Index: 3}) 390 txn.Commit() 391 <-watch // wait for propagation 392 393 assert.Equal(t, 0, nodeAddrs.NumObjects(db.ReadTxn()), "expected no NodeAddresses after device deletion") 394 } 395 396 // TestNodeAddressHostDevice checks that the for cilium_host the link scope'd 397 // addresses are always picked regardless of the max scope. 398 // More context in commit 080857bdedca67d58ec39f8f96c5f38b22f6dc0b. 399 func TestNodeAddressHostDevice(t *testing.T) { 400 t.Parallel() 401 402 db, devices, nodeAddrs, _ := fixture(t, int(RT_SCOPE_SITE), nil) 403 404 txn := db.WriteTxn(devices) 405 _, watch := nodeAddrs.AllWatch(txn) 406 407 devices.Insert(txn, &Device{ 408 Index: 1, 409 Name: "cilium_host", 410 Flags: net.FlagUp, 411 Addrs: []DeviceAddress{ 412 // <SITE 413 {Addr: ip.MustAddrFromIP(ciliumHostIP), Scope: RT_SCOPE_UNIVERSE}, 414 // >SITE, but included 415 {Addr: ip.MustAddrFromIP(ciliumHostIPLinkScoped), Scope: RT_SCOPE_LINK}, 416 // >SITE, skipped 417 {Addr: netip.MustParseAddr("10.0.0.1"), Scope: RT_SCOPE_HOST}, 418 }, 419 Selected: false, 420 }) 421 422 txn.Commit() 423 <-watch // wait for propagation 424 425 addrs := statedb.Collect(nodeAddrs.All(db.ReadTxn())) 426 427 if assert.Len(t, addrs, 2) { 428 // The addresses are sorted by IP, so we see the link-scoped address first. 429 assert.Equal(t, addrs[0].Addr.String(), ciliumHostIPLinkScoped.String()) 430 assert.False(t, addrs[0].Primary) 431 432 assert.Equal(t, addrs[1].Addr.String(), ciliumHostIP.String()) 433 assert.True(t, addrs[1].Primary) 434 } 435 } 436 437 // TestNodeAddressLoopback tests that non-loopback addresses from the loopback 438 // device are always taken, regardless of whether the lo device gets selected or not. 439 // This allows assigning VIPs to the loopback device and make Cilium consider them 440 // as node IPs. 441 func TestNodeAddressLoopback(t *testing.T) { 442 t.Parallel() 443 444 db, devices, nodeAddrs, _ := fixture(t, int(RT_SCOPE_SITE), nil) 445 446 txn := db.WriteTxn(devices) 447 _, watch := nodeAddrs.AllWatch(txn) 448 449 devices.Insert(txn, &Device{ 450 Index: 1, 451 Name: "lo", 452 Flags: net.FlagUp | net.FlagLoopback, 453 Addrs: []DeviceAddress{ 454 {Addr: netip.MustParseAddr("10.0.0.1"), Scope: RT_SCOPE_UNIVERSE}, 455 {Addr: netip.MustParseAddr("2001::1"), Scope: RT_SCOPE_UNIVERSE}, 456 }, 457 Selected: false, 458 }) 459 460 txn.Commit() 461 <-watch // wait for propagation 462 463 addrs := statedb.Collect(nodeAddrs.All(db.ReadTxn())) 464 465 if assert.Len(t, addrs, 4) { 466 assert.Equal(t, addrs[0].Addr.String(), "10.0.0.1") 467 assert.Equal(t, addrs[0].DeviceName, "*") 468 assert.True(t, addrs[0].Primary) 469 assert.False(t, addrs[0].NodePort) 470 471 assert.Equal(t, addrs[1].Addr.String(), "10.0.0.1") 472 assert.Equal(t, addrs[1].DeviceName, "lo") 473 assert.True(t, addrs[1].Primary) 474 assert.True(t, addrs[1].NodePort) 475 476 assert.Equal(t, addrs[2].Addr.String(), "2001::1") 477 assert.Equal(t, addrs[2].DeviceName, "*") 478 assert.True(t, addrs[2].Primary) 479 assert.False(t, addrs[2].NodePort) 480 481 assert.Equal(t, addrs[3].Addr.String(), "2001::1") 482 assert.Equal(t, addrs[3].DeviceName, "lo") 483 assert.True(t, addrs[3].Primary) 484 assert.True(t, addrs[3].NodePort) 485 486 } 487 } 488 489 var nodeAddressWhitelistTests = []struct { 490 name string 491 cidrs string // --nodeport-addresses 492 addrs []DeviceAddress // Addresses to add to the "test" device 493 wantLocal []net.IP // e.g. LocalAddresses() 494 wantNodePort []net.IP // e.g. LoadBalancerNodeAddresses() 495 wantFallback []net.IP // Fallback addresses, e.g. addresses of "*" device 496 }{ 497 { 498 name: "ipv4", 499 cidrs: "10.0.0.0/8", 500 addrs: []DeviceAddress{ 501 { 502 Addr: netip.MustParseAddr("10.0.0.1"), 503 Scope: RT_SCOPE_SITE, 504 }, 505 { 506 Addr: netip.MustParseAddr("11.0.0.1"), 507 Scope: RT_SCOPE_SITE, 508 }, 509 }, 510 wantLocal: []net.IP{ 511 ciliumHostIP, 512 ciliumHostIPLinkScoped, 513 net.ParseIP("10.0.0.1"), 514 net.ParseIP("11.0.0.1"), 515 }, 516 wantNodePort: []net.IP{ 517 net.ParseIP("10.0.0.1"), 518 }, 519 wantFallback: []net.IP{ 520 net.ParseIP("11.0.0.1"), // public over private 521 }, 522 }, 523 { 524 name: "ipv6", 525 cidrs: "2001::/16", 526 addrs: []DeviceAddress{ 527 { 528 Addr: netip.MustParseAddr("2001:db8::1"), 529 Scope: RT_SCOPE_SITE, 530 }, 531 { 532 Addr: netip.MustParseAddr("2600:beef::2"), 533 Scope: RT_SCOPE_SITE, 534 }, 535 }, 536 wantLocal: []net.IP{ 537 ciliumHostIP, 538 ciliumHostIPLinkScoped, 539 net.ParseIP("2001:db8::1"), 540 net.ParseIP("2600:beef::2"), 541 }, 542 wantNodePort: []net.IP{ 543 net.ParseIP("2001:db8::1"), 544 }, 545 wantFallback: []net.IP{ 546 net.ParseIP("2600:beef::2"), 547 }, 548 }, 549 { 550 name: "v4-v6 mix", 551 cidrs: "2001::/16,10.0.0.0/8", 552 addrs: []DeviceAddress{ 553 { 554 Addr: netip.MustParseAddr("10.0.0.1"), 555 Scope: RT_SCOPE_SITE, 556 }, 557 { 558 Addr: netip.MustParseAddr("11.0.0.1"), 559 Scope: RT_SCOPE_UNIVERSE, 560 }, 561 { 562 Addr: netip.MustParseAddr("2001:db8::1"), 563 Scope: RT_SCOPE_UNIVERSE, 564 }, 565 { 566 Addr: netip.MustParseAddr("2600:beef::2"), 567 Scope: RT_SCOPE_SITE, 568 }, 569 }, 570 571 wantLocal: []net.IP{ 572 ciliumHostIP, 573 ciliumHostIPLinkScoped, 574 net.ParseIP("10.0.0.1"), 575 net.ParseIP("11.0.0.1"), 576 net.ParseIP("2001:db8::1"), 577 net.ParseIP("2600:beef::2"), 578 }, 579 wantNodePort: []net.IP{ 580 net.ParseIP("10.0.0.1"), 581 net.ParseIP("2001:db8::1"), 582 }, 583 wantFallback: []net.IP{ 584 net.ParseIP("11.0.0.1"), // public over private 585 net.ParseIP("2600:beef::2"), 586 }, 587 }, 588 } 589 590 func TestNodeAddressWhitelist(t *testing.T) { 591 t.Parallel() 592 593 for _, tt := range nodeAddressWhitelistTests { 594 t.Run(tt.name, func(t *testing.T) { 595 db, devices, nodeAddrs, _ := fixture(t, defaults.AddressScopeMax, 596 func(h *hive.Hive) { 597 h.Viper().Set("nodeport-addresses", tt.cidrs) 598 }) 599 600 txn := db.WriteTxn(devices) 601 _, watch := nodeAddrs.AllWatch(txn) 602 603 devices.Insert(txn, &Device{ 604 Index: 1, 605 Name: "cilium_host", 606 Flags: net.FlagUp, 607 Addrs: []DeviceAddress{ 608 {Addr: ip.MustAddrFromIP(ciliumHostIP), Scope: RT_SCOPE_UNIVERSE}, 609 {Addr: ip.MustAddrFromIP(ciliumHostIPLinkScoped), Scope: RT_SCOPE_LINK}, 610 }, 611 Selected: false, 612 }) 613 614 shuffleSlice(tt.addrs) // For extra bit of randomness 615 devices.Insert(txn, 616 &Device{ 617 Index: 2, 618 Name: "test", 619 Selected: true, 620 Flags: net.FlagUp, 621 Addrs: tt.addrs, 622 }) 623 624 txn.Commit() 625 <-watch // wait for propagation 626 627 iter := nodeAddrs.All(db.ReadTxn()) 628 local := []string{} 629 nodePort := []string{} 630 fallback := []string{} 631 for addr, _, ok := iter.Next(); ok; addr, _, ok = iter.Next() { 632 if addr.DeviceName == WildcardDeviceName { 633 fallback = append(fallback, addr.Addr.String()) 634 continue 635 } 636 local = append(local, addr.Addr.String()) 637 if addr.NodePort { 638 nodePort = append(nodePort, addr.Addr.String()) 639 } 640 } 641 assert.ElementsMatch(t, local, ipStrings(tt.wantLocal), "LocalAddresses do not match") 642 assert.ElementsMatch(t, nodePort, ipStrings(tt.wantNodePort), "LoadBalancerNodeAddresses do not match") 643 assert.ElementsMatch(t, fallback, ipStrings(tt.wantFallback), "fallback addresses do not match") 644 }) 645 } 646 } 647 648 // TestNodeAddressUpdate tests incremental updates to the node addresses. 649 func TestNodeAddressUpdate(t *testing.T) { 650 db, devices, nodeAddrs, _ := fixture(t, defaults.AddressScopeMax, func(*hive.Hive) {}) 651 652 // Insert 10.0.0.1 653 txn := db.WriteTxn(devices) 654 _, watch := nodeAddrs.AllWatch(txn) 655 devices.Insert(txn, &Device{ 656 Index: 1, 657 Name: "test", 658 Flags: net.FlagUp, 659 Addrs: []DeviceAddress{ 660 {Addr: netip.MustParseAddr("10.0.0.1"), Scope: RT_SCOPE_UNIVERSE}, 661 }, 662 Selected: true, 663 }) 664 txn.Commit() 665 <-watch // wait for propagation 666 667 addrs := statedb.Collect(nodeAddrs.All(db.ReadTxn())) 668 if assert.Len(t, addrs, 2) { 669 assert.Equal(t, addrs[0].Addr.String(), "10.0.0.1") 670 assert.Equal(t, addrs[0].DeviceName, "*") 671 assert.Equal(t, addrs[1].Addr.String(), "10.0.0.1") 672 assert.Equal(t, addrs[1].DeviceName, "test") 673 } 674 675 // Insert 10.0.0.2 and validate that both present. 676 txn = db.WriteTxn(devices) 677 _, watch = nodeAddrs.AllWatch(txn) 678 679 devices.Insert(txn, &Device{ 680 Index: 1, 681 Name: "test", 682 Flags: net.FlagUp, 683 Addrs: []DeviceAddress{ 684 {Addr: netip.MustParseAddr("10.0.0.1"), Scope: RT_SCOPE_UNIVERSE}, 685 {Addr: netip.MustParseAddr("10.0.0.2"), Scope: RT_SCOPE_UNIVERSE}, 686 }, 687 Selected: true, 688 }) 689 txn.Commit() 690 <-watch // wait for propagation 691 692 addrs = statedb.Collect(nodeAddrs.All(db.ReadTxn())) 693 if assert.Len(t, addrs, 3) { 694 assert.Equal(t, addrs[0].Addr.String(), "10.0.0.1") 695 assert.Equal(t, addrs[0].DeviceName, "*") 696 assert.Equal(t, addrs[1].Addr.String(), "10.0.0.1") 697 assert.Equal(t, addrs[1].DeviceName, "test") 698 assert.True(t, addrs[1].Primary) 699 assert.True(t, addrs[1].NodePort) 700 assert.Equal(t, addrs[2].Addr.String(), "10.0.0.2") 701 assert.Equal(t, addrs[2].DeviceName, "test") 702 assert.False(t, addrs[2].Primary) 703 assert.False(t, addrs[2].NodePort) 704 } 705 706 // Drop 10.0.0.1 707 txn = db.WriteTxn(devices) 708 _, watch = nodeAddrs.AllWatch(txn) 709 710 devices.Insert(txn, &Device{ 711 Index: 1, 712 Name: "test", 713 Flags: net.FlagUp, 714 Addrs: []DeviceAddress{ 715 {Addr: netip.MustParseAddr("10.0.0.2"), Scope: RT_SCOPE_UNIVERSE}, 716 }, 717 Selected: true, 718 }) 719 txn.Commit() 720 <-watch // wait for propagation 721 722 addrs = statedb.Collect(nodeAddrs.All(db.ReadTxn())) 723 if assert.Len(t, addrs, 2) { 724 assert.Equal(t, addrs[0].Addr.String(), "10.0.0.2") 725 assert.Equal(t, addrs[0].DeviceName, "*") 726 assert.Equal(t, addrs[1].Addr.String(), "10.0.0.2") 727 assert.Equal(t, addrs[1].DeviceName, "test") 728 assert.True(t, addrs[1].Primary) 729 assert.True(t, addrs[1].NodePort) 730 } 731 732 // Drop 10.0.0.2 733 txn = db.WriteTxn(devices) 734 _, watch = nodeAddrs.AllWatch(txn) 735 736 devices.Insert(txn, &Device{ 737 Index: 1, 738 Name: "test", 739 Flags: net.FlagUp, 740 Addrs: []DeviceAddress{}, 741 Selected: true, 742 }) 743 txn.Commit() 744 <-watch // wait for propagation 745 746 assert.Zero(t, nodeAddrs.NumObjects(db.ReadTxn())) 747 } 748 749 func TestNodeAddressNodeIPChange(t *testing.T) { 750 db, devices, nodeAddrs, localNodeStore := fixture(t, defaults.AddressScopeMax, func(*hive.Hive) {}) 751 752 // Insert 10.0.0.1 and the current node IP 753 txn := db.WriteTxn(devices) 754 _, watch := nodeAddrs.AllWatch(txn) 755 devices.Insert(txn, &Device{ 756 Index: 1, 757 Name: "test", 758 Flags: net.FlagUp, 759 Addrs: []DeviceAddress{ 760 {Addr: netip.MustParseAddr("10.0.0.1"), Scope: RT_SCOPE_UNIVERSE}, 761 {Addr: testNodeIPv4, Scope: RT_SCOPE_UNIVERSE}, 762 }, 763 Selected: true, 764 }) 765 txn.Commit() 766 <-watch // wait for propagation 767 768 iter, watch := nodeAddrs.ListWatch(db.ReadTxn(), NodeAddressNodePortIndex.Query(true)) 769 addrs := statedb.Collect(iter) 770 if assert.Len(t, addrs, 1) { 771 assert.Equal(t, testNodeIPv4, addrs[0].Addr) 772 assert.Equal(t, "test", addrs[0].DeviceName) 773 } 774 775 // Make the 10.0.0.1 the new NodeIP. 776 localNodeStore.Update(func(n *node.LocalNode) { 777 n.SetNodeExternalIP(net.ParseIP("10.0.0.1")) 778 }) 779 <-watch 780 781 // The new node IP should now be preferred for NodePort. 782 iter = nodeAddrs.List(db.ReadTxn(), NodeAddressNodePortIndex.Query(true)) 783 addrs = statedb.Collect(iter) 784 if assert.Len(t, addrs, 1) { 785 assert.Equal(t, "10.0.0.1", addrs[0].Addr.String()) 786 assert.Equal(t, "test", addrs[0].DeviceName) 787 } 788 } 789 790 func fixture(t *testing.T, addressScopeMax int, beforeStart func(*hive.Hive)) (*statedb.DB, statedb.RWTable[*Device], statedb.Table[NodeAddress], *node.LocalNodeStore) { 791 var ( 792 db *statedb.DB 793 devices statedb.RWTable[*Device] 794 nodeAddrs statedb.Table[NodeAddress] 795 localNodeStore *node.LocalNodeStore 796 ) 797 h := hive.New( 798 NodeAddressCell, 799 node.LocalNodeStoreCell, 800 cell.Provide( 801 NewDeviceTable, 802 statedb.RWTable[*Device].ToTable, 803 ), 804 cell.Provide(func() node.LocalNodeSynchronizer { return testLocalNodeSync{} }), 805 cell.Invoke(func(db_ *statedb.DB, d statedb.RWTable[*Device], na statedb.Table[NodeAddress], lns *node.LocalNodeStore) { 806 db = db_ 807 devices = d 808 nodeAddrs = na 809 localNodeStore = lns 810 db.RegisterTable(d) 811 }), 812 813 // option.DaemonConfig needed for AddressMaxScope. This flag will move into NodeAddressConfig 814 // in a follow-up PR. 815 cell.Provide(func() *option.DaemonConfig { 816 return &option.DaemonConfig{ 817 AddressScopeMax: addressScopeMax, 818 } 819 }), 820 ) 821 if beforeStart != nil { 822 beforeStart(h) 823 } 824 825 tlog := hivetest.Logger(t) 826 require.NoError(t, h.Start(tlog, context.TODO()), "Start") 827 828 t.Cleanup(func() { 829 assert.NoError(t, h.Stop(tlog, context.TODO()), "Stop") 830 }) 831 return db, devices, nodeAddrs, localNodeStore 832 } 833 834 type testLocalNodeSync struct { 835 } 836 837 // InitLocalNode implements node.LocalNodeSynchronizer. 838 func (t testLocalNodeSync) InitLocalNode(_ context.Context, n *node.LocalNode) error { 839 n.SetNodeExternalIP(testNodeIPv4.AsSlice()) 840 n.SetNodeExternalIP(testNodeIPv6.AsSlice()) 841 return nil 842 } 843 844 // SyncLocalNode implements node.LocalNodeSynchronizer. 845 func (t testLocalNodeSync) SyncLocalNode(context.Context, *node.LocalNodeStore) { 846 } 847 848 var _ node.LocalNodeSynchronizer = testLocalNodeSync{} 849 850 // ipStrings converts net.IP to a string. Used to assert equalence without having to deal 851 // with e.g. IPv4-mapped IPv6 presentation etc. 852 func ipStrings(ips []net.IP) (ss []string) { 853 for i := range ips { 854 ss = append(ss, ips[i].String()) 855 } 856 sort.Strings(ss) 857 return 858 } 859 860 func shuffleSlice[T any](xs []T) []T { 861 rand.Shuffle( 862 len(xs), 863 func(i, j int) { 864 xs[i], xs[j] = xs[j], xs[i] 865 }) 866 return xs 867 } 868 869 func assertOnePrimaryPerDevice(t *testing.T, addrs []NodeAddress) { 870 ipv4 := map[string]netip.Addr{} 871 ipv6 := map[string]netip.Addr{} 872 hasPrimary := map[string]bool{} 873 874 for _, addr := range addrs { 875 hasPrimary[addr.DeviceName] = hasPrimary[addr.DeviceName] || addr.Primary 876 if !addr.Primary { 877 continue 878 } 879 if addr.Addr.Is4() { 880 if other, ok := ipv4[addr.DeviceName]; ok && other != addr.Addr { 881 assert.Failf(t, "multiple primary IPv4 addresses", "device %q had multiple primary IPv4 addresses: %q and %q", addr.DeviceName, addr.Addr, other) 882 } 883 ipv4[addr.DeviceName] = addr.Addr 884 } else { 885 if other, ok := ipv6[addr.DeviceName]; ok && other != addr.Addr { 886 assert.Failf(t, "multiple primary IPv6 addresses", "device %q had multiple primary IPv6 addresses: %q and %q", addr.DeviceName, addr.Addr, other) 887 } 888 ipv6[addr.DeviceName] = addr.Addr 889 } 890 } 891 892 for dev, primary := range hasPrimary { 893 if !primary { 894 assert.Failf(t, "no primary address", "device %q had no primary addresses", dev) 895 } 896 } 897 } 898 899 func TestSortedAddresses(t *testing.T) { 900 // Test cases to consider. These are in the order we expect. The test shuffles 901 // them and verifies that expected order is recovered. 902 testCases := [][]DeviceAddress{ 903 // Primary vs Secondary 904 { 905 {Addr: netip.MustParseAddr("2.2.2.2"), Scope: RT_SCOPE_SITE}, 906 {Addr: netip.MustParseAddr("1.1.1.1"), Scope: RT_SCOPE_UNIVERSE, Secondary: true}, 907 }, 908 { 909 {Addr: netip.MustParseAddr("1002::1"), Scope: RT_SCOPE_SITE}, 910 {Addr: netip.MustParseAddr("1001::1"), Scope: RT_SCOPE_UNIVERSE, Secondary: true}, 911 }, 912 913 // Scope 914 { 915 {Addr: netip.MustParseAddr("2.2.2.2"), Scope: RT_SCOPE_UNIVERSE}, 916 {Addr: netip.MustParseAddr("1.1.1.1"), Scope: RT_SCOPE_SITE}, 917 }, 918 { 919 {Addr: netip.MustParseAddr("1002::1"), Scope: RT_SCOPE_UNIVERSE}, 920 {Addr: netip.MustParseAddr("1001::1"), Scope: RT_SCOPE_SITE}, 921 }, 922 923 // Public vs private 924 { 925 {Addr: netip.MustParseAddr("200.0.0.1"), Scope: RT_SCOPE_UNIVERSE}, 926 {Addr: netip.MustParseAddr("192.168.1.1"), Scope: RT_SCOPE_UNIVERSE}, 927 }, 928 { 929 {Addr: netip.MustParseAddr("1001::1"), Scope: RT_SCOPE_UNIVERSE}, 930 {Addr: netip.MustParseAddr("100::1"), Scope: RT_SCOPE_UNIVERSE}, 931 }, 932 933 // Address itself 934 { 935 {Addr: netip.MustParseAddr("1.1.1.1"), Scope: RT_SCOPE_UNIVERSE}, 936 {Addr: netip.MustParseAddr("2.2.2.2"), Scope: RT_SCOPE_UNIVERSE}, 937 }, 938 { 939 {Addr: netip.MustParseAddr("1001::1"), Scope: RT_SCOPE_UNIVERSE}, 940 {Addr: netip.MustParseAddr("1002::1"), Scope: RT_SCOPE_UNIVERSE}, 941 }, 942 } 943 944 for _, expected := range testCases { 945 actual := SortedAddresses(shuffleSlice(slices.Clone(expected))) 946 assert.EqualValues(t, expected, actual) 947 948 // Shuffle again. 949 actual = SortedAddresses(shuffleSlice(slices.Clone(expected))) 950 assert.Equal(t, expected, actual) 951 } 952 953 } 954 955 func TestFallbackAddresses(t *testing.T) { 956 var f fallbackAddresses 957 958 updated := f.update(&Device{ 959 Index: 2, 960 Addrs: []DeviceAddress{ 961 {Addr: netip.MustParseAddr("10.0.0.1"), Scope: RT_SCOPE_SITE}, 962 }, 963 }) 964 assert.Equal(t, f.ipv4.addr.Addr.String(), "10.0.0.1") 965 assert.True(t, updated, "updated") 966 967 updated = f.update(&Device{ 968 Index: 3, 969 Addrs: []DeviceAddress{ 970 {Addr: netip.MustParseAddr("1001::1"), Scope: RT_SCOPE_SITE}, 971 }, 972 }) 973 assert.Equal(t, f.ipv6.addr.Addr.String(), "1001::1") 974 assert.True(t, updated, "updated") 975 976 // Lower scope wins 977 updated = f.update(&Device{ 978 Index: 4, 979 Addrs: []DeviceAddress{ 980 {Addr: netip.MustParseAddr("10.0.0.2"), Scope: RT_SCOPE_UNIVERSE}, 981 }, 982 }) 983 assert.Equal(t, f.ipv4.addr.Addr.String(), "10.0.0.2") 984 assert.True(t, updated, "updated") 985 986 // Lower ifindex wins 987 updated = f.update(&Device{ 988 Index: 1, 989 Addrs: []DeviceAddress{ 990 {Addr: netip.MustParseAddr("10.0.0.3"), Scope: RT_SCOPE_UNIVERSE}, 991 }, 992 }) 993 assert.Equal(t, f.ipv4.addr.Addr.String(), "10.0.0.3") 994 assert.True(t, updated, "updated") 995 996 // Public wins over private 997 updated = f.update(&Device{ 998 Index: 5, 999 Addrs: []DeviceAddress{ 1000 {Addr: netip.MustParseAddr("20.0.0.1"), Scope: RT_SCOPE_SITE}, 1001 }, 1002 }) 1003 assert.Equal(t, f.ipv4.addr.Addr.String(), "20.0.0.1") 1004 assert.True(t, updated, "updated") 1005 1006 // Update with the same set of addresses does nothing. 1007 updated = f.update(&Device{ 1008 Index: 5, 1009 Addrs: []DeviceAddress{ 1010 {Addr: netip.MustParseAddr("20.0.0.1"), Scope: RT_SCOPE_SITE}, 1011 }, 1012 }) 1013 assert.Equal(t, f.ipv4.addr.Addr.String(), "20.0.0.1") 1014 assert.False(t, updated, "updated") 1015 }