Browse code

allow running `dockerd` in an unprivileged user namespace (rootless mode)

Please refer to `docs/rootless.md`.

TLDR:
* Make sure `/etc/subuid` and `/etc/subgid` contain the entry for you
* `dockerd-rootless.sh --experimental`
* `docker -H unix://$XDG_RUNTIME_DIR/docker.sock run ...`

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>

Akihiro Suda authored on 2018/10/15 16:52:53
Showing 36 changed files
... ...
@@ -161,7 +161,12 @@ ENV INSTALL_BINARY_NAME=tini
161 161
 COPY hack/dockerfile/install/$INSTALL_BINARY_NAME.installer ./
162 162
 RUN PREFIX=/build ./install.sh $INSTALL_BINARY_NAME
163 163
 
164
-
164
+FROM base AS rootlesskit
165
+ENV INSTALL_BINARY_NAME=rootlesskit
166
+COPY hack/dockerfile/install/install.sh ./install.sh
167
+COPY hack/dockerfile/install/$INSTALL_BINARY_NAME.installer ./
168
+RUN PREFIX=/build/ ./install.sh $INSTALL_BINARY_NAME
169
+COPY ./contrib/dockerd-rootless.sh /build
165 170
 
166 171
 # TODO: Some of this is only really needed for testing, it would be nice to split this up
167 172
 FROM runtime-dev AS dev
... ...
@@ -233,6 +238,7 @@ RUN cd /docker-py \
233 233
 	&& pip install paramiko==2.4.2 \
234 234
 	&& pip install yamllint==1.5.0 \
235 235
 	&& pip install -r test-requirements.txt
236
+COPY --from=rootlesskit /build/ /usr/local/bin/
236 237
 
237 238
 ENV PATH=/usr/local/cli:$PATH
238 239
 ENV DOCKER_BUILDTAGS apparmor seccomp selinux
... ...
@@ -13,6 +13,8 @@ var (
13 13
 )
14 14
 
15 15
 // Dir returns the path to the configuration directory as specified by the DOCKER_CONFIG environment variable.
16
+// If DOCKER_CONFIG is unset, Dir returns ~/.docker .
17
+// Dir ignores XDG_CONFIG_HOME (same as the docker client).
16 18
 // TODO: this was copied from cli/config/configfile and should be removed once cmd/dockerd moves
17 19
 func Dir() string {
18 20
 	return configDir
... ...
@@ -17,8 +17,20 @@ const (
17 17
 )
18 18
 
19 19
 // installCommonConfigFlags adds flags to the pflag.FlagSet to configure the daemon
20
-func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
20
+func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
21 21
 	var maxConcurrentDownloads, maxConcurrentUploads int
22
+	defaultPidFile, err := getDefaultPidFile()
23
+	if err != nil {
24
+		return err
25
+	}
26
+	defaultDataRoot, err := getDefaultDataRoot()
27
+	if err != nil {
28
+		return err
29
+	}
30
+	defaultExecRoot, err := getDefaultExecRoot()
31
+	if err != nil {
32
+		return err
33
+	}
22 34
 
23 35
 	installRegistryServiceFlags(&conf.ServiceOptions, flags)
24 36
 
... ...
@@ -80,6 +92,7 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
80 80
 
81 81
 	conf.MaxConcurrentDownloads = &maxConcurrentDownloads
82 82
 	conf.MaxConcurrentUploads = &maxConcurrentUploads
83
+	return nil
83 84
 }
84 85
 
85 86
 func installRegistryServiceFlags(options *registry.ServiceOptions, flags *pflag.FlagSet) {
... ...
@@ -3,17 +3,48 @@
3 3
 package main
4 4
 
5 5
 import (
6
+	"path/filepath"
7
+
6 8
 	"github.com/docker/docker/api/types"
7 9
 	"github.com/docker/docker/daemon/config"
8 10
 	"github.com/docker/docker/opts"
11
+	"github.com/docker/docker/pkg/homedir"
12
+	"github.com/docker/docker/rootless"
9 13
 	"github.com/spf13/pflag"
10 14
 )
11 15
 
12
-var (
13
-	defaultPidFile  = "/var/run/docker.pid"
14
-	defaultDataRoot = "/var/lib/docker"
15
-	defaultExecRoot = "/var/run/docker"
16
-)
16
+func getDefaultPidFile() (string, error) {
17
+	if !rootless.RunningWithNonRootUsername() {
18
+		return "/var/run/docker.pid", nil
19
+	}
20
+	runtimeDir, err := homedir.GetRuntimeDir()
21
+	if err != nil {
22
+		return "", err
23
+	}
24
+	return filepath.Join(runtimeDir, "docker.pid"), nil
25
+}
26
+
27
+func getDefaultDataRoot() (string, error) {
28
+	if !rootless.RunningWithNonRootUsername() {
29
+		return "/var/lib/docker", nil
30
+	}
31
+	dataHome, err := homedir.GetDataHome()
32
+	if err != nil {
33
+		return "", err
34
+	}
35
+	return filepath.Join(dataHome, "docker"), nil
36
+}
37
+
38
+func getDefaultExecRoot() (string, error) {
39
+	if !rootless.RunningWithNonRootUsername() {
40
+		return "/var/run/docker", nil
41
+	}
42
+	runtimeDir, err := homedir.GetRuntimeDir()
43
+	if err != nil {
44
+		return "", err
45
+	}
46
+	return filepath.Join(runtimeDir, "docker"), nil
47
+}
17 48
 
18 49
 // installUnixConfigFlags adds command-line options to the top-level flag parser for
19 50
 // the current process that are common across Unix platforms.
... ...
@@ -5,14 +5,17 @@ package main
5 5
 import (
6 6
 	"github.com/docker/docker/daemon/config"
7 7
 	"github.com/docker/docker/opts"
8
+	"github.com/docker/docker/rootless"
8 9
 	"github.com/docker/go-units"
9 10
 	"github.com/spf13/pflag"
10 11
 )
11 12
 
12 13
 // installConfigFlags adds flags to the pflag.FlagSet to configure the daemon
13
-func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
14
+func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
14 15
 	// First handle install flags which are consistent cross-platform
15
-	installCommonConfigFlags(conf, flags)
16
+	if err := installCommonConfigFlags(conf, flags); err != nil {
17
+		return err
18
+	}
16 19
 
17 20
 	// Then install flags common to unix platforms
18 21
 	installUnixConfigFlags(conf, flags)
... ...
@@ -46,5 +49,7 @@ func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
46 46
 	flags.BoolVar(&conf.NoNewPrivileges, "no-new-privileges", false, "Set no-new-privileges by default for new containers")
47 47
 	flags.StringVar(&conf.IpcMode, "default-ipc-mode", config.DefaultIpcMode, `Default mode for containers ipc ("shareable" | "private")`)
48 48
 	flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "Default address pools for node specific local networks")
49
-
49
+	// Mostly users don't need to set this flag explicitly.
50
+	flags.BoolVar(&conf.Rootless, "rootless", rootless.RunningWithNonRootUsername(), "Enable rootless mode (experimental)")
51
+	return nil
50 52
 }
