Skip to content

Commit fc1848b

Browse files
committed
"ns:" network mode to use existing network namespace
Signed-off-by: Dan Cavallaro <[email protected]>
1 parent 427f1cb commit fc1848b

File tree

8 files changed

+82
-8
lines changed

8 files changed

+82
-8
lines changed

cmd/nerdctl/container/container_run.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,11 @@ func setCreateFlags(cmd *cobra.Command) {
112112

113113
// #region network flags
114114
// network (net) is defined as StringSlice, not StringArray, to allow specifying "--network=cni1,cni2"
115-
cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:<container>"|<CNI>)`)
115+
cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:<container>"|"ns:<path>"|<CNI>)`)
116116
cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
117117
return completion.NetworkNames(cmd, []string{})
118118
})
119-
cmd.Flags().StringSlice("net", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|<CNI>)`)
119+
cmd.Flags().StringSlice("net", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:<container>"|"ns:<path>"|<CNI>)`)
120120
cmd.RegisterFlagCompletionFunc("net", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
121121
return completion.NetworkNames(cmd, []string{})
122122
})

cmd/nerdctl/container/container_run_network_linux_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@ import (
2121
"io"
2222
"net"
2323
"os"
24+
"os/exec"
2425
"regexp"
2526
"runtime"
2627
"strings"
2728
"testing"
29+
"time"
2830

31+
"github.com/containernetworking/plugins/pkg/ns"
32+
"github.com/vishvananda/netlink"
2933
"gotest.tools/v3/assert"
3034
"gotest.tools/v3/icmd"
3135

36+
"github.com/containerd/containerd/v2/pkg/netns"
3237
"github.com/containerd/errdefs"
3338

3439
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
@@ -499,6 +504,41 @@ func TestSharedNetworkStack(t *testing.T) {
499504
AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet)
500505
}
501506

