github.com/Serizao/go-winio@v0.0.0-20230906082528-f02f7f4ad6e8/pipe.go (about)

     1  //go:build windows
     2  // +build windows
     3  
     4  package winio
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"os"
    13  	"runtime"
    14  	"time"
    15  	"unsafe"
    16  
    17  	"golang.org/x/sys/windows"
    18  
    19  	"github.com/Serizao/go-winio/internal/fs"
    20  )
    21  
    22  //sys connectNamedPipe(pipe windows.Handle, o *windows.Overlapped) (err error) = ConnectNamedPipe
    23  //sys createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error)  [failretval==windows.InvalidHandle] = CreateNamedPipeW
    24  //sys disconnectNamedPipe(pipe windows.Handle) (err error) = DisconnectNamedPipe
    25  //sys getNamedPipeInfo(pipe windows.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) = GetNamedPipeInfo
    26  //sys getNamedPipeHandleState(pipe windows.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
    27  //sys ntCreateNamedPipeFile(pipe *windows.Handle, access ntAccessMask, oa *objectAttributes, iosb *ioStatusBlock, share ntFileShareMode, disposition ntFileCreationDisposition, options ntFileOptions, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) = ntdll.NtCreateNamedPipeFile
    28  //sys rtlNtStatusToDosError(status ntStatus) (winerr error) = ntdll.RtlNtStatusToDosErrorNoTeb
    29  //sys rtlDosPathNameToNtPathName(name *uint16, ntName *unicodeString, filePart uintptr, reserved uintptr) (status ntStatus) = ntdll.RtlDosPathNameToNtPathName_U
    30  //sys rtlDefaultNpAcl(dacl *uintptr) (status ntStatus) = ntdll.RtlDefaultNpAcl
    31  
    32  type PipeConn interface {
    33  	net.Conn
    34  	Disconnect() error
    35  	Flush() error
    36  }
    37  
    38  // type aliases for mkwinsyscall code
    39  type (
    40  	ntAccessMask              = fs.AccessMask
    41  	ntFileShareMode           = fs.FileShareMode
    42  	ntFileCreationDisposition = fs.NTFileCreationDisposition
    43  	ntFileOptions             = fs.NTCreateOptions
    44  )
    45  
    46  type ioStatusBlock struct {
    47  	Status, Information uintptr
    48  }
    49  
    50  //	typedef struct _OBJECT_ATTRIBUTES {
    51  //	  ULONG           Length;
    52  //	  HANDLE          RootDirectory;
    53  //	  PUNICODE_STRING ObjectName;
    54  //	  ULONG           Attributes;
    55  //	  PVOID           SecurityDescriptor;
    56  //	  PVOID           SecurityQualityOfService;
    57  //	} OBJECT_ATTRIBUTES;
    58  //
    59  // https://learn.microsoft.com/en-us/windows/win32/api/ntdef/ns-ntdef-_object_attributes
    60  type objectAttributes struct {
    61  	Length             uintptr
    62  	RootDirectory      uintptr
    63  	ObjectName         *unicodeString
    64  	Attributes         uintptr
    65  	SecurityDescriptor *securityDescriptor
    66  	SecurityQoS        uintptr
    67  }
    68  
    69  type unicodeString struct {
    70  	Length        uint16
    71  	MaximumLength uint16
    72  	Buffer        uintptr
    73  }
    74  
    75  //	typedef struct _SECURITY_DESCRIPTOR {
    76  //	  BYTE                        Revision;
    77  //	  BYTE                        Sbz1;
    78  //	  SECURITY_DESCRIPTOR_CONTROL Control;
    79  //	  PSID                        Owner;
    80  //	  PSID                        Group;
    81  //	  PACL                        Sacl;
    82  //	  PACL                        Dacl;
    83  //	} SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;
    84  //
    85  // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_descriptor
    86  type securityDescriptor struct {
    87  	Revision byte
    88  	Sbz1     byte
    89  	Control  uint16
    90  	Owner    uintptr
    91  	Group    uintptr
    92  	Sacl     uintptr //revive:disable-line:var-naming SACL, not Sacl
    93  	Dacl     uintptr //revive:disable-line:var-naming DACL, not Dacl
    94  }
    95  
    96  type ntStatus int32
    97  
    98  func (status ntStatus) Err() error {
    99  	if status >= 0 {
   100  		return nil
   101  	}
   102  	return rtlNtStatusToDosError(status)
   103  }
   104  
   105  var (
   106  	// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
   107  	ErrPipeListenerClosed = net.ErrClosed
   108  
   109  	errPipeWriteClosed = errors.New("pipe has been closed for write")
   110  )
   111  
   112  type win32Pipe struct {
   113  	*win32File
   114  	path string
   115  }
   116  
   117  var _ PipeConn = (*win32Pipe)(nil)
   118  
   119  type win32MessageBytePipe struct {
   120  	win32Pipe
   121  	writeClosed bool
   122  	readEOF     bool
   123  }
   124  
   125  type pipeAddress string
   126  
   127  func (f *win32Pipe) LocalAddr() net.Addr {
   128  	return pipeAddress(f.path)
   129  }
   130  
   131  func (f *win32Pipe) RemoteAddr() net.Addr {
   132  	return pipeAddress(f.path)
   133  }
   134  
   135  func (f *win32Pipe) SetDeadline(t time.Time) error {
   136  	if err := f.SetReadDeadline(t); err != nil {
   137  		return err
   138  	}
   139  	return f.SetWriteDeadline(t)
   140  }
   141  
   142  func (f *win32Pipe) Disconnect() error {
   143  	return disconnectNamedPipe(f.win32File.handle)
   144  }
   145  
   146  // CloseWrite closes the write side of a message pipe in byte mode.
   147  func (f *win32MessageBytePipe) CloseWrite() error {
   148  	if f.writeClosed {
   149  		return errPipeWriteClosed
   150  	}
   151  	err := f.win32File.Flush()
   152  	if err != nil {
   153  		return err
   154  	}
   155  	_, err = f.win32File.Write(nil)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	f.writeClosed = true
   160  	return nil
   161  }
   162  
   163  // Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since
   164  // they are used to implement CloseWrite().
   165  func (f *win32MessageBytePipe) Write(b []byte) (int, error) {
   166  	if f.writeClosed {
   167  		return 0, errPipeWriteClosed
   168  	}
   169  	if len(b) == 0 {
   170  		return 0, nil
   171  	}
   172  	return f.win32File.Write(b)
   173  }
   174  
   175  // Read reads bytes from a message pipe in byte mode. A read of a zero-byte message on a message
   176  // mode pipe will return io.EOF, as will all subsequent reads.
   177  func (f *win32MessageBytePipe) Read(b []byte) (int, error) {
   178  	if f.readEOF {
   179  		return 0, io.EOF
   180  	}
   181  	n, err := f.win32File.Read(b)
   182  	if err == io.EOF { //nolint:errorlint
   183  		// If this was the result of a zero-byte read, then
   184  		// it is possible that the read was due to a zero-size
   185  		// message. Since we are simulating CloseWrite with a
   186  		// zero-byte message, ensure that all future Read() calls
   187  		// also return EOF.
   188  		f.readEOF = true
   189  	} else if err == windows.ERROR_MORE_DATA { //nolint:errorlint // err is Errno
   190  		// ERROR_MORE_DATA indicates that the pipe's read mode is message mode
   191  		// and the message still has more bytes. Treat this as a success, since
   192  		// this package presents all named pipes as byte streams.
   193  		err = nil
   194  	}
   195  	return n, err
   196  }
   197  
   198  func (pipeAddress) Network() string {
   199  	return "pipe"
   200  }
   201  
   202  func (s pipeAddress) String() string {
   203  	return string(s)
   204  }
   205  
   206  // tryDialPipe attempts to dial the pipe at `path` until `ctx` cancellation or timeout.
   207  func tryDialPipe(ctx context.Context, path *string, access fs.AccessMask) (windows.Handle, error) {
   208  	for {
   209  		select {
   210  		case <-ctx.Done():
   211  			return windows.Handle(0), ctx.Err()
   212  		default:
   213  			h, err := fs.CreateFile(*path,
   214  				access,
   215  				0,   // mode
   216  				nil, // security attributes
   217  				fs.OPEN_EXISTING,
   218  				fs.FILE_FLAG_OVERLAPPED|fs.SECURITY_SQOS_PRESENT|fs.SECURITY_ANONYMOUS,
   219  				0, // template file handle
   220  			)
   221  			if err == nil {
   222  				return h, nil
   223  			}
   224  			if err != windows.ERROR_PIPE_BUSY { //nolint:errorlint // err is Errno
   225  				return h, &os.PathError{Err: err, Op: "open", Path: *path}
   226  			}
   227  			// Wait 10 msec and try again. This is a rather simplistic
   228  			// view, as we always try each 10 milliseconds.
   229  			time.Sleep(10 * time.Millisecond)
   230  		}
   231  	}
   232  }
   233  
   234  // DialPipe connects to a named pipe by path, timing out if the connection
   235  // takes longer than the specified duration. If timeout is nil, then we use
   236  // a default timeout of 2 seconds.  (We do not use WaitNamedPipe.)
   237  func DialPipe(path string, timeout *time.Duration) (net.Conn, error) {
   238  	var absTimeout time.Time
   239  	if timeout != nil {
   240  		absTimeout = time.Now().Add(*timeout)
   241  	} else {
   242  		absTimeout = time.Now().Add(2 * time.Second)
   243  	}
   244  	ctx, cancel := context.WithDeadline(context.Background(), absTimeout)
   245  	defer cancel()
   246  	conn, err := DialPipeContext(ctx, path)
   247  	if errors.Is(err, context.DeadlineExceeded) {
   248  		return nil, ErrTimeout
   249  	}
   250  	return conn, err
   251  }
   252  
   253  // DialPipeContext attempts to connect to a named pipe by `path` until `ctx`
   254  // cancellation or timeout.
   255  func DialPipeContext(ctx context.Context, path string) (net.Conn, error) {
   256  	return DialPipeAccess(ctx, path, uint32(fs.GENERIC_READ|fs.GENERIC_WRITE))
   257  }
   258  
   259  // DialPipeAccess attempts to connect to a named pipe by `path` with `access` until `ctx`
   260  // cancellation or timeout.
   261  func DialPipeAccess(ctx context.Context, path string, access uint32) (net.Conn, error) {
   262  	var err error
   263  	var h windows.Handle
   264  	h, err = tryDialPipe(ctx, &path, fs.AccessMask(access))
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  
   269  	var flags uint32
   270  	err = getNamedPipeInfo(h, &flags, nil, nil, nil)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	f, err := makeWin32File(h)
   276  	if err != nil {
   277  		windows.Close(h)
   278  		return nil, err
   279  	}
   280  
   281  	// If the pipe is in message mode, return a message byte pipe, which
   282  	// supports CloseWrite().
   283  	if flags&windows.PIPE_TYPE_MESSAGE != 0 {
   284  		return &win32MessageBytePipe{
   285  			win32Pipe: win32Pipe{win32File: f, path: path},
   286  		}, nil
   287  	}
   288  	return &win32Pipe{win32File: f, path: path}, nil
   289  }
   290  
   291  type acceptResponse struct {
   292  	f   *win32File
   293  	err error
   294  }
   295  
   296  type win32PipeListener struct {
   297  	firstHandle windows.Handle
   298  	path        string
   299  	config      PipeConfig
   300  	acceptCh    chan (chan acceptResponse)
   301  	closeCh     chan int
   302  	doneCh      chan int
   303  }
   304  
   305  func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (windows.Handle, error) {
   306  	path16, err := windows.UTF16FromString(path)
   307  	if err != nil {
   308  		return 0, &os.PathError{Op: "open", Path: path, Err: err}
   309  	}
   310  
   311  	var oa objectAttributes
   312  	oa.Length = unsafe.Sizeof(oa)
   313  
   314  	var ntPath unicodeString
   315  	if err := rtlDosPathNameToNtPathName(&path16[0],
   316  		&ntPath,
   317  		0,
   318  		0,
   319  	).Err(); err != nil {
   320  		return 0, &os.PathError{Op: "open", Path: path, Err: err}
   321  	}
   322  	defer windows.LocalFree(windows.Handle(ntPath.Buffer)) //nolint:errcheck
   323  	oa.ObjectName = &ntPath
   324  	oa.Attributes = windows.OBJ_CASE_INSENSITIVE
   325  
   326  	// The security descriptor is only needed for the first pipe.
   327  	if first {
   328  		if sd != nil {
   329  			//todo: does `sdb` need to be allocated on the heap, or can go allocate it?
   330  			l := uint32(len(sd))
   331  			sdb, err := windows.LocalAlloc(0, l)
   332  			if err != nil {
   333  				return 0, fmt.Errorf("LocalAlloc for security descriptor with of length %d: %w", l, err)
   334  			}
   335  			defer windows.LocalFree(windows.Handle(sdb)) //nolint:errcheck
   336  			copy((*[0xffff]byte)(unsafe.Pointer(sdb))[:], sd)
   337  			oa.SecurityDescriptor = (*securityDescriptor)(unsafe.Pointer(sdb))
   338  		} else {
   339  			// Construct the default named pipe security descriptor.
   340  			var dacl uintptr
   341  			if err := rtlDefaultNpAcl(&dacl).Err(); err != nil {
   342  				return 0, fmt.Errorf("getting default named pipe ACL: %w", err)
   343  			}
   344  			defer windows.LocalFree(windows.Handle(dacl)) //nolint:errcheck
   345  
   346  			sdb := &securityDescriptor{
   347  				Revision: 1,
   348  				Control:  windows.SE_DACL_PRESENT,
   349  				Dacl:     dacl,
   350  			}
   351  			oa.SecurityDescriptor = sdb
   352  		}
   353  	}
   354  
   355  	typ := uint32(windows.FILE_PIPE_ACCEPT_REMOTE_CLIENTS)
   356  	if c.MessageMode {
   357  		typ |= windows.FILE_PIPE_MESSAGE_TYPE
   358  	}
   359  
   360  	disposition := fs.FILE_OPEN
   361  	access := fs.GENERIC_READ | fs.GENERIC_WRITE | fs.SYNCHRONIZE
   362  	if first {
   363  		disposition = fs.FILE_CREATE
   364  		// By not asking for read or write access, the named pipe file system
   365  		// will put this pipe into an initially disconnected state, blocking
   366  		// client connections until the next call with first == false.
   367  		access = fs.SYNCHRONIZE
   368  	}
   369  
   370  	timeout := int64(-50 * 10000) // 50ms
   371  
   372  	var (
   373  		h    windows.Handle
   374  		iosb ioStatusBlock
   375  	)
   376  	err = ntCreateNamedPipeFile(&h,
   377  		access,
   378  		&oa,
   379  		&iosb,
   380  		fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE,
   381  		disposition,
   382  		0,
   383  		typ,
   384  		0,
   385  		0,
   386  		0xffffffff,
   387  		uint32(c.InputBufferSize),
   388  		uint32(c.OutputBufferSize),
   389  		&timeout).Err()
   390  	if err != nil {
   391  		return 0, &os.PathError{Op: "open", Path: path, Err: err}
   392  	}
   393  
   394  	runtime.KeepAlive(ntPath)
   395  	return h, nil
   396  }
   397  
   398  func (l *win32PipeListener) makeServerPipe() (*win32File, error) {
   399  	h, err := makeServerPipeHandle(l.path, nil, &l.config, false)
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  	f, err := makeWin32File(h)
   404  	if err != nil {
   405  		windows.Close(h)
   406  		return nil, err
   407  	}
   408  	return f, nil
   409  }
   410  
   411  func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) {
   412  	p, err := l.makeServerPipe()
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	// Wait for the client to connect.
   418  	ch := make(chan error)
   419  	go func(p *win32File) {
   420  		ch <- connectPipe(p)
   421  	}(p)
   422  
   423  	select {
   424  	case err = <-ch:
   425  		if err != nil {
   426  			p.Close()
   427  			p = nil
   428  		}
   429  	case <-l.closeCh:
   430  		// Abort the connect request by closing the handle.
   431  		p.Close()
   432  		p = nil
   433  		err = <-ch
   434  		if err == nil || err == ErrFileClosed { //nolint:errorlint // err is Errno
   435  			err = ErrPipeListenerClosed
   436  		}
   437  	}
   438  	return p, err
   439  }
   440  
   441  func (l *win32PipeListener) listenerRoutine() {
   442  	closed := false
   443  	for !closed {
   444  		select {
   445  		case <-l.closeCh:
   446  			closed = true
   447  		case responseCh := <-l.acceptCh:
   448  			var (
   449  				p   *win32File
   450  				err error
   451  			)
   452  			for {
   453  				p, err = l.makeConnectedServerPipe()
   454  				// If the connection was immediately closed by the client, try
   455  				// again.
   456  				if err != windows.ERROR_NO_DATA { //nolint:errorlint // err is Errno
   457  					break
   458  				}
   459  			}
   460  			responseCh <- acceptResponse{p, err}
   461  			closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno
   462  		}
   463  	}
   464  	windows.Close(l.firstHandle)
   465  	l.firstHandle = 0
   466  	// Notify Close() and Accept() callers that the handle has been closed.
   467  	close(l.doneCh)
   468  }
   469  
   470  // PipeConfig contain configuration for the pipe listener.
   471  type PipeConfig struct {
   472  	// SecurityDescriptor contains a Windows security descriptor in SDDL format.
   473  	SecurityDescriptor string
   474  
   475  	// MessageMode determines whether the pipe is in byte or message mode. In either
   476  	// case the pipe is read in byte mode by default. The only practical difference in
   477  	// this implementation is that CloseWrite() is only supported for message mode pipes;
   478  	// CloseWrite() is implemented as a zero-byte write, but zero-byte writes are only
   479  	// transferred to the reader (and returned as io.EOF in this implementation)
   480  	// when the pipe is in message mode.
   481  	MessageMode bool
   482  
   483  	// InputBufferSize specifies the size of the input buffer, in bytes.
   484  	InputBufferSize int32
   485  
   486  	// OutputBufferSize specifies the size of the output buffer, in bytes.
   487  	OutputBufferSize int32
   488  }
   489  
   490  // ListenPipe creates a listener on a Windows named pipe path, e.g. \\.\pipe\mypipe.
   491  // The pipe must not already exist.
   492  func ListenPipe(path string, c *PipeConfig) (net.Listener, error) {
   493  	var (
   494  		sd  []byte
   495  		err error
   496  	)
   497  	if c == nil {
   498  		c = &PipeConfig{}
   499  	}
   500  	if c.SecurityDescriptor != "" {
   501  		sd, err = SddlToSecurityDescriptor(c.SecurityDescriptor)
   502  		if err != nil {
   503  			return nil, err
   504  		}
   505  	}
   506  	h, err := makeServerPipeHandle(path, sd, c, true)
   507  	if err != nil {
   508  		return nil, err
   509  	}
   510  	l := &win32PipeListener{
   511  		firstHandle: h,
   512  		path:        path,
   513  		config:      *c,
   514  		acceptCh:    make(chan (chan acceptResponse)),
   515  		closeCh:     make(chan int),
   516  		doneCh:      make(chan int),
   517  	}
   518  	go l.listenerRoutine()
   519  	return l, nil
   520  }
   521  
   522  func connectPipe(p *win32File) error {
   523  	c, err := p.prepareIO()
   524  	if err != nil {
   525  		return err
   526  	}
   527  	defer p.wg.Done()
   528  
   529  	err = connectNamedPipe(p.handle, &c.o)
   530  	_, err = p.asyncIO(c, nil, 0, err)
   531  	if err != nil && err != windows.ERROR_PIPE_CONNECTED { //nolint:errorlint // err is Errno
   532  		return err
   533  	}
   534  	return nil
   535  }
   536  
   537  func (l *win32PipeListener) Accept() (net.Conn, error) {
   538  	ch := make(chan acceptResponse)
   539  	select {
   540  	case l.acceptCh <- ch:
   541  		response := <-ch
   542  		err := response.err
   543  		if err != nil {
   544  			return nil, err
   545  		}
   546  		if l.config.MessageMode {
   547  			return &win32MessageBytePipe{
   548  				win32Pipe: win32Pipe{win32File: response.f, path: l.path},
   549  			}, nil
   550  		}
   551  		return &win32Pipe{win32File: response.f, path: l.path}, nil
   552  	case <-l.doneCh:
   553  		return nil, ErrPipeListenerClosed
   554  	}
   555  }
   556  
   557  func (l *win32PipeListener) Close() error {
   558  	select {
   559  	case l.closeCh <- 1:
   560  		<-l.doneCh
   561  	case <-l.doneCh:
   562  	}
   563  	return nil
   564  }
   565  
   566  func (l *win32PipeListener) Addr() net.Addr {
   567  	return pipeAddress(l.path)
   568  }