//go:build windows package windowstray // This file is the small native service endpoint. It intentionally uses one // request per pipe connection: the tray is a presentation client, not a // long-lived command channel, and a bounded connection makes cancellation and // peer verification straightforward. The service never hands the tray a // store handle or command payload. import ( "context" "errors" "fmt" "io" "os" "time" "unsafe" winapi "golang.org/x/sys/windows" ) const ( pipeBufferBytes = 64 << 10 pipeInstances = 8 ) var ( ErrUnsupported = errors.New("Windows tray IPC is unavailable on this platform") pipeSDDL = "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;IU)" ) // Serve accepts bounded tray requests until ctx is cancelled. It is safe to // run before any interactive user logs in; in that state no client can pass // the interactive-peer authorization check. func Serve(ctx context.Context, handler Handler) error { if handler == nil { return errors.New("tray handler is required") } for { pipe, err := newTrayPipe() if err != nil { return fmt.Errorf("create tray pipe: %w", err) } connected := make(chan error, 1) go func() { connected <- winapi.ConnectNamedPipe(winapi.Handle(pipe.Fd()), nil) }() select { case <-ctx.Done(): _ = pipe.Close() return nil case err := <-connected: if err != nil && !errors.Is(err, winapi.ERROR_PIPE_CONNECTED) { _ = pipe.Close() if ctx.Err() != nil { return nil } continue } go serveTrayPipe(ctx, pipe, handler) } } } func newTrayPipe() (*os.File, error) { name, err := winapi.UTF16PtrFromString(PipeName) if err != nil { return nil, err } descriptor, err := winapi.SecurityDescriptorFromString(pipeSDDL) if err != nil { return nil, err } attributes := &winapi.SecurityAttributes{ Length: uint32(unsafe.Sizeof(winapi.SecurityAttributes{})), SecurityDescriptor: descriptor, } mode := uint32(winapi.PIPE_ACCESS_DUPLEX | winapi.PIPE_TYPE_MESSAGE | winapi.PIPE_READMODE_MESSAGE | winapi.PIPE_WAIT | winapi.PIPE_REJECT_REMOTE_CLIENTS | winapi.SECURITY_IDENTIFICATION) handle, err := winapi.CreateNamedPipe(name, mode, pipeInstances, pipeBufferBytes, pipeBufferBytes, 0, 0, attributes) if err != nil { return nil, err } return os.NewFile(uintptr(handle), "rvbox-tray-pipe"), nil } func serveTrayPipe(ctx context.Context, pipe *os.File, handler Handler) { if pipe == nil { return } defer pipe.Close() // A blocked Read must be interrupted when service shutdown cancels ctx. readDone := make(chan struct{}) go func() { select { case <-ctx.Done(): _ = pipe.Close() case <-readDone: } }() defer close(readDone) peer, err := peerFromPipe(winapi.Handle(pipe.Fd())) if err != nil { writeTrayResponse(pipe, err) return } request, err := readTrayFrame(pipe) if err != nil { writeTrayResponse(pipe, err) return } if err := Authorize(peer, request.Action); err != nil { writeTrayResponse(pipe, err) return } response, err := handler(ctx, peer, request) if err != nil { writeTrayResponse(pipe, err) return } if response.Action == 0 { response.Action = ActionStatus } if response.Action != ActionStatus { response = Frame{Action: ActionStatus} } writeTrayResponse(pipe, response) } func readTrayFrame(reader io.Reader) (Frame, error) { buffer := make([]byte, maxFrameBytes) count, err := reader.Read(buffer) if err != nil { return Frame{}, err } if count == len(buffer) { return Frame{}, ErrFrameTooLarge } return Decode(buffer[:count]) } func writeTrayResponse(writer *os.File, value any) { frame := Frame{Action: ActionStatus} switch response := value.(type) { case Frame: frame = response case error: message := response.Error() if len(message) > maxPayloadBytes { message = message[:maxPayloadBytes] } frame.Payload = []byte("error: " + message) } encoded, err := Encode(frame) if err != nil { return } _, _ = writer.Write(encoded) _ = winapi.FlushFileBuffers(winapi.Handle(writer.Fd())) } func peerFromPipe(pipe winapi.Handle) (Peer, error) { if pipe == 0 || pipe == winapi.InvalidHandle { return Peer{}, ErrInvalidPeer } var pid uint32 if err := winapi.GetNamedPipeClientProcessId(pipe, &pid); err != nil || pid == 0 { return Peer{}, ErrInvalidPeer } process, err := winapi.OpenProcess(winapi.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) if err != nil { return Peer{}, ErrInvalidPeer } defer winapi.CloseHandle(process) var token winapi.Token if err := winapi.OpenProcessToken(process, winapi.TOKEN_QUERY, &token); err != nil { return Peer{}, ErrInvalidPeer } defer token.Close() user, err := token.GetTokenUser() if err != nil || user.User.Sid == nil { return Peer{}, ErrInvalidPeer } var sessionID uint32 var returned uint32 if err := winapi.GetTokenInformation(token, winapi.TokenSessionId, (*byte)(unsafe.Pointer(&sessionID)), uint32(unsafe.Sizeof(sessionID)), &returned); err != nil || returned != uint32(unsafe.Sizeof(sessionID)) { return Peer{}, ErrInvalidPeer } adminSID, err := winapi.CreateWellKnownSid(winapi.WinBuiltinAdministratorsSid) if err != nil { return Peer{}, ErrInvalidPeer } admin, err := token.IsMember(adminSID) if err != nil { return Peer{}, ErrInvalidPeer } sid := user.User.Sid.String() return Peer{PID: pid, SessionID: sessionID, SID: sid, TokenVerified: true, Interactive: sessionID != 0, Administrator: admin, System: sid == "S-1-5-18"}, nil } // Request opens exactly one local pipe connection and exchanges one frame. // It retries only the transient ERROR_PIPE_BUSY state and never falls back to // an arbitrary filesystem/socket path. func Request(ctx context.Context, request Frame) (Frame, error) { encoded, err := Encode(request) if err != nil { return Frame{}, err } var pipe *os.File for { if ctx.Err() != nil { return Frame{}, ctx.Err() } name, nameErr := winapi.UTF16PtrFromString(PipeName) if nameErr != nil { return Frame{}, nameErr } handle, openErr := winapi.CreateFile(name, winapi.GENERIC_READ|winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, 0, 0) if openErr == nil { pipe = os.NewFile(uintptr(handle), "rvbox-tray-client") break } if !errors.Is(openErr, winapi.ERROR_PIPE_BUSY) { return Frame{}, openErr } timer := time.NewTimer(100 * time.Millisecond) select { case <-ctx.Done(): timer.Stop() return Frame{}, ctx.Err() case <-timer.C: } } defer pipe.Close() state := uint32(winapi.PIPE_READMODE_MESSAGE) _ = winapi.SetNamedPipeHandleState(winapi.Handle(pipe.Fd()), &state, nil, nil) if _, err := pipe.Write(encoded); err != nil { return Frame{}, err } if err := winapi.FlushFileBuffers(winapi.Handle(pipe.Fd())); err != nil { return Frame{}, err } return readTrayFrame(pipe) }