Files
rvbox/internal/client/windowstray/protocol.go
T

142 lines
4.3 KiB
Go

// 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
}
}