github.com/sylabs/singularity/v4@v4.1.3/pkg/network/network_linux.go (about)

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