Browse code

Windows: Add named pipe mount support

Current insider builds of Windows have support for mounting individual
named pipe servers from the host to the guest. This allows, for example,
exposing the docker engine's named pipe to a container.

This change allows the user to request such a mount via the normal bind
mount syntax in the CLI:

docker run -v \\.\pipe\docker_engine:\\.\pipe\docker_engine <args>

Signed-off-by: John Starks <jostarks@microsoft.com>

John Starks authored on 2017/08/01 06:23:52
Showing 10 changed files
... ...
@@ -15,6 +15,8 @@ const (
15 15
 	TypeVolume Type = "volume"
16 16
 	// TypeTmpfs is the type for mounting tmpfs
17 17
 	TypeTmpfs Type = "tmpfs"
18
+	// TypeNamedPipe is the type for mounting Windows named pipes
19
+	TypeNamedPipe Type = "npipe"
18 20
 )
19 21
 
20 22
 // Mount represents a mount (volume).
21 23
new file mode 100644
... ...
@@ -0,0 +1,71 @@
0
+// +build windows
1
+
2
+package main
3
+
4
+import (
5
+	"fmt"
6
+	"io/ioutil"
7
+	"math/rand"
8
+	"net/http"
9
+	"strings"
10
+
11
+	winio "github.com/Microsoft/go-winio"
12
+	"github.com/docker/docker/integration-cli/checker"
13
+	"github.com/docker/docker/integration-cli/request"
14
+	"github.com/go-check/check"
15
+)
16
+
17
+func (s *DockerSuite) TestContainersAPICreateMountsBindNamedPipe(c *check.C) {
18
+	testRequires(c, SameHostDaemon, DaemonIsWindowsAtLeastBuild(16210)) // Named pipe support was added in RS3
19
+
20
+	// Create a host pipe to map into the container
21
+	hostPipeName := fmt.Sprintf(`\\.\pipe\docker-cli-test-pipe-%x`, rand.Uint64())
22
+	pc := &winio.PipeConfig{
23
+		SecurityDescriptor: "D:P(A;;GA;;;AU)", // Allow all users access to the pipe
24
+	}
25
+	l, err := winio.ListenPipe(hostPipeName, pc)
26
+	if err != nil {
27
+		c.Fatal(err)
28
+	}
29
+	defer l.Close()
30
+
31
+	// Asynchronously read data that the container writes to the mapped pipe.
32
+	var b []byte
33
+	ch := make(chan error)
34
+	go func() {
35
+		conn, err := l.Accept()
36
+		if err == nil {
37
+			b, err = ioutil.ReadAll(conn)
38
+			conn.Close()
39
+		}
40
+		ch <- err
41
+	}()
42
+
43
+	containerPipeName := `\\.\pipe\docker-cli-test-pipe`
44
+	text := "hello from a pipe"
45
+	cmd := fmt.Sprintf("echo %s > %s", text, containerPipeName)
46
+
47
+	name := "test-bind-npipe"
48
+	data := map[string]interface{}{
49
+		"Image":      testEnv.MinimalBaseImage(),
50
+		"Cmd":        []string{"cmd", "/c", cmd},
51
+		"HostConfig": map[string]interface{}{"Mounts": []map[string]interface{}{{"Type": "npipe", "Source": hostPipeName, "Target": containerPipeName}}},
52
+	}
53
+
54
+	status, resp, err := request.SockRequest("POST", "/containers/create?name="+name, data, daemonHost())
55
+	c.Assert(err, checker.IsNil, check.Commentf(string(resp)))
56
+	c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(resp)))
57
+
58
+	status, _, err = request.SockRequest("POST", "/containers/"+name+"/start", nil, daemonHost())
59
+	c.Assert(err, checker.IsNil)
60
+	c.Assert(status, checker.Equals, http.StatusNoContent)
61
+
62
+	err = <-ch
63
+	if err != nil {
64
+		c.Fatal(err)
65
+	}
66
+	result := strings.TrimSpace(string(b))
67
+	if result != text {
68
+		c.Errorf("expected pipe to contain %s, got %s", text, result)
69
+	}
70
+}
... ...
@@ -4610,10 +4610,7 @@ func (s *DockerSuite) TestRunAddDeviceCgroupRule(c *check.C) {
4610 4610
 
4611 4611
 // Verifies that running as local system is operating correctly on Windows
4612 4612
 func (s *DockerSuite) TestWindowsRunAsSystem(c *check.C) {
4613
-	testRequires(c, DaemonIsWindows)
4614
-	if testEnv.DaemonKernelVersionNumeric() < 15000 {
4615
-		c.Skip("Requires build 15000 or later")
4616
-	}
4613
+	testRequires(c, DaemonIsWindowsAtLeastBuild(15000))
4617 4614
 	out, _ := dockerCmd(c, "run", "--net=none", `--user=nt authority\system`, "--hostname=XYZZY", minimalBaseImage(), "cmd", "/c", `@echo %USERNAME%`)
4618 4615
 	c.Assert(strings.TrimSpace(out), checker.Equals, "XYZZY$")
4619 4616
 }
... ...
@@ -37,6 +37,12 @@ func DaemonIsWindows() bool {
37 37
 	return PlatformIs("windows")
38 38
 }
39 39
 
40
+func DaemonIsWindowsAtLeastBuild(buildNumber int) func() bool {
41
+	return func() bool {
42
+		return DaemonIsWindows() && testEnv.DaemonKernelVersionNumeric() >= buildNumber
43
+	}
44
+}
45
+
40 46
 func DaemonIsLinux() bool {
41 47
 	return PlatformIs("linux")
42 48
 }
... ...
@@ -16,6 +16,7 @@ import (
16 16
 
17 17
 	"github.com/Microsoft/hcsshim"
18 18
 	"github.com/docker/docker/pkg/sysinfo"
19
+	"github.com/docker/docker/pkg/system"
19 20
 	opengcs "github.com/jhowardmsft/opengcs/gogcs/client"
20 21
 	specs "github.com/opencontainers/runtime-spec/specs-go"
21 22
 	"github.com/sirupsen/logrus"
... ...
@@ -230,20 +231,35 @@ func (clnt *client) createWindows(containerID string, checkpoint string, checkpo
230 230
 	}
231 231
 
232 232
 	// Add the mounts (volumes, bind mounts etc) to the structure
233
-	mds := make([]hcsshim.MappedDir, len(spec.Mounts))
234
-	for i, mount := range spec.Mounts {
235
-		mds[i] = hcsshim.MappedDir{
236
-			HostPath:      mount.Source,
237
-			ContainerPath: mount.Destination,
238
-			ReadOnly:      false,
239
-		}
240
-		for _, o := range mount.Options {
241
-			if strings.ToLower(o) == "ro" {
242
-				mds[i].ReadOnly = true
233
+	var mds []hcsshim.MappedDir
234
+	var mps []hcsshim.MappedPipe
235
+	for _, mount := range spec.Mounts {
236
+		const pipePrefix = `\\.\pipe\`
237
+		if strings.HasPrefix(mount.Destination, pipePrefix) {
238
+			mp := hcsshim.MappedPipe{
239
+				HostPath:          mount.Source,
240
+				ContainerPipeName: mount.Destination[len(pipePrefix):],
241
+			}
242
+			mps = append(mps, mp)
243
+		} else {
244
+			md := hcsshim.MappedDir{
245
+				HostPath:      mount.Source,
246
+				ContainerPath: mount.Destination,
247
+				ReadOnly:      false,
243 248
 			}
249
+			for _, o := range mount.Options {
250
+				if strings.ToLower(o) == "ro" {
251
+					md.ReadOnly = true
252
+				}
253
+			}
254
+			mds = append(mds, md)
244 255
 		}
245 256
 	}
246 257
 	configuration.MappedDirectories = mds
258
+	if len(mps) > 0 && system.GetOSVersion().Build < 16210 { // replace with Win10 RS3 build number at RTM
259
+		return errors.New("named pipe mounts are not supported on this version of Windows")
260
+	}
261
+	configuration.MappedPipes = mps
247 262
 
248 263
 	hcsContainer, err := hcsshim.CreateContainer(containerID, configuration)
249 264
 	if err != nil {
... ...
@@ -4,7 +4,7 @@ import (
4 4
 	"errors"
5 5
 	"fmt"
6 6
 	"os"
7
-	"path/filepath"
7
+	"runtime"
8 8
 
9 9
 	"github.com/docker/docker/api/types/mount"
10 10
 )
... ...
@@ -12,8 +12,7 @@ import (
12 12
 var errBindNotExist = errors.New("bind source path does not exist")
13 13
 
14 14
 type validateOpts struct {
15
-	skipBindSourceCheck   bool
16
-	skipAbsolutePathCheck bool
15
+	skipBindSourceCheck bool
17 16
 }
18 17
 
19 18
 func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error {
... ...
@@ -30,10 +29,8 @@ func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error
30 30
 		return &errMountConfig{mnt, err}
31 31
 	}
32 32
 
33
-	if !opts.skipAbsolutePathCheck {
34
-		if err := validateAbsolute(mnt.Target); err != nil {
35
-			return &errMountConfig{mnt, err}
36
-		}
33
+	if err := validateAbsolute(mnt.Target); err != nil {
34
+		return &errMountConfig{mnt, err}
37 35
 	}
38 36
 
39 37
 	switch mnt.Type {
... ...
@@ -97,6 +94,31 @@ func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error
97 97
 		if _, err := ConvertTmpfsOptions(mnt.TmpfsOptions, mnt.ReadOnly); err != nil {
98 98
 			return &errMountConfig{mnt, err}
99 99
 		}
100
+	case mount.TypeNamedPipe:
101
+		if runtime.GOOS != "windows" {
102
+			return &errMountConfig{mnt, errors.New("named pipe bind mounts are not supported on this OS")}
103
+		}
104
+
105
+		if len(mnt.Source) == 0 {
106
+			return &errMountConfig{mnt, errMissingField("Source")}
107
+		}
108
+
109
+		if mnt.BindOptions != nil {
110
+			return &errMountConfig{mnt, errExtraField("BindOptions")}
111
+		}
112
+
113
+		if mnt.ReadOnly {
114
+			return &errMountConfig{mnt, errExtraField("ReadOnly")}
115
+		}
116
+
117
+		if detectMountType(mnt.Source) != mount.TypeNamedPipe {
118
+			return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Source)}
119
+		}
120
+
121
+		if detectMountType(mnt.Target) != mount.TypeNamedPipe {
122
+			return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Target)}
123
+		}
124
+
100 125
 	default:
101 126
 		return &errMountConfig{mnt, errors.New("mount type unknown")}
102 127
 	}
... ...
@@ -121,7 +143,7 @@ func errMissingField(name string) error {
121 121
 
122 122
 func validateAbsolute(p string) error {
123 123
 	p = convertSlash(p)
124
-	if filepath.IsAbs(p) {
124
+	if isAbsPath(p) {
125 125
 		return nil
126 126
 	}
127 127
 	return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
... ...
@@ -3,7 +3,6 @@ package volume
3 3
 import (
4 4
 	"fmt"
5 5
 	"os"
6
-	"path/filepath"
7 6
 	"strings"
8 7
 	"syscall"
9 8
 	"time"
... ...
@@ -284,12 +283,7 @@ func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
284 284
 		return nil, errInvalidMode(mode)
285 285
 	}
286 286
 
287
-	if filepath.IsAbs(spec.Source) {
288
-		spec.Type = mounttypes.TypeBind
289
-	} else {
290
-		spec.Type = mounttypes.TypeVolume
291
-	}
292
-
287
+	spec.Type = detectMountType(spec.Source)
293 288
 	spec.ReadOnly = !ReadWrite(mode)
294 289
 
295 290
 	// cannot assume that if a volume driver is passed in that we should set it
... ...
@@ -350,7 +344,7 @@ func ParseMountSpec(cfg mounttypes.Mount, options ...func(*validateOpts)) (*Moun
350 350
 				mp.CopyData = false
351 351
 			}
352 352
 		}
353
-	case mounttypes.TypeBind:
353
+	case mounttypes.TypeBind, mounttypes.TypeNamedPipe:
354 354
 		mp.Source = clean(convertSlash(cfg.Source))
355 355
 		if cfg.BindOptions != nil && len(cfg.BindOptions.Propagation) > 0 {
356 356
 			mp.Propagation = cfg.BindOptions.Propagation
... ...
@@ -143,6 +143,7 @@ func TestParseMountRaw(t *testing.T) {
143 143
 type testParseMountRaw struct {
144 144
 	bind      string
145 145
 	driver    string
146
+	expType   mount.Type
146 147
 	expDest   string
147 148
 	expSource string
148 149
 	expName   string
... ...
@@ -155,28 +156,31 @@ func TestParseMountRawSplit(t *testing.T) {
155 155
 	var cases []testParseMountRaw
156 156
 	if runtime.GOOS == "windows" {
157 157
 		cases = []testParseMountRaw{
158
-			{`c:\:d:`, "local", `d:`, `c:\`, ``, "", true, false},
159
-			{`c:\:d:\`, "local", `d:\`, `c:\`, ``, "", true, false},
160
-			{`c:\:d:\:ro`, "local", `d:\`, `c:\`, ``, "", false, false},
161
-			{`c:\:d:\:rw`, "local", `d:\`, `c:\`, ``, "", true, false},
162
-			{`c:\:d:\:foo`, "local", `d:\`, `c:\`, ``, "", false, true},
163
-			{`name:d::rw`, "local", `d:`, ``, `name`, "local", true, false},
164
-			{`name:d:`, "local", `d:`, ``, `name`, "local", true, false},
165
-			{`name:d::ro`, "local", `d:`, ``, `name`, "local", false, false},
166
-			{`name:c:`, "", ``, ``, ``, "", true, true},
167
-			{`driver/name:c:`, "", ``, ``, ``, "", true, true},
158
+			{`c:\:d:`, "local", mount.TypeBind, `d:`, `c:\`, ``, "", true, false},
159
+			{`c:\:d:\`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false},
160
+			{`c:\:d:\:ro`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, false},
161
+			{`c:\:d:\:rw`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false},
162
+			{`c:\:d:\:foo`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, true},
163
+			{`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, false},
164
+			{`\\.\pipe\foo:c:\foo\bar`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
165
+			{`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true},
166
+			{`name:d::rw`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false},
167
+			{`name:d:`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false},
168
+			{`name:d::ro`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", false, false},
169
+			{`name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
170
+			{`driver/name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true},
168 171
 		}
169 172
 	} else {
170 173
 		cases = []testParseMountRaw{
171
-			{"/tmp:/tmp1", "", "/tmp1", "/tmp", "", "", true, false},
172
-			{"/tmp:/tmp2:ro", "", "/tmp2", "/tmp", "", "", false, false},
173
-			{"/tmp:/tmp3:rw", "", "/tmp3", "/tmp", "", "", true, false},
174
-			{"/tmp:/tmp4:foo", "", "", "", "", "", false, true},
175
-			{"name:/named1", "", "/named1", "", "name", "", true, false},
176
-			{"name:/named2", "external", "/named2", "", "name", "external", true, false},
177
-			{"name:/named3:ro", "local", "/named3", "", "name", "local", false, false},
178
-			{"local/name:/tmp:rw", "", "/tmp", "", "local/name", "", true, false},
179
-			{"/tmp:tmp", "", "", "", "", "", true, true},
174
+			{"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false},
175
+			{"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false},
176
+			{"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false},
177
+			{"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, true},
178
+			{"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false},
179
+			{"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false},
180
+			{"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false},
181
+			{"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false},
182
+			{"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, true},
180 183
 		}
181 184
 	}
182 185
 
... ...
@@ -195,8 +199,12 @@ func TestParseMountRawSplit(t *testing.T) {
195 195
 			continue
196 196
 		}
197 197
 
198
+		if m.Type != c.expType {
199
+			t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind)
200
+		}
201
+
198 202
 		if m.Destination != c.expDest {
199
-			t.Fatalf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind)
203
+			t.Fatalf("Expected destination '%s', was '%s', for spec '%s'", c.expDest, m.Destination, c.bind)
200 204
 		}
201 205
 
202 206
 		if m.Source != c.expSource {
... ...
@@ -124,7 +124,12 @@ func validateCopyMode(mode bool) error {
124 124
 }
125 125
 
126 126
 func convertSlash(p string) string {
127
-	return filepath.ToSlash(p)
127
+	return p
128
+}
129
+
130
+// isAbsPath reports whether the path is absolute.
131
+func isAbsPath(p string) bool {
132
+	return filepath.IsAbs(p)
128 133
 }
129 134
 
130 135
 func splitRawSpec(raw string) ([]string, error) {
... ...
@@ -139,6 +144,13 @@ func splitRawSpec(raw string) ([]string, error) {
139 139
 	return arr, nil
140 140
 }
141 141
 
142
+func detectMountType(p string) mounttypes.Type {
143
+	if filepath.IsAbs(p) {
144
+		return mounttypes.TypeBind
145
+	}
146
+	return mounttypes.TypeVolume
147
+}
148
+
142 149
 func clean(p string) string {
143 150
 	return filepath.Clean(p)
144 151
 }
... ...
@@ -6,6 +6,8 @@ import (
6 6
 	"path/filepath"
7 7
 	"regexp"
8 8
 	"strings"
9
+
10
+	mounttypes "github.com/docker/docker/api/types/mount"
9 11
 )
10 12
 
11 13
 // read-write modes
... ...
@@ -18,14 +20,7 @@ var roModes = map[string]bool{
18 18
 	"ro": true,
19 19
 }
20 20
 
21
-var platformRawValidationOpts = []func(*validateOpts){
22
-	// filepath.IsAbs is weird on Windows:
23
-	//	`c:` is not considered an absolute path
24
-	//	`c:\` is considered an absolute path
25
-	// In any case, the regex matching below ensures absolute paths
26
-	// TODO: consider this a bug with filepath.IsAbs (?)
27
-	func(o *validateOpts) { o.skipAbsolutePathCheck = true },
28
-}
21
+var platformRawValidationOpts = []func(*validateOpts){}
29 22
 
30 23
 const (
31 24
 	// Spec should be in the format [source:]destination[:mode]
... ...
@@ -49,11 +44,13 @@ const (
49 49
 	RXHostDir = `[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\?)*`
50 50
 	// RXName is the second option of a source
51 51
 	RXName = `[^\\/:*?"<>|\r\n]+`
52
+	// RXPipe is a named path pipe (starts with `\\.\pipe\`, possibly with / instead of \)
53
+	RXPipe = `[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+`
52 54
 	// RXReservedNames are reserved names not possible on Windows
53 55
 	RXReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])`
54 56
 
55 57
 	// RXSource is the combined possibilities for a source
56
-	RXSource = `((?P<source>((` + RXHostDir + `)|(` + RXName + `))):)?`
58
+	RXSource = `((?P<source>((` + RXHostDir + `)|(` + RXName + `)|(` + RXPipe + `))):)?`
57 59
 
58 60
 	// Source. Can be either a host directory, a name, or omitted:
59 61
 	//  HostDir:
... ...
@@ -69,8 +66,10 @@ const (
69 69
 	//    -  And then followed by a colon which is not in the capture group
70 70
 	//    -  And can be optional
71 71
 
72
+	// RXDestinationDir is the file path option for the mount destination
73
+	RXDestinationDir = `([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?)`
72 74
 	// RXDestination is the regex expression for the mount destination
73
-	RXDestination = `(?P<destination>([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?))`
75
+	RXDestination = `(?P<destination>(` + RXDestinationDir + `)|(` + RXPipe + `))`
74 76
 	// Destination (aka container path):
75 77
 	//    -  Variation on hostdir but can be a drive followed by colon as well
76 78
 	//    -  If a path, must be absolute. Can include spaces
... ...
@@ -140,6 +139,15 @@ func splitRawSpec(raw string) ([]string, error) {
140 140
 	return split, nil
141 141
 }
142 142
 
143
+func detectMountType(p string) mounttypes.Type {
144
+	if strings.HasPrefix(filepath.FromSlash(p), `\\.\pipe\`) {
145
+		return mounttypes.TypeNamedPipe
146
+	} else if filepath.IsAbs(p) {
147
+		return mounttypes.TypeBind
148
+	}
149
+	return mounttypes.TypeVolume
150
+}
151
+
143 152
 // IsVolumeNameValid checks a volume name in a platform specific manner.
144 153
 func IsVolumeNameValid(name string) (bool, error) {
145 154
 	nameExp := regexp.MustCompile(`^` + RXName + `$`)
... ...
@@ -186,8 +194,19 @@ func convertSlash(p string) string {
186 186
 	return filepath.FromSlash(p)
187 187
 }
188 188
 
189
+// isAbsPath returns whether a path is absolute for the purposes of mounting into a container
190
+// (absolute paths, drive letter paths such as X:, and paths starting with `\\.\` to support named pipes).
191
+func isAbsPath(p string) bool {
192
+	return filepath.IsAbs(p) ||
193
+		strings.HasPrefix(p, `\\.\`) ||
194
+		(len(p) == 2 && p[1] == ':' && ((p[0] >= 'a' && p[0] <= 'z') || (p[0] >= 'A' && p[0] <= 'Z')))
195
+}
196
+
197
+// Do not clean plain drive letters or paths starting with `\\.\`.
198
+var cleanRegexp = regexp.MustCompile(`^([a-z]:|[/\\]{2}\.[/\\].*)$`)
199
+
189 200
 func clean(p string) string {
190
-	if match, _ := regexp.MatchString("^[a-z]:$", p); match {
201
+	if match := cleanRegexp.MatchString(p); match {
191 202
 		return p
192 203
 	}
193 204
 	return filepath.Clean(p)