... ...
@@ -15,7 +15,8 @@ func TestDaemonParseShmSize(t *testing.T) {
15 15
 	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
16 16
 
17 17
 	conf := &config.Config{}
18
-	installConfigFlags(conf, flags)
18
+	err := installConfigFlags(conf, flags)
19
+	assert.NilError(t, err)
19 20
 	// By default `--default-shm-size=64M`
20 21
 	assert.Check(t, is.Equal(int64(64*1024*1024), conf.ShmSize.Value()))
21 22
 	assert.Check(t, flags.Set("default-shm-size", "128M"))
... ...
@@ -8,19 +8,28 @@ import (
8 8
 	"github.com/spf13/pflag"
9 9
 )
10 10
 
11
-var (
12
-	defaultPidFile  string
13
-	defaultDataRoot = filepath.Join(os.Getenv("programdata"), "docker")
14
-	defaultExecRoot = filepath.Join(os.Getenv("programdata"), "docker", "exec-root")
15
-)
11
+func getDefaultPidFile() (string, error) {
12
+	return "", nil
13
+}
14
+
15
+func getDefaultDataRoot() (string, error) {
16
+	return filepath.Join(os.Getenv("programdata"), "docker"), nil
17
+}
18
+
19
+func getDefaultExecRoot() (string, error) {
20
+	return filepath.Join(os.Getenv("programdata"), "docker", "exec-root"), nil
21
+}
16 22
 
17 23
 // installConfigFlags adds flags to the pflag.FlagSet to configure the daemon
18
-func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
24
+func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
19 25
 	// First handle install flags which are consistent cross-platform
20
-	installCommonConfigFlags(conf, flags)
26
+	if err := installCommonConfigFlags(conf, flags); err != nil {
27
+		return err
28
+	}
21 29
 
22 30
 	// Then platform-specific install flags.
23 31
 	flags.StringVar(&conf.BridgeConfig.FixedCIDR, "fixed-cidr", "", "IPv4 subnet for fixed IPs")
24 32
 	flags.StringVarP(&conf.BridgeConfig.Iface, "bridge", "b", "", "Attach containers to a virtual switch")
25 33
 	flags.StringVarP(&conf.SocketGroup, "group", "G", "", "Users or groups that can access the named pipe")
34
+	return nil
26 35
 }
... ...
@@ -40,12 +40,14 @@ import (
40 40
 	"github.com/docker/docker/libcontainerd/supervisor"
41 41
 	dopts "github.com/docker/docker/opts"
42 42
 	"github.com/docker/docker/pkg/authorization"
43
+	"github.com/docker/docker/pkg/homedir"
43 44
 	"github.com/docker/docker/pkg/jsonmessage"
44 45
 	"github.com/docker/docker/pkg/pidfile"
45 46
 	"github.com/docker/docker/pkg/plugingetter"
46 47
 	"github.com/docker/docker/pkg/signal"
47 48
 	"github.com/docker/docker/pkg/system"
48 49
 	"github.com/docker/docker/plugin"
50
+	"github.com/docker/docker/rootless"
49 51
 	"github.com/docker/docker/runconfig"
50 52
 	"github.com/docker/go-connections/tlsconfig"
51 53
 	swarmapi "github.com/docker/swarmkit/api"
... ...
@@ -97,6 +99,17 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
97 97
 
98 98
 	if cli.Config.Experimental {
99 99
 		logrus.Warn("Running experimental build")
100
+		if cli.Config.IsRootless() {
101
+			logrus.Warn("Running in rootless mode. Cgroups, AppArmor, and CRIU are disabled.")
102
+		}
103
+	} else {
104
+		if cli.Config.IsRootless() {
105
+			return fmt.Errorf("rootless mode is supported only when running in experimental mode")
106
+		}
107
+	}
108
+	// return human-friendly error before creating files
109
+	if runtime.GOOS == "linux" && os.Geteuid() != 0 {
110
+		return fmt.Errorf("dockerd needs to be started with root. To see how to run dockerd in rootless mode with unprivileged user, see the documentation")
100 111
 	}
101 112
 
102 113
 	system.InitLCOW(cli.Config.Experimental)
... ...
@@ -115,11 +128,14 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
115 115
 		return err
116 116
 	}
117 117
 
