github.com/cilium/cilium@v1.16.2/pkg/datapath/iptables/ipset/ipset_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package ipset
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/binary"
    10  	"errors"
    11  	"fmt"
    12  	"html/template"
    13  	"io"
    14  	"net/netip"
    15  	"strings"
    16  	"sync/atomic"
    17  	"testing"
    18  
    19  	"github.com/cilium/hive/cell"
    20  	"github.com/cilium/hive/hivetest"
    21  	"github.com/cilium/statedb"
    22  	"github.com/cilium/statedb/reconciler"
    23  	"github.com/sirupsen/logrus"
    24  	"github.com/stretchr/testify/assert"
    25  	"github.com/stretchr/testify/require"
    26  	"go.uber.org/goleak"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  
    29  	"github.com/cilium/cilium/pkg/datapath/tables"
    30  	"github.com/cilium/cilium/pkg/hive"
    31  	"github.com/cilium/cilium/pkg/lock"
    32  	"github.com/cilium/cilium/pkg/time"
    33  )
    34  
    35  // ipset list output template
    36  const textTmpl = `{{range $name, $addrs := . -}}Name: {{$name}}
    37  Type: hash:ip
    38  Revision: 6
    39  Header: family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0x4d9d24f1
    40  Size in memory: 216
    41  References: 0
    42  Number of entries: {{len $addrs}}
    43  Members:
    44  {{range $addr, $_ := $addrs -}}{{$addr}}
    45  {{else}}{{end}}{{end}}`
    46  
    47  func TestManager(t *testing.T) {
    48  	defer goleak.VerifyNone(t)
    49  
    50  	var mgr Manager
    51  
    52  	ipsets := make(map[string]AddrSet) // mocked kernel IP sets
    53  	var mu lock.Mutex                  // protect the ipsets map
    54  
    55  	tmpl := template.Must(template.New("ipsets").Parse(textTmpl))
    56  
    57  	hive := hive.New(
    58  
    59  		cell.Module(
    60  			"ipset-manager-test",
    61  			"ipset-manager-test",
    62  
    63  			cell.Provide(func() config {
    64  				return config{NodeIPSetNeeded: true}
    65  			}),
    66  
    67  			cell.Provide(
    68  				newIPSetManager,
    69  				tables.NewIPSetTable,
    70  				newOps,
    71  				newReconciler,
    72  			),
    73  			cell.Provide(func(ops *ops) reconciler.Operations[*tables.IPSetEntry] {
    74  				return ops
    75  			}),
    76  
    77  			cell.Provide(func(logger logrus.FieldLogger) *ipset {
    78  				return &ipset{
    79  					executable: funcExecutable(
    80  						func(ctx context.Context, command string, stdin string, arg ...string) ([]byte, error) {
    81  							mu.Lock()
    82  							defer mu.Unlock()
    83  
    84  							var commands [][]string
    85  							if arg[0] == "restore" {
    86  								lines := strings.Split(stdin, "\n")
    87  								for _, line := range lines {
    88  									if len(line) > 0 {
    89  										commands = append(commands, strings.Split(line, " "))
    90  									}
    91  								}
    92  							} else {
    93  								commands = [][]string{arg}
    94  							}
    95  
    96  							for _, arg := range commands {
    97  								subCommand := arg[0]
    98  								name := arg[1]
    99  								t.Logf("%s %s", subCommand, strings.Join(arg[1:], " "))
   100  
   101  								switch subCommand {
   102  								case "create":
   103  									if _, found := ipsets[name]; !found {
   104  										ipsets[name] = AddrSet{}
   105  									}
   106  								case "destroy":
   107  									if _, found := ipsets[name]; !found {
   108  										return nil, fmt.Errorf("ipset %s not found", name)
   109  									}
   110  									delete(ipsets, name)
   111  								case "list":
   112  									if _, found := ipsets[name]; !found {
   113  										return nil, fmt.Errorf("ipset %s not found", name)
   114  									}
   115  									var bb bytes.Buffer
   116  									if err := tmpl.Execute(&bb, map[string]AddrSet{name: ipsets[name]}); err != nil {
   117  										return nil, err
   118  									}
   119  									b := bb.Bytes()
   120  									return b, nil
   121  								case "add":
   122  									if _, found := ipsets[name]; !found {
   123  										return nil, fmt.Errorf("ipset %s not found", name)
   124  									}
   125  									addr := netip.MustParseAddr(arg[len(arg)-2])
   126  									ipsets[name] = ipsets[name].Insert(addr)
   127  								case "del":
   128  									if _, found := ipsets[name]; !found {
   129  										return nil, fmt.Errorf("ipset %s not found", name)
   130  									}
   131  									addr := netip.MustParseAddr(arg[len(arg)-2])
   132  									if !ipsets[name].Has(addr) {
   133  										return nil, nil
   134  									}
   135  									ipsets[name] = ipsets[name].Delete(addr)
   136  								default:
   137  									return nil, fmt.Errorf("unexpected ipset subcommand %s", arg[1])
   138  								}
   139  							}
   140  							return nil, nil
   141  						},
   142  					),
   143  					log: logger,
   144  				}
   145  			}),
   146  		),
   147  
   148  		cell.Invoke(func(m Manager) {
   149  			mgr = m
   150  		}),
   151  	)
   152  
   153  	testCases := []struct {
   154  		name     string
   155  		action   func()
   156  		expected map[string]AddrSet
   157  	}{
   158  		{
   159  			name:   "check Cilium ipsets have been created",
   160  			action: func() {},
   161  			expected: map[string]AddrSet{
   162  				CiliumNodeIPSetV4: {},
   163  				CiliumNodeIPSetV6: {},
   164  			},
   165  		},
   166  		{
   167  			name: "add an IPv4 address",
   168  			action: func() {
   169  				mgr.AddToIPSet(CiliumNodeIPSetV4, INetFamily, netip.MustParseAddr("1.1.1.1"))
   170  			},
   171  			expected: map[string]AddrSet{
   172  				CiliumNodeIPSetV4: sets.New(
   173  					netip.MustParseAddr("1.1.1.1"),
   174  				),
   175  				CiliumNodeIPSetV6: {},
   176  			},
   177  		},
   178  		{
   179  			name: "add another IPv4 address",
   180  			action: func() {
   181  				mgr.AddToIPSet(CiliumNodeIPSetV4, INetFamily, netip.MustParseAddr("2.2.2.2"))
   182  			},
   183  			expected: map[string]AddrSet{
   184  				CiliumNodeIPSetV4: sets.New(
   185  					netip.MustParseAddr("1.1.1.1"),
   186  					netip.MustParseAddr("2.2.2.2"),
   187  				),
   188  				CiliumNodeIPSetV6: {},
   189  			},
   190  		},
   191  		{
   192  			name: "add the same IPv4 address",
   193  			action: func() {
   194  				mgr.AddToIPSet(CiliumNodeIPSetV4, INetFamily, netip.MustParseAddr("2.2.2.2"))
   195  			},
   196  			expected: map[string]AddrSet{
   197  				CiliumNodeIPSetV4: sets.New(
   198  					netip.MustParseAddr("1.1.1.1"),
   199  					netip.MustParseAddr("2.2.2.2"),
   200  				),
   201  				CiliumNodeIPSetV6: {},
   202  			},
   203  		},
   204  		{
   205  			name: "remove an IPv4 address",
   206  			action: func() {
   207  				mgr.RemoveFromIPSet(CiliumNodeIPSetV4, netip.MustParseAddr("1.1.1.1"))
   208  			},
   209  			expected: map[string]AddrSet{
   210  				CiliumNodeIPSetV4: sets.New(
   211  					netip.MustParseAddr("2.2.2.2"),
   212  				),
   213  				CiliumNodeIPSetV6: {},
   214  			},
   215  		},
   216  		{
   217  			name: "remove a missing IPv4 address",
   218  			action: func() {
   219  				mgr.RemoveFromIPSet(CiliumNodeIPSetV4, netip.MustParseAddr("3.3.3.3"))
   220  			},
   221  			expected: map[string]AddrSet{
   222  				CiliumNodeIPSetV4: sets.New(
   223  					netip.MustParseAddr("2.2.2.2"),
   224  				),
   225  				CiliumNodeIPSetV6: {},
   226  			},
   227  		},
   228  		{
   229  			name: "add an IPv6 address",
   230  			action: func() {
   231  				mgr.AddToIPSet(CiliumNodeIPSetV6, INet6Family, netip.MustParseAddr("cafe::1"))
   232  			},
   233  			expected: map[string]AddrSet{
   234  				CiliumNodeIPSetV4: sets.New(
   235  					netip.MustParseAddr("2.2.2.2"),
   236  				),
   237  				CiliumNodeIPSetV6: sets.New(
   238  					netip.MustParseAddr("cafe::1"),
   239  				),
   240  			},
   241  		},
   242  		{
   243  			name: "remove an IPv6 address",
   244  			action: func() {
   245  				mgr.RemoveFromIPSet(CiliumNodeIPSetV6, netip.MustParseAddr("cafe::1"))
   246  			},
   247  			expected: map[string]AddrSet{
   248  				CiliumNodeIPSetV4: sets.New(
   249  					netip.MustParseAddr("2.2.2.2"),
   250  				),
   251  				CiliumNodeIPSetV6: {},
   252  			},
   253  		},
   254  	}
   255  
   256  	time.MaxInternalTimerDelay = time.Millisecond
   257  	t.Cleanup(func() { time.MaxInternalTimerDelay = 0 })
   258  
   259  	tlog := hivetest.Logger(t)
   260  	assert.NoError(t, hive.Start(tlog, context.Background()))
   261  
   262  	for _, tc := range testCases {
   263  		t.Run(tc.name, func(t *testing.T) {
   264  			tc.action()
   265  			assert.Eventually(t, func() bool {
   266  				mu.Lock()
   267  				defer mu.Unlock()
   268  
   269  				if len(ipsets) != len(tc.expected) {
   270  					return false
   271  				}
   272  				for name, expectedAddrs := range tc.expected {
   273  					t.Logf("expected: %#v, actual: %#v", expectedAddrs, ipsets[name])
   274  					addrs, found := ipsets[name]
   275  					if !found || !addrs.Equal(expectedAddrs) {
   276  						return false
   277  					}
   278  				}
   279  				return true
   280  			}, 1*time.Second, 50*time.Millisecond)
   281  		})
   282  	}
   283  
   284  	assert.NoError(t, hive.Stop(tlog, context.Background()))
   285  }
   286  
   287  func TestManagerNodeIpsetNotNeeded(t *testing.T) {
   288  	defer goleak.VerifyNone(t)
   289  
   290  	ipsets := make(map[string]AddrSet) // mocked kernel IP sets
   291  	var mu lock.Mutex                  // protect the ipsets map
   292  
   293  	hive := hive.New(
   294  		cell.Module(
   295  			"ipset-manager-test",
   296  			"ipset-manager-test",
   297  
   298  			cell.Provide(func() config {
   299  				return config{NodeIPSetNeeded: false}
   300  			}),
   301  
   302  			cell.Provide(
   303  				newIPSetManager,
   304  				tables.NewIPSetTable,
   305  				newOps,
   306  				newReconciler,
   307  			),
   308  			cell.Provide(func(ops *ops) reconciler.Operations[*tables.IPSetEntry] {
   309  				return ops
   310  			}),
   311  			cell.Provide(func(logger logrus.FieldLogger) *ipset {
   312  				return &ipset{
   313  					executable: funcExecutable(func(ctx context.Context, command string, stdin string, arg ...string) ([]byte, error) {
   314  						mu.Lock()
   315  						defer mu.Unlock()
   316  
   317  						t.Logf("%s %s", command, strings.Join(arg, " "))
   318  
   319  						if arg[0] == "destroy" {
   320  							name := arg[1]
   321  							if _, found := ipsets[name]; !found {
   322  								return nil, fmt.Errorf("ipset %s not found", name)
   323  							}
   324  							delete(ipsets, name)
   325  						}
   326  						return nil, nil
   327  					}),
   328  					log: logger,
   329  				}
   330  			}),
   331  			// force manager instantiation
   332  			cell.Invoke(func(_ Manager) {}),
   333  		),
   334  	)
   335  
   336  	time.MaxInternalTimerDelay = time.Millisecond
   337  	t.Cleanup(func() { time.MaxInternalTimerDelay = 0 })
   338  
   339  	// create ipv4 and ipv6 node ipsets to simulate stale entries from previous Cilium run
   340  	withLocked(&mu, func() {
   341  		ipsets[CiliumNodeIPSetV4] = sets.New(netip.MustParseAddr("2.2.2.2"))
   342  		ipsets[CiliumNodeIPSetV6] = sets.New(netip.MustParseAddr("cafe::1"))
   343  	})
   344  
   345  	tlog := hivetest.Logger(t)
   346  	assert.NoError(t, hive.Start(tlog, context.Background()))
   347  
   348  	// Cilium node ipsets should eventually be pruned
   349  	assert.Eventually(t, func() bool {
   350  		mu.Lock()
   351  		defer mu.Unlock()
   352  
   353  		if _, found := ipsets[CiliumNodeIPSetV4]; found {
   354  			return false
   355  		}
   356  		if _, found := ipsets[CiliumNodeIPSetV6]; found {
   357  			return false
   358  		}
   359  
   360  		return true
   361  	}, 1*time.Second, 50*time.Millisecond)
   362  
   363  	// create a custom ipset (not managed by Cilium)
   364  	withLocked(&mu, func() {
   365  		ipsets["unmanaged-ipset"] = AddrSet{}
   366  	})
   367  
   368  	assert.NoError(t, hive.Stop(tlog, context.Background()))
   369  
   370  	// ipset managed by Cilium should not have been created again
   371  	withLocked(&mu, func() {
   372  		assert.NotContains(t, ipsets, CiliumNodeIPSetV4)
   373  		assert.NotContains(t, ipsets, CiliumNodeIPSetV6)
   374  	})
   375  
   376  	// ipset not managed by Cilium should not have been pruned
   377  	withLocked(&mu, func() {
   378  		assert.Contains(t, ipsets, "unmanaged-ipset")
   379  	})
   380  }
   381  
   382  func withLocked(m *lock.Mutex, f func()) {
   383  	m.Lock()
   384  	defer m.Unlock()
   385  
   386  	f()
   387  }
   388  
   389  func TestOpsPruneEnabled(t *testing.T) {
   390  	fakeLogger := logrus.New()
   391  	fakeLogger.SetOutput(io.Discard)
   392  
   393  	db := statedb.New()
   394  	table, _ := statedb.NewTable("ipsets", tables.IPSetEntryIndex)
   395  	require.NoError(t, db.RegisterTable(table))
   396  
   397  	txn := db.WriteTxn(table)
   398  	table.Insert(txn, &tables.IPSetEntry{
   399  		Name:   CiliumNodeIPSetV4,
   400  		Family: string(INetFamily),
   401  		Addr:   netip.MustParseAddr("1.1.1.1"),
   402  		Status: reconciler.StatusDone(),
   403  	})
   404  	table.Insert(txn, &tables.IPSetEntry{
   405  		Name:   CiliumNodeIPSetV4,
   406  		Family: string(INetFamily),
   407  		Addr:   netip.MustParseAddr("2.2.2.2"),
   408  		Status: reconciler.StatusDone(),
   409  	})
   410  	table.Insert(txn, &tables.IPSetEntry{
   411  		Name:   CiliumNodeIPSetV6,
   412  		Family: string(INet6Family),
   413  		Addr:   netip.MustParseAddr("cafe::1"),
   414  		Status: reconciler.StatusPending(),
   415  	})
   416  	txn.Commit()
   417  
   418  	var nCalled atomic.Bool // true if the ipset utility has been called
   419  
   420  	ipset := &ipset{
   421  		executable: funcExecutable(func(ctx context.Context, command string, stdin string, arg ...string) ([]byte, error) {
   422  			nCalled.Store(true)
   423  			t.Logf("%s %s", command, strings.Join(arg, " "))
   424  			return nil, nil
   425  		}),
   426  		log: fakeLogger,
   427  	}
   428  
   429  	ops := newOps(fakeLogger, ipset, config{NodeIPSetNeeded: true})
   430  
   431  	// prune operation should be skipped when it is not enabled
   432  	iter := table.All(db.ReadTxn())
   433  	assert.NoError(t, ops.Prune(context.TODO(), db.ReadTxn(), iter))
   434  	assert.False(t, nCalled.Load())
   435  
   436  	ops.enablePrune()
   437  
   438  	// prune operation should now be completed
   439  	iter = table.All(db.ReadTxn())
   440  	assert.NoError(t, ops.Prune(context.TODO(), db.ReadTxn(), iter))
   441  	assert.True(t, nCalled.Load())
   442  }
   443  
   444  func TestIPSetList(t *testing.T) {
   445  	testCases := []struct {
   446  		name     string
   447  		ipsets   map[string]AddrSet
   448  		expected AddrSet
   449  	}{
   450  		{
   451  			name: "empty ipset",
   452  			ipsets: map[string]AddrSet{
   453  				"ciliumtest": {},
   454  			},
   455  			expected: AddrSet{},
   456  		},
   457  		{
   458  			name: "ipset with a single IP",
   459  			ipsets: map[string]AddrSet{
   460  				"ciliumtest": sets.New(netip.MustParseAddr("1.1.1.1")),
   461  			},
   462  			expected: sets.New(netip.MustParseAddr("1.1.1.1")),
   463  		},
   464  		{
   465  			name: "ipset with multiple IPs",
   466  			ipsets: map[string]AddrSet{
   467  				"ciliumtest": sets.New(netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")),
   468  			},
   469  			expected: sets.New(netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")),
   470  		},
   471  	}
   472  
   473  	fakeLogger := logrus.New()
   474  	fakeLogger.SetOutput(io.Discard)
   475  
   476  	tmpl := template.Must(template.New("ipsets").Parse(textTmpl))
   477  
   478  	for _, tc := range testCases {
   479  		t.Run(tc.name, func(t *testing.T) {
   480  			var bb bytes.Buffer
   481  			if err := tmpl.Execute(&bb, tc.ipsets); err != nil {
   482  				t.Fatalf("unable to execute ipset list output template: %s", err)
   483  			}
   484  			ipset := &ipset{
   485  				&mockExec{t, bb.Bytes(), nil},
   486  				fakeLogger,
   487  			}
   488  			got, err := ipset.list(context.Background(), "")
   489  			if err != nil {
   490  				t.Fatal(err)
   491  			}
   492  			if !got.Equal(tc.expected) {
   493  				t.Fatalf("expected addresses in ipset to be %v, got %v", tc.expected, got)
   494  			}
   495  		})
   496  	}
   497  }
   498  
   499  func TestIPSetListInexistentIPSet(t *testing.T) {
   500  	fakeLogger := logrus.New()
   501  	fakeLogger.SetOutput(io.Discard)
   502  
   503  	expectedErr := errors.New("ipset v7.19: The set with the given name does not exist")
   504  	ipset := &ipset{
   505  		&mockExec{t, nil, expectedErr},
   506  		fakeLogger,
   507  	}
   508  
   509  	_, err := ipset.list(context.Background(), "")
   510  	if err == nil {
   511  		t.Fatal("expected error, got nil")
   512  	}
   513  }
   514  
   515  type mockExec struct {
   516  	t   *testing.T
   517  	out []byte
   518  	err error
   519  }
   520  
   521  func (e *mockExec) exec(ctx context.Context, name string, stdin string, arg ...string) ([]byte, error) {
   522  	return e.out, e.err
   523  }
   524  
   525  func BenchmarkManager(b *testing.B) {
   526  
   527  	var (
   528  		mgr         Manager
   529  		initializer Initializer
   530  		addCount    atomic.Int32
   531  		deleteCount atomic.Int32
   532  	)
   533  
   534  	hive := hive.New(
   535  		cell.Module(
   536  			"ipset-manager-test",
   537  			"ipset-manager-test",
   538  
   539  			cell.Provide(func() config {
   540  				return config{NodeIPSetNeeded: true}
   541  			}),
   542  
   543  			cell.Provide(
   544  				newIPSetManager,
   545  				tables.NewIPSetTable,
   546  				newOps,
   547  				newReconciler,
   548  			),
   549  			cell.Provide(func(ops *ops) reconciler.Operations[*tables.IPSetEntry] {
   550  				return ops
   551  			}),
   552  
   553  			cell.Provide(func(logger logrus.FieldLogger) *ipset {
   554  				return &ipset{
   555  					executable: funcExecutable(
   556  						func(ctx context.Context, command string, stdin string, arg ...string) ([]byte, error) {
   557  							// exec of ipset add takes about ~0.51ms
   558  							time.Sleep(time.Millisecond)
   559  							if arg[0] == "add" {
   560  								addCount.Add(1)
   561  							} else if arg[0] == "del" {
   562  								deleteCount.Add(1)
   563  							}
   564  
   565  							if arg[0] == "restore" {
   566  								count := strings.Count(stdin, "\n")
   567  								if strings.HasPrefix(stdin, "add") {
   568  									addCount.Add(int32(count))
   569  								} else {
   570  									deleteCount.Add(int32(count))
   571  								}
   572  							}
   573  							return nil, nil
   574  						}),
   575  					log: logger,
   576  				}
   577  			}),
   578  		),
   579  
   580  		cell.Invoke(func(m Manager) {
   581  			// Add an initializer to stop the pruning
   582  			initializer = m.NewInitializer()
   583  			mgr = m
   584  		}),
   585  	)
   586  
   587  	tlog := hivetest.Logger(b)
   588  	assert.NoError(b, hive.Start(tlog, context.Background()))
   589  
   590  	b.ResetTimer()
   591  
   592  	numEntries := 1000
   593  
   594  	toNetIP := func(i int) netip.Addr {
   595  		var addr1 [4]byte
   596  		binary.BigEndian.PutUint32(addr1[:], 0x02000000+uint32(i))
   597  		return netip.AddrFrom4(addr1)
   598  	}
   599  
   600  	for n := 0; n < b.N; n++ {
   601  		for i := 0; i < numEntries; i++ {
   602  			ip := toNetIP(i)
   603  			mgr.AddToIPSet(CiliumNodeIPSetV4, INetFamily, ip)
   604  		}
   605  
   606  		// Wait for all ops to be done
   607  		for addCount.Load() != int32(numEntries) {
   608  			time.Sleep(time.Millisecond)
   609  
   610  		}
   611  		for i := 0; i < numEntries; i++ {
   612  			ip := toNetIP(i)
   613  			mgr.RemoveFromIPSet(CiliumNodeIPSetV4, ip)
   614  		}
   615  
   616  		for deleteCount.Load() != int32(numEntries) {
   617  			time.Sleep(time.Millisecond)
   618  		}
   619  
   620  		addCount.Store(0)
   621  		deleteCount.Store(0)
   622  	}
   623  
   624  	b.StopTimer()
   625  
   626  	b.ReportMetric(float64(2 /*add&delete*/ *b.N*numEntries)/b.Elapsed().Seconds(), "ops/sec")
   627  
   628  	initializer.InitDone()
   629  
   630  	assert.NoError(b, hive.Stop(tlog, context.Background()))
   631  }