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  }