From fc1848b54b398486426f03ec08a993ce2400f180 Mon Sep 17 00:00:00 2001 From: Dan Cavallaro Date: Fri, 11 Oct 2024 15:23:00 -0400 Subject: [PATCH] "ns:" network mode to use existing network namespace Signed-off-by: Dan Cavallaro --- cmd/nerdctl/container/container_run.go | 4 +- .../container_run_network_linux_test.go | 40 +++++++++++++++++++ docs/command-reference.md | 5 ++- pkg/cmd/container/kill.go | 2 +- pkg/composer/serviceparser/serviceparser.go | 2 +- .../container_network_manager.go | 31 +++++++++++++- pkg/netutil/nettype/nettype.go | 4 ++ pkg/ocihook/ocihook.go | 2 +- 8 files changed, 82 insertions(+), 8 deletions(-) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index f3efa0bf286..df41bbc1273 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -112,11 +112,11 @@ func setCreateFlags(cmd *cobra.Command) { // #region network flags // network (net) is defined as StringSlice, not StringArray, to allow specifying "--network=cni1,cni2" - cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|)`) + cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|"ns:"|)`) cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.NetworkNames(cmd, []string{}) }) - cmd.Flags().StringSlice("net", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|)`) + cmd.Flags().StringSlice("net", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|"ns:"|)`) cmd.RegisterFlagCompletionFunc("net", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.NetworkNames(cmd, []string{}) }) diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index 4c3c7d572a5..15c92e7c03b 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -21,14 +21,19 @@ import ( "io" "net" "os" + "os/exec" "regexp" "runtime" "strings" "testing" + "time" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/vishvananda/netlink" "gotest.tools/v3/assert" "gotest.tools/v3/icmd" + "github.com/containerd/containerd/v2/pkg/netns" "github.com/containerd/errdefs" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" @@ -499,6 +504,41 @@ func TestSharedNetworkStack(t *testing.T) { AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet) } +func TestRunContainerInExistingNetNS(t *testing.T) { + if rootlessutil.IsRootless() { + t.Skip("Can't create new netns in rootless mode") + } + testutil.DockerIncompatible(t) + base := testutil.NewBase(t) + + netNS, err := netns.NewNetNS(t.TempDir() + "/netns") + assert.NilError(t, err) + err = netNS.Do(func(netns ns.NetNS) error { + loopback, err := netlink.LinkByName("lo") + assert.NilError(t, err) + err = netlink.LinkSetUp(loopback) + assert.NilError(t, err) + return nil + }) + assert.NilError(t, err) + defer netNS.Remove() + + containerName := testutil.Identifier(t) + defer base.Cmd("rm", "-f", containerName).AssertOK() + base.Cmd("run", "-d", "--name", containerName, + "--network=ns:"+netNS.GetPath(), testutil.NginxAlpineImage).AssertOK() + base.EnsureContainerStarted(containerName) + time.Sleep(3 * time.Second) + + err = netNS.Do(func(netns ns.NetNS) error { + stdout, err := exec.Command("curl", "-s", "http://127.0.0.1:80").Output() + assert.NilError(t, err) + assert.Assert(t, strings.Contains(string(stdout), testutil.NginxAlpineIndexHTMLSnippet)) + return nil + }) + assert.NilError(t, err) +} + func TestRunContainerWithMACAddress(t *testing.T) { base := testutil.NewBase(t) tID := testutil.Identifier(t) diff --git a/docs/command-reference.md b/docs/command-reference.md index 2fbd0dc6479..070c7db395e 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -175,9 +175,10 @@ Isolation flags: Network flags: -- :whale: `--net, --network=(bridge|host|none|container:|)`: Connect a container to a network. +- :whale: `--net, --network=(bridge|host|none|container:|ns:|)`: Connect a container to a network. - Default: "bridge" - - 'container:': reuse another container's network stack, container has to be precreated. + - `container:`: reuse another container's network stack, container has to be precreated. + - :nerd_face: `ns:`: run inside an existing network namespace - :nerd_face: Unlike Docker, this flag can be specified multiple times (`--net foo --net bar`) - :whale: `-p, --publish`: Publish a container's port(s) to the host - :whale: `--dns`: Set custom DNS servers diff --git a/pkg/cmd/container/kill.go b/pkg/cmd/container/kill.go index 4e53b4f9e3b..b077589473d 100644 --- a/pkg/cmd/container/kill.go +++ b/pkg/cmd/container/kill.go @@ -151,7 +151,7 @@ func cleanupNetwork(ctx context.Context, container containerd.Container, globalO } switch netType { - case nettype.Host, nettype.None, nettype.Container: + case nettype.Host, nettype.None, nettype.Container, nettype.Namespace: // NOP case nettype.CNI: e, err := netutil.NewCNIEnv(globalOpts.CNIPath, globalOpts.CNINetConfPath, netutil.WithNamespace(globalOpts.Namespace), netutil.WithDefaultNetwork()) diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index e59f7cc4368..635b93594b4 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -385,7 +385,7 @@ func getNetworks(project *types.Project, svc types.ServiceConfig) ([]networkName return nil, errors.New("net and network_mode must not be set together") } if strings.Contains(svc.NetworkMode, ":") { - if !strings.HasPrefix(svc.NetworkMode, "container:") { + if !strings.HasPrefix(svc.NetworkMode, "container:") && !strings.HasPrefix(svc.NetworkMode, "ns:") { return nil, fmt.Errorf("unsupported network_mode: %q", svc.NetworkMode) } } diff --git a/pkg/containerutil/container_network_manager.go b/pkg/containerutil/container_network_manager.go index 7f7b97b13e4..5bc34e1e921 100644 --- a/pkg/containerutil/container_network_manager.go +++ b/pkg/containerutil/container_network_manager.go @@ -132,6 +132,10 @@ func NewNetworkingOptionsManager(globalOptions types.GlobalCommandOptions, netOp manager = &containerNetworkManager{globalOptions, netOpts, client} case nettype.CNI: manager = &cniNetworkManager{globalOptions, netOpts, client, cniNetworkManagerPlatform{}} + case nettype.Namespace: + // We'll handle Namespace networking identically to Host-mode networking, but + // put the container in the specified network namespace instead of the root. + manager = &hostNetworkManager{globalOptions, netOpts, client} default: return nil, fmt.Errorf("unexpected container networking type: %q", netType) } @@ -491,6 +495,26 @@ func copyFileContent(src string, dst string) error { return nil } +// getHostNetworkingNamespace Returns an oci.SpecOpts representing the network namespace to +// be used by the hostNetworkManager. When running with `--network=host` this would be the host's +// root namespace, but `--network=ns:` can be used to run a container in an existing netns. +func getHostNetworkingNamespace(netModeArg string) (oci.SpecOpts, error) { + if !strings.Contains(netModeArg, ":") { + // Use the host root namespace by default + return oci.WithHostNamespace(specs.NetworkNamespace), nil + } + + netItems := strings.Split(netModeArg, ":") + if len(netItems) < 2 { + return nil, fmt.Errorf("namespace networking argument format must be 'ns:', got: %q", netModeArg) + } + netnsPath := netItems[1] + return oci.WithLinuxNamespace(specs.LinuxNamespace{ + Type: specs.NetworkNamespace, + Path: netnsPath, + }), nil +} + // ContainerNetworkingOpts Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent // the network specs which need to be applied to the container with the given ID. func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containerID string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { @@ -525,8 +549,13 @@ func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containe return nil, nil, err } + netModeArg := m.netOpts.NetworkSlice[0] + netNamespace, err := getHostNetworkingNamespace(netModeArg) + if err != nil { + return nil, nil, err + } specs := []oci.SpecOpts{ - oci.WithHostNamespace(specs.NetworkNamespace), + netNamespace, withDedupMounts("/etc/hosts", withCustomHosts(etcHostsPath)), withDedupMounts("/etc/resolv.conf", withCustomResolvConf(resolvConfPath)), } diff --git a/pkg/netutil/nettype/nettype.go b/pkg/netutil/nettype/nettype.go index 721167037c7..d319254260d 100644 --- a/pkg/netutil/nettype/nettype.go +++ b/pkg/netutil/nettype/nettype.go @@ -29,6 +29,7 @@ const ( Host CNI Container + Namespace ) var netTypeToName = map[interface{}]string{ @@ -37,6 +38,7 @@ var netTypeToName = map[interface{}]string{ Host: "host", CNI: "cni", Container: "container", + Namespace: "ns", } func Detect(names []string) (Type, error) { @@ -54,6 +56,8 @@ func Detect(names []string) (Type, error) { tmp = Host case "container": tmp = Container + case "ns": + tmp = Namespace default: tmp = CNI } diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index a98c84d1d37..b1b1068c1bb 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -170,7 +170,7 @@ func newHandlerOpts(state *specs.State, dataStore, cniPath, cniNetconfPath strin } switch netType { - case nettype.Host, nettype.None, nettype.Container: + case nettype.Host, nettype.None, nettype.Container, nettype.Namespace: // NOP case nettype.CNI: e, err := netutil.NewCNIEnv(cniPath, cniNetconfPath, netutil.WithNamespace(namespace), netutil.WithDefaultNetwork())