github.com/containerd/nerdctl@v1.7.7/pkg/containerutil/container_network_manager.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package containerutil 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "os" 25 "path/filepath" 26 "reflect" 27 "runtime" 28 "strings" 29 30 "github.com/containerd/containerd" 31 "github.com/containerd/containerd/containers" 32 "github.com/containerd/containerd/oci" 33 "github.com/containerd/nerdctl/pkg/api/types" 34 "github.com/containerd/nerdctl/pkg/clientutil" 35 "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore" 36 "github.com/containerd/nerdctl/pkg/idutil/containerwalker" 37 "github.com/containerd/nerdctl/pkg/labels" 38 "github.com/containerd/nerdctl/pkg/mountutil" 39 "github.com/containerd/nerdctl/pkg/netutil" 40 "github.com/containerd/nerdctl/pkg/netutil/nettype" 41 "github.com/containerd/nerdctl/pkg/strutil" 42 "github.com/opencontainers/runtime-spec/specs-go" 43 ) 44 45 const ( 46 UtsNamespaceHost = "host" 47 ) 48 49 func withCustomResolvConf(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { 50 return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { 51 s.Mounts = append(s.Mounts, specs.Mount{ 52 Destination: "/etc/resolv.conf", 53 Type: "bind", 54 Source: src, 55 Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable 56 }) 57 return nil 58 } 59 } 60 61 func withCustomEtcHostname(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { 62 return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { 63 s.Mounts = append(s.Mounts, specs.Mount{ 64 Destination: "/etc/hostname", 65 Type: "bind", 66 Source: src, 67 Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable 68 }) 69 return nil 70 } 71 } 72 73 func withCustomHosts(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { 74 return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { 75 s.Mounts = append(s.Mounts, specs.Mount{ 76 Destination: "/etc/hosts", 77 Type: "bind", 78 Source: src, 79 Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable 80 }) 81 return nil 82 } 83 } 84 85 // NetworkOptionsManager types.NetworkOptionsManager is an interface for reading/setting networking 86 // options for containers based on the provided command flags. 87 type NetworkOptionsManager interface { 88 // NetworkOptions Returns a copy of the internal types.NetworkOptions. 89 NetworkOptions() types.NetworkOptions 90 91 // VerifyNetworkOptions Verifies that the internal network settings are correct. 92 VerifyNetworkOptions(context.Context) error 93 94 // SetupNetworking Performs setup actions required for the container with the given ID. 95 SetupNetworking(context.Context, string) error 96 97 // CleanupNetworking Performs any required cleanup actions for the given container. 98 // Should only be called to revert any setup steps performed in SetupNetworking. 99 CleanupNetworking(context.Context, containerd.Container) error 100 101 // InternalNetworkingOptionLabels Returns the set of NetworkingOptions which should be set as labels on the container. 102 // 103 // These options can potentially differ from the actual networking options 104 // that the NetworkOptionsManager was initially instantiated with. 105 // E.g: in container networking mode, the label will be normalized to an ID: 106 // `--net=container:myContainer` => `--net=container:<ID of myContainer>`. 107 InternalNetworkingOptionLabels(context.Context) (types.NetworkOptions, error) 108 109 // ContainerNetworkingOpts Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent 110 // the network specs which need to be applied to the container with the given ID. 111 ContainerNetworkingOpts(context.Context, string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) 112 } 113 114 // NewNetworkingOptionsManager Returns a types.NetworkOptionsManager based on the provided command's flags. 115 func NewNetworkingOptionsManager(globalOptions types.GlobalCommandOptions, netOpts types.NetworkOptions, client *containerd.Client) (NetworkOptionsManager, error) { 116 netType, err := nettype.Detect(netOpts.NetworkSlice) 117 if err != nil { 118 return nil, err 119 } 120 121 var manager NetworkOptionsManager 122 switch netType { 123 case nettype.None: 124 manager = &noneNetworkManager{globalOptions, netOpts, client} 125 case nettype.Host: 126 manager = &hostNetworkManager{globalOptions, netOpts, client} 127 case nettype.Container: 128 manager = &containerNetworkManager{globalOptions, netOpts, client} 129 case nettype.CNI: 130 manager = &cniNetworkManager{globalOptions, netOpts, client, cniNetworkManagerPlatform{}} 131 default: 132 return nil, fmt.Errorf("unexpected container networking type: %q", netType) 133 } 134 135 return manager, nil 136 } 137 138 // No-op types.NetworkOptionsManager for network-less containers. 139 type noneNetworkManager struct { 140 globalOptions types.GlobalCommandOptions 141 netOpts types.NetworkOptions 142 client *containerd.Client 143 } 144 145 // NetworkOptions Returns a copy of the internal types.NetworkOptions. 146 func (m *noneNetworkManager) NetworkOptions() types.NetworkOptions { 147 return m.netOpts 148 } 149 150 // VerifyNetworkOptions Verifies that the internal network settings are correct. 151 func (m *noneNetworkManager) VerifyNetworkOptions(_ context.Context) error { 152 // No options to verify if no network settings are provided. 153 return nil 154 } 155 156 // SetupNetworking Performs setup actions required for the container with the given ID. 157 func (m *noneNetworkManager) SetupNetworking(_ context.Context, _ string) error { 158 return nil 159 } 160 161 // CleanupNetworking Performs any required cleanup actions for the given container. 162 // Should only be called to revert any setup steps performed in SetupNetworking. 163 func (m *noneNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error { 164 return nil 165 } 166 167 // InternalNetworkingOptionLabels Returns the set of NetworkingOptions which should be set as labels on the container. 168 func (m *noneNetworkManager) InternalNetworkingOptionLabels(_ context.Context) (types.NetworkOptions, error) { 169 return m.netOpts, nil 170 } 171 172 // ContainerNetworkingOpts Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent 173 // the network specs which need to be applied to the container with the given ID. 174 func (m *noneNetworkManager) ContainerNetworkingOpts(_ context.Context, _ string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { 175 // No options to return if no network settings are provided. 176 return []oci.SpecOpts{}, []containerd.NewContainerOpts{}, nil 177 } 178 179 // types.NetworkOptionsManager implementation for container networking settings. 180 type containerNetworkManager struct { 181 globalOptions types.GlobalCommandOptions 182 netOpts types.NetworkOptions 183 client *containerd.Client 184 } 185 186 // NetworkOptions Returns a copy of the internal types.NetworkOptions. 187 func (m *containerNetworkManager) NetworkOptions() types.NetworkOptions { 188 return m.netOpts 189 } 190 191 // VerifyNetworkOptions Verifies that the internal network settings are correct. 192 func (m *containerNetworkManager) VerifyNetworkOptions(_ context.Context) error { 193 // TODO: check host OS, not client-side OS. 194 if runtime.GOOS != "linux" { 195 return errors.New("container networking mode is currently only supported on Linux") 196 } 197 198 if len(m.netOpts.NetworkSlice) > 1 { 199 return errors.New("conflicting options: only one network specification is allowed when using '--network=container:<container>'") 200 } 201 202 nonZeroParams := nonZeroMapValues(map[string]interface{}{ 203 "--hostname": m.netOpts.Hostname, 204 "--mac-address": m.netOpts.MACAddress, 205 // NOTE: an empty slice still counts as a non-zero value so we check its length: 206 "-p/--publish": len(m.netOpts.PortMappings) != 0, 207 "--dns": len(m.netOpts.DNSServers) != 0, 208 "--add-host": len(m.netOpts.AddHost) != 0, 209 }) 210 211 if len(nonZeroParams) != 0 { 212 return fmt.Errorf("conflicting options: the following arguments are not supported when using `--network=container:<container>`: %s", nonZeroParams) 213 } 214 215 return nil 216 } 217 218 // Returns the relevant paths of the `hostname`, `resolv.conf`, and `hosts` files 219 // in the datastore of the container with the given ID. 220 func (m *containerNetworkManager) getContainerNetworkFilePaths(containerID string) (string, string, string, error) { 221 dataStore, err := clientutil.DataStore(m.globalOptions.DataRoot, m.globalOptions.Address) 222 if err != nil { 223 return "", "", "", err 224 } 225 conStateDir, err := ContainerStateDirPath(m.globalOptions.Namespace, dataStore, containerID) 226 if err != nil { 227 return "", "", "", err 228 } 229 230 hostnamePath := filepath.Join(conStateDir, "hostname") 231 resolvConfPath := filepath.Join(conStateDir, "resolv.conf") 232 etcHostsPath := hostsstore.HostsPath(dataStore, m.globalOptions.Namespace, containerID) 233 234 return hostnamePath, resolvConfPath, etcHostsPath, nil 235 } 236 237 // SetupNetworking Performs setup actions required for the container with the given ID. 238 func (m *containerNetworkManager) SetupNetworking(_ context.Context, _ string) error { 239 // NOTE: container networking simply reuses network config files from the 240 // bridged container so there are no setup/teardown steps required. 241 return nil 242 } 243 244 // CleanupNetworking Performs any required cleanup actions for the given container. 245 // Should only be called to revert any setup steps performed in SetupNetworking. 246 func (m *containerNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error { 247 // NOTE: container networking simply reuses network config files from the 248 // bridged container so there are no setup/teardown steps required. 249 return nil 250 } 251 252 // Searches for and returns the networking container for the given network argument. 253 func (m *containerNetworkManager) getNetworkingContainerForArgument(ctx context.Context, containerNetArg string, client *containerd.Client) (containerd.Container, error) { 254 netItems := strings.Split(containerNetArg, ":") 255 if len(netItems) < 2 { 256 return nil, fmt.Errorf("container networking argument format must be 'container:<id|name>', got: %q", containerNetArg) 257 } 258 containerName := netItems[1] 259 260 var foundContainer containerd.Container 261 walker := &containerwalker.ContainerWalker{ 262 Client: client, 263 OnFound: func(ctx context.Context, found containerwalker.Found) error { 264 if found.MatchCount > 1 { 265 return fmt.Errorf("container networking: multiple containers found with prefix: %s", containerName) 266 } 267 foundContainer = found.Container 268 return nil 269 }, 270 } 271 n, err := walker.Walk(ctx, containerName) 272 if err != nil { 273 return nil, err 274 } 275 if n == 0 { 276 return nil, fmt.Errorf("container networking: could not find container: %s", containerName) 277 } 278 279 return foundContainer, nil 280 } 281 282 // InternalNetworkingOptionLabels Returns the set of NetworkingOptions which should be set as labels on the container. 283 func (m *containerNetworkManager) InternalNetworkingOptionLabels(ctx context.Context) (types.NetworkOptions, error) { 284 opts := m.netOpts 285 if m.netOpts.NetworkSlice == nil || len(m.netOpts.NetworkSlice) != 1 { 286 return opts, fmt.Errorf("conflicting options: exactly one network specification is allowed when using '--network=container:<container>'") 287 } 288 289 container, err := m.getNetworkingContainerForArgument(ctx, m.netOpts.NetworkSlice[0], m.client) 290 if err != nil { 291 return opts, err 292 } 293 containerID := container.ID() 294 opts.NetworkSlice = []string{fmt.Sprintf("container:%s", containerID)} 295 return opts, nil 296 } 297 298 // ContainerNetworkingOpts Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent 299 // the network specs which need to be applied to the container with the given ID. 300 func (m *containerNetworkManager) ContainerNetworkingOpts(ctx context.Context, _ string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { 301 opts := []oci.SpecOpts{} 302 cOpts := []containerd.NewContainerOpts{} 303 304 container, err := m.getNetworkingContainerForArgument(ctx, m.netOpts.NetworkSlice[0], m.client) 305 if err != nil { 306 return nil, nil, err 307 } 308 containerID := container.ID() 309 310 s, err := container.Spec(ctx) 311 if err != nil { 312 return nil, nil, err 313 } 314 hostname := s.Hostname 315 316 netNSPath, err := ContainerNetNSPath(ctx, container) 317 if err != nil { 318 return nil, nil, err 319 } 320 321 hostnamePath, resolvConfPath, etcHostsPath, err := m.getContainerNetworkFilePaths(containerID) 322 if err != nil { 323 return nil, nil, err 324 } 325 326 opts = append(opts, 327 oci.WithLinuxNamespace(specs.LinuxNamespace{ 328 Type: specs.NetworkNamespace, 329 Path: netNSPath, 330 }), 331 withCustomResolvConf(resolvConfPath), 332 withCustomHosts(etcHostsPath), 333 oci.WithHostname(hostname), 334 withCustomEtcHostname(hostnamePath), 335 ) 336 337 return opts, cOpts, nil 338 } 339 340 // types.NetworkOptionsManager implementation for host networking settings. 341 type hostNetworkManager struct { 342 globalOptions types.GlobalCommandOptions 343 netOpts types.NetworkOptions 344 client *containerd.Client 345 } 346 347 // NetworkOptions Returns a copy of the internal types.NetworkOptions. 348 func (m *hostNetworkManager) NetworkOptions() types.NetworkOptions { 349 return m.netOpts 350 } 351 352 // VerifyNetworkOptions Verifies that the internal network settings are correct. 353 func (m *hostNetworkManager) VerifyNetworkOptions(_ context.Context) error { 354 // TODO: check host OS, not client-side OS. 355 if runtime.GOOS == "windows" { 356 return errors.New("cannot use host networking on Windows") 357 } 358 359 if m.netOpts.MACAddress != "" { 360 return errors.New("conflicting options: mac-address and the network mode") 361 } 362 363 return validateUtsSettings(m.netOpts) 364 } 365 366 // SetupNetworking Performs setup actions required for the container with the given ID. 367 func (m *hostNetworkManager) SetupNetworking(_ context.Context, _ string) error { 368 // NOTE: there are no setup steps required for host networking. 369 return nil 370 } 371 372 // CleanupNetworking Performs any required cleanup actions for the given container. 373 // Should only be called to revert any setup steps performed in SetupNetworking. 374 func (m *hostNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error { 375 // NOTE: there are no setup steps required for host networking. 376 return nil 377 } 378 379 // InternalNetworkingOptionLabels Returns the set of NetworkingOptions which should be set as labels on the container. 380 func (m *hostNetworkManager) InternalNetworkingOptionLabels(_ context.Context) (types.NetworkOptions, error) { 381 opts := m.netOpts 382 // Cannot have a MAC address in host networking mode. 383 opts.MACAddress = "" 384 return opts, nil 385 } 386 387 // withDedupMounts Returns the specOpts if the mountPath is not in existing mounts. 388 // for https://github.com/containerd/nerdctl/issues/2685 389 func withDedupMounts(mountPath string, defaultSpec oci.SpecOpts) oci.SpecOpts { 390 return func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error { 391 for _, m := range s.Mounts { 392 if m.Destination == mountPath { 393 return nil 394 } 395 } 396 return defaultSpec(ctx, client, c, s) 397 } 398 } 399 400 func copyFileContent(src string, dst string) error { 401 data, err := os.ReadFile(src) 402 if err != nil { 403 return err 404 } 405 err = os.WriteFile(dst, data, 0644) 406 if err != nil { 407 return err 408 } 409 return nil 410 } 411 412 // ContainerNetworkingOpts Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent 413 // the network specs which need to be applied to the container with the given ID. 414 func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containerID string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { 415 416 cOpts := []containerd.NewContainerOpts{} 417 418 dataStore, err := clientutil.DataStore(m.globalOptions.DataRoot, m.globalOptions.Address) 419 if err != nil { 420 return nil, nil, err 421 } 422 423 stateDir, err := ContainerStateDirPath(m.globalOptions.Namespace, dataStore, containerID) 424 if err != nil { 425 return nil, nil, err 426 } 427 428 resolvConfPath := filepath.Join(stateDir, "resolv.conf") 429 copyFileContent("/etc/resolv.conf", resolvConfPath) 430 431 etcHostsPath, err := hostsstore.AllocHostsFile(dataStore, m.globalOptions.Namespace, containerID) 432 if err != nil { 433 return nil, nil, err 434 } 435 copyFileContent("/etc/hosts", etcHostsPath) 436 437 specs := []oci.SpecOpts{ 438 oci.WithHostNamespace(specs.NetworkNamespace), 439 withDedupMounts("/etc/hosts", withCustomHosts(etcHostsPath)), 440 withDedupMounts("/etc/resolv.conf", withCustomResolvConf(resolvConfPath)), 441 } 442 443 // `/etc/hostname` does not exist on FreeBSD 444 if runtime.GOOS == "linux" && m.netOpts.UTSNamespace != UtsNamespaceHost { 445 // If no hostname is set, default to first 12 characters of the container ID. 446 hostname := m.netOpts.Hostname 447 if hostname == "" { 448 hostname = containerID 449 if len(hostname) > 12 { 450 hostname = hostname[0:12] 451 } 452 } 453 m.netOpts.Hostname = hostname 454 455 hostnameOpts, err := writeEtcHostnameForContainer(m.globalOptions, m.netOpts.Hostname, containerID) 456 if err != nil { 457 return nil, nil, err 458 } 459 if hostnameOpts != nil { 460 specs = append(specs, hostnameOpts...) 461 } 462 } 463 464 return specs, cOpts, nil 465 } 466 467 // types.NetworkOptionsManager implementation for CNI networking settings. 468 // This is a more specialized and OS-dependendant networking model so this 469 // struct provides different implementations on different platforms. 470 type cniNetworkManager struct { 471 globalOptions types.GlobalCommandOptions 472 netOpts types.NetworkOptions 473 client *containerd.Client 474 cniNetworkManagerPlatform 475 } 476 477 // NetworkOptions Returns a copy of the internal types.NetworkOptions. 478 func (m *cniNetworkManager) NetworkOptions() types.NetworkOptions { 479 return m.netOpts 480 } 481 482 func validateUtsSettings(netOpts types.NetworkOptions) error { 483 utsNamespace := netOpts.UTSNamespace 484 if utsNamespace == "" { 485 return nil 486 } 487 488 // Docker considers this a validation error so keep compat. 489 // https://docs.docker.com/engine/reference/run/#uts-settings---uts 490 if utsNamespace == UtsNamespaceHost && netOpts.Hostname != "" { 491 return fmt.Errorf("conflicting options: cannot set a --hostname with --uts=host") 492 } 493 494 return nil 495 } 496 497 // Writes the provided hostname string in a "hostname" file in the Container's 498 // Nerdctl-managed datastore and returns the oci.SpecOpts required in the container 499 // spec for the file to be mounted under /etc/hostname in the new container. 500 // If the hostname is empty, the leading 12 characters of the containerID 501 func writeEtcHostnameForContainer(globalOptions types.GlobalCommandOptions, hostname string, containerID string) ([]oci.SpecOpts, error) { 502 if containerID == "" { 503 return nil, fmt.Errorf("container ID is required for setting up hostname file") 504 } 505 506 dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address) 507 if err != nil { 508 return nil, err 509 } 510 511 stateDir, err := ContainerStateDirPath(globalOptions.Namespace, dataStore, containerID) 512 if err != nil { 513 return nil, err 514 } 515 516 hostnamePath := filepath.Join(stateDir, "hostname") 517 if err := os.WriteFile(hostnamePath, []byte(hostname+"\n"), 0644); err != nil { 518 return nil, err 519 } 520 521 return []oci.SpecOpts{oci.WithHostname(hostname), withCustomEtcHostname(hostnamePath)}, nil 522 } 523 524 // Loads all available networks and verifies that every selected network 525 // from the networkSlice is of a type within supportedTypes. 526 func verifyNetworkTypes(env *netutil.CNIEnv, networkSlice []string, supportedTypes []string) (map[string]*netutil.NetworkConfig, error) { 527 netMap, err := env.NetworkMap() 528 if err != nil { 529 return nil, err 530 } 531 532 res := make(map[string]*netutil.NetworkConfig, len(networkSlice)) 533 for _, netstr := range networkSlice { 534 netConfig, ok := netMap[netstr] 535 if !ok { 536 return nil, fmt.Errorf("network %s not found", netstr) 537 } 538 netType := netConfig.Plugins[0].Network.Type 539 if supportedTypes != nil && !strutil.InStringSlice(supportedTypes, netType) { 540 return nil, fmt.Errorf("network type %q is not supported for network mapping %q, must be one of: %v", netType, netstr, supportedTypes) 541 } 542 543 res[netstr] = netConfig 544 } 545 546 return res, nil 547 } 548 549 // NetworkOptionsFromSpec Returns the NetworkOptions used in a container's creation from its spec.Annotations. 550 func NetworkOptionsFromSpec(spec *specs.Spec) (types.NetworkOptions, error) { 551 opts := types.NetworkOptions{} 552 553 if spec == nil { 554 return opts, fmt.Errorf("cannot determine networking options from nil spec") 555 } 556 if spec.Annotations == nil { 557 return opts, fmt.Errorf("cannot determine networking options from nil spec.Annotations") 558 } 559 560 opts.Hostname = spec.Hostname 561 562 if macAddress, ok := spec.Annotations[labels.MACAddress]; ok { 563 opts.MACAddress = macAddress 564 } 565 566 if ipAddress, ok := spec.Annotations[labels.IPAddress]; ok { 567 opts.IPAddress = ipAddress 568 } 569 570 var networks []string 571 networksJSON := spec.Annotations[labels.Networks] 572 if err := json.Unmarshal([]byte(networksJSON), &networks); err != nil { 573 return opts, err 574 } 575 opts.NetworkSlice = networks 576 577 if portsJSON := spec.Annotations[labels.Ports]; portsJSON != "" { 578 if err := json.Unmarshal([]byte(portsJSON), &opts.PortMappings); err != nil { 579 return opts, err 580 } 581 } 582 583 return opts, nil 584 } 585 586 // Returns a lslice of keys of the values in the map that are invalid or are a non-zero-value 587 // for their respective type. (e.g. anything other than a `""` for string type) 588 // Note that the zero-values for innately pointer-types slices/maps/chans are `nil`, 589 // and NOT a zero-length container value like `[]Any{}`. 590 func nonZeroMapValues(values map[string]interface{}) []string { 591 nonZero := []string{} 592 593 for k, v := range values { 594 if !reflect.ValueOf(v).IsZero() { 595 nonZero = append(nonZero, k) 596 } 597 } 598 599 return nonZero 600 }