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 }