mirror of
				https://github.com/moby/moby.git
				synced 2022-11-09 12:21:53 -05:00 
			
		
		
		
	Add utility/support package for user namespace support
The `pkg/idtools` package supports the creation of user(s) for
retrieving /etc/sub{u,g}id ranges and creation of the UID/GID mappings
provided to clone() to add support for user namespaces in Docker.
Docker-DCO-1.1-Signed-off-by: Phil Estes <estesp@linux.vnet.ibm.com> (github: estesp)
			
			
This commit is contained in:
		
							parent
							
								
									7787d6dc28
								
							
						
					
					
						commit
						9a3ab0358e
					
				
					 3 changed files with 374 additions and 0 deletions
				
			
		
							
								
								
									
										207
									
								
								pkg/idtools/idtools.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								pkg/idtools/idtools.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,207 @@
 | 
			
		|||
package idtools
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/docker/pkg/system"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// IDMap contains a single entry for user namespace range remapping. An array
 | 
			
		||||
// of IDMap entries represents the structure that will be provided to the Linux
 | 
			
		||||
// kernel for creating a user namespace.
 | 
			
		||||
type IDMap struct {
 | 
			
		||||
	ContainerID int `json:"container_id"`
 | 
			
		||||
	HostID      int `json:"host_id"`
 | 
			
		||||
	Size        int `json:"size"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type subIDRange struct {
 | 
			
		||||
	Start  int
 | 
			
		||||
	Length int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ranges []subIDRange
 | 
			
		||||
 | 
			
		||||
func (e ranges) Len() int           { return len(e) }
 | 
			
		||||
func (e ranges) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
 | 
			
		||||
func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start }
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	subuidFileName string = "/etc/subuid"
 | 
			
		||||
	subgidFileName string = "/etc/subgid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MkdirAllAs creates a directory (include any along the path) and then modifies
 | 
			
		||||
// ownership to the requested uid/gid.  If the directory already exists, this
 | 
			
		||||
// function will still change ownership to the requested uid/gid pair.
 | 
			
		||||
func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
 | 
			
		||||
	return mkdirAs(path, mode, ownerUID, ownerGID, true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MkdirAs creates a directory and then modifies ownership to the requested uid/gid.
 | 
			
		||||
// If the directory already exists, this function still changes ownership
 | 
			
		||||
func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
 | 
			
		||||
	return mkdirAs(path, mode, ownerUID, ownerGID, false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll bool) error {
 | 
			
		||||
	if mkAll {
 | 
			
		||||
		if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if err := os.Mkdir(path, mode); err != nil && !os.IsExist(err) {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// even if it existed, we will chown to change ownership as requested
 | 
			
		||||
	if err := os.Chown(path, ownerUID, ownerGID); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps.
 | 
			
		||||
// If the maps are empty, then the root uid/gid will default to "real" 0/0
 | 
			
		||||
func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) {
 | 
			
		||||
	var uid, gid int
 | 
			
		||||
 | 
			
		||||
	if uidMap != nil {
 | 
			
		||||
		xUID, err := ToHost(0, uidMap)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return -1, -1, err
 | 
			
		||||
		}
 | 
			
		||||
		uid = xUID
 | 
			
		||||
	}
 | 
			
		||||
	if gidMap != nil {
 | 
			
		||||
		xGID, err := ToHost(0, gidMap)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return -1, -1, err
 | 
			
		||||
		}
 | 
			
		||||
		gid = xGID
 | 
			
		||||
	}
 | 
			
		||||
	return uid, gid, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ToContainer takes an id mapping, and uses it to translate a
 | 
			
		||||
// host ID to the remapped ID. If no map is provided, then the translation
 | 
			
		||||
// assumes a 1-to-1 mapping and returns the passed in id
 | 
			
		||||
func ToContainer(hostID int, idMap []IDMap) (int, error) {
 | 
			
		||||
	if idMap == nil {
 | 
			
		||||
		return hostID, nil
 | 
			
		||||
	}
 | 
			
		||||
	for _, m := range idMap {
 | 
			
		||||
		if (hostID >= m.HostID) && (hostID <= (m.HostID + m.Size - 1)) {
 | 
			
		||||
			contID := m.ContainerID + (hostID - m.HostID)
 | 
			
		||||
			return contID, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ToHost takes an id mapping and a remapped ID, and translates the
 | 
			
		||||
// ID to the mapped host ID. If no map is provided, then the translation
 | 
			
		||||
// assumes a 1-to-1 mapping and returns the passed in id #
 | 
			
		||||
func ToHost(contID int, idMap []IDMap) (int, error) {
 | 
			
		||||
	if idMap == nil {
 | 
			
		||||
		return contID, nil
 | 
			
		||||
	}
 | 
			
		||||
	for _, m := range idMap {
 | 
			
		||||
		if (contID >= m.ContainerID) && (contID <= (m.ContainerID + m.Size - 1)) {
 | 
			
		||||
			hostID := m.HostID + (contID - m.ContainerID)
 | 
			
		||||
			return hostID, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateIDMappings takes a requested user and group name and
 | 
			
		||||
// using the data from /etc/sub{uid,gid} ranges, creates the
 | 
			
		||||
// proper uid and gid remapping ranges for that user/group pair
 | 
			
		||||
func CreateIDMappings(username, groupname string) ([]IDMap, []IDMap, error) {
 | 
			
		||||
	subuidRanges, err := parseSubuid(username)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
	subgidRanges, err := parseSubgid(groupname)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if len(subuidRanges) == 0 {
 | 
			
		||||
		return nil, nil, fmt.Errorf("No subuid ranges found for user %q", username)
 | 
			
		||||
	}
 | 
			
		||||
	if len(subgidRanges) == 0 {
 | 
			
		||||
		return nil, nil, fmt.Errorf("No subgid ranges found for group %q", groupname)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return createIDMap(subuidRanges), createIDMap(subgidRanges), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createIDMap(subidRanges ranges) []IDMap {
 | 
			
		||||
	idMap := []IDMap{}
 | 
			
		||||
 | 
			
		||||
	// sort the ranges by lowest ID first
 | 
			
		||||
	sort.Sort(subidRanges)
 | 
			
		||||
	containerID := 0
 | 
			
		||||
	for _, idrange := range subidRanges {
 | 
			
		||||
		idMap = append(idMap, IDMap{
 | 
			
		||||
			ContainerID: containerID,
 | 
			
		||||
			HostID:      idrange.Start,
 | 
			
		||||
			Size:        idrange.Length,
 | 
			
		||||
		})
 | 
			
		||||
		containerID = containerID + idrange.Length
 | 
			
		||||
	}
 | 
			
		||||
	return idMap
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseSubuid(username string) (ranges, error) {
 | 
			
		||||
	return parseSubidFile(subuidFileName, username)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseSubgid(username string) (ranges, error) {
 | 
			
		||||
	return parseSubidFile(subgidFileName, username)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseSubidFile(path, username string) (ranges, error) {
 | 
			
		||||
	var rangeList ranges
 | 
			
		||||
 | 
			
		||||
	subidFile, err := os.Open(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return rangeList, err
 | 
			
		||||
	}
 | 
			
		||||
	defer subidFile.Close()
 | 
			
		||||
 | 
			
		||||
	s := bufio.NewScanner(subidFile)
 | 
			
		||||
	for s.Scan() {
 | 
			
		||||
		if err := s.Err(); err != nil {
 | 
			
		||||
			return rangeList, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		text := strings.TrimSpace(s.Text())
 | 
			
		||||
		if text == "" {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		parts := strings.Split(text, ":")
 | 
			
		||||
		if len(parts) != 3 {
 | 
			
		||||
			return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path)
 | 
			
		||||
		}
 | 
			
		||||
		if parts[0] == username {
 | 
			
		||||
			// return the first entry for a user; ignores potential for multiple ranges per user
 | 
			
		||||
			startid, err := strconv.Atoi(parts[1])
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
 | 
			
		||||
			}
 | 
			
		||||
			length, err := strconv.Atoi(parts[2])
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
 | 
			
		||||
			}
 | 
			
		||||
			rangeList = append(rangeList, subIDRange{startid, length})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return rangeList, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										155
									
								
								pkg/idtools/usergroupadd_linux.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								pkg/idtools/usergroupadd_linux.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,155 @@
 | 
			
		|||
package idtools
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// add a user and/or group to Linux /etc/passwd, /etc/group using standard
 | 
			
		||||
// Linux distribution commands:
 | 
			
		||||
// adduser --uid <id> --shell /bin/login --no-create-home --disabled-login --ingroup <groupname> <username>
 | 
			
		||||
// useradd -M -u <id> -s /bin/nologin -N -g <groupname> <username>
 | 
			
		||||
// addgroup --gid <id> <groupname>
 | 
			
		||||
// groupadd -g <id> <groupname>
 | 
			
		||||
 | 
			
		||||
const baseUID int = 10000
 | 
			
		||||
const baseGID int = 10000
 | 
			
		||||
const idMAX int = 65534
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	userCommand  string
 | 
			
		||||
	groupCommand string
 | 
			
		||||
 | 
			
		||||
	cmdTemplates = map[string]string{
 | 
			
		||||
		"adduser":  "--uid %d --shell /bin/false --no-create-home --disabled-login --ingroup %s %s",
 | 
			
		||||
		"useradd":  "-M -u %d -s /bin/false -N -g %s %s",
 | 
			
		||||
		"addgroup": "--gid %d %s",
 | 
			
		||||
		"groupadd": "-g %d %s",
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	// set up which commands are used for adding users/groups dependent on distro
 | 
			
		||||
	if _, err := resolveBinary("adduser"); err == nil {
 | 
			
		||||
		userCommand = "adduser"
 | 
			
		||||
	} else if _, err := resolveBinary("useradd"); err == nil {
 | 
			
		||||
		userCommand = "useradd"
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := resolveBinary("addgroup"); err == nil {
 | 
			
		||||
		groupCommand = "addgroup"
 | 
			
		||||
	} else if _, err := resolveBinary("groupadd"); err == nil {
 | 
			
		||||
		groupCommand = "groupadd"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func resolveBinary(binname string) (string, error) {
 | 
			
		||||
	binaryPath, err := exec.LookPath(binname)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	resolvedPath, err := filepath.EvalSymlinks(binaryPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	//only return no error if the final resolved binary basename
 | 
			
		||||
	//matches what was searched for
 | 
			
		||||
	if filepath.Base(resolvedPath) == binname {
 | 
			
		||||
		return resolvedPath, nil
 | 
			
		||||
	}
 | 
			
		||||
	return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair
 | 
			
		||||
// and calls the appropriate helper function to add the group and then
 | 
			
		||||
// the user to the group in /etc/group and /etc/passwd respectively.
 | 
			
		||||
// This new user's /etc/sub{uid,gid} ranges will be used for user namespace
 | 
			
		||||
// mapping ranges in containers.
 | 
			
		||||
func AddNamespaceRangesUser(name string) (int, int, error) {
 | 
			
		||||
	// Find unused uid, gid pair
 | 
			
		||||
	uid, err := findUnusedUID(baseUID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return -1, -1, fmt.Errorf("Unable to find unused UID: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	gid, err := findUnusedGID(baseGID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return -1, -1, fmt.Errorf("Unable to find unused GID: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// First add the group that we will use
 | 
			
		||||
	if err := addGroup(name, gid); err != nil {
 | 
			
		||||
		return -1, -1, fmt.Errorf("Error adding group %q: %v", name, err)
 | 
			
		||||
	}
 | 
			
		||||
	// Add the user as a member of the group
 | 
			
		||||
	if err := addUser(name, uid, name); err != nil {
 | 
			
		||||
		return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err)
 | 
			
		||||
	}
 | 
			
		||||
	return uid, gid, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addUser(userName string, uid int, groupName string) error {
 | 
			
		||||
 | 
			
		||||
	if userCommand == "" {
 | 
			
		||||
		return fmt.Errorf("Cannot add user; no useradd/adduser binary found")
 | 
			
		||||
	}
 | 
			
		||||
	args := fmt.Sprintf(cmdTemplates[userCommand], uid, groupName, userName)
 | 
			
		||||
	return execAddCmd(userCommand, args)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addGroup(groupName string, gid int) error {
 | 
			
		||||
 | 
			
		||||
	if groupCommand == "" {
 | 
			
		||||
		return fmt.Errorf("Cannot add group; no groupadd/addgroup binary found")
 | 
			
		||||
	}
 | 
			
		||||
	args := fmt.Sprintf(cmdTemplates[groupCommand], gid, groupName)
 | 
			
		||||
	// only error out if the error isn't that the group already exists
 | 
			
		||||
	// if the group exists then our needs are already met
 | 
			
		||||
	if err := execAddCmd(groupCommand, args); err != nil && !strings.Contains(err.Error(), "already exists") {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func execAddCmd(cmd, args string) error {
 | 
			
		||||
	execCmd := exec.Command(cmd, strings.Split(args, " ")...)
 | 
			
		||||
	out, err := execCmd.CombinedOutput()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Failed to add user/group with error: %v; output: %q", err, string(out))
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func findUnusedUID(startUID int) (int, error) {
 | 
			
		||||
	return findUnused("passwd", startUID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func findUnusedGID(startGID int) (int, error) {
 | 
			
		||||
	return findUnused("group", startGID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func findUnused(file string, id int) (int, error) {
 | 
			
		||||
	for {
 | 
			
		||||
		cmdStr := fmt.Sprintf("cat /etc/%s | cut -d: -f3 | grep '^%d$'", file, id)
 | 
			
		||||
		cmd := exec.Command("sh", "-c", cmdStr)
 | 
			
		||||
		if err := cmd.Run(); err != nil {
 | 
			
		||||
			// if a non-zero return code occurs, then we know the ID was not found
 | 
			
		||||
			// and is usable
 | 
			
		||||
			if exiterr, ok := err.(*exec.ExitError); ok {
 | 
			
		||||
				// The program has exited with an exit code != 0
 | 
			
		||||
				if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
 | 
			
		||||
					if status.ExitStatus() == 1 {
 | 
			
		||||
						//no match, we can use this ID
 | 
			
		||||
						return id, nil
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return -1, fmt.Errorf("Error looking in /etc/%s for unused ID: %v", file, err)
 | 
			
		||||
		}
 | 
			
		||||
		id++
 | 
			
		||||
		if id > idMAX {
 | 
			
		||||
			return -1, fmt.Errorf("Maximum id in %q reached with finding unused numeric ID", file)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								pkg/idtools/usergroupadd_unsupported.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								pkg/idtools/usergroupadd_unsupported.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
// +build !linux
 | 
			
		||||
 | 
			
		||||
package idtools
 | 
			
		||||
 | 
			
		||||
import "fmt"
 | 
			
		||||
 | 
			
		||||
// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair
 | 
			
		||||
// and calls the appropriate helper function to add the group and then
 | 
			
		||||
// the user to the group in /etc/group and /etc/passwd respectively.
 | 
			
		||||
func AddNamespaceRangesUser(name string) (int, int, error) {
 | 
			
		||||
	return -1, -1, fmt.Errorf("No support for adding users or groups on this OS")
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue