Files
RemoteDesk/transport/hysteria2-agent/windows_agent_control.go
T
曾志威 19a8e03a83
ci / rust (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / package-preview (push) Canceled after 0s
ci / package-installer (push) Canceled after 0s
ci / linux-agent (push) Canceled after 0s
ci / edge-service (push) Canceled after 0s
ci / coturn-pop (push) Canceled after 0s
ci / package-windows-host (push) Canceled after 0s
Document all-Rust migration and extend native media stack
2026-08-14 14:31:57 +08:00

255 lines
9.0 KiB
Go

package main
import (
"bufio"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"time"
)
const (
agentControlLineMax = 16 * 1024
agentPipeTokenEnv = "REMOTEDESK_AGENT_PIPE_TOKEN"
agentPipeAuthDomain = "RemoteDesk Windows Agent pipe auth v1\x00"
)
type mediaRingDescriptor struct {
SchemaVersion uint16 `json:"schema_version"`
SessionID string `json:"session_id"`
Generation uint64 `json:"generation"`
OwnerPID uint32 `json:"owner_pid"`
RingPath string `json:"ring_path"`
SlotCount uint32 `json:"slot_count"`
SlotBytes uint32 `json:"slot_bytes"`
VideoTransport string `json:"video_transport"`
AudioTransport string `json:"audio_transport"`
AudioRing *audioRingDescriptor `json:"audio_ring,omitempty"`
}
type audioRingDescriptor struct {
SchemaVersion uint16 `json:"schema_version"`
SessionID string `json:"session_id"`
Generation uint64 `json:"generation"`
OwnerPID uint32 `json:"owner_pid"`
RingPath string `json:"ring_path"`
SlotCount uint32 `json:"slot_count"`
SlotBytes uint32 `json:"slot_bytes"`
AudioTransport string `json:"audio_transport"`
}
type encodedDesktopOpened struct {
Kind string `json:"kind"`
Error string `json:"error"`
VideoCodec string `json:"video_codec"`
MediaTransport string `json:"media_transport"`
Ring mediaRingDescriptor `json:"ring"`
}
type agentHello struct {
Kind string `json:"kind"`
Authenticated bool `json:"authenticated"`
}
type agentAuthChallenge struct {
Kind string `json:"kind"`
ProtocolVersion uint8 `json:"protocol_version"`
Nonce string `json:"nonce"`
}
func openWindowsEncodedSession(address, sessionID string, fps uint8, allowSoftwareFallback bool) (net.Conn, mediaRingDescriptor, error) {
if err := validateAgentControlAddress(address); err != nil {
return nil, mediaRingDescriptor{}, err
}
if !validSessionID(sessionID) || fps == 0 || fps > 120 {
return nil, mediaRingDescriptor{}, errors.New("Windows Agent encoded session parameters are invalid")
}
isPipe := isWindowsAgentPipe(address)
connection, err := dialWindowsAgentControl(address, 2*time.Second)
if err != nil {
return nil, mediaRingDescriptor{}, fmt.Errorf("connect Windows Agent control: %w", err)
}
closeOnError := true
defer func() {
if closeOnError {
_ = connection.Close()
}
}()
_ = connection.SetDeadline(time.Now().Add(10 * time.Second))
reader := bufio.NewReaderSize(connection, agentControlLineMax+1)
if isPipe {
if err := authenticateWindowsAgentPipe(reader, connection); err != nil {
return nil, mediaRingDescriptor{}, err
}
}
var hello agentHello
if err := readBoundedAgentJSON(reader, &hello); err != nil || hello.Kind != "windows_agent_hello" || (isPipe && !hello.Authenticated) {
return nil, mediaRingDescriptor{}, errors.New("Windows Agent control hello is invalid")
}
request := struct {
Kind string `json:"kind"`
SessionID string `json:"session_id"`
CaptureMode string `json:"capture_mode"`
VideoCodec string `json:"video_codec"`
FramesPerSecond uint8 `json:"frames_per_second"`
AllowSoftwareFallback bool `json:"allow_software_fallback"`
}{
Kind: "open_encoded_desktop", SessionID: sessionID,
CaptureMode: "compatibility", VideoCodec: "h264", FramesPerSecond: fps,
AllowSoftwareFallback: allowSoftwareFallback,
}
encoded, err := json.Marshal(request)
if err != nil {
return nil, mediaRingDescriptor{}, err
}
encoded = append(encoded, '\n')
if _, err := connection.Write(encoded); err != nil {
return nil, mediaRingDescriptor{}, fmt.Errorf("open Windows encoded desktop: %w", err)
}
var opened encodedDesktopOpened
if err := readBoundedAgentJSON(reader, &opened); err != nil {
return nil, mediaRingDescriptor{}, err
}
if opened.Kind != "encoded_desktop_opened" || opened.VideoCodec != "h264" ||
opened.MediaTransport != "shared_memory_encoded_access_units" {
return nil, mediaRingDescriptor{}, fmt.Errorf("Windows encoded desktop unavailable: %s", opened.Error)
}
if err := validateMediaRingDescriptor(opened.Ring, sessionID); err != nil {
return nil, mediaRingDescriptor{}, err
}
_ = connection.SetDeadline(time.Time{})
closeOnError = false
return connection, opened.Ring, nil
}
func dialWindowsAgentControl(address string, timeout time.Duration) (net.Conn, error) {
if isWindowsAgentPipe(address) {
return dialWindowsAgentPipe(address, timeout)
}
return net.DialTimeout("tcp", address, timeout)
}
func authenticateWindowsAgentPipe(reader *bufio.Reader, connection net.Conn) error {
var challenge agentAuthChallenge
if err := readBoundedAgentJSON(reader, &challenge); err != nil {
return err
}
if challenge.Kind != "agent_auth_challenge" || challenge.ProtocolVersion != 1 {
return errors.New("Windows Agent Pipe challenge is invalid")
}
nonce, err := base64.RawURLEncoding.DecodeString(challenge.Nonce)
if err != nil || len(nonce) != 32 {
return errors.New("Windows Agent Pipe nonce is invalid")
}
encodedToken, ok := os.LookupEnv(agentPipeTokenEnv)
if !ok {
return fmt.Errorf("%s is required for authenticated Windows Agent Pipe", agentPipeTokenEnv)
}
_ = os.Unsetenv(agentPipeTokenEnv)
key, err := base64.RawURLEncoding.DecodeString(encodedToken)
if err != nil || len(key) != 32 {
return errors.New("Windows Agent Pipe token must be 32-byte base64url")
}
mac := windowsAgentPipeMAC(key, nonce)
request := struct {
Kind string `json:"kind"`
ProtocolVersion uint8 `json:"protocol_version"`
MAC string `json:"mac"`
}{
Kind: "agent_auth",
ProtocolVersion: 1,
MAC: base64.RawURLEncoding.EncodeToString(mac),
}
encoded, err := json.Marshal(request)
if err != nil {
return err
}
encoded = append(encoded, '\n')
if _, err := connection.Write(encoded); err != nil {
return fmt.Errorf("authenticate Windows Agent Pipe: %w", err)
}
return nil
}
func windowsAgentPipeMAC(key, nonce []byte) []byte {
mac := hmac.New(sha256.New, key)
_, _ = mac.Write([]byte(agentPipeAuthDomain))
_, _ = mac.Write(nonce)
return mac.Sum(nil)
}
func readBoundedAgentJSON(reader *bufio.Reader, output any) error {
line, err := reader.ReadSlice('\n')
if err != nil {
return fmt.Errorf("read Windows Agent control: %w", err)
}
if len(line) == 0 || len(line) > agentControlLineMax || line[len(line)-1] != '\n' {
return errors.New("Windows Agent control response exceeds its bound")
}
if err := json.Unmarshal(line, output); err != nil {
return errors.New("Windows Agent control response is invalid JSON")
}
return nil
}
func validateAgentControlAddress(address string) error {
if isWindowsAgentPipe(address) {
if len(address) > 256 || !strings.HasPrefix(strings.ToLower(address), `\\.\pipe\remotedesk\`) || strings.Contains(address, "..") {
return errors.New("Windows Agent Pipe name is outside the RemoteDesk namespace")
}
return nil
}
host, port, err := net.SplitHostPort(address)
if err != nil || port == "" {
return errors.New("Windows Agent control must be an IP socket address")
}
ip := net.ParseIP(host)
if ip == nil || !ip.IsLoopback() {
return errors.New("Windows Agent control must use a loopback IP")
}
return nil
}
func isWindowsAgentPipe(address string) bool {
return strings.HasPrefix(strings.ToLower(address), `\\.\pipe\`)
}
func validateMediaRingDescriptor(descriptor mediaRingDescriptor, sessionID string) error {
if descriptor.SchemaVersion != 1 || descriptor.SessionID != sessionID || descriptor.Generation == 0 || descriptor.OwnerPID == 0 ||
descriptor.VideoTransport != "shared_memory_encoded_access_units" || descriptor.AudioTransport != "separate_transport" ||
!filepath.IsAbs(descriptor.RingPath) || descriptor.SlotCount < 2 || descriptor.SlotCount > maxRingSlots ||
descriptor.SlotCount&(descriptor.SlotCount-1) != 0 || descriptor.SlotBytes == 0 || descriptor.SlotBytes > maxRingSlotBytes {
return errors.New("Windows Agent media ring descriptor is invalid")
}
if descriptor.AudioRing != nil {
audio := descriptor.AudioRing
if audio.SchemaVersion != 1 || audio.SessionID != sessionID || audio.Generation != descriptor.Generation ||
audio.OwnerPID != descriptor.OwnerPID || audio.AudioTransport != "shared_memory_opus_packets" ||
!filepath.IsAbs(audio.RingPath) || audio.SlotCount < 2 || audio.SlotCount > maxRingSlots ||
audio.SlotCount&(audio.SlotCount-1) != 0 || audio.SlotBytes == 0 || audio.SlotBytes > maxRingSlotBytes {
return errors.New("Windows Agent audio ring descriptor is invalid")
}
}
return nil
}
func validSessionID(value string) bool {
if len(value) == 0 || len(value) > 128 {
return false
}
for _, character := range []byte(value) {
if !((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') ||
(character >= '0' && character <= '9') || character == '-' || character == '_') {
return false
}
}
return true
}