diff --git a/cmd/nerdctl/container/container_run_runtime_linux_test.go b/cmd/nerdctl/container/container_run_runtime_linux_test.go index ea7473f2d20..2d42734ec8a 100644 --- a/cmd/nerdctl/container/container_run_runtime_linux_test.go +++ b/cmd/nerdctl/container/container_run_runtime_linux_test.go @@ -27,3 +27,31 @@ func TestRunSysctl(t *testing.T) { base := testutil.NewBase(t) base.Cmd("run", "--rm", "--sysctl", "net.ipv4.ip_forward=1", testutil.AlpineImage, "cat", "/proc/sys/net/ipv4/ip_forward").AssertOutExactly("1\n") } + +func TestRunSysctl_DefaultUnprivilegedPortStart(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + + // No --sysctl flags, default network mode (non-host). + // We expect net.ipv4.ip_unprivileged_port_start=0 inside the container, + // because withDefaultUnprivilegedPortSysctl should apply the default. + base.Cmd( + "run", "--rm", + testutil.AlpineImage, + "cat", "/proc/sys/net/ipv4/ip_unprivileged_port_start", + ).AssertOutExactly("0\n") +} + +func TestRunSysctl_UnprivilegedPortStartOverride(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + + // User explicitly sets net.ipv4.ip_unprivileged_port_start=1000. + // We must NOT override this; the container should see "1000". + base.Cmd( + "run", "--rm", + "--sysctl", "net.ipv4.ip_unprivileged_port_start=1000", + testutil.AlpineImage, + "cat", "/proc/sys/net/ipv4/ip_unprivileged_port_start", + ).AssertOutExactly("1000\n") +} diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 69e6919ccd3..b877643bdd7 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -27,6 +27,7 @@ import ( "path/filepath" "reflect" "runtime" + "slices" "strconv" "strings" @@ -330,6 +331,10 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa } opts = append(opts, umaskOpts...) + if !isHostNetwork(netLabelOpts) { + opts = append(opts, withDefaultUnprivilegedPortSysctl()) + } + rtCOpts, err := generateRuntimeCOpts(options.GOptions.CgroupManager, options.Runtime) if err != nil { return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err @@ -563,6 +568,31 @@ func GenerateLogURI(dataStore string) (*url.URL, error) { return cio.LogURIGenerator("binary", selfExe, args) } +func isHostNetwork(netOpts types.NetworkOptions) bool { + return slices.Contains(netOpts.NetworkSlice, "host") +} + +// withDefaultUnprivilegedPortSysctl ensures that containers can bind to +// privileged ports (<1024) without requiring CAP_NET_BIND_SERVICE inside +// the container by defaulting net.ipv4.ip_unprivileged_port_start to 0 +// in the container's network namespace. +func withDefaultUnprivilegedPortSysctl() oci.SpecOpts { + const key = "net.ipv4.ip_unprivileged_port_start" + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + if s.Linux == nil { + s.Linux = &specs.Linux{} + } + if s.Linux.Sysctl == nil { + s.Linux.Sysctl = make(map[string]string) + } + + if _, exists := s.Linux.Sysctl[key]; !exists { + s.Linux.Sysctl[key] = "0" + } + return nil + } +} + func withNerdctlOCIHook(cmd string, args []string) (oci.SpecOpts, error) { if rootlessutil.IsRootless() { detachedNetNS, err := rootlessutil.DetachedNetNS()