namedpipe.go raw
1 // Copyright 2021 The Go Authors. All rights reserved.
2 // Copyright 2015 Microsoft
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file.
5
6 //go:build windows
7
8 // Package namedpipe implements a net.Conn and net.Listener around Windows named pipes.
9 package namedpipe
10
11 import (
12 "context"
13 "io"
14 "net"
15 "os"
16 "runtime"
17 "sync/atomic"
18 "time"
19 "unsafe"
20
21 "golang.org/x/sys/windows"
22 )
23
24 type pipe struct {
25 *file
26 path string
27 }
28
29 type messageBytePipe struct {
30 pipe
31 writeClosed atomic.Bool
32 readEOF bool
33 }
34
35 type pipeAddress string
36
37 func (f *pipe) LocalAddr() net.Addr {
38 return pipeAddress(f.path)
39 }
40
41 func (f *pipe) RemoteAddr() net.Addr {
42 return pipeAddress(f.path)
43 }
44
45 func (f *pipe) SetDeadline(t time.Time) error {
46 f.SetReadDeadline(t)
47 f.SetWriteDeadline(t)
48 return nil
49 }
50
51 // CloseWrite closes the write side of a message pipe in byte mode.
52 func (f *messageBytePipe) CloseWrite() error {
53 if !f.writeClosed.CompareAndSwap(false, true) {
54 return io.ErrClosedPipe
55 }
56 err := f.file.Flush()
57 if err != nil {
58 f.writeClosed.Store(false)
59 return err
60 }
61 _, err = f.file.Write(nil)
62 if err != nil {
63 f.writeClosed.Store(false)
64 return err
65 }
66 return nil
67 }
68
69 // Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since
70 // they are used to implement CloseWrite.
71 func (f *messageBytePipe) Write(b []byte) (int, error) {
72 if f.writeClosed.Load() {
73 return 0, io.ErrClosedPipe
74 }
75 if len(b) == 0 {
76 return 0, nil
77 }
78 return f.file.Write(b)
79 }
80
81 // Read reads bytes from a message pipe in byte mode. A read of a zero-byte message on a message
82 // mode pipe will return io.EOF, as will all subsequent reads.
83 func (f *messageBytePipe) Read(b []byte) (int, error) {
84 if f.readEOF {
85 return 0, io.EOF
86 }
87 n, err := f.file.Read(b)
88 if err == io.EOF {
89 // If this was the result of a zero-byte read, then
90 // it is possible that the read was due to a zero-size
91 // message. Since we are simulating CloseWrite with a
92 // zero-byte message, ensure that all future Read calls
93 // also return EOF.
94 f.readEOF = true
95 } else if err == windows.ERROR_MORE_DATA {
96 // ERROR_MORE_DATA indicates that the pipe's read mode is message mode
97 // and the message still has more bytes. Treat this as a success, since
98 // this package presents all named pipes as byte streams.
99 err = nil
100 }
101 return n, err
102 }
103
104 func (f *pipe) Handle() windows.Handle {
105 return f.handle
106 }
107
108 func (s pipeAddress) Network() string {
109 return "pipe"
110 }
111
112 func (s pipeAddress) String() string {
113 return string(s)
114 }
115
116 // tryDialPipe attempts to dial the specified pipe until cancellation or timeout.
117 func tryDialPipe(ctx context.Context, path *string) (windows.Handle, error) {
118 for {
119 select {
120 case <-ctx.Done():
121 return 0, ctx.Err()
122 default:
123 path16, err := windows.UTF16PtrFromString(*path)
124 if err != nil {
125 return 0, err
126 }
127 h, err := windows.CreateFile(path16, windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OVERLAPPED|windows.SECURITY_SQOS_PRESENT|windows.SECURITY_ANONYMOUS, 0)
128 if err == nil {
129 return h, nil
130 }
131 if err != windows.ERROR_PIPE_BUSY {
132 return h, &os.PathError{Err: err, Op: "open", Path: *path}
133 }
134 // Wait 10 msec and try again. This is a rather simplistic
135 // view, as we always try each 10 milliseconds.
136 time.Sleep(10 * time.Millisecond)
137 }
138 }
139 }
140
141 // DialConfig exposes various options for use in Dial and DialContext.
142 type DialConfig struct {
143 ExpectedOwner *windows.SID // If non-nil, the pipe is verified to be owned by this SID.
144 }
145
146 // DialTimeout connects to the specified named pipe by path, timing out if the
147 // connection takes longer than the specified duration. If timeout is zero, then
148 // we use a default timeout of 2 seconds.
149 func (config *DialConfig) DialTimeout(path string, timeout time.Duration) (net.Conn, error) {
150 if timeout == 0 {
151 timeout = time.Second * 2
152 }
153 absTimeout := time.Now().Add(timeout)
154 ctx, _ := context.WithDeadline(context.Background(), absTimeout)
155 conn, err := config.DialContext(ctx, path)
156 if err == context.DeadlineExceeded {
157 return nil, os.ErrDeadlineExceeded
158 }
159 return conn, err
160 }
161
162 // DialContext attempts to connect to the specified named pipe by path.
163 func (config *DialConfig) DialContext(ctx context.Context, path string) (net.Conn, error) {
164 var err error
165 var h windows.Handle
166 h, err = tryDialPipe(ctx, &path)
167 if err != nil {
168 return nil, err
169 }
170
171 if config.ExpectedOwner != nil {
172 sd, err := windows.GetSecurityInfo(h, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION)
173 if err != nil {
174 windows.Close(h)
175 return nil, err
176 }
177 realOwner, _, err := sd.Owner()
178 if err != nil {
179 windows.Close(h)
180 return nil, err
181 }
182 if !realOwner.Equals(config.ExpectedOwner) {
183 windows.Close(h)
184 return nil, windows.ERROR_ACCESS_DENIED
185 }
186 }
187
188 var flags uint32
189 err = windows.GetNamedPipeInfo(h, &flags, nil, nil, nil)
190 if err != nil {
191 windows.Close(h)
192 return nil, err
193 }
194
195 f, err := makeFile(h)
196 if err != nil {
197 windows.Close(h)
198 return nil, err
199 }
200
201 // If the pipe is in message mode, return a message byte pipe, which
202 // supports CloseWrite.
203 if flags&windows.PIPE_TYPE_MESSAGE != 0 {
204 return &messageBytePipe{
205 pipe: pipe{file: f, path: path},
206 }, nil
207 }
208 return &pipe{file: f, path: path}, nil
209 }
210
211 var defaultDialer DialConfig
212
213 // DialTimeout calls DialConfig.DialTimeout using an empty configuration.
214 func DialTimeout(path string, timeout time.Duration) (net.Conn, error) {
215 return defaultDialer.DialTimeout(path, timeout)
216 }
217
218 // DialContext calls DialConfig.DialContext using an empty configuration.
219 func DialContext(ctx context.Context, path string) (net.Conn, error) {
220 return defaultDialer.DialContext(ctx, path)
221 }
222
223 type acceptResponse struct {
224 f *file
225 err error
226 }
227
228 type pipeListener struct {
229 firstHandle windows.Handle
230 path string
231 config ListenConfig
232 acceptCh chan chan acceptResponse
233 closeCh chan int
234 doneCh chan int
235 }
236
237 func makeServerPipeHandle(path string, sd *windows.SECURITY_DESCRIPTOR, c *ListenConfig, isFirstPipe bool) (windows.Handle, error) {
238 path16, err := windows.UTF16PtrFromString(path)
239 if err != nil {
240 return 0, &os.PathError{Op: "open", Path: path, Err: err}
241 }
242
243 var oa windows.OBJECT_ATTRIBUTES
244 oa.Length = uint32(unsafe.Sizeof(oa))
245
246 var ntPath windows.NTUnicodeString
247 if err := windows.RtlDosPathNameToNtPathName(path16, &ntPath, nil, nil); err != nil {
248 if ntstatus, ok := err.(windows.NTStatus); ok {
249 err = ntstatus.Errno()
250 }
251 return 0, &os.PathError{Op: "open", Path: path, Err: err}
252 }
253 defer windows.LocalFree(windows.Handle(unsafe.Pointer(ntPath.Buffer)))
254 oa.ObjectName = &ntPath
255
256 // The security descriptor is only needed for the first pipe.
257 if isFirstPipe {
258 if sd != nil {
259 oa.SecurityDescriptor = sd
260 } else {
261 // Construct the default named pipe security descriptor.
262 var acl *windows.ACL
263 if err := windows.RtlDefaultNpAcl(&acl); err != nil {
264 return 0, err
265 }
266 defer windows.LocalFree(windows.Handle(unsafe.Pointer(acl)))
267 sd, err = windows.NewSecurityDescriptor()
268 if err != nil {
269 return 0, err
270 }
271 if err = sd.SetDACL(acl, true, false); err != nil {
272 return 0, err
273 }
274 oa.SecurityDescriptor = sd
275 }
276 }
277
278 typ := uint32(windows.FILE_PIPE_REJECT_REMOTE_CLIENTS)
279 if c.MessageMode {
280 typ |= windows.FILE_PIPE_MESSAGE_TYPE
281 }
282
283 disposition := uint32(windows.FILE_OPEN)
284 access := uint32(windows.GENERIC_READ | windows.GENERIC_WRITE | windows.SYNCHRONIZE)
285 if isFirstPipe {
286 disposition = windows.FILE_CREATE
287 // By not asking for read or write access, the named pipe file system
288 // will put this pipe into an initially disconnected state, blocking
289 // client connections until the next call with isFirstPipe == false.
290 access = windows.SYNCHRONIZE
291 }
292
293 timeout := int64(-50 * 10000) // 50ms
294
295 var (
296 h windows.Handle
297 iosb windows.IO_STATUS_BLOCK
298 )
299 err = windows.NtCreateNamedPipeFile(&h, access, &oa, &iosb, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, disposition, 0, typ, 0, 0, 0xffffffff, uint32(c.InputBufferSize), uint32(c.OutputBufferSize), &timeout)
300 if err != nil {
301 if ntstatus, ok := err.(windows.NTStatus); ok {
302 err = ntstatus.Errno()
303 }
304 return 0, &os.PathError{Op: "open", Path: path, Err: err}
305 }
306
307 runtime.KeepAlive(ntPath)
308 return h, nil
309 }
310
311 func (l *pipeListener) makeServerPipe() (*file, error) {
312 h, err := makeServerPipeHandle(l.path, nil, &l.config, false)
313 if err != nil {
314 return nil, err
315 }
316 f, err := makeFile(h)
317 if err != nil {
318 windows.Close(h)
319 return nil, err
320 }
321 return f, nil
322 }
323
324 func (l *pipeListener) makeConnectedServerPipe() (*file, error) {
325 p, err := l.makeServerPipe()
326 if err != nil {
327 return nil, err
328 }
329
330 // Wait for the client to connect.
331 ch := make(chan error)
332 go func(p *file) {
333 ch <- connectPipe(p)
334 }(p)
335
336 select {
337 case err = <-ch:
338 if err != nil {
339 p.Close()
340 p = nil
341 }
342 case <-l.closeCh:
343 // Abort the connect request by closing the handle.
344 p.Close()
345 p = nil
346 err = <-ch
347 if err == nil || err == os.ErrClosed {
348 err = net.ErrClosed
349 }
350 }
351 return p, err
352 }
353
354 func (l *pipeListener) listenerRoutine() {
355 closed := false
356 for !closed {
357 select {
358 case <-l.closeCh:
359 closed = true
360 case responseCh := <-l.acceptCh:
361 var (
362 p *file
363 err error
364 )
365 for {
366 p, err = l.makeConnectedServerPipe()
367 // If the connection was immediately closed by the client, try
368 // again.
369 if err != windows.ERROR_NO_DATA {
370 break
371 }
372 }
373 responseCh <- acceptResponse{p, err}
374 closed = err == net.ErrClosed
375 }
376 }
377 windows.Close(l.firstHandle)
378 l.firstHandle = 0
379 // Notify Close and Accept callers that the handle has been closed.
380 close(l.doneCh)
381 }
382
383 // ListenConfig contains configuration for the pipe listener.
384 type ListenConfig struct {
385 // SecurityDescriptor contains a Windows security descriptor. If nil, the default from RtlDefaultNpAcl is used.
386 SecurityDescriptor *windows.SECURITY_DESCRIPTOR
387
388 // MessageMode determines whether the pipe is in byte or message mode. In either
389 // case the pipe is read in byte mode by default. The only practical difference in
390 // this implementation is that CloseWrite is only supported for message mode pipes;
391 // CloseWrite is implemented as a zero-byte write, but zero-byte writes are only
392 // transferred to the reader (and returned as io.EOF in this implementation)
393 // when the pipe is in message mode.
394 MessageMode bool
395
396 // InputBufferSize specifies the initial size of the input buffer, in bytes, which the OS will grow as needed.
397 InputBufferSize int32
398
399 // OutputBufferSize specifies the initial size of the output buffer, in bytes, which the OS will grow as needed.
400 OutputBufferSize int32
401 }
402
403 // Listen creates a listener on a Windows named pipe path,such as \\.\pipe\mypipe.
404 // The pipe must not already exist.
405 func (c *ListenConfig) Listen(path string) (net.Listener, error) {
406 h, err := makeServerPipeHandle(path, c.SecurityDescriptor, c, true)
407 if err != nil {
408 return nil, err
409 }
410 l := &pipeListener{
411 firstHandle: h,
412 path: path,
413 config: *c,
414 acceptCh: make(chan chan acceptResponse),
415 closeCh: make(chan int),
416 doneCh: make(chan int),
417 }
418 // The first connection is swallowed on Windows 7 & 8, so synthesize it.
419 if maj, min, _ := windows.RtlGetNtVersionNumbers(); maj < 6 || (maj == 6 && min < 4) {
420 path16, err := windows.UTF16PtrFromString(path)
421 if err == nil {
422 h, err = windows.CreateFile(path16, 0, 0, nil, windows.OPEN_EXISTING, windows.SECURITY_SQOS_PRESENT|windows.SECURITY_ANONYMOUS, 0)
423 if err == nil {
424 windows.CloseHandle(h)
425 }
426 }
427 }
428 go l.listenerRoutine()
429 return l, nil
430 }
431
432 var defaultListener ListenConfig
433
434 // Listen calls ListenConfig.Listen using an empty configuration.
435 func Listen(path string) (net.Listener, error) {
436 return defaultListener.Listen(path)
437 }
438
439 func connectPipe(p *file) error {
440 c, err := p.prepareIo()
441 if err != nil {
442 return err
443 }
444 defer p.wg.Done()
445
446 err = windows.ConnectNamedPipe(p.handle, &c.o)
447 _, err = p.asyncIo(c, nil, 0, err)
448 if err != nil && err != windows.ERROR_PIPE_CONNECTED {
449 return err
450 }
451 return nil
452 }
453
454 func (l *pipeListener) Accept() (net.Conn, error) {
455 ch := make(chan acceptResponse)
456 select {
457 case l.acceptCh <- ch:
458 response := <-ch
459 err := response.err
460 if err != nil {
461 return nil, err
462 }
463 if l.config.MessageMode {
464 return &messageBytePipe{
465 pipe: pipe{file: response.f, path: l.path},
466 }, nil
467 }
468 return &pipe{file: response.f, path: l.path}, nil
469 case <-l.doneCh:
470 return nil, net.ErrClosed
471 }
472 }
473
474 func (l *pipeListener) Close() error {
475 select {
476 case l.closeCh <- 1:
477 <-l.doneCh
478 case <-l.doneCh:
479 }
480 return nil
481 }
482
483 func (l *pipeListener) Addr() net.Addr {
484 return pipeAddress(l.path)
485 }
486