From 557c7cb888ad8e2f1f378c9cf34e5fba14551904 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Thu, 7 Jan 2016 22:43:11 -0500 Subject: [PATCH] Move userns support out of experimental into master Adds the `--userns-remap` flag to the master build Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- daemon/config_experimental.go | 113 +----------- daemon/config_unix.go | 1 + daemon/daemon_experimental.go | 82 +-------- daemon/daemon_stub.go | 21 +-- daemon/daemon_unix.go | 173 ++++++++++++++++++ daemon/daemon_windows.go | 15 ++ hack/make.sh | 2 +- integration-cli/docker_api_containers_test.go | 8 +- 8 files changed, 199 insertions(+), 216 deletions(-) diff --git a/daemon/config_experimental.go b/daemon/config_experimental.go index f1c4bb925d..ceb7c38225 100644 --- a/daemon/config_experimental.go +++ b/daemon/config_experimental.go @@ -2,118 +2,7 @@ package daemon -import ( - "fmt" - "strconv" - "strings" - - "github.com/docker/docker/pkg/idtools" - flag "github.com/docker/docker/pkg/mflag" - "github.com/opencontainers/runc/libcontainer/user" -) +import flag "github.com/docker/docker/pkg/mflag" func (config *Config) attachExperimentalFlags(cmd *flag.FlagSet, usageFn func(string) string) { - cmd.StringVar(&config.RemappedRoot, []string{"-userns-remap"}, "", usageFn("User/Group setting for user namespaces")) -} - -const ( - defaultIDSpecifier string = "default" - defaultRemappedID string = "dockremap" -) - -// Parse the remapped root (user namespace) option, which can be one of: -// username - valid username from /etc/passwd -// username:groupname - valid username; valid groupname from /etc/group -// uid - 32-bit unsigned int valid Linux UID value -// uid:gid - uid value; 32-bit unsigned int Linux GID value -// -// If no groupname is specified, and a username is specified, an attempt -// will be made to lookup a gid for that username as a groupname -// -// If names are used, they are verified to exist in passwd/group -func parseRemappedRoot(usergrp string) (string, string, error) { - - var ( - userID, groupID int - username, groupname string - ) - - idparts := strings.Split(usergrp, ":") - if len(idparts) > 2 { - return "", "", fmt.Errorf("Invalid user/group specification in --userns-remap: %q", usergrp) - } - - if uid, err := strconv.ParseInt(idparts[0], 10, 32); err == nil { - // must be a uid; take it as valid - userID = int(uid) - luser, err := user.LookupUid(userID) - if err != nil { - return "", "", fmt.Errorf("Uid %d has no entry in /etc/passwd: %v", userID, err) - } - username = luser.Name - if len(idparts) == 1 { - // if the uid was numeric and no gid was specified, take the uid as the gid - groupID = userID - lgrp, err := user.LookupGid(groupID) - if err != nil { - return "", "", fmt.Errorf("Gid %d has no entry in /etc/group: %v", groupID, err) - } - groupname = lgrp.Name - } - } else { - lookupName := idparts[0] - // special case: if the user specified "default", they want Docker to create or - // use (after creation) the "dockremap" user/group for root remapping - if lookupName == defaultIDSpecifier { - lookupName = defaultRemappedID - } - luser, err := user.LookupUser(lookupName) - if err != nil && idparts[0] != defaultIDSpecifier { - // error if the name requested isn't the special "dockremap" ID - return "", "", fmt.Errorf("Error during uid lookup for %q: %v", lookupName, err) - } else if err != nil { - // special case-- if the username == "default", then we have been asked - // to create a new entry pair in /etc/{passwd,group} for which the /etc/sub{uid,gid} - // ranges will be used for the user and group mappings in user namespaced containers - _, _, err := idtools.AddNamespaceRangesUser(defaultRemappedID) - if err == nil { - return defaultRemappedID, defaultRemappedID, nil - } - return "", "", fmt.Errorf("Error during %q user creation: %v", defaultRemappedID, err) - } - userID = luser.Uid - username = luser.Name - if len(idparts) == 1 { - // we only have a string username, and no group specified; look up gid from username as group - group, err := user.LookupGroup(lookupName) - if err != nil { - return "", "", fmt.Errorf("Error during gid lookup for %q: %v", lookupName, err) - } - groupID = group.Gid - groupname = group.Name - } - } - - if len(idparts) == 2 { - // groupname or gid is separately specified and must be resolved - // to a unsigned 32-bit gid - if gid, err := strconv.ParseInt(idparts[1], 10, 32); err == nil { - // must be a gid, take it as valid - groupID = int(gid) - lgrp, err := user.LookupGid(groupID) - if err != nil { - return "", "", fmt.Errorf("Gid %d has no entry in /etc/passwd: %v", groupID, err) - } - groupname = lgrp.Name - } else { - // not a number; attempt a lookup - group, err := user.LookupGroup(idparts[1]) - if err != nil { - return "", "", fmt.Errorf("Error during gid lookup for %q: %v", idparts[1], err) - } - groupID = group.Gid - groupname = idparts[1] - } - } - return username, groupname, nil } diff --git a/daemon/config_unix.go b/daemon/config_unix.go index ce14f9fe54..a25df90704 100644 --- a/daemon/config_unix.go +++ b/daemon/config_unix.go @@ -79,6 +79,7 @@ func (config *Config) InstallFlags(cmd *flag.FlagSet, usageFn func(string) strin cmd.BoolVar(&config.EnableCors, []string{"#api-enable-cors", "#-api-enable-cors"}, false, usageFn("Enable CORS headers in the remote API, this is deprecated by --api-cors-header")) cmd.StringVar(&config.CorsHeaders, []string{"-api-cors-header"}, "", usageFn("Set CORS headers in the remote API")) cmd.StringVar(&config.CgroupParent, []string{"-cgroup-parent"}, "", usageFn("Set parent cgroup for all containers")) + cmd.StringVar(&config.RemappedRoot, []string{"-userns-remap"}, "", usageFn("User/Group setting for user namespaces")) config.attachExperimentalFlags(cmd, usageFn) } diff --git a/daemon/daemon_experimental.go b/daemon/daemon_experimental.go index cc3852c853..3fd0e765da 100644 --- a/daemon/daemon_experimental.go +++ b/daemon/daemon_experimental.go @@ -2,88 +2,8 @@ package daemon -import ( - "fmt" - "os" - "path/filepath" - "runtime" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/pkg/idtools" - "github.com/docker/engine-api/types/container" -) - -func setupRemappedRoot(config *Config) ([]idtools.IDMap, []idtools.IDMap, error) { - if runtime.GOOS != "linux" && config.RemappedRoot != "" { - return nil, nil, fmt.Errorf("User namespaces are only supported on Linux") - } - - // if the daemon was started with remapped root option, parse - // the config option to the int uid,gid values - var ( - uidMaps, gidMaps []idtools.IDMap - ) - if config.RemappedRoot != "" { - username, groupname, err := parseRemappedRoot(config.RemappedRoot) - if err != nil { - return nil, nil, err - } - if username == "root" { - // Cannot setup user namespaces with a 1-to-1 mapping; "--root=0:0" is a no-op - // effectively - logrus.Warnf("User namespaces: root cannot be remapped with itself; user namespaces are OFF") - return uidMaps, gidMaps, nil - } - logrus.Infof("User namespaces: ID ranges will be mapped to subuid/subgid ranges of: %s:%s", username, groupname) - // update remapped root setting now that we have resolved them to actual names - config.RemappedRoot = fmt.Sprintf("%s:%s", username, groupname) - - uidMaps, gidMaps, err = idtools.CreateIDMappings(username, groupname) - if err != nil { - return nil, nil, fmt.Errorf("Can't create ID mappings: %v", err) - } - } - return uidMaps, gidMaps, nil -} - -func setupDaemonRoot(config *Config, rootDir string, rootUID, rootGID int) error { - config.Root = rootDir - // the docker root metadata directory needs to have execute permissions for all users (o+x) - // so that syscalls executing as non-root, operating on subdirectories of the graph root - // (e.g. mounted layers of a container) can traverse this path. - // The user namespace support will create subdirectories for the remapped root host uid:gid - // pair owned by that same uid:gid pair for proper write access to those needed metadata and - // layer content subtrees. - if _, err := os.Stat(rootDir); err == nil { - // root current exists; verify the access bits are correct by setting them - if err = os.Chmod(rootDir, 0701); err != nil { - return err - } - } else if os.IsNotExist(err) { - // no root exists yet, create it 0701 with root:root ownership - if err := os.MkdirAll(rootDir, 0701); err != nil { - return err - } - } - - // if user namespaces are enabled we will create a subtree underneath the specified root - // with any/all specified remapped root uid/gid options on the daemon creating - // a new subdirectory with ownership set to the remapped uid/gid (so as to allow - // `chdir()` to work for containers namespaced to that uid/gid) - if config.RemappedRoot != "" { - config.Root = filepath.Join(rootDir, fmt.Sprintf("%d.%d", rootUID, rootGID)) - logrus.Debugf("Creating user namespaced daemon root: %s", config.Root) - // Create the root directory if it doesn't exists - if err := idtools.MkdirAllAs(config.Root, 0700, rootUID, rootGID); err != nil { - return fmt.Errorf("Cannot create daemon root: %s: %v", config.Root, err) - } - } - return nil -} +import "github.com/docker/engine-api/types/container" func (daemon *Daemon) verifyExperimentalContainerSettings(hostConfig *container.HostConfig, config *container.Config) ([]string, error) { - if hostConfig.Privileged && daemon.configStore.RemappedRoot != "" { - return nil, fmt.Errorf("Privileged mode is incompatible with user namespace mappings") - } return nil, nil } diff --git a/daemon/daemon_stub.go b/daemon/daemon_stub.go index d60f063847..40e8ddc881 100644 --- a/daemon/daemon_stub.go +++ b/daemon/daemon_stub.go @@ -2,26 +2,7 @@ package daemon -import ( - "os" - - "github.com/docker/docker/pkg/idtools" - "github.com/docker/docker/pkg/system" - "github.com/docker/engine-api/types/container" -) - -func setupRemappedRoot(config *Config) ([]idtools.IDMap, []idtools.IDMap, error) { - return nil, nil, nil -} - -func setupDaemonRoot(config *Config, rootDir string, rootUID, rootGID int) error { - config.Root = rootDir - // Create the root directory if it doesn't exists - if err := system.MkdirAll(config.Root, 0700); err != nil && !os.IsExist(err) { - return err - } - return nil -} +import "github.com/docker/engine-api/types/container" func (daemon *Daemon) verifyExperimentalContainerSettings(hostConfig *container.HostConfig, config *container.Config) ([]string, error) { return nil, nil diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go index 78ca7595ac..be673bcf08 100644 --- a/daemon/daemon_unix.go +++ b/daemon/daemon_unix.go @@ -7,6 +7,7 @@ import ( "net" "os" "path/filepath" + "runtime" "strconv" "strings" "syscall" @@ -33,6 +34,7 @@ import ( "github.com/docker/libnetwork/types" blkiodev "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/label" + "github.com/opencontainers/runc/libcontainer/user" ) const ( @@ -42,6 +44,9 @@ const ( platformSupported = true // It's not kernel limit, we want this 4M limit to supply a reasonable functional container linuxMinMemory = 4194304 + // constants for remapped root settings + defaultIDSpecifier string = "default" + defaultRemappedID string = "dockremap" ) func getBlkioWeightDevices(config *containertypes.HostConfig) ([]*blkiodev.WeightDevice, error) { @@ -375,6 +380,9 @@ func verifyPlatformContainerSettings(daemon *Daemon, hostConfig *containertypes. warnings = append(warnings, "IPv4 forwarding is disabled. Networking will not work.") logrus.Warnf("IPv4 forwarding is disabled. Networking will not work") } + if hostConfig.Privileged && daemon.configStore.RemappedRoot != "" { + return warnings, fmt.Errorf("Privileged mode is incompatible with user namespace mappings") + } return warnings, nil } @@ -674,6 +682,171 @@ func setupInitLayer(initLayer string, rootUID, rootGID int) error { return nil } +// Parse the remapped root (user namespace) option, which can be one of: +// username - valid username from /etc/passwd +// username:groupname - valid username; valid groupname from /etc/group +// uid - 32-bit unsigned int valid Linux UID value +// uid:gid - uid value; 32-bit unsigned int Linux GID value +// +// If no groupname is specified, and a username is specified, an attempt +// will be made to lookup a gid for that username as a groupname +// +// If names are used, they are verified to exist in passwd/group +func parseRemappedRoot(usergrp string) (string, string, error) { + + var ( + userID, groupID int + username, groupname string + ) + + idparts := strings.Split(usergrp, ":") + if len(idparts) > 2 { + return "", "", fmt.Errorf("Invalid user/group specification in --userns-remap: %q", usergrp) + } + + if uid, err := strconv.ParseInt(idparts[0], 10, 32); err == nil { + // must be a uid; take it as valid + userID = int(uid) + luser, err := user.LookupUid(userID) + if err != nil { + return "", "", fmt.Errorf("Uid %d has no entry in /etc/passwd: %v", userID, err) + } + username = luser.Name + if len(idparts) == 1 { + // if the uid was numeric and no gid was specified, take the uid as the gid + groupID = userID + lgrp, err := user.LookupGid(groupID) + if err != nil { + return "", "", fmt.Errorf("Gid %d has no entry in /etc/group: %v", groupID, err) + } + groupname = lgrp.Name + } + } else { + lookupName := idparts[0] + // special case: if the user specified "default", they want Docker to create or + // use (after creation) the "dockremap" user/group for root remapping + if lookupName == defaultIDSpecifier { + lookupName = defaultRemappedID + } + luser, err := user.LookupUser(lookupName) + if err != nil && idparts[0] != defaultIDSpecifier { + // error if the name requested isn't the special "dockremap" ID + return "", "", fmt.Errorf("Error during uid lookup for %q: %v", lookupName, err) + } else if err != nil { + // special case-- if the username == "default", then we have been asked + // to create a new entry pair in /etc/{passwd,group} for which the /etc/sub{uid,gid} + // ranges will be used for the user and group mappings in user namespaced containers + _, _, err := idtools.AddNamespaceRangesUser(defaultRemappedID) + if err == nil { + return defaultRemappedID, defaultRemappedID, nil + } + return "", "", fmt.Errorf("Error during %q user creation: %v", defaultRemappedID, err) + } + userID = luser.Uid + username = luser.Name + if len(idparts) == 1 { + // we only have a string username, and no group specified; look up gid from username as group + group, err := user.LookupGroup(lookupName) + if err != nil { + return "", "", fmt.Errorf("Error during gid lookup for %q: %v", lookupName, err) + } + groupID = group.Gid + groupname = group.Name + } + } + + if len(idparts) == 2 { + // groupname or gid is separately specified and must be resolved + // to a unsigned 32-bit gid + if gid, err := strconv.ParseInt(idparts[1], 10, 32); err == nil { + // must be a gid, take it as valid + groupID = int(gid) + lgrp, err := user.LookupGid(groupID) + if err != nil { + return "", "", fmt.Errorf("Gid %d has no entry in /etc/passwd: %v", groupID, err) + } + groupname = lgrp.Name + } else { + // not a number; attempt a lookup + group, err := user.LookupGroup(idparts[1]) + if err != nil { + return "", "", fmt.Errorf("Error during gid lookup for %q: %v", idparts[1], err) + } + groupID = group.Gid + groupname = idparts[1] + } + } + return username, groupname, nil +} + +func setupRemappedRoot(config *Config) ([]idtools.IDMap, []idtools.IDMap, error) { + if runtime.GOOS != "linux" && config.RemappedRoot != "" { + return nil, nil, fmt.Errorf("User namespaces are only supported on Linux") + } + + // if the daemon was started with remapped root option, parse + // the config option to the int uid,gid values + var ( + uidMaps, gidMaps []idtools.IDMap + ) + if config.RemappedRoot != "" { + username, groupname, err := parseRemappedRoot(config.RemappedRoot) + if err != nil { + return nil, nil, err + } + if username == "root" { + // Cannot setup user namespaces with a 1-to-1 mapping; "--root=0:0" is a no-op + // effectively + logrus.Warnf("User namespaces: root cannot be remapped with itself; user namespaces are OFF") + return uidMaps, gidMaps, nil + } + logrus.Infof("User namespaces: ID ranges will be mapped to subuid/subgid ranges of: %s:%s", username, groupname) + // update remapped root setting now that we have resolved them to actual names + config.RemappedRoot = fmt.Sprintf("%s:%s", username, groupname) + + uidMaps, gidMaps, err = idtools.CreateIDMappings(username, groupname) + if err != nil { + return nil, nil, fmt.Errorf("Can't create ID mappings: %v", err) + } + } + return uidMaps, gidMaps, nil +} + +func setupDaemonRoot(config *Config, rootDir string, rootUID, rootGID int) error { + config.Root = rootDir + // the docker root metadata directory needs to have execute permissions for all users (o+x) + // so that syscalls executing as non-root, operating on subdirectories of the graph root + // (e.g. mounted layers of a container) can traverse this path. + // The user namespace support will create subdirectories for the remapped root host uid:gid + // pair owned by that same uid:gid pair for proper write access to those needed metadata and + // layer content subtrees. + if _, err := os.Stat(rootDir); err == nil { + // root current exists; verify the access bits are correct by setting them + if err = os.Chmod(rootDir, 0701); err != nil { + return err + } + } else if os.IsNotExist(err) { + // no root exists yet, create it 0701 with root:root ownership + if err := os.MkdirAll(rootDir, 0701); err != nil { + return err + } + } + + // if user namespaces are enabled we will create a subtree underneath the specified root + // with any/all specified remapped root uid/gid options on the daemon creating + // a new subdirectory with ownership set to the remapped uid/gid (so as to allow + // `chdir()` to work for containers namespaced to that uid/gid) + if config.RemappedRoot != "" { + config.Root = filepath.Join(rootDir, fmt.Sprintf("%d.%d", rootUID, rootGID)) + logrus.Debugf("Creating user namespaced daemon root: %s", config.Root) + // Create the root directory if it doesn't exists + if err := idtools.MkdirAllAs(config.Root, 0700, rootUID, rootGID); err != nil { + return fmt.Errorf("Cannot create daemon root: %s: %v", config.Root, err) + } + } + return nil +} + // registerLinks writes the links to a file. func (daemon *Daemon) registerLinks(container *container.Container, hostConfig *containertypes.HostConfig) error { if hostConfig == nil || hostConfig.Links == nil { diff --git a/daemon/daemon_windows.go b/daemon/daemon_windows.go index 1e36892e01..3b571b6c1b 100644 --- a/daemon/daemon_windows.go +++ b/daemon/daemon_windows.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "os" "path/filepath" "runtime" "strings" @@ -18,6 +19,7 @@ import ( containertypes "github.com/docker/engine-api/types/container" // register the windows graph driver "github.com/docker/docker/daemon/graphdriver/windows" + "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/system" "github.com/docker/libnetwork" blkiodev "github.com/opencontainers/runc/libcontainer/configs" @@ -135,6 +137,19 @@ func (daemon *Daemon) cleanupMounts() error { return nil } +func setupRemappedRoot(config *Config) ([]idtools.IDMap, []idtools.IDMap, error) { + return nil, nil, nil +} + +func setupDaemonRoot(config *Config, rootDir string, rootUID, rootGID int) error { + config.Root = rootDir + // Create the root directory if it doesn't exists + if err := system.MkdirAll(config.Root, 0700); err != nil && !os.IsExist(err) { + return err + } + return nil +} + // conditionalMountOnStart is a platform specific helper function during the // container start to call mount. func (daemon *Daemon) conditionalMountOnStart(container *container.Container) error { diff --git a/hack/make.sh b/hack/make.sh index 5aa044dddd..78f25cdcbc 100755 --- a/hack/make.sh +++ b/hack/make.sh @@ -99,7 +99,7 @@ if [ ! "$GOPATH" ]; then exit 1 fi -if [ "$DOCKER_EXPERIMENTAL" ] || [ "$DOCKER_REMAP_ROOT" ]; then +if [ "$DOCKER_EXPERIMENTAL" ]; then echo >&2 '# WARNING! DOCKER_EXPERIMENTAL is set: building experimental features' echo >&2 DOCKER_BUILDTAGS+=" experimental pkcs11" diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index 568beca238..c11229f4b7 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/integration-cli/docker_api_containers_test.go @@ -652,10 +652,14 @@ func (s *DockerSuite) TestContainerApiCreateWithDomainName(c *check.C) { c.Assert(containerJSON.Config.Domainname, checker.Equals, domainName, check.Commentf("Mismatched Domainname")) } -func (s *DockerSuite) TestContainerApiCreateNetworkMode(c *check.C) { +func (s *DockerSuite) TestContainerApiCreateBridgeNetworkMode(c *check.C) { testRequires(c, DaemonIsLinux) - UtilCreateNetworkMode(c, "host") UtilCreateNetworkMode(c, "bridge") +} + +func (s *DockerSuite) TestContainerApiCreateOtherNetworkModes(c *check.C) { + testRequires(c, DaemonIsLinux, NotUserNamespace) + UtilCreateNetworkMode(c, "host") UtilCreateNetworkMode(c, "container:web1") }