// Package windowstray contains the small, versioned protocol between the // per-session notification-area process and the machine-wide service. The // tray never receives command payloads or opens the client spool. package windowstray import ( "bytes" "context" "encoding/binary" "errors" "fmt" "unicode/utf8" ) // PipeName is the machine-local service endpoint used by every tray session. // The native implementation creates it with PIPE_REJECT_REMOTE_CLIENTS and // an explicit SYSTEM/Administrators/interactive-user ACL. Keeping the name // here (rather than deriving it from user input) prevents cross-session and // path-confusion bugs. const PipeName = `\\.\pipe\RVBoxClientTrayV1` // Handler is invoked by the service after the native adapter has verified the // connecting process token, SID, and session. A response is always encoded // as ActionStatus; requests other than status deliberately carry no payload. type Handler func(context.Context, Peer, Frame) (Frame, error) const ( protocolVersion uint16 = 1 maxFrameBytes = 64 << 10 maxPayloadBytes = 4 << 10 ) var ( ErrInvalidFrame = errors.New("invalid tray protocol frame") ErrFrameTooLarge = errors.New("tray protocol frame is too large") ErrUnauthorized = errors.New("tray peer is not authorized for this action") ErrInvalidPeer = errors.New("tray peer identity is not verified") ) type Action uint16 const ( ActionStatus Action = iota + 1 ActionOpenConfig ActionOpenLog ActionStartService ActionStopService ActionRestartService ActionSetAutomatic ActionSetManual ActionExitTray ) func (action Action) valid() bool { return action >= ActionStatus && action <= ActionExitTray } // Frame is deliberately not an RPC envelope. Payloads are bounded display // text only (status/detail); service mutations use an enum and are rechecked // by the service under the caller's token. type Frame struct { Action Action Payload []byte } func Encode(frame Frame) ([]byte, error) { if !frame.Action.valid() || len(frame.Payload) > maxPayloadBytes || !utf8.Valid(frame.Payload) { return nil, ErrInvalidFrame } if frame.Action != ActionStatus && len(frame.Payload) != 0 { return nil, ErrInvalidFrame } total := 4 + 2 + 2 + 4 + len(frame.Payload) if total > maxFrameBytes { return nil, ErrFrameTooLarge } encoded := make([]byte, total) copy(encoded[:4], []byte("RVTY")) binary.BigEndian.PutUint16(encoded[4:6], protocolVersion) binary.BigEndian.PutUint16(encoded[6:8], uint16(frame.Action)) binary.BigEndian.PutUint32(encoded[8:12], uint32(len(frame.Payload))) copy(encoded[12:], frame.Payload) return encoded, nil } func Decode(encoded []byte) (Frame, error) { if len(encoded) > maxFrameBytes { return Frame{}, ErrFrameTooLarge } if len(encoded) < 12 || !bytes.Equal(encoded[:4], []byte("RVTY")) || binary.BigEndian.Uint16(encoded[4:6]) != protocolVersion { return Frame{}, ErrInvalidFrame } action := Action(binary.BigEndian.Uint16(encoded[6:8])) length := binary.BigEndian.Uint32(encoded[8:12]) if !action.valid() || length > maxPayloadBytes || uint64(length)+12 != uint64(len(encoded)) { return Frame{}, ErrInvalidFrame } payload := bytes.Clone(encoded[12:]) if !utf8.Valid(payload) || action != ActionStatus && len(payload) != 0 { return Frame{}, ErrInvalidFrame } return Frame{Action: action, Payload: payload}, nil } type Peer struct { PID uint32 SessionID uint32 SID string TokenVerified bool Interactive bool Administrator bool System bool } func (peer Peer) Validate() error { if peer.PID == 0 || peer.SessionID == ^uint32(0) || peer.SID == "" || !peer.TokenVerified { return ErrInvalidPeer } return nil } func Authorize(peer Peer, action Action) error { if !action.valid() { return ErrInvalidFrame } if err := peer.Validate(); err != nil { return err } if !peer.Interactive { return fmt.Errorf("%w: tray peer is not interactive", ErrUnauthorized) } switch action { case ActionStatus, ActionOpenConfig, ActionOpenLog, ActionExitTray: return nil case ActionStartService, ActionStopService, ActionRestartService, ActionSetAutomatic, ActionSetManual: if peer.Administrator || peer.System { return nil } return fmt.Errorf("%w: service mutation requires administrator authorization", ErrUnauthorized) default: return ErrInvalidFrame } }