github.com/apptainer/singularity@v3.1.1+incompatible/pkg/network/network.go (about)

     1  package network
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"os"
     7  	"sort"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/containernetworking/cni/libcni"
    12  	"github.com/containernetworking/cni/pkg/types"
    13  	"github.com/containernetworking/cni/pkg/types/current"
    14  	"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
    15  	"github.com/sylabs/singularity/internal/pkg/util/env"
    16  )
    17  
    18  type netError string
    19  
    20  func (e netError) Error() string { return string(e) }
    21  
    22  const (
    23  	// ErrNoCNIConfig corresponds to a missing CNI configuration path
    24  	ErrNoCNIConfig = netError("no CNI configuration path provided")
    25  	// ErrNoCNIPlugin corresponds to a missing CNI plugin path
    26  	ErrNoCNIPlugin = netError("no CNI plugin path provided")
    27  )
    28  
    29  // CNIPath contains path to CNI configuration directory and path to executable
    30  // CNI plugins directory
    31  type CNIPath struct {
    32  	Conf   string
    33  	Plugin string
    34  }
    35  
    36  // Setup contains network installation setup
    37  type Setup struct {
    38  	networks        []string
    39  	networkConfList []*libcni.NetworkConfigList
    40  	runtimeConf     []*libcni.RuntimeConf
    41  	result          []types.Result
    42  	cniPath         *CNIPath
    43  	containerID     string
    44  	netNS           string
    45  	envPath         string
    46  }
    47  
    48  // PortMapEntry describes a port mapping between host and container
    49  type PortMapEntry struct {
    50  	HostPort      int    `json:"hostPort"`
    51  	ContainerPort int    `json:"containerPort"`
    52  	Protocol      string `json:"protocol"`
    53  	HostIP        string `json:"hostIP,omitempty"`
    54  }
    55  
    56  // GetAllNetworkConfigList lists configured networks in configuration path directory
    57  // provided by cniPath
    58  func GetAllNetworkConfigList(cniPath *CNIPath) ([]*libcni.NetworkConfigList, error) {
    59  	networks := make([]*libcni.NetworkConfigList, 0)
    60  
    61  	if cniPath == nil {
    62  		return networks, ErrNoCNIConfig
    63  	}
    64  	if cniPath.Conf == "" {
    65  		return networks, ErrNoCNIConfig
    66  	}
    67  
    68  	files, err := libcni.ConfFiles(cniPath.Conf, []string{".conf", ".json", ".conflist"})
    69  	if err != nil {
    70  		return nil, err
    71  	} else if len(files) == 0 {
    72  		return nil, libcni.NoConfigsFoundError{Dir: cniPath.Conf}
    73  	}
    74  	sort.Strings(files)
    75  
    76  	for _, file := range files {
    77  		if strings.HasSuffix(file, ".conflist") {
    78  			conf, err := libcni.ConfListFromFile(file)
    79  			if err != nil {
    80  				return nil, err
    81  			}
    82  			networks = append(networks, conf)
    83  		} else {
    84  			conf, err := libcni.ConfFromFile(file)
    85  			if err != nil {
    86  				return nil, err
    87  			}
    88  			confList, err := libcni.ConfListFromConf(conf)
    89  			if err != nil {
    90  				return nil, err
    91  			}
    92  			networks = append(networks, confList)
    93  		}
    94  	}
    95  
    96  	return networks, nil
    97  }
    98  
    99  // NewSetup creates and returns a network setup to configure, add and remove
   100  // network interfaces in container
   101  func NewSetup(networks []string, containerID string, netNS string, cniPath *CNIPath) (*Setup, error) {
   102  	id := containerID
   103  
   104  	if id == "" {
   105  		id = strconv.Itoa(os.Getpid())
   106  	}
   107  
   108  	if cniPath == nil {
   109  		return nil, ErrNoCNIConfig
   110  	}
   111  	if cniPath.Conf == "" {
   112  		return nil, ErrNoCNIConfig
   113  	}
   114  	if cniPath.Plugin == "" {
   115  		return nil, ErrNoCNIPlugin
   116  	}
   117  
   118  	networkConfList := make([]*libcni.NetworkConfigList, 0)
   119  	runtimeConf := make([]*libcni.RuntimeConf, 0)
   120  
   121  	ifIndex := 0
   122  	for _, network := range networks {
   123  		nlist, err := libcni.LoadConfList(cniPath.Conf, network)
   124  		if err != nil {
   125  			return nil, err
   126  		}
   127  
   128  		rt := &libcni.RuntimeConf{
   129  			ContainerID:    containerID,
   130  			NetNS:          netNS,
   131  			IfName:         fmt.Sprintf("eth%d", ifIndex),
   132  			CapabilityArgs: make(map[string]interface{}, 0),
   133  			Args:           make([][2]string, 0),
   134  		}
   135  
   136  		runtimeConf = append(runtimeConf, rt)
   137  		networkConfList = append(networkConfList, nlist)
   138  
   139  		ifIndex++
   140  	}
   141  
   142  	return &Setup{
   143  			networks:        networks,
   144  			networkConfList: networkConfList,
   145  			runtimeConf:     runtimeConf,
   146  			cniPath:         cniPath,
   147  			netNS:           netNS,
   148  			containerID:     id,
   149  		},
   150  		nil
   151  }
   152  
   153  // NewSetupFromConfig creates and returns network setup to configure from
   154  // a network configuration list
   155  func NewSetupFromConfig(networkConfList []*libcni.NetworkConfigList, containerID string, netNS string, cniPath *CNIPath) (*Setup, error) {
   156  	id := containerID
   157  
   158  	if id == "" {
   159  		id = strconv.Itoa(os.Getpid())
   160  	}
   161  
   162  	if cniPath == nil {
   163  		return nil, ErrNoCNIConfig
   164  	}
   165  	if cniPath.Conf == "" {
   166  		return nil, ErrNoCNIConfig
   167  	}
   168  	if cniPath.Plugin == "" {
   169  		return nil, ErrNoCNIPlugin
   170  	}
   171  
   172  	runtimeConf := make([]*libcni.RuntimeConf, len(networkConfList))
   173  	networks := make([]string, len(networkConfList))
   174  
   175  	ifIndex := 0
   176  	for i, conf := range networkConfList {
   177  		runtimeConf[i] = &libcni.RuntimeConf{
   178  			ContainerID:    containerID,
   179  			NetNS:          netNS,
   180  			IfName:         fmt.Sprintf("eth%d", ifIndex),
   181  			CapabilityArgs: make(map[string]interface{}, 0),
   182  			Args:           make([][2]string, 0),
   183  		}
   184  
   185  		networks[i] = conf.Name
   186  
   187  		ifIndex++
   188  	}
   189  
   190  	return &Setup{
   191  			networks:        networks,
   192  			networkConfList: networkConfList,
   193  			runtimeConf:     runtimeConf,
   194  			cniPath:         cniPath,
   195  			netNS:           netNS,
   196  			containerID:     id,
   197  		},
   198  		nil
   199  }
   200  
   201  func parseArg(arg string) ([][2]string, error) {
   202  	argList := make([][2]string, 0)
   203  
   204  	pairs := strings.Split(arg, ";")
   205  	for _, pair := range pairs {
   206  		keyVal := strings.Split(pair, "=")
   207  		if len(keyVal) != 2 {
   208  			return nil, fmt.Errorf("invalid argument: %s", pair)
   209  		}
   210  		argList = append(argList, [2]string{keyVal[0], keyVal[1]})
   211  	}
   212  	return argList, nil
   213  }
   214  
   215  // SetCapability sets capability arguments for the corresponding network plugin
   216  // uses by a configured network
   217  func (m *Setup) SetCapability(network string, capName string, args interface{}) error {
   218  	for i := range m.networks {
   219  		if m.networks[i] == network {
   220  			hasCap := false
   221  			for _, plugin := range m.networkConfList[i].Plugins {
   222  				if plugin.Network.Capabilities[capName] {
   223  					hasCap = true
   224  					break
   225  				}
   226  			}
   227  
   228  			if !hasCap {
   229  				return fmt.Errorf("%s network doesn't have %s capability", network, capName)
   230  			}
   231  
   232  			switch args.(type) {
   233  			case PortMapEntry:
   234  				if m.runtimeConf[i].CapabilityArgs[capName] == nil {
   235  					m.runtimeConf[i].CapabilityArgs[capName] = make([]PortMapEntry, 0)
   236  				}
   237  				m.runtimeConf[i].CapabilityArgs[capName] = append(
   238  					m.runtimeConf[i].CapabilityArgs[capName].([]PortMapEntry),
   239  					args.(PortMapEntry),
   240  				)
   241  			case []allocator.Range:
   242  				if m.runtimeConf[i].CapabilityArgs[capName] == nil {
   243  					m.runtimeConf[i].CapabilityArgs[capName] = []allocator.RangeSet{args.([]allocator.Range)}
   244  				}
   245  			}
   246  		}
   247  	}
   248  	return nil
   249  }
   250  
   251  // SetArgs affects arguments to corresponding network plugins
   252  func (m *Setup) SetArgs(args []string) error {
   253  	if len(m.networks) < 1 {
   254  		return fmt.Errorf("there is no configured network in list")
   255  	}
   256  
   257  	for _, arg := range args {
   258  		var splitted []string
   259  		networkName := ""
   260  
   261  		if strings.IndexByte(arg, ':') > strings.IndexByte(arg, '=') {
   262  			splitted = []string{m.networks[0], arg}
   263  		} else {
   264  			splitted = strings.SplitN(arg, ":", 2)
   265  		}
   266  		if len(splitted) < 1 && len(splitted) > 2 {
   267  			return fmt.Errorf("argument must be of form '<network>:KEY1=value1;KEY2=value1' or 'KEY1=value1;KEY2=value1'")
   268  		}
   269  		n := len(splitted) - 1
   270  		if n == 0 {
   271  			networkName = m.networks[0]
   272  		} else {
   273  			networkName = splitted[0]
   274  		}
   275  		hasNetwork := false
   276  		for _, network := range m.networks {
   277  			if network == networkName {
   278  				hasNetwork = true
   279  				break
   280  			}
   281  		}
   282  		if !hasNetwork {
   283  			return fmt.Errorf("network %s wasn't specified in --network option", networkName)
   284  		}
   285  		argList, err := parseArg(splitted[n])
   286  		if err != nil {
   287  			return err
   288  		}
   289  		for _, kv := range argList {
   290  			key := kv[0]
   291  			value := kv[1]
   292  			if key == "portmap" {
   293  				pm := &PortMapEntry{}
   294  
   295  				splittedPort := strings.SplitN(value, "/", 2)
   296  				if len(splittedPort) != 2 {
   297  					return fmt.Errorf("badly formatted portmap argument '%s', must be of form portmap=hostPort:containerPort/protocol", splitted[1])
   298  				}
   299  				pm.Protocol = splittedPort[1]
   300  				if pm.Protocol != "tcp" && pm.Protocol != "udp" {
   301  					return fmt.Errorf("only tcp and udp protocol can be specified")
   302  				}
   303  				ports := strings.Split(splittedPort[0], ":")
   304  				if len(ports) != 1 && len(ports) != 2 {
   305  					return fmt.Errorf("portmap port argument is badly formatted")
   306  				}
   307  				if n, err := strconv.ParseInt(ports[0], 0, 16); err == nil {
   308  					pm.HostPort = int(n)
   309  					if pm.HostPort <= 0 {
   310  						return fmt.Errorf("host port must be greater than zero")
   311  					}
   312  				} else {
   313  					return fmt.Errorf("can't convert host port '%s': %s", ports[0], err)
   314  				}
   315  				if len(ports) == 2 {
   316  					if n, err := strconv.ParseInt(ports[1], 0, 16); err == nil {
   317  						pm.ContainerPort = int(n)
   318  						if pm.ContainerPort <= 0 {
   319  							return fmt.Errorf("container port must be greater than zero")
   320  						}
   321  					} else {
   322  						return fmt.Errorf("can't convert container port '%s': %s", ports[1], err)
   323  					}
   324  				} else {
   325  					pm.ContainerPort = pm.HostPort
   326  				}
   327  				if err := m.SetCapability(networkName, "portMappings", *pm); err != nil {
   328  					return err
   329  				}
   330  			} else if key == "ipRange" {
   331  				ipRange := make([]allocator.Range, 1)
   332  				_, subnet, err := net.ParseCIDR(value)
   333  				if err != nil {
   334  					return err
   335  				}
   336  				ipRange[0].Subnet = types.IPNet(*subnet)
   337  				if err := m.SetCapability(networkName, "ipRanges", ipRange); err != nil {
   338  					return err
   339  				}
   340  			} else {
   341  				for i := range m.networks {
   342  					if m.networks[i] == networkName {
   343  						m.runtimeConf[i].Args = append(m.runtimeConf[i].Args, kv)
   344  					}
   345  				}
   346  			}
   347  		}
   348  	}
   349  	return nil
   350  }
   351  
   352  // GetNetworkIP returns IP associated with a configured network, if network
   353  // is empty, the function returns IP for the first configured network
   354  func (m *Setup) GetNetworkIP(network string, version string) (net.IP, error) {
   355  	n := network
   356  	if n == "" && len(m.networkConfList) > 0 {
   357  		n = m.networkConfList[0].Name
   358  	}
   359  
   360  	for i := 0; i < len(m.networkConfList); i++ {
   361  		if m.networkConfList[i].Name == n {
   362  			res, _ := current.GetResult(m.result[i])
   363  			for _, ipResult := range res.IPs {
   364  				if ipResult.Version == version {
   365  					return ipResult.Address.IP, nil
   366  				}
   367  			}
   368  			break
   369  		}
   370  	}
   371  
   372  	return nil, fmt.Errorf("no IP found for network %s", network)
   373  }
   374  
   375  // GetNetworkInterface returns container network interface associated
   376  // with a network, if network is empty, the function returns interface
   377  // for the first configured network
   378  func (m *Setup) GetNetworkInterface(network string) (string, error) {
   379  	n := network
   380  	if n == "" && len(m.networkConfList) > 0 {
   381  		n = m.networkConfList[0].Name
   382  	}
   383  
   384  	for i := 0; i < len(m.networkConfList); i++ {
   385  		if m.networkConfList[i].Name == network {
   386  			return m.runtimeConf[i].IfName, nil
   387  		}
   388  	}
   389  
   390  	return "", fmt.Errorf("no interface found for network %s", network)
   391  }
   392  
   393  // SetEnvPath allows to define custom paths for PATH environment
   394  // variables used during CNI plugin execution
   395  func (m *Setup) SetEnvPath(envPath string) {
   396  	m.envPath = envPath
   397  }
   398  
   399  // AddNetworks brings up networks interface in container
   400  func (m *Setup) AddNetworks() error {
   401  	return m.command("ADD")
   402  }
   403  
   404  // DelNetworks tears down networks interface in container
   405  func (m *Setup) DelNetworks() error {
   406  	return m.command("DEL")
   407  }
   408  
   409  func (m *Setup) command(command string) error {
   410  	if m.envPath != "" {
   411  		backupEnv := os.Environ()
   412  		os.Clearenv()
   413  		os.Setenv("PATH", m.envPath)
   414  		defer env.SetFromList(backupEnv)
   415  	}
   416  
   417  	config := &libcni.CNIConfig{Path: []string{m.cniPath.Plugin}}
   418  
   419  	if command == "ADD" {
   420  		m.result = make([]types.Result, len(m.networkConfList))
   421  		for i := 0; i < len(m.networkConfList); i++ {
   422  			var err error
   423  			if m.result[i], err = config.AddNetworkList(m.networkConfList[i], m.runtimeConf[i]); err != nil {
   424  				for j := i - 1; j >= 0; j-- {
   425  					if err := config.DelNetworkList(m.networkConfList[j], m.runtimeConf[j]); err != nil {
   426  						return err
   427  					}
   428  				}
   429  				return err
   430  			}
   431  		}
   432  	} else if command == "DEL" {
   433  		for i := 0; i < len(m.networkConfList); i++ {
   434  			if err := config.DelNetworkList(m.networkConfList[i], m.runtimeConf[i]); err != nil {
   435  				return err
   436  			}
   437  		}
   438  	}
   439  	return nil
   440  }