507+
func TestRunContainerInExistingNetNS(t *testing.T) {
508+
if rootlessutil.IsRootless() {
509+
t.Skip("Can't create new netns in rootless mode")
510+
}
511+
testutil.DockerIncompatible(t)
512+
base := testutil.NewBase(t)
513+
514+
netNS, err := netns.NewNetNS(t.TempDir() + "/netns")
515+
assert.NilError(t, err)
516+
err = netNS.Do(func(netns ns.NetNS) error {
517+
loopback, err := netlink.LinkByName("lo")
518+
assert.NilError(t, err)
519+
err = netlink.LinkSetUp(loopback)
520+
assert.NilError(t, err)
521+
return nil
522+
})
523+
assert.NilError(t, err)
524+
defer netNS.Remove()
525+
526+
containerName := testutil.Identifier(t)
527+
defer base.Cmd("rm", "-f", containerName).AssertOK()
528+
base.Cmd("run", "-d", "--name", containerName,
529+
"--network=ns:"+netNS.GetPath(), testutil.NginxAlpineImage).AssertOK()
530+
base.EnsureContainerStarted(containerName)
531+
time.Sleep(3 * time.Second)
532+
533+
err = netNS.Do(func(netns ns.NetNS) error {
534+
stdout, err := exec.Command("curl", "-s", "http://127.0.0.1:80").Output()
535+
assert.NilError(t, err)
536+
assert.Assert(t, strings.Contains(string(stdout), testutil.NginxAlpineIndexHTMLSnippet))
537+
return nil
538+
})
539+
assert.NilError(t, err)
540+
}
541+
502542
func TestRunContainerWithMACAddress(t *testing.T) {
503543
base := testutil.NewBase(t)
504544
tID := testutil.Identifier(t)

docs/command-reference.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,10 @@ Isolation flags:
175175

176176
Network flags:
177177

178-
- :whale: `--net, --network=(bridge|host|none|container:<container>|<CNI>)`: Connect a container to a network.
178+
- :whale: `--net, --network=(bridge|host|none|container:<container>|ns:<path>|<CNI>)`: Connect a container to a network.
179179
- Default: "bridge"
180-
- 'container:<name|id>': reuse another container's network stack, container has to be precreated.
180+
- `container:<name|id>`: reuse another container's network stack, container has to be precreated.
181+
- :nerd_face: `ns:<path>`: run inside an existing network namespace
181182
- :nerd_face: Unlike Docker, this flag can be specified multiple times (`--net foo --net bar`)
182183
- :whale: `-p, --publish`: Publish a container's port(s) to the host
183184
- :whale: `--dns`: Set custom DNS servers

pkg/cmd/container/kill.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func cleanupNetwork(ctx context.Context, container containerd.Container, globalO
151151
}
152152

153153
switch netType {
154-
case nettype.Host, nettype.None, nettype.Container:
154+
case nettype.Host, nettype.None, nettype.Container, nettype.Namespace:
155155
// NOP
156156
case nettype.CNI:
157157
e, err := netutil.NewCNIEnv(globalOpts.CNIPath, globalOpts.CNINetConfPath, netutil.WithNamespace(globalOpts.Namespace), netutil.WithDefaultNetwork())

pkg/composer/serviceparser/serviceparser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ func getNetworks(project *types.Project, svc types.ServiceConfig) ([]networkName
385385
return nil, errors.New("net and network_mode must not be set together")
386386
}
387387
if strings.Contains(svc.NetworkMode, ":") {
388-
if !strings.HasPrefix(svc.NetworkMode, "container:") {
388+
if !strings.HasPrefix(svc.NetworkMode, "container:") && !strings.HasPrefix(svc.NetworkMode, "ns:") {
389389
return nil, fmt.Errorf("unsupported network_mode: %q", svc.NetworkMode)
390390
}
391391
}

pkg/containerutil/container_network_manager.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ func NewNetworkingOptionsManager(globalOptions types.GlobalCommandOptions, netOp
132132
manager = &containerNetworkManager{globalOptions, netOpts, client}
133133
case nettype.CNI:
134134
manager = &cniNetworkManager{globalOptions, netOpts, client, cniNetworkManagerPlatform{}}
135+
case nettype.Namespace:
136+
// We'll handle Namespace networking identically to Host-mode networking, but
137+
// put the container in the specified network namespace instead of the root.
138+
manager = &hostNetworkManager{globalOptions, netOpts, client}
135139
default:
136140
return nil, fmt.Errorf("unexpected container networking type: %q", netType)
137141
}
@@ -491,6 +495,26 @@ func copyFileContent(src string, dst string) error {
491495
return nil
492496
}
493497

498+
// getHostNetworkingNamespace Returns an oci.SpecOpts representing the network namespace to
499+
// be used by the hostNetworkManager. When running with `--network=host` this would be the host's
500+
// root namespace, but `--network=ns:<path>` can be used to run a container in an existing netns.
501+
func getHostNetworkingNamespace(netModeArg string) (oci.SpecOpts, error) {
502+
if !strings.Contains(netModeArg, ":") {
503+
// Use the host root namespace by default
504+
return oci.WithHostNamespace(specs.NetworkNamespace), nil
505+
}
506+
507+
netItems := strings.Split(netModeArg, ":")
508+
if len(netItems) < 2 {
509+
return nil, fmt.Errorf("namespace networking argument format must be 'ns:<path>', got: %q", netModeArg)
510+
}
511+
netnsPath := netItems[1]
512+
return oci.WithLinuxNamespace(specs.LinuxNamespace{
513+
Type: specs.NetworkNamespace,
514+
Path: netnsPath,
515+
}), nil
516+
}
517+
494518
// ContainerNetworkingOpts Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent
495519
// the network specs which need to be applied to the container with the given ID.
496520
func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containerID string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) {
@@ -525,8 +549,13 @@ func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containe
525549
return nil, nil, err
526550
}
527551

552+
netModeArg := m.netOpts.NetworkSlice[0]
553+
netNamespace, err := getHostNetworkingNamespace(netModeArg)
554+
if err != nil {
555+
return nil, nil, err
556+
}
528557
specs := []oci.SpecOpts{
529-
oci.WithHostNamespace(specs.NetworkNamespace),
558+
netNamespace,
530559
withDedupMounts("/etc/hosts", withCustomHosts(etcHostsPath)),
531560
withDedupMounts("/etc/resolv.conf", withCustomResolvConf(resolvConfPath)),
532561
}

pkg/netutil/nettype/nettype.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
Host
3030
CNI
3131
Container
32+
Namespace
3233
)
3334

3435
var netTypeToName = map[interface{}]string{
@@ -37,6 +38,7 @@ var netTypeToName = map[interface{}]string{
3738
Host: "host",
3839
CNI: "cni",
3940
Container: "container",
41+
Namespace: "ns",
4042
}
4143

4244
func Detect(names []string) (Type, error) {
@@ -54,6 +56,8 @@ func Detect(names []string) (Type, error) {
5456
tmp = Host
5557
case "container":
5658
tmp = Container
59+
case "ns":
60+
tmp = Namespace
5761
default:
5862
tmp = CNI
5963
}

pkg/ocihook/ocihook.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ func newHandlerOpts(state *specs.State, dataStore, cniPath, cniNetconfPath strin
170170
}
171171

172172
switch netType {
173-
case nettype.Host, nettype.None, nettype.Container:
173+
case nettype.Host, nettype.None, nettype.Container, nettype.Namespace:
174174
// NOP
175175
case nettype.CNI:
176176
e, err := netutil.NewCNIEnv(cniPath, cniNetconfPath, netutil.WithNamespace(namespace), netutil.WithDefaultNetwork())

0 commit comments

Comments
 (0)