github.com/sylabs/singularity/v4@v4.1.3/pkg/network/network_linux_test.go (about) 1 // Copyright (c) 2019-2022, Sylabs Inc. All rights reserved. 2 // This software is licensed under a 3-clause BSD license. Please consult the 3 // LICENSE.md file distributed with the sources of this project regarding your 4 // rights to use or distribute this software. 5 6 //go:build integration_test 7 8 package network 9 10 import ( 11 "context" 12 "fmt" 13 "io" 14 "net" 15 "os" 16 "os/exec" 17 "path/filepath" 18 "reflect" 19 "strings" 20 "syscall" 21 "testing" 22 23 "github.com/containernetworking/cni/libcni" 24 "github.com/sylabs/singularity/v4/internal/pkg/buildcfg" 25 "github.com/sylabs/singularity/v4/internal/pkg/test" 26 ) 27 28 var confFiles = []struct { 29 name string 30 file string 31 content string 32 }{ 33 { 34 name: "test-bridge", 35 file: "00_test-bridge.conflist", 36 content: `{ 37 "cniVersion": "1.0.0", 38 "name": "test-bridge", 39 "plugins": [ 40 { 41 "type": "loopback" 42 }, 43 { 44 "type": "bridge", 45 "bridge": "tbr0", 46 "isGateway": true, 47 "ipMasq": true, 48 "ipam": { 49 "type": "host-local", 50 "subnet": "10.111.111.0/24", 51 "routes": [ 52 { "dst": "0.0.0.0/0" } 53 ] 54 } 55 }, 56 { 57 "type": "portmap", 58 "capabilities": {"portMappings": true}, 59 "snat": true 60 } 61 ] 62 }`, 63 }, 64 { 65 name: "test-badbridge", 66 file: "10_badbridge.conflist", 67 content: `{ 68 "cniVersion": "1.0.0", 69 "name": "test-badbridge", 70 "plugins": [ 71 { 72 "type": "badbridge", 73 "bridge": "bbr0" 74 } 75 ] 76 }`, 77 }, 78 { 79 name: "test-bridge-iprange", 80 file: "20_bridge_iprange.conflist", 81 content: `{ 82 "cniVersion": "1.0.0", 83 "name": "test-bridge-iprange", 84 "plugins": [ 85 { 86 "type": "loopback" 87 }, 88 { 89 "type": "bridge", 90 "bridge": "tipbr0", 91 "isGateway": true, 92 "ipMasq": true, 93 "capabilities": {"ipRanges": true}, 94 "ipam": { 95 "type": "host-local", 96 "routes": [ 97 { "dst": "0.0.0.0/0" } 98 ] 99 } 100 }, 101 { 102 "type": "portmap", 103 "capabilities": {"portMappings": true}, 104 "snat": true 105 } 106 ] 107 }`, 108 }, 109 } 110 111 // defaultCNIConfPath is the default directory to CNI network configuration files 112 var defaultCNIConfPath = "" 113 114 // defaultCNIPluginPath is the default directory to CNI plugins executables 115 var defaultCNIPluginPath = filepath.Join(buildcfg.LIBEXECDIR, "singularity", "cni") 116 117 // testNetworks will contains configured network 118 var testNetworks []string 119 120 func TestGetAllNetworkConfigList(t *testing.T) { 121 test.EnsurePrivilege(t) 122 123 emptyDir := t.TempDir() 124 125 testCNIPath := []struct { 126 name string 127 cniPath *CNIPath 128 success bool 129 validationFunc func([]*libcni.NetworkConfigList) error 130 }{ 131 { 132 name: "nil CNIPath", 133 cniPath: nil, 134 success: false, 135 }, 136 { 137 name: "empty configuration path", 138 cniPath: &CNIPath{ 139 Conf: "", 140 Plugin: "", 141 }, 142 success: false, 143 }, 144 { 145 name: "empty configuration directory", 146 cniPath: &CNIPath{ 147 Conf: emptyDir, 148 Plugin: "", 149 }, 150 success: false, 151 }, 152 { 153 name: "default configuration/plugin path", 154 cniPath: &CNIPath{ 155 Conf: defaultCNIConfPath, 156 Plugin: defaultCNIPluginPath, 157 }, 158 success: true, 159 validationFunc: func(networkList []*libcni.NetworkConfigList) error { 160 var networks []string 161 for _, n := range networkList { 162 networks = append(networks, n.Name) 163 } 164 if !reflect.DeepEqual(networks, testNetworks) { 165 return fmt.Errorf("wrong network list returned: %v", networks) 166 } 167 return nil 168 }, 169 }, 170 } 171 172 for _, c := range testCNIPath { 173 networkList, err := GetAllNetworkConfigList(c.cniPath) 174 if err != nil && c.success { 175 t.Errorf("unexpected failure for %q test: %s", c.name, err) 176 } else if err == nil && !c.success { 177 t.Errorf("unexpected success for %q test", c.name) 178 } else if c.validationFunc != nil { 179 if err := c.validationFunc(networkList); err != nil { 180 t.Error(err) 181 } 182 } 183 } 184 } 185 186 func testSetArgs(setup *Setup, t *testing.T) { 187 testArgs := []struct { 188 desc string 189 args []string 190 success bool 191 }{ 192 { 193 desc: "empty arg", 194 args: []string{""}, 195 success: false, 196 }, 197 { 198 desc: "badly formatted arg #1", 199 args: []string{"test-bridge:"}, 200 success: false, 201 }, 202 { 203 desc: "badly formatted arg #2", 204 args: []string{":portmap=80/tcp"}, 205 success: false, 206 }, 207 { 208 desc: "badly formatted arg #3", 209 args: []string{"portmap=80/tcp;portmap="}, 210 success: false, 211 }, 212 { 213 desc: "empty portmap", 214 args: []string{"test-bridge:portmap="}, 215 success: false, 216 }, 217 { 218 desc: "unknown portmap protocol", 219 args: []string{"test-bridge:portmap=80/icmp"}, 220 success: false, 221 }, 222 { 223 desc: "portmap 0", 224 args: []string{"test-bridge:portmap=0/tcp"}, 225 success: false, 226 }, 227 { 228 desc: "good portmap arg #1", 229 args: []string{"test-bridge:portmap=80:80/tcp", "portmap=80:80/tcp"}, 230 success: true, 231 }, 232 { 233 desc: "good portmap arg #2", 234 args: []string{"portmap=80:80/tcp;portmap=8080/udp"}, 235 success: true, 236 }, 237 { 238 desc: "good 1-1 portmap arg", 239 args: []string{"test-bridge:portmap=80/udp", "test-bridge-iprange:portmap=8080/tcp"}, 240 success: true, 241 }, 242 { 243 desc: "good port range", 244 args: []string{"test-bridge:portmap=65530/tcp"}, 245 success: true, 246 }, 247 { 248 desc: "bad port range", 249 args: []string{"test-bridge:portmap=65550/tcp"}, 250 success: false, 251 }, 252 { 253 desc: "ipRange not supported arg", 254 args: []string{"test-bridge:ipRange=10.1.1.0/16"}, 255 success: false, 256 }, 257 { 258 desc: "good ipRange arg", 259 args: []string{"test-bridge-iprange:ipRange=10.1.1.0/16"}, 260 success: true, 261 }, 262 { 263 desc: "bad ipRange arg", 264 args: []string{"test-bridge-iprange:ipRange=1024.1.1.0/16"}, 265 success: false, 266 }, 267 { 268 desc: "IP arg", 269 args: []string{"test-bridge:IP=10.1.1.1"}, 270 success: true, 271 }, 272 { 273 desc: "Any arg", 274 args: []string{"test-bridge:any=test"}, 275 success: true, 276 }, 277 } 278 for _, a := range testArgs { 279 err := setup.SetArgs(a.args) 280 if err != nil && a.success { 281 t.Errorf("unexpected failure for %q test: %s", a.desc, err) 282 } else if err == nil && !a.success { 283 t.Errorf("unexpected success for %q test", a.desc) 284 } 285 } 286 } 287 288 func TestNewSetup(t *testing.T) { 289 test.EnsurePrivilege(t) 290 291 cniPath := &CNIPath{ 292 Conf: defaultCNIConfPath, 293 Plugin: defaultCNIPluginPath, 294 } 295 testSetup := []struct { 296 desc string 297 networks []string 298 id string 299 nspath string 300 cniPath *CNIPath 301 success bool 302 subTest func(*Setup, *testing.T) 303 }{ 304 { 305 desc: "no name network", 306 networks: []string{""}, 307 id: "testing", 308 nspath: "/proc/self/net/ns", 309 cniPath: cniPath, 310 success: false, 311 }, 312 { 313 desc: "bad network", 314 networks: []string{"fake-network"}, 315 id: "testing", 316 nspath: "/proc/self/net/ns", 317 cniPath: cniPath, 318 success: false, 319 }, 320 { 321 desc: "bad networks", 322 networks: []string{"test-bridge", "fake-network"}, 323 id: "testing", 324 nspath: "/proc/self/net/ns", 325 cniPath: cniPath, 326 success: false, 327 }, 328 { 329 desc: "good network", 330 networks: []string{"test-bridge"}, 331 nspath: "/proc/self/net/ns", 332 cniPath: cniPath, 333 success: true, 334 }, 335 { 336 desc: "good networks", 337 networks: []string{"test-bridge", "test-bridge-iprange"}, 338 nspath: "/proc/self/net/ns", 339 cniPath: cniPath, 340 success: true, 341 subTest: testSetArgs, 342 }, 343 { 344 desc: "nil cni path", 345 networks: []string{""}, 346 id: "testing", 347 success: false, 348 }, 349 } 350 351 for _, s := range testSetup { 352 setup, err := NewSetup(s.networks, s.id, s.nspath, s.cniPath) 353 if err != nil && s.success { 354 t.Errorf("unexpected failure for %q test: %s", s.desc, err) 355 } else if err == nil && !s.success { 356 t.Errorf("unexpected success for %q test", s.desc) 357 } else if s.subTest != nil { 358 s.subTest(setup, t) 359 } 360 } 361 } 362 363 // ping requested IP from host 364 func testPingIP(nsPath string, cniPath *CNIPath, stdin io.WriteCloser, stdout io.ReadCloser) error { 365 testIP := "10.111.111.10" 366 367 setup, err := NewSetup([]string{"test-bridge"}, "test_", nsPath, cniPath) 368 if err != nil { 369 return err 370 } 371 setup.SetArgs([]string{"IP=" + testIP}) 372 if err := setup.AddNetworks(context.Background()); err != nil { 373 return err 374 } 375 defer setup.DelNetworks(context.Background()) 376 377 ip, err := setup.GetNetworkIP("test-bridge", "4") 378 if err != nil { 379 return err 380 } 381 cmdPath, err := exec.LookPath("ping") 382 if err != nil { 383 return err 384 } 385 if ip.String() != testIP { 386 return fmt.Errorf("%s doesn't match with requested ip %s", ip.String(), testIP) 387 } 388 cmd := exec.Command(cmdPath, "-c", "1", testIP) 389 if err := cmd.Run(); err != nil { 390 return err 391 } 392 return nil 393 } 394 395 // ping random acquired IP from host 396 func testPingRandomIP(nsPath string, cniPath *CNIPath, stdin io.WriteCloser, stdout io.ReadCloser) error { 397 setup, err := NewSetup([]string{"test-bridge"}, "test_", nsPath, cniPath) 398 if err != nil { 399 return err 400 } 401 if err := setup.AddNetworks(context.Background()); err != nil { 402 return err 403 } 404 defer setup.DelNetworks(context.Background()) 405 406 ip, err := setup.GetNetworkIP("test-bridge", "4") 407 if err != nil { 408 return err 409 } 410 cmdPath, err := exec.LookPath("ping") 411 if err != nil { 412 return err 413 } 414 cmd := exec.Command(cmdPath, "-c", "1", ip.String()) 415 if err := cmd.Run(); err != nil { 416 return err 417 } 418 return nil 419 } 420 421 // ping IP from host within requested IP range 422 func testPingIPRange(nsPath string, cniPath *CNIPath, stdin io.WriteCloser, stdout io.ReadCloser) error { 423 setup, err := NewSetup([]string{"test-bridge-iprange"}, "test_", nsPath, cniPath) 424 if err != nil { 425 return err 426 } 427 setup.SetArgs([]string{"ipRange=10.111.112.0/24"}) 428 if err := setup.AddNetworks(context.Background()); err != nil { 429 return err 430 } 431 defer setup.DelNetworks(context.Background()) 432 433 ip, err := setup.GetNetworkIP("test-bridge", "4") 434 if err != nil { 435 ip, err = setup.GetNetworkIP("test-bridge-iprange", "4") 436 if err != nil { 437 return err 438 } 439 } 440 cmdPath, err := exec.LookPath("ping") 441 if err != nil { 442 return err 443 } 444 if !strings.HasPrefix(ip.String(), "10.111.112") { 445 return fmt.Errorf("ip address %s not in net range 10.111.112.0/24", ip.String()) 446 } 447 cmd := exec.Command(cmdPath, "-c", "1", ip.String()) 448 if err := cmd.Run(); err != nil { 449 return err 450 } 451 return nil 452 } 453 454 // test port mapping by connecting to port 80 mapped inside container 455 // to 31080 on host 456 func testHTTPPortmap(nsPath string, cniPath *CNIPath, stdin io.WriteCloser, stdout io.ReadCloser) error { 457 setup, err := NewSetup([]string{"test-bridge"}, "test_", nsPath, cniPath) 458 if err != nil { 459 return err 460 } 461 setup.SetArgs([]string{"portmap=31080:80/tcp"}) 462 if err := setup.AddNetworks(context.Background()); err != nil { 463 return err 464 } 465 defer setup.DelNetworks(context.Background()) 466 467 eth, err := setup.GetNetworkInterface("test-bridge-iprange") 468 if err != nil { 469 eth, err = setup.GetNetworkInterface("test-bridge") 470 if err != nil { 471 return err 472 } 473 } 474 if eth != "eth0" { 475 return fmt.Errorf("unexpected interface %s", eth) 476 } 477 conn, err := net.Dial("tcp", "127.0.0.1:31080") 478 if err != nil { 479 return err 480 } 481 message := "test\r\n" 482 483 if _, err := conn.Write([]byte(message)); err != nil { 484 return err 485 } 486 conn.Close() 487 488 received, err := io.ReadAll(stdout) 489 if err != nil { 490 return err 491 } 492 if string(received) != message { 493 return fmt.Errorf("received data doesn't match message: %s", string(received)) 494 } 495 return nil 496 } 497 498 // try with an non existent plugin 499 func testBadBridge(nsPath string, cniPath *CNIPath, stdin io.WriteCloser, stdout io.ReadCloser) error { 500 setup, err := NewSetup([]string{"test-badbridge"}, "", nsPath, cniPath) 501 if err != nil { 502 return err 503 } 504 if err := setup.AddNetworks(context.Background()); err == nil { 505 return fmt.Errorf("unexpected success while calling non existent plugin") 506 } 507 defer setup.DelNetworks(context.Background()) 508 509 return nil 510 } 511 512 func TestAddDelNetworks(t *testing.T) { 513 test.EnsurePrivilege(t) 514 515 cniPath := &CNIPath{ 516 Conf: defaultCNIConfPath, 517 Plugin: defaultCNIPluginPath, 518 } 519 520 for _, c := range []struct { 521 name string 522 command string 523 args []string 524 runFunc func(string, *CNIPath, io.WriteCloser, io.ReadCloser) error 525 }{ 526 { 527 name: "TestPingIP", 528 command: "cat", 529 runFunc: testPingIP, 530 }, 531 { 532 name: "TestPingRandomIP", 533 command: "cat", 534 runFunc: testPingRandomIP, 535 }, 536 { 537 name: "TestHTTPPortmap", 538 command: "nc", 539 args: []string{"-l", "0.0.0.0", "80"}, 540 runFunc: testHTTPPortmap, 541 }, 542 { 543 name: "TestPingIPRange", 544 command: "cat", 545 runFunc: testPingIPRange, 546 }, 547 { 548 name: "TestBadBridge", 549 command: "cat", 550 runFunc: testBadBridge, 551 }, 552 } { 553 var err error 554 var cmdPath string 555 var stdinPipe io.WriteCloser 556 var stdoutPipe io.ReadCloser 557 558 cmdPath, err = exec.LookPath(c.command) 559 if err != nil { 560 t.Fatal(err) 561 } 562 cmd := exec.Command(cmdPath, c.args...) 563 cmd.SysProcAttr = &syscall.SysProcAttr{} 564 cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWNET 565 566 stdinPipe, err = cmd.StdinPipe() 567 if err != nil { 568 t.Fatal(err) 569 } 570 stdoutPipe, err = cmd.StdoutPipe() 571 if err != nil { 572 t.Fatal(err) 573 } 574 575 if err := cmd.Start(); err != nil { 576 t.Fatal(err) 577 } 578 579 nsPath := fmt.Sprintf("/proc/%d/ns/net", cmd.Process.Pid) 580 if err := c.runFunc(nsPath, cniPath, stdinPipe, stdoutPipe); err != nil { 581 t.Errorf("unexpected failure for %q: %s", c.name, err) 582 if err := cmd.Process.Kill(); err != nil { 583 t.Fatalf("error killing process %q: %s", cmdPath, err) 584 } 585 } 586 587 stdoutPipe.Close() 588 stdinPipe.Close() 589 590 if err := cmd.Wait(); err != nil { 591 t.Error(err) 592 } 593 } 594 } 595 596 func TestMain(m *testing.M) { 597 var err error 598 599 test.EnsurePrivilege(nil) 600 601 defaultCNIConfPath, err = os.MkdirTemp("", "conf_test_") 602 if err != nil { 603 os.Exit(1) 604 } 605 606 for _, conf := range confFiles { 607 testNetworks = append(testNetworks, conf.name) 608 path := filepath.Join(defaultCNIConfPath, conf.file) 609 if err := os.WriteFile(path, []byte(conf.content), 0o644); err != nil { 610 os.RemoveAll(defaultCNIConfPath) 611 os.Exit(1) 612 } 613 } 614 615 e := m.Run() 616 os.RemoveAll(defaultCNIConfPath) 617 os.Exit(e) 618 }