Skip to content

Commit

Permalink
"ns:" network mode to use existing network namespace
Browse files Browse the repository at this point in the history
Signed-off-by: Dan Cavallaro <dan.t.cavallaro@gmail.com>
  • Loading branch information
dancavallaro committed Oct 15, 2024
1 parent 427f1cb commit 92a5288
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 8 deletions.
4 changes: 2 additions & 2 deletions cmd/nerdctl/container/container_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:<container>"|<CNI>)`)
cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:<container>"|"ns:<path>"|<CNI>)`)
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"|<CNI>)`)
cmd.Flags().StringSlice("net", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:<container>"|"ns:<path>"|<CNI>)`)
cmd.RegisterFlagCompletionFunc("net", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completion.NetworkNames(cmd, []string{})
})
Expand Down
52 changes: 52 additions & 0 deletions cmd/nerdctl/container/container_run_network_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ import (
"io"
"net"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
"testing"

"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"
Expand Down Expand Up @@ -499,6 +503,54 @@ func TestSharedNetworkStack(t *testing.T) {
AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet)
}

func TestRunContainerInExistingNetNS(t *testing.T) {
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)

containerNameJoin := testutil.Identifier(t) + "-network"
defer base.Cmd("rm", "-f", containerNameJoin).AssertOK()
base.Cmd("run",
"-d",
"--name", containerNameJoin,
"--network=ns:"+netNS.GetPath(),
testutil.CommonImage,
"sleep", "infinity").AssertOK()

base.Cmd("exec", containerNameJoin, "wget", "-qO-", "http://127.0.0.1:80").
AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet)

base.Cmd("restart", containerName).AssertOK()
base.Cmd("stop", "--time=1", containerNameJoin).AssertOK()
base.Cmd("start", containerNameJoin).AssertOK()
base.Cmd("exec", containerNameJoin, "wget", "-qO-", "http://127.0.0.1:80").
AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet)

err = netNS.Do(func(netns ns.NetNS) error {
stdout, err := exec.Command("wget", "-qO-", "http://127.0.0.1:80").Output()
assert.Assert(t, strings.Contains(string(stdout), testutil.NginxAlpineIndexHTMLSnippet))
return err
})
assert.NilError(t, err)
}

func TestRunContainerWithMACAddress(t *testing.T) {
base := testutil.NewBase(t)
tID := testutil.Identifier(t)
Expand Down
5 changes: 3 additions & 2 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,10 @@ Isolation flags:

Network flags:

- :whale: `--net, --network=(bridge|host|none|container:<container>|<CNI>)`: Connect a container to a network.
- :whale: `--net, --network=(bridge|host|none|container:<container>|ns:<path>|<CNI>)`: Connect a container to a network.
- Default: "bridge"
- 'container:<name|id>': reuse another container's network stack, container has to be precreated.
- `container:<name|id>`: reuse another container's network stack, container has to be precreated.
- :nerd_face: `ns:<path>`: 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
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/container/kill.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion pkg/composer/serviceparser/serviceparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
31 changes: 30 additions & 1 deletion pkg/containerutil/container_network_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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:<path>` 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:<path>', 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) {
Expand Down Expand Up @@ -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)),
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/netutil/nettype/nettype.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
Host
CNI
Container
Namespace
)

var netTypeToName = map[interface{}]string{
Expand All @@ -37,6 +38,7 @@ var netTypeToName = map[interface{}]string{
Host: "host",
CNI: "cni",
Container: "container",
Namespace: "ns",
}

func Detect(names []string) (Type, error) {
Expand All @@ -54,6 +56,8 @@ func Detect(names []string) (Type, error) {
tmp = Host
case "container":
tmp = Container
case "ns":
tmp = Namespace
default:
tmp = CNI
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/ocihook/ocihook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down

0 comments on commit 92a5288

Please sign in to comment.