From 1d72f651e4c5118c020a1ab7281d3de0bf31899e Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Wed, 24 Jul 2019 11:20:31 +0200 Subject: podman: support --userns=ns|container allow to join the user namespace of another container. Closes: https://github.com/containers/libpod/issues/3629 Signed-off-by: Giuseppe Scrivano --- docs/podman-create.1.md | 2 ++ docs/podman-run.1.md | 2 ++ libpod/container_internal_linux.go | 5 +++++ libpod/options.go | 1 + pkg/namespaces/namespaces.go | 29 ++++++++++++++++++++++++++--- pkg/spec/createconfig.go | 24 ++++++++++++++++++++++-- pkg/spec/spec.go | 4 ++-- test/e2e/run_userns_test.go | 15 +++++++++++++++ 8 files changed, 75 insertions(+), 7 deletions(-) diff --git a/docs/podman-create.1.md b/docs/podman-create.1.md index 4008b64e6..d796c2586 100644 --- a/docs/podman-create.1.md +++ b/docs/podman-create.1.md @@ -751,6 +751,7 @@ Without this argument the command will be run as root in the container. **--userns**=*host* **--userns**=*keep-id* +**--userns**=container:container **--userns**=*ns:my_namespace* Set the user namespace mode for the container. It defaults to the **PODMAN_USERNS** environment variable. An empty value means user namespaces are disabled. @@ -758,6 +759,7 @@ Set the user namespace mode for the container. It defaults to the **PODMAN_USER - `host`: run in the user namespace of the caller. This is the default if no user namespace options are set. The processes running in the container will have the same privileges on the host as any other process launched by the calling user. - `keep-id`: creates a user namespace where the current rootless user's UID:GID are mapped to the same values in the container. This option is ignored for containers created by the root user. - `ns`: run the container in the given existing user namespace. +- `container`: join the user namespace of the specified container. This option is incompatible with --gidmap, --uidmap, --subuid and --subgid diff --git a/docs/podman-run.1.md b/docs/podman-run.1.md index d6c7ae055..8d7d2c287 100644 --- a/docs/podman-run.1.md +++ b/docs/podman-run.1.md @@ -785,6 +785,7 @@ Without this argument the command will be run as root in the container. **--userns**=host **--userns**=keep-id +**--userns**=container:container **--userns**=ns:my_namespace Set the user namespace mode for the container. It defaults to the **PODMAN_USERNS** environment variable. An empty value means user namespaces are disabled. @@ -792,6 +793,7 @@ Set the user namespace mode for the container. It defaults to the **PODMAN_USER - `host`: run in the user namespace of the caller. This is the default if no user namespace options are set. The processes running in the container will have the same privileges on the host as any other process launched by the calling user. - `keep-id`: creates a user namespace where the current rootless user's UID:GID are mapped to the same values in the container. This option is ignored for containers created by the root user. - `ns`: run the container in the given existing user namespace. +- `container`: join the user namespace of the specified container. This option is incompatible with --gidmap, --uidmap, --subuid and --subgid diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 6e775cd28..afcf51a11 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -318,6 +318,11 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { if err := c.addNamespaceContainer(&g, UserNS, c.config.UserNsCtr, spec.UserNamespace); err != nil { return nil, err } + if len(g.Config.Linux.UIDMappings) == 0 { + // runc complains if no mapping is specified, even if we join another ns. So provide a dummy mapping + g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1)) + g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) + } } if c.config.UTSNsCtr != "" { if err := c.addNamespaceContainer(&g, UTSNS, c.config.UTSNsCtr, spec.UTSNamespace); err != nil { diff --git a/libpod/options.go b/libpod/options.go index 8d41764a9..81d3aa64f 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -847,6 +847,7 @@ func WithUserNSFrom(nsCtr *Container) CtrCreateOption { } ctr.config.UserNsCtr = nsCtr.ID() + ctr.config.IDMappings = nsCtr.config.IDMappings return nil } diff --git a/pkg/namespaces/namespaces.go b/pkg/namespaces/namespaces.go index 7ed95bd0f..35298796f 100644 --- a/pkg/namespaces/namespaces.go +++ b/pkg/namespaces/namespaces.go @@ -76,27 +76,50 @@ func (n UsernsMode) IsKeepID() bool { // IsPrivate indicates whether the container uses the a private userns. func (n UsernsMode) IsPrivate() bool { - return !(n.IsHost()) + return !(n.IsHost() || n.IsContainer()) } // Valid indicates whether the userns is valid. func (n UsernsMode) Valid() bool { parts := strings.Split(string(n), ":") switch mode := parts[0]; mode { - case "", "host", "keep-id": + case "", "host", "keep-id", "ns": + case "container": + if len(parts) != 2 || parts[1] == "" { + return false + } default: return false } return true } +// IsNS indicates a userns namespace passed in by path (ns:) +func (n UsernsMode) IsNS() bool { + return strings.HasPrefix(string(n), "ns:") +} + +// NS gets the path associated with a ns: userns ns +func (n UsernsMode) NS() string { + parts := strings.SplitN(string(n), ":", 2) + if len(parts) > 1 { + return parts[1] + } + return "" +} + // IsContainer indicates whether container uses a container userns. func (n UsernsMode) IsContainer() bool { - return false + parts := strings.SplitN(string(n), ":", 2) + return len(parts) > 1 && parts[0] == "container" } // Container is the id of the container which network this container is connected to. func (n UsernsMode) Container() string { + parts := strings.SplitN(string(n), ":", 2) + if len(parts) > 1 { + return parts[1] + } return "" } diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 1fb1f829b..214a3c5ed 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -266,7 +266,8 @@ func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *l } options = append(options, libpod.WithNetNSFrom(connectedCtr)) } else if !c.NetMode.IsHost() && !c.NetMode.IsNone() { - postConfigureNetNS := c.NetMode.IsSlirp4netns() || (len(c.IDMappings.UIDMap) > 0 || len(c.IDMappings.GIDMap) > 0) && !c.UsernsMode.IsHost() + hasUserns := c.UsernsMode.IsContainer() || c.UsernsMode.IsNS() || len(c.IDMappings.UIDMap) > 0 || len(c.IDMappings.GIDMap) > 0 + postConfigureNetNS := c.NetMode.IsSlirp4netns() || (hasUserns && !c.UsernsMode.IsHost()) options = append(options, libpod.WithNetNS(portBindings, postConfigureNetNS, string(c.NetMode), networks)) } @@ -287,6 +288,26 @@ func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *l options = append(options, libpod.WithCgroupNSFrom(connectedCtr)) } + if c.UsernsMode.IsNS() { + ns := c.UsernsMode.NS() + if ns == "" { + return nil, errors.Errorf("invalid empty user-defined user namespace") + } + _, err := os.Stat(ns) + if err != nil { + return nil, err + } + options = append(options, libpod.WithIDMappings(*c.IDMappings)) + } else if c.UsernsMode.IsContainer() { + connectedCtr, err := runtime.LookupContainer(c.UsernsMode.Container()) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", c.UsernsMode.Container()) + } + options = append(options, libpod.WithUserNSFrom(connectedCtr)) + } else { + options = append(options, libpod.WithIDMappings(*c.IDMappings)) + } + if c.PidMode.IsContainer() { connectedCtr, err := runtime.LookupContainer(c.PidMode.Container()) if err != nil { @@ -379,7 +400,6 @@ func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *l } options = append(options, libpod.WithShmSize(c.Resources.ShmSize)) options = append(options, libpod.WithGroups(c.GroupAdd)) - options = append(options, libpod.WithIDMappings(*c.IDMappings)) if c.Rootfs != "" { options = append(options, libpod.WithRootFS(c.Rootfs)) } diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 824c99025..15c8c77fa 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -46,7 +46,8 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM canMountSys := true isRootless := rootless.IsRootless() - inUserNS := isRootless || (len(config.IDMappings.UIDMap) > 0 || len(config.IDMappings.GIDMap) > 0) && !config.UsernsMode.IsHost() + hasUserns := config.UsernsMode.IsContainer() || config.UsernsMode.IsNS() || len(config.IDMappings.UIDMap) > 0 || len(config.IDMappings.GIDMap) > 0 + inUserNS := isRootless || (hasUserns && !config.UsernsMode.IsHost()) if inUserNS && config.NetMode.IsHost() { canMountSys = false @@ -554,7 +555,6 @@ func addUserNS(config *CreateConfig, g *generate.Generator) error { if err := g.AddOrReplaceLinuxNamespace(spec.UserNamespace, NS(string(config.UsernsMode))); err != nil { return err } - // runc complains if no mapping is specified, even if we join another ns. So provide a dummy mapping g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1)) g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) diff --git a/test/e2e/run_userns_test.go b/test/e2e/run_userns_test.go index be3f7df49..e873f5abe 100644 --- a/test/e2e/run_userns_test.go +++ b/test/e2e/run_userns_test.go @@ -85,4 +85,19 @@ var _ = Describe("Podman UserNS support", func() { ok, _ := session.GrepString(uid) Expect(ok).To(BeTrue()) }) + + It("podman --userns=container:CTR", func() { + ctrName := "userns-ctr" + session := podmanTest.Podman([]string{"run", "-d", "--uidmap=0:0:1", "--uidmap=1:1:4998", "--name", ctrName, "alpine", "top"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + // runc has an issue and we also need to join the IPC namespace. + session = podmanTest.Podman([]string{"run", "--rm", "--userns=container:" + ctrName, "--ipc=container:" + ctrName, "alpine", "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + ok, _ := session.GrepString("4998") + Expect(ok).To(BeTrue()) + }) }) -- cgit v1.2.3-54-g00ecf