118
+	potentiallyUnderRuntimeDir := []string{cli.Config.ExecRoot}
119
+
118 120
 	if cli.Pidfile != "" {
119 121
 		pf, err := pidfile.New(cli.Pidfile)
120 122
 		if err != nil {
121 123
 			return errors.Wrap(err, "failed to start daemon")
122 124
 		}
125
+		potentiallyUnderRuntimeDir = append(potentiallyUnderRuntimeDir, cli.Pidfile)
123 126
 		defer func() {
124 127
 			if err := pf.Remove(); err != nil {
125 128
 				logrus.Error(err)
... ...
@@ -127,6 +143,12 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
127 127
 		}()
128 128
 	}
129 129
 
130
+	// Set sticky bit if XDG_RUNTIME_DIR is set && the file is actually under XDG_RUNTIME_DIR
131
+	if _, err := homedir.StickRuntimeDirContents(potentiallyUnderRuntimeDir); err != nil {
132
+		// StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset
133
+		logrus.WithError(err).Warn("cannot set sticky bit on files under XDG_RUNTIME_DIR")
134
+	}
135
+
130 136
 	serverConfig, err := newAPIServerConfig(cli)
131 137
 	if err != nil {
132 138
 		return errors.Wrap(err, "failed to create API server")
... ...
@@ -140,7 +162,11 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
140 140
 
141 141
 	ctx, cancel := context.WithCancel(context.Background())
142 142
 	if cli.Config.ContainerdAddr == "" && runtime.GOOS != "windows" {
143
-		if !systemContainerdRunning() {
143
+		systemContainerdAddr, ok, err := systemContainerdRunning(cli.Config.IsRootless())
144
+		if err != nil {
145
+			return errors.Wrap(err, "could not determine whether the system containerd is running")
146
+		}
147
+		if !ok {
144 148
 			opts, err := cli.getContainerdDaemonOpts()
145 149
 			if err != nil {
146 150
 				cancel()
... ...
@@ -157,7 +183,7 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
157 157
 			// Try to wait for containerd to shutdown
158 158
 			defer r.WaitTimeout(10 * time.Second)
159 159
 		} else {
160
-			cli.Config.ContainerdAddr = containerddefaults.DefaultAddress
160
+			cli.Config.ContainerdAddr = systemContainerdAddr
161 161
 		}
162 162
 	}
163 163
 	defer cancel()
... ...
@@ -403,9 +429,11 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) {
403 403
 	}
404 404
 
405 405
 	if conf.TrustKeyPath == "" {
406
-		conf.TrustKeyPath = filepath.Join(
407
-			getDaemonConfDir(conf.Root),
408
-			defaultTrustKeyFile)
406
+		daemonConfDir, err := getDaemonConfDir(conf.Root)
407
+		if err != nil {
408
+			return nil, err
409
+		}
410
+		conf.TrustKeyPath = filepath.Join(daemonConfDir, defaultTrustKeyFile)
409 411
 	}
410 412
 
411 413
 	if flags.Changed("graph") && flags.Changed("data-root") {
... ...
@@ -585,7 +613,7 @@ func loadListeners(cli *DaemonCli, serverConfig *apiserver.Config) ([]string, er
585 585
 	var hosts []string
586 586
 	for i := 0; i < len(cli.Config.Hosts); i++ {
587 587
 		var err error
588
-		if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, cli.Config.Hosts[i]); err != nil {
588
+		if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, rootless.RunningWithNonRootUsername(), cli.Config.Hosts[i]); err != nil {
589 589
 			return nil, errors.Wrapf(err, "error parsing -H %s", cli.Config.Hosts[i])
590 590
 		}
591 591
 
... ...
@@ -662,9 +690,17 @@ func validateAuthzPlugins(requestedPlugins []string, pg plugingetter.PluginGette
662 662
 	return nil
663 663
 }
664 664
 
665
-func systemContainerdRunning() bool {
666
-	_, err := os.Lstat(containerddefaults.DefaultAddress)
667
-	return err == nil
665
+func systemContainerdRunning(isRootless bool) (string, bool, error) {
666
+	addr := containerddefaults.DefaultAddress
667
+	if isRootless {
668
+		runtimeDir, err := homedir.GetRuntimeDir()
669
+		if err != nil {
670
+			return "", false, err
671
+		}
672
+		addr = filepath.Join(runtimeDir, "containerd", "containerd.sock")
673
+	}
674
+	_, err := os.Lstat(addr)
675
+	return addr, err == nil, nil
668 676
 }
669 677
 
670 678
 // configureDaemonLogs sets the logrus logging level and formatting
... ...
@@ -11,18 +11,22 @@ import (
11 11
 	"gotest.tools/fs"
12 12
 )
13 13
 
14
-func defaultOptions(configFile string) *daemonOptions {
14
+func defaultOptions(t *testing.T, configFile string) *daemonOptions {
15 15
 	opts := newDaemonOptions(&config.Config{})
16 16
 	opts.flags = &pflag.FlagSet{}
17 17
 	opts.InstallFlags(opts.flags)
18
-	installConfigFlags(opts.daemonConfig, opts.flags)
18
+	if err := installConfigFlags(opts.daemonConfig, opts.flags); err != nil {
19
+		t.Fatal(err)
20
+	}
21
+	defaultDaemonConfigFile, err := getDefaultDaemonConfigFile()
22
+	assert.NilError(t, err)
19 23
 	opts.flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "")
20 24
 	opts.configFile = configFile
21 25
 	return opts
22 26
 }
23 27
 
24 28
 func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) {
25
-	opts := defaultOptions("")
29
+	opts := defaultOptions(t, "")
26 30
 	opts.Debug = true
27 31
 
28 32
 	loadedConfig, err := loadDaemonCliConfig(opts)
... ...
@@ -34,7 +38,7 @@ func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) {
34 34
 }
35 35
 
36 36
 func TestLoadDaemonCliConfigWithTLS(t *testing.T) {
37
-	opts := defaultOptions("")
37
+	opts := defaultOptions(t, "")
38 38
 	opts.TLSOptions.CAFile = "/tmp/ca.pem"
39 39
 	opts.TLS = true
40 40
 
... ...
@@ -49,7 +53,7 @@ func TestLoadDaemonCliConfigWithConflicts(t *testing.T) {
49 49
 	defer tempFile.Remove()
50 50
 	configFile := tempFile.Path()
51 51
 
52
-	opts := defaultOptions(configFile)
52
+	opts := defaultOptions(t, configFile)
53 53
 	flags := opts.flags
54 54
 
55 55
 	assert.Check(t, flags.Set("config-file", configFile))
... ...
@@ -65,7 +69,7 @@ func TestLoadDaemonCliWithConflictingNodeGenericResources(t *testing.T) {
65 65
 	defer tempFile.Remove()
66 66
 	configFile := tempFile.Path()
67 67
 
68
-	opts := defaultOptions(configFile)
68
+	opts := defaultOptions(t, configFile)
69 69
 	flags := opts.flags
70 70
 
71 71
 	assert.Check(t, flags.Set("config-file", configFile))
... ...
@@ -77,7 +81,7 @@ func TestLoadDaemonCliWithConflictingNodeGenericResources(t *testing.T) {
77 77
 }
78 78
 
79 79
 func TestLoadDaemonCliWithConflictingLabels(t *testing.T) {
80
-	opts := defaultOptions("")
80
+	opts := defaultOptions(t, "")
81 81
 	flags := opts.flags
82 82
 
83 83
 	assert.Check(t, flags.Set("label", "foo=bar"))
... ...
@@ -88,7 +92,7 @@ func TestLoadDaemonCliWithConflictingLabels(t *testing.T) {
88 88
 }
89 89
 
90 90
 func TestLoadDaemonCliWithDuplicateLabels(t *testing.T) {
91
-	opts := defaultOptions("")
91
+	opts := defaultOptions(t, "")
92 92
 	flags := opts.flags
93 93
 
94 94
 	assert.Check(t, flags.Set("label", "foo=the-same"))
... ...
@@ -102,7 +106,7 @@ func TestLoadDaemonCliConfigWithTLSVerify(t *testing.T) {
102 102
 	tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": true}`))
103 103
 	defer tempFile.Remove()
104 104
 
105
-	opts := defaultOptions(tempFile.Path())
105
+	opts := defaultOptions(t, tempFile.Path())
106 106
 	opts.TLSOptions.CAFile = "/tmp/ca.pem"
107 107
 
108 108
 	loadedConfig, err := loadDaemonCliConfig(opts)
... ...
@@ -115,7 +119,7 @@ func TestLoadDaemonCliConfigWithExplicitTLSVerifyFalse(t *testing.T) {
115 115
 	tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": false}`))
116 116
 	defer tempFile.Remove()
117 117
 
118
-	opts := defaultOptions(tempFile.Path())
118
+	opts := defaultOptions(t, tempFile.Path())
119 119
 	opts.TLSOptions.CAFile = "/tmp/ca.pem"
120 120
 
121 121
 	loadedConfig, err := loadDaemonCliConfig(opts)
... ...
@@ -128,7 +132,7 @@ func TestLoadDaemonCliConfigWithoutTLSVerify(t *testing.T) {
128 128
 	tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`))
129 129
 	defer tempFile.Remove()
130 130
 
131
-	opts := defaultOptions(tempFile.Path())
131
+	opts := defaultOptions(t, tempFile.Path())
132 132
 	opts.TLSOptions.CAFile = "/tmp/ca.pem"
133 133
 
134 134
 	loadedConfig, err := loadDaemonCliConfig(opts)
... ...
@@ -141,7 +145,7 @@ func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) {
141 141
 	tempFile := fs.NewFile(t, "config", fs.WithContent(`{"log-level": "warn"}`))
142 142
 	defer tempFile.Remove()
143 143
 
144
-	opts := defaultOptions(tempFile.Path())
144
+	opts := defaultOptions(t, tempFile.Path())
145 145
 	loadedConfig, err := loadDaemonCliConfig(opts)
146 146
 	assert.NilError(t, err)
147 147
 	assert.Assert(t, loadedConfig != nil)
... ...
@@ -153,7 +157,7 @@ func TestLoadDaemonConfigWithEmbeddedOptions(t *testing.T) {
153 153
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
154 154
 	defer tempFile.Remove()
155 155
 
156
-	opts := defaultOptions(tempFile.Path())
156
+	opts := defaultOptions(t, tempFile.Path())
157 157
 	loadedConfig, err := loadDaemonCliConfig(opts)
158 158
 	assert.NilError(t, err)
159 159
 	assert.Assert(t, loadedConfig != nil)
... ...
@@ -170,7 +174,7 @@ func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) {
170 170
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
171 171
 	defer tempFile.Remove()
172 172
 
173
-	opts := defaultOptions(tempFile.Path())
173
+	opts := defaultOptions(t, tempFile.Path())
174 174
 	loadedConfig, err := loadDaemonCliConfig(opts)
175 175
 	assert.NilError(t, err)
176 176
 	assert.Assert(t, loadedConfig != nil)
... ...
@@ -15,11 +15,33 @@ import (
15 15
 	"github.com/docker/docker/daemon"
16 16
 	"github.com/docker/docker/daemon/config"
17 17
 	"github.com/docker/docker/libcontainerd/supervisor"
18
+	"github.com/docker/docker/pkg/homedir"
19
+	"github.com/docker/docker/rootless"
18 20
 	"github.com/docker/libnetwork/portallocator"
19 21
 	"golang.org/x/sys/unix"
20 22
 )
21 23
 
22
-const defaultDaemonConfigFile = "/etc/docker/daemon.json"
24
+func getDefaultDaemonConfigDir() (string, error) {
25
+	if !rootless.RunningWithNonRootUsername() {
26
+		return "/etc/docker", nil
27
+	}
28
+	// NOTE: CLI uses ~/.docker while the daemon uses ~/.config/docker, because
29
+	// ~/.docker was not designed to store daemon configurations.
30
+	// In future, the daemon directory may be renamed to ~/.config/moby-engine (?).
31
+	configHome, err := homedir.GetConfigHome()
32
+	if err != nil {
33
+		return "", nil
34
+	}
35
+	return filepath.Join(configHome, "docker"), nil
36
+}
37
+
38
+func getDefaultDaemonConfigFile() (string, error) {
39
+	dir, err := getDefaultDaemonConfigDir()
40
+	if err != nil {
41
+		return "", err
42
+	}
43
+	return filepath.Join(dir, "daemon.json"), nil
44
+}
23 45
 
24 46
 // setDefaultUmask sets the umask to 0022 to avoid problems
25 47
 // caused by custom umask
... ...
@@ -33,8 +55,8 @@ func setDefaultUmask() error {
33 33
 	return nil
34 34
 }
35 35
 
36
-func getDaemonConfDir(_ string) string {
37
-	return "/etc/docker"
36
+func getDaemonConfDir(_ string) (string, error) {
37
+	return getDefaultDaemonConfigDir()
38 38
 }
39 39
 
40 40
 func (cli *DaemonCli) getPlatformContainerdDaemonOpts() ([]supervisor.DaemonOpt, error) {
... ...
@@ -16,7 +16,7 @@ func TestLoadDaemonCliConfigWithDaemonFlags(t *testing.T) {
16 16
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
17 17
 	defer tempFile.Remove()
18 18
 
19
-	opts := defaultOptions(tempFile.Path())
19
+	opts := defaultOptions(t, tempFile.Path())
20 20
 	opts.Debug = true
21 21
 	opts.LogLevel = "info"
22 22
 	assert.Check(t, opts.flags.Set("selinux-enabled", "true"))
... ...
@@ -37,7 +37,7 @@ func TestLoadDaemonConfigWithNetwork(t *testing.T) {
37 37
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
38 38
 	defer tempFile.Remove()
39 39
 
40
-	opts := defaultOptions(tempFile.Path())
40
+	opts := defaultOptions(t, tempFile.Path())
41 41
 	loadedConfig, err := loadDaemonCliConfig(opts)
42 42
 	assert.NilError(t, err)
43 43
 	assert.Assert(t, loadedConfig != nil)
... ...
@@ -54,7 +54,7 @@ func TestLoadDaemonConfigWithMapOptions(t *testing.T) {
54 54
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
55 55
 	defer tempFile.Remove()
56 56
 
57
-	opts := defaultOptions(tempFile.Path())
57
+	opts := defaultOptions(t, tempFile.Path())
58 58
 	loadedConfig, err := loadDaemonCliConfig(opts)
59 59
 	assert.NilError(t, err)
60 60
 	assert.Assert(t, loadedConfig != nil)
... ...
@@ -71,7 +71,7 @@ func TestLoadDaemonConfigWithTrueDefaultValues(t *testing.T) {
71 71
 	tempFile := fs.NewFile(t, "config", fs.WithContent(content))
72 72
 	defer tempFile.Remove()
73 73
 
74
-	opts := defaultOptions(tempFile.Path())
74
+	opts := defaultOptions(t, tempFile.Path())
75 75
 	loadedConfig, err := loadDaemonCliConfig(opts)
76 76
 	assert.NilError(t, err)
77 77
 	assert.Assert(t, loadedConfig != nil)
... ...
@@ -90,7 +90,7 @@ func TestLoadDaemonConfigWithTrueDefaultValuesLeaveDefaults(t *testing.T) {
90 90
 	tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`))
91 91
 	defer tempFile.Remove()
92 92
 
93
-	opts := defaultOptions(tempFile.Path())
93
+	opts := defaultOptions(t, tempFile.Path())
94 94
 	loadedConfig, err := loadDaemonCliConfig(opts)
95 95
 	assert.NilError(t, err)
96 96
 	assert.Assert(t, loadedConfig != nil)
... ...
@@ -12,15 +12,17 @@ import (
12 12
 	"golang.org/x/sys/windows"
13 13
 )
14 14
 
15
-var defaultDaemonConfigFile = ""
15
+func getDefaultDaemonConfigFile() (string, error) {
16
+	return "", nil
17
+}
16 18
 
17 19
 // setDefaultUmask doesn't do anything on windows
18 20
 func setDefaultUmask() error {
19 21
 	return nil
20 22
 }
21 23
 
22
-func getDaemonConfDir(root string) string {
23
-	return filepath.Join(root, `\config`)
24
+func getDaemonConfDir(root string) (string, error) {
25
+	return filepath.Join(root, `\config`), nil
24 26
 }
25 27
 
26 28
 // preNotifySystem sends a message to the host when the API is active, but before the daemon is
... ...
@@ -16,7 +16,7 @@ import (
16 16
 	"github.com/spf13/cobra"
17 17
 )
18 18
 
19
-func newDaemonCommand() *cobra.Command {
19
+func newDaemonCommand() (*cobra.Command, error) {
20 20
 	opts := newDaemonOptions(config.New())
21 21
 
22 22
 	cmd := &cobra.Command{
... ...
@@ -36,12 +36,18 @@ func newDaemonCommand() *cobra.Command {
36 36
 
37 37
 	flags := cmd.Flags()
38 38
 	flags.BoolP("version", "v", false, "Print version information and quit")
39
+	defaultDaemonConfigFile, err := getDefaultDaemonConfigFile()
40
+	if err != nil {
41
+		return nil, err
42
+	}
39 43
 	flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "Daemon configuration file")
40 44
 	opts.InstallFlags(flags)
41
-	installConfigFlags(opts.daemonConfig, flags)
45
+	if err := installConfigFlags(opts.daemonConfig, flags); err != nil {
46
+		return nil, err
47
+	}
42 48
 	installServiceFlags(flags)
43 49
 
44
-	return cmd
50
+	return cmd, nil
45 51
 }
46 52
 
47 53
 func init() {
... ...
@@ -72,10 +78,17 @@ func main() {
72 72
 		logrus.SetOutput(stderr)
73 73
 	}
74 74
 
75
-	cmd := newDaemonCommand()
76
-	cmd.SetOutput(stdout)
77
-	if err := cmd.Execute(); err != nil {
75
+	onError := func(err error) {
78 76
 		fmt.Fprintf(stderr, "%s\n", err)
79 77
 		os.Exit(1)
80 78
 	}
79
+
80
+	cmd, err := newDaemonCommand()
81
+	if err != nil {
82
+		onError(err)
83
+	}
84
+	cmd.SetOutput(stdout)
85
+	if err := cmd.Execute(); err != nil {
86
+		onError(err)
87
+	}
81 88
 }
... ...
@@ -49,6 +49,8 @@ func newDaemonOptions(config *config.Config) *daemonOptions {
49 49
 // InstallFlags adds flags for the common options on the FlagSet
50 50
 func (o *daemonOptions) InstallFlags(flags *pflag.FlagSet) {
51 51
 	if dockerCertPath == "" {
52
+		// cliconfig.Dir returns $DOCKER_CONFIG or ~/.docker.
53
+		// cliconfig.Dir does not look up $XDG_CONFIG_HOME
52 54
 		dockerCertPath = cliconfig.Dir()
53 55
 	}
54 56
 
55 57
new file mode 100755
... ...
@@ -0,0 +1,77 @@
0
+#!/bin/sh
1
+# dockerd-rootless.sh executes dockerd in rootless mode.
2
+#
3
+# Usage: dockerd-rootless.sh --experimental [DOCKERD_OPTIONS]
4
+# Currently, specifying --experimental is mandatory.
5
+#
6
+# External dependencies:
7
+# * newuidmap and newgidmap needs to be installed.
8
+# * /etc/subuid and /etc/subgid needs to be configured for the current user.
9
+# * Either slirp4netns (v0.3+) or VPNKit needs to be installed.
10
+#
11
+# See the documentation for the further information.
12
+
13
+set -e -x
14
+if ! [ -w $XDG_RUNTIME_DIR ]; then
15
+	echo "XDG_RUNTIME_DIR needs to be set and writable"
16
+	exit 1
17
+fi
18
+if ! [ -w $HOME ]; then
19
+	echo "HOME needs to be set and writable"
20
+	exit 1
21
+fi
22
+
23
+rootlesskit=""
24
+for f in docker-rootlesskit rootlesskit; do
25
+	if which $f >/dev/null 2>&1; then
26
+		rootlesskit=$f
27
+		break
28
+	fi
29
+done
30
+if [ -z $rootlesskit ]; then
31
+	echo "rootlesskit needs to be installed"
32
+	exit 1
33
+fi
34
+
35
+net=""
36
+mtu=""
37
+if which slirp4netns >/dev/null 2>&1; then
38
+	if slirp4netns --help | grep -- --disable-host-loopback; then
39
+		net=slirp4netns
40
+		mtu=65520
41
+	else
42
+		echo "slirp4netns does not support --disable-host-loopback. Falling back to VPNKit."
43
+	fi
44
+fi
45
+if [ -z $net ]; then
46
+	if which vpnkit >/dev/null 2>&1; then
47
+		net=vpnkit
48
+		mtu=1500
49
+	else
50
+		echo "Either slirp4netns (v0.3+) or vpnkit needs to be installed"
51
+		exit 1
52
+	fi
53
+fi
54
+
55
+if [ -z $_DOCKERD_ROOTLESS_CHILD ]; then
56
+	_DOCKERD_ROOTLESS_CHILD=1
57
+	export _DOCKERD_ROOTLESS_CHILD
58
+	# Re-exec the script via RootlessKit, so as to create unprivileged {user,mount,network} namespaces.
59
+	#
60
+	# --copy-up allows removing/creating files in the directories by creating tmpfs and symlinks
61
+	# * /etc: copy-up is required so as to prevent `/etc/resolv.conf` in the
62
+	#         namespace from being unexpectedly unmounted when `/etc/resolv.conf` is recreated on the host
63
+	#         (by either systemd-networkd or NetworkManager)
64
+	# * /run: copy-up is required so that we can create /run/docker (hardcoded for plugins) in our namespace
65
+	$rootlesskit \
66
+		--net=$net --mtu=$mtu --disable-host-loopback \
67
+		--copy-up=/etc --copy-up=/run \
68
+		$DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS \
69
+		$0 $@
70
+else
71
+	[ $_DOCKERD_ROOTLESS_CHILD = 1 ]
72
+	# remove the symlinks for the existing files in the parent namespace if any,
73
+	# so that we can create our own files in our mount namespace.
74
+	rm -f /run/docker /run/xtables.lock
75
+	dockerd $@
76
+fi
... ...
@@ -39,6 +39,7 @@ type Config struct {
39 39
 	IpcMode              string                   `json:"default-ipc-mode,omitempty"`
40 40
 	// ResolvConf is the path to the configuration of the host resolver
41 41
 	ResolvConf string `json:"resolv-conf,omitempty"`
42
+	Rootless   bool   `json:"rootless,omitempty"`
42 43
 }
43 44
 
44 45
 // BridgeConfig stores all the bridge driver specific
... ...
@@ -87,3 +88,8 @@ func verifyDefaultIpcMode(mode string) error {
87 87
 func (conf *Config) ValidatePlatformConfig() error {
88 88
 	return verifyDefaultIpcMode(conf.IpcMode)
89 89
 }
90
+
91
+// IsRootless returns conf.Rootless
92
+func (conf *Config) IsRootless() bool {
93
+	return conf.Rootless
94
+}
... ...
@@ -55,3 +55,8 @@ func (conf *Config) IsSwarmCompatible() error {
55 55
 func (conf *Config) ValidatePlatformConfig() error {
56 56
 	return nil
57 57
 }
58
+
59
+// IsRootless returns conf.Rootless on Unix but false on Windows
60
+func (conf *Config) IsRootless() bool {
61
+	return false
62
+}
... ...
@@ -800,6 +800,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
800 800
 		logrus.Warnf("Failed to configure golang's threads limit: %v", err)
801 801
 	}
802 802
 
803
+	// ensureDefaultAppArmorProfile does nothing if apparmor is disabled
803 804
 	if err := ensureDefaultAppArmorProfile(); err != nil {
804 805
 		logrus.Errorf(err.Error())
805 806
 	}
... ...
@@ -745,9 +745,6 @@ func verifyDaemonSettings(conf *config.Config) error {
745 745
 
746 746
 // checkSystem validates platform-specific requirements
747 747
 func checkSystem() error {
748
-	if os.Geteuid() != 0 {
749
-		return fmt.Errorf("The Docker daemon needs to be run as root")
750
-	}
751 748
 	return checkKernel()
752 749
 }
753 750
 
... ...
@@ -175,6 +175,9 @@ func (daemon *Daemon) fillSecurityOptions(v *types.Info, sysInfo *sysinfo.SysInf
175 175
 	if rootIDs := daemon.idMapping.RootPair(); rootIDs.UID != 0 || rootIDs.GID != 0 {
176 176
 		securityOptions = append(securityOptions, "name=userns")
177 177
 	}
178
+	if daemon.configStoreRootless() {
179
+		securityOptions = append(securityOptions, "name=rootless")
180
+	}
178 181
 	v.SecurityOptions = securityOptions
179 182
 }
180 183
 
... ...
@@ -245,3 +245,7 @@ func parseRuncVersion(v string) (version string, commit string, err error) {
245 245
 	}
246 246
 	return version, commit, err
247 247
 }
248
+
249
+func (daemon *Daemon) configStoreRootless() bool {
250
+	return daemon.configStore.Rootless
251
+}
... ...
@@ -13,3 +13,7 @@ func (daemon *Daemon) fillPlatformVersion(v *types.Version) {}
13 13
 
14 14
 func fillDriverWarnings(v *types.Info) {
15 15
 }
16
+
17
+func (daemon *Daemon) configStoreRootless() bool {
18
+	return false
19
+}
... ...
@@ -8,6 +8,7 @@ import (
8 8
 	"strconv"
9 9
 
10 10
 	"github.com/coreos/go-systemd/activation"
11
+	"github.com/docker/docker/pkg/homedir"
11 12
 	"github.com/docker/go-connections/sockets"
12 13
 	"github.com/sirupsen/logrus"
13 14
 )
... ...
@@ -45,6 +46,10 @@ func Init(proto, addr, socketGroup string, tlsConfig *tls.Config) ([]net.Listene
45 45
 		if err != nil {
46 46
 			return nil, fmt.Errorf("can't create unix socket %s: %v", addr, err)
47 47
 		}
48
+		if _, err := homedir.StickRuntimeDirContents([]string{addr}); err != nil {
49
+			// StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset
50
+			logrus.WithError(err).Warnf("cannot set sticky bit on socket %s under XDG_RUNTIME_DIR", addr)
51
+		}
48 52
 		ls = append(ls, l)
49 53
 	default:
50 54
 		return nil, fmt.Errorf("invalid protocol format: %q", proto)
... ...
@@ -17,10 +17,12 @@ import (
17 17
 	"github.com/docker/docker/oci/caps"
18 18
 	"github.com/docker/docker/pkg/idtools"
19 19
 	"github.com/docker/docker/pkg/mount"
20
+	"github.com/docker/docker/rootless/specconv"
20 21
 	volumemounts "github.com/docker/docker/volume/mounts"
21 22
 	"github.com/opencontainers/runc/libcontainer/apparmor"
22 23
 	"github.com/opencontainers/runc/libcontainer/cgroups"
23 24
 	"github.com/opencontainers/runc/libcontainer/devices"
25
+	rsystem "github.com/opencontainers/runc/libcontainer/system"
24 26
 	"github.com/opencontainers/runc/libcontainer/user"
25 27
 	"github.com/opencontainers/runtime-spec/specs-go"
26 28
 	"github.com/pkg/errors"
... ...
@@ -89,7 +91,7 @@ func setDevices(s *specs.Spec, c *container.Container) error {
89 89
 	// Build lists of devices allowed and created within the container.
90 90
 	var devs []specs.LinuxDevice
91 91
 	devPermissions := s.Linux.Resources.Devices
92
-	if c.HostConfig.Privileged {
92
+	if c.HostConfig.Privileged && !rsystem.RunningInUserNS() {
93 93
 		hostDevices, err := devices.HostDevices()
94 94
 		if err != nil {
95 95
 			return err
... ...
@@ -867,6 +869,11 @@ func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, e
867 867
 		s.Linux.ReadonlyPaths = c.HostConfig.ReadonlyPaths
868 868
 	}
869 869
 
870
+	if daemon.configStore.Rootless {
871
+		if err := specconv.ToRootless(&s); err != nil {
872
+			return nil, err
873
+		}
874
+	}
870 875
 	return &s, nil
871 876
 }
872 877
 
873 878
new file mode 100644
... ...
@@ -0,0 +1,92 @@
0
+# Rootless mode (Experimental)
1
+
2
+The rootless mode allows running `dockerd` as an unprivileged user, using `user_namespaces(7)`, `mount_namespaces(7)`, `network_namespaces(7)`.
3
+
4
+No SETUID/SETCAP binary is required except `newuidmap` and `newgidmap`.
5
+
6
+## Requirements
7
+* `newuidmap` and `newgidmap` need to be installed on the host. These commands are provided by the `uidmap` package on most distros.
8
+
9
+* `/etc/subuid` and `/etc/subgid` should contain >= 65536 sub-IDs. e.g. `penguin:231072:65536`.
10
+
11
+```console
12
+$ id -u
13
+1001
14
+$ whoami
15
+penguin
16
+$ grep ^$(whoami): /etc/subuid
17
+penguin:231072:65536
18
+$ grep ^$(whoami): /etc/subgid
19
+penguin:231072:65536
20
+```
21
+
22
+* Either [slirp4netns](https://github.com/rootless-containers/slirp4netns) (v0.3+) or [VPNKit](https://github.com/moby/vpnkit) needs to be installed. slirp4netns is preferred for the best performance.
23
+
24
+### Distribution-specific hint
25
+
26
+#### Debian (excluding Ubuntu)
27
+* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required
28
+
29
+#### Arch Linux
30
+* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required
31
+
32
+#### openSUSE
33
+* `sudo modprobe ip_tables iptable_mangle iptable_nat iptable_filter` is required. (This is likely to be required on other distros as well)
34
+
35
+#### RHEL/CentOS 7
36
+* `sudo sh -c "echo 28633 > /proc/sys/user/max_user_namespaces"` is required
37
+* [COPR package `vbatts/shadow-utils-newxidmap`](https://copr.fedorainfracloud.org/coprs/vbatts/shadow-utils-newxidmap/) needs to be installed
38
+
39
+## Restrictions
40
+
41
+* Only `vfs` graphdriver is supported. However, on [Ubuntu](http://kernel.ubuntu.com/git/ubuntu/ubuntu-artful.git/commit/fs/overlayfs?h=Ubuntu-4.13.0-25.29&id=0a414bdc3d01f3b61ed86cfe3ce8b63a9240eba7) and a few distros, `overlay2` and `overlay` are also supported.
42
+* Following features are not supported:
43
+  * Cgroups (including `docker top`, which depends on the cgroups device controller)
44
+  * Apparmor
45
+  * Checkpoint
46
+  * Overlay network
47
+
48
+## Usage
49
+
50
+### Daemon
51
+
52
+You need to run `dockerd-rootless.sh` instead of `dockerd`.
53
+
54
+```console
55
+$ dockerd-rootless.sh --experimental"
56
+```
57
+As Rootless mode is experimental per se, currently you always need to run `dockerd-rootless.sh` with `--experimental`.
58
+
59
+Remarks:
60
+* The socket path is set to `$XDG_RUNTIME_DIR/docker.sock` by default. `$XDG_RUNTIME_DIR` is typically set to `/run/user/$UID`.
61
+* The data dir is set to `~/.local/share/docker` by default.
62
+* The exec dir is set to `$XDG_RUNTIME_DIR/docker` by default.
63
+* The daemon config dir is set to `~/.config/docker` (not `~/.docker`, which is used by the client) by default.
64
+* The `dockerd-rootless.sh` script executes `dockerd` in its own user, mount, and network namespaces. You can enter the namespaces by running `nsenter -U --preserve-credentials -n -m -t $(cat $XDG_RUNTIME_DIR/docker.pid)`.
65
+
66
+### Client
67
+
68
+You can just use the upstream Docker client but you need to set the socket path explicitly.
69
+
70
+```console
71
+$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d nginx
72
+```
73
+
74
+### Exposing ports
75
+
76
+In addition to exposing container ports to the `dockerd` network namespace, you also need to expose the ports in the `dockerd` network namespace to the host network namespace.
77
+
78
+```console
79
+$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d -p 80:80 nginx
80
+$ socat -t -- TCP-LISTEN:8080,reuseaddr,fork EXEC:"nsenter -U -n -t $(cat $XDG_RUNTIME_DIR/docker.pid) socat -t -- STDIN TCP4\:127.0.0.1\:80"
81
+```
82
+
83
+In future, `dockerd` will be able to expose the ports automatically.
84
+
85
+### Routing ping packets
86
+
87
+To route ping packets, you need to set up `net.ipv4.ping_group_range` properly as the root.
88
+
89
+```console
90
+$ sudo sh -c "echo 0   2147483647  > /proc/sys/net/ipv4/ping_group_range"
91
+```
0 92
new file mode 100755
... ...
@@ -0,0 +1,34 @@
0
+#!/bin/sh
1
+
2
+# v0.3.0-alpha.0
3
+ROOTLESSKIT_COMMIT=3c4582e950e3a67795c2832179c125b258b78124
4
+
5
+install_rootlesskit() {
6
+	case "$1" in
7
+	"dynamic")
8
+		install_rootlesskit_dynamic
9
+		return
10
+		;;
11
+	"")
12
+		export CGO_ENABLED=0
13
+		_install_rootlesskit
14
+		;;
15
+	*)
16
+		echo 'Usage: $0 [dynamic]'
17
+		;;
18
+	esac
19
+}
20
+
21
+install_rootlesskit_dynamic() {
22
+	export ROOTLESSKIT_LDFLAGS="-linkmode=external" install_rootlesskit
23
+	export BUILD_MODE="-buildmode=pie"
24
+	_install_rootlesskit
25
+}
26
+
27
+_install_rootlesskit() {
28
+	echo "Install rootlesskit version $ROOTLESSKIT_COMMIT"
29
+	git clone https://github.com/rootless-containers/rootlesskit.git "$GOPATH/src/github.com/rootless-containers/rootlesskit"
30
+	cd "$GOPATH/src/github.com/rootless-containers/rootlesskit"
31
+	git checkout -q "$ROOTLESSKIT_COMMIT"
32
+	go build $BUILD_MODE -ldflags="$ROOTLESSKIT_LDFLAGS" -o "${PREFIX}/rootlesskit" github.com/rootless-containers/rootlesskit/cmd/rootlesskit
33
+}
... ...
@@ -7,3 +7,5 @@ DOCKER_CONTAINERD_CTR_BINARY_NAME='ctr'
7 7
 DOCKER_CONTAINERD_SHIM_BINARY_NAME='containerd-shim'
8 8
 DOCKER_PROXY_BINARY_NAME='docker-proxy'
9 9
 DOCKER_INIT_BINARY_NAME='docker-init'
10
+DOCKER_ROOTLESSKIT_BINARY_NAME='rootlesskit'
11
+DOCKER_DAEMON_ROOTLESS_SH_BINARY_NAME='dockerd-rootless.sh'
... ...
@@ -14,7 +14,7 @@ copy_binaries() {
14 14
 		return
15 15
 	fi
16 16
 	echo "Copying nested executables into $dir"
17
-	for file in containerd containerd-shim ctr runc docker-init docker-proxy; do
17
+	for file in containerd containerd-shim ctr runc docker-init docker-proxy rootlesskit dockerd-rootless.sh; do
18 18
 		cp -f `which "$file"` "$dir/"
19 19
 		if [ "$hash" == "hash" ]; then
20 20
 			hash_files "$dir/$file"
... ...
@@ -26,4 +26,6 @@ install_binary() {
26 26
 	install_binary "${DEST}/${DOCKER_CONTAINERD_SHIM_BINARY_NAME}"
27 27
 	install_binary "${DEST}/${DOCKER_PROXY_BINARY_NAME}"
28 28
 	install_binary "${DEST}/${DOCKER_INIT_BINARY_NAME}"
29
+	install_binary "${DEST}/${DOCKER_ROOTLESSKIT_BINARY_NAME}"
30
+	install_binary "${DEST}/${DOCKER_DAEMON_ROOTLESS_SH_BINARY_NAME}"
29 31
 )
... ...
@@ -4,8 +4,11 @@ import (
4 4
 	"fmt"
5 5
 	"net"
6 6
 	"net/url"
7
+	"path/filepath"
7 8
 	"strconv"
8 9
 	"strings"
10
+
11
+	"github.com/docker/docker/pkg/homedir"
9 12
 )
10 13
 
11 14
 var (
... ...
@@ -41,12 +44,20 @@ func ValidateHost(val string) (string, error) {
41 41
 	return val, nil
42 42
 }
43 43
 
44
-// ParseHost and set defaults for a Daemon host string
45
-func ParseHost(defaultToTLS bool, val string) (string, error) {
44
+// ParseHost and set defaults for a Daemon host string.
45
+// defaultToTLS is preferred over defaultToUnixRootless.
46
+func ParseHost(defaultToTLS, defaultToUnixRootless bool, val string) (string, error) {
46 47
 	host := strings.TrimSpace(val)
47 48
 	if host == "" {
48 49
 		if defaultToTLS {
49 50
 			host = DefaultTLSHost
51
+		} else if defaultToUnixRootless {
52
+			runtimeDir, err := homedir.GetRuntimeDir()
53
+			if err != nil {
54
+				return "", err
55
+			}
56
+			socket := filepath.Join(runtimeDir, "docker.sock")
57
+			host = "unix://" + socket
50 58
 		} else {
51 59
 			host = DefaultHost
52 60
 		}
... ...
@@ -38,13 +38,13 @@ func TestParseHost(t *testing.T) {
38 38
 	}
39 39
 
40 40
 	for _, value := range invalid {
41
-		if _, err := ParseHost(false, value); err == nil {
41
+		if _, err := ParseHost(false, false, value); err == nil {
42 42
 			t.Errorf("Expected an error for %v, got [nil]", value)
43 43
 		}
44 44
 	}
45 45
 
46 46
 	for value, expected := range valid {
47
-		if actual, err := ParseHost(false, value); err != nil || actual != expected {
47
+		if actual, err := ParseHost(false, false, value); err != nil || actual != expected {
48 48
 			t.Errorf("Expected for %v [%v], got [%v, %v]", value, expected, actual, err)
49 49
 		}
50 50
 	}
... ...
@@ -1,7 +1,10 @@
1 1
 package homedir // import "github.com/docker/docker/pkg/homedir"
2 2
 
3 3
 import (
4
+	"errors"
4 5
 	"os"
6
+	"path/filepath"
7
+	"strings"
5 8
 
6 9
 	"github.com/docker/docker/pkg/idtools"
7 10
 )
... ...
@@ -19,3 +22,88 @@ func GetStatic() (string, error) {
19 19
 	}
20 20
 	return usr.Home, nil
21 21
 }
22
+
23
+// GetRuntimeDir returns XDG_RUNTIME_DIR.
24
+// XDG_RUNTIME_DIR is typically configured via pam_systemd.
25
+// GetRuntimeDir returns non-nil error if XDG_RUNTIME_DIR is not set.
26
+//
27
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
28
+func GetRuntimeDir() (string, error) {
29
+	if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" {
30
+		return xdgRuntimeDir, nil
31
+	}
32
+	return "", errors.New("could not get XDG_RUNTIME_DIR")
33
+}
34
+
35
+// StickRuntimeDirContents sets the sticky bit on files that are under
36
+// XDG_RUNTIME_DIR, so that the files won't be periodically removed by the system.
37
+//
38
+// StickyRuntimeDir returns slice of sticked files.
39
+// StickyRuntimeDir returns nil error if XDG_RUNTIME_DIR is not set.
40
+//
41
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
42
+func StickRuntimeDirContents(files []string) ([]string, error) {
43
+	runtimeDir, err := GetRuntimeDir()
44
+	if err != nil {
45
+		// ignore error if runtimeDir is empty
46
+		return nil, nil
47
+	}
48
+	runtimeDir, err = filepath.Abs(runtimeDir)
49
+	if err != nil {
50
+		return nil, err
51
+	}
52
+	var sticked []string
53
+	for _, f := range files {
54
+		f, err = filepath.Abs(f)
55
+		if err != nil {
56
+			return sticked, err
57
+		}
58
+		if strings.HasPrefix(f, runtimeDir+"/") {
59
+			if err = stick(f); err != nil {
60
+				return sticked, err
61
+			}
62
+			sticked = append(sticked, f)
63
+		}
64
+	}
65
+	return sticked, nil
66
+}
67
+
68
+func stick(f string) error {
69
+	st, err := os.Stat(f)
70
+	if err != nil {
71
+		return err
72
+	}
73
+	m := st.Mode()
74
+	m |= os.ModeSticky
75
+	return os.Chmod(f, m)
76
+}
77
+
78
+// GetDataHome returns XDG_DATA_HOME.
79
+// GetDataHome returns $HOME/.local/share and nil error if XDG_DATA_HOME is not set.
80
+//
81
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
82
+func GetDataHome() (string, error) {
83
+	if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" {
84
+		return xdgDataHome, nil
85
+	}
86
+	home := os.Getenv("HOME")
87
+	if home == "" {
88
+		return "", errors.New("could not get either XDG_DATA_HOME or HOME")
89
+	}
90
+	return filepath.Join(home, ".local", "share"), nil
91
+}
92
+
93
+// GetConfigHome returns XDG_CONFIG_HOME.
94
+// GetConfigHome returns $HOME/.config and nil error if XDG_CONFIG_HOME is not set.
95
+//
96
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
97
+func GetConfigHome() (string, error) {
98
+	if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
99
+		return xdgConfigHome, nil
100
+	}
101
+	home := os.Getenv("HOME")
102
+	if home == "" {
103
+		return "", errors.New("could not get either XDG_CONFIG_HOME or HOME")
104
+	}
105
+	return filepath.Join(home, ".config"), nil
106
+}
... ...
@@ -11,3 +11,23 @@ import (
11 11
 func GetStatic() (string, error) {
12 12
 	return "", errors.New("homedir.GetStatic() is not supported on this system")
13 13
 }
14
+
15
+// GetRuntimeDir is unsupported on non-linux system.
16
+func GetRuntimeDir() (string, error) {
17
+	return "", errors.New("homedir.GetRuntimeDir() is not supported on this system")
18
+}
19
+
20
+// StickRuntimeDirContents is unsupported on non-linux system.
21
+func StickRuntimeDirContents(files []string) ([]string, error) {
22
+	return nil, errors.New("homedir.StickRuntimeDirContents() is not supported on this system")
23
+}
24
+
25
+// GetDataHome is unsupported on non-linux system.
26
+func GetDataHome() (string, error) {
27
+	return "", errors.New("homedir.GetDataHome() is not supported on this system")
28
+}
29
+
30
+// GetConfigHome is unsupported on non-linux system.
31
+func GetConfigHome() (string, error) {
32
+	return "", errors.New("homedir.GetConfigHome() is not supported on this system")
33
+}
... ...
@@ -51,7 +51,9 @@ func New(quiet bool) *SysInfo {
51 51
 
52 52
 	// Check if AppArmor is supported.
53 53
 	if _, err := os.Stat("/sys/kernel/security/apparmor"); !os.IsNotExist(err) {
54
-		sysInfo.AppArmor = true
54
+		if _, err := ioutil.ReadFile("/sys/kernel/security/apparmor/profiles"); err == nil {
55
+			sysInfo.AppArmor = true
56
+		}
55 57
 	}
56 58
 
57 59
 	// Check if Seccomp is supported, via CONFIG_SECCOMP.
58 60
new file mode 100644
... ...
@@ -0,0 +1,26 @@
0
+package rootless
1
+
2
+import (
3
+	"os"
4
+	"sync"
5
+)
6
+
7
+var (
8
+	runningWithNonRootUsername     bool
9
+	runningWithNonRootUsernameOnce sync.Once
10
+)
11
+
12
+// RunningWithNonRootUsername returns true if we $USER is set to a non-root value,
13
+// regardless to the UID/EUID value.
14
+//
15
+// The value of this variable is mostly used for configuring default paths.
16
+// If the value is true, $HOME and $XDG_RUNTIME_DIR should be honored for setting up the default paths.
17
+// If false (not only EUID==0 but also $USER==root), $HOME and $XDG_RUNTIME_DIR should be ignored
18
+// even if we are in a user namespace.
19
+func RunningWithNonRootUsername() bool {
20
+	runningWithNonRootUsernameOnce.Do(func() {
21
+		u := os.Getenv("USER")
22
+		runningWithNonRootUsername = u != "" && u != "root"
23
+	})
24
+	return runningWithNonRootUsername
25
+}
0 26
new file mode 100644
... ...
@@ -0,0 +1,38 @@
0
+package specconv
1
+
2
+import (
3
+	"io/ioutil"
4
+	"strconv"
5
+
6
+	"github.com/opencontainers/runtime-spec/specs-go"
7
+)
8
+
9
+// ToRootless converts spec to be compatible with "rootless" runc.
10
+// * Remove cgroups (will be supported in separate PR when delegation permission is configured)
11
+// * Fix up OOMScoreAdj
12
+func ToRootless(spec *specs.Spec) error {
13
+	return toRootless(spec, getCurrentOOMScoreAdj())
14
+}
15
+
16
+func getCurrentOOMScoreAdj() int {
17
+	b, err := ioutil.ReadFile("/proc/self/oom_score_adj")
18
+	if err != nil {
19
+		return 0
20
+	}
21
+	i, err := strconv.Atoi(string(b))
22
+	if err != nil {
23
+		return 0
24
+	}
25
+	return i
26
+}
27
+
28
+func toRootless(spec *specs.Spec, currentOOMScoreAdj int) error {
29
+	// Remove cgroup settings.
30
+	spec.Linux.Resources = nil
31
+	spec.Linux.CgroupsPath = ""
32
+
33
+	if spec.Process.OOMScoreAdj != nil && *spec.Process.OOMScoreAdj < currentOOMScoreAdj {
34
+		*spec.Process.OOMScoreAdj = currentOOMScoreAdj
35
+	}
36
+	return nil
37
+}