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  }