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 }