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 }