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
518 lines
33 KiB
Markdown
518 lines
33 KiB
Markdown
# RemoteDesk 协议设计
|
||
|
||
## 1. 目标
|
||
|
||
协议服务于 Windows 客户端与自研 Linux Agent,不用于 Windows RDP。设计目标:
|
||
|
||
- 设备先配对,再创建桌面或终端会话。
|
||
- 信令、输入、媒体、剪贴板和文件相互隔离。
|
||
- 支持断线恢复、能力协商和动态网络质量。
|
||
- 消息可演进,旧版本能明确拒绝不兼容能力。
|
||
- 所有入口有大小、速率、状态和权限校验。
|
||
|
||
## 2. 分层
|
||
|
||
```text
|
||
Application
|
||
├── Pairing / Device authorization
|
||
├── Session lifecycle
|
||
├── Input / Clipboard / File / Terminal
|
||
└── Quality control
|
||
|
||
Transport
|
||
├── TLS WebSocket authentication/signaling
|
||
├── WebRTC RTP/RTCP video/audio/feedback
|
||
└── WebRTC DataChannel control/data
|
||
```
|
||
|
||
本地模式由 Agent 监听 TLS WebSocket;边缘模式由 Agent 主动连接 Rendezvous Gateway。WebRTC 媒体和 DataChannel 始终在会话进程与客户端之间建立,agentd/Gateway 只转发 SDP/ICE 信令。
|
||
|
||
## 3. 协议版本
|
||
|
||
每个 Envelope 包含:
|
||
|
||
```protobuf
|
||
message Envelope {
|
||
uint32 protocol_major = 1;
|
||
uint32 protocol_minor = 2;
|
||
bytes message_id = 3; // 16-byte UUID
|
||
bytes session_id = 4; // empty before session creation
|
||
uint64 sequence = 5;
|
||
uint64 sender_mono_us = 6; // only meaningful within one sender
|
||
MessageType type = 7;
|
||
bytes payload = 8;
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- 主版本不一致直接返回 `INCOMPATIBLE_PROTOCOL`。
|
||
- 次版本通过 capability bitset 协商。
|
||
- 未识别的可选字段忽略。
|
||
- 未满足的 required capability 导致请求失败,不静默降级。
|
||
- monotonic timestamp 不跨机器比较;RTT 使用带 nonce 的 Ping/Pong 测量。
|
||
- message/session ID 使用随机 UUID,不能由递增数据库 ID 替代。
|
||
|
||
## 4. WebSocket 帧
|
||
|
||
- 一个 binary WebSocket message 对应一个 Protobuf Envelope。
|
||
- 默认最大信令消息 1 MiB;握手、输入和控制使用更小的类型级限制。
|
||
- 压缩默认关闭,避免对包含认证数据的消息产生压缩侧信道。
|
||
- 未认证连接只能发送 `ClientHello`、配对相关消息和有限 Ping。
|
||
- 认证前执行连接数、消息速率和失败次数限制。
|
||
|
||
## 5. 设备配对
|
||
|
||
### 5.1 设备身份
|
||
|
||
- Agent 安装时生成长期 Ed25519 设备密钥。
|
||
- Windows 客户端首次启动生成长期客户端设备密钥。
|
||
- 私钥从不通过协议传输。
|
||
- 已配对记录绑定客户端公钥、名称、权限和撤销状态。
|
||
|
||
### 5.2 首次配对流程
|
||
|
||
```text
|
||
Windows Client Linux agentd/local UI
|
||
| |
|
||
|---- TLS ClientHello ------------------->|
|
||
|<--- AgentHello + cert fingerprint ------|
|
||
|---- PairStart(pairing code) ------------>|
|
||
|<--- PairChallenge + nonce ---------------|
|
||
|---- client key + signed transcript ------>|
|
||
|<--- local confirmation pending -----------|
|
||
| user compares short fingerprint |
|
||
|<--- PairAccepted + permissions -----------|
|
||
```
|
||
|
||
配对码必须限时、限次且只能由 Linux 本地用户开启。双方显示由完整握手 transcript 派生的短指纹,由用户在 Linux 本地确认客户端名称和指纹。最终实现应使用经过审查的 PAKE/安全配对库;不得自行组合加密原语。
|
||
|
||
### 5.3 后续连接
|
||
|
||
- 使用已保存设备密钥进行双向认证。
|
||
- TLS certificate/fingerprint 变化时中止自动连接。
|
||
- 每次连接使用新 nonce,签名覆盖双方 nonce、协议版本和 capability digest。
|
||
- 被撤销客户端在认证阶段拒绝,不能进入会话创建。
|
||
|
||
## 6. 权限模型
|
||
|
||
配对设备按能力授权:
|
||
|
||
```text
|
||
DESKTOP_VIEW
|
||
DESKTOP_INPUT
|
||
AUDIO_RECEIVE
|
||
CLIPBOARD_READ
|
||
CLIPBOARD_WRITE
|
||
FILE_SEND_TO_HOST
|
||
FILE_RECEIVE_FROM_HOST
|
||
TERMINAL_OPEN
|
||
TERMINAL_RESUME
|
||
UNATTENDED_REQUEST
|
||
```
|
||
|
||
桌面查看不隐含输入权限,终端权限不隐含文件权限。权限变化立即增加 `authorization_epoch`;旧 epoch 的会话票据失效。
|
||
|
||
## 7. 能力协商
|
||
|
||
客户端和 Agent 交换:
|
||
|
||
- Session types:Wayland、Xorg、Terminal。
|
||
- Video codecs/profiles/levels。
|
||
- Hardware encode/decode results,不只报告插件名称。
|
||
- Raw-frame memory paths:DMA-BUF/D3D11 surface、GPU copy、CPU upload、software,以及是否经过短帧验证。
|
||
- Agent GPU/DRM identity、DMA-BUF modifier/import 与硬件编码器的同设备匹配结果。
|
||
- Audio codec 和 FEC/DTX。
|
||
- Display count、尺寸、scale 和 capture type。
|
||
- Input types:absolute、relative、keyboard、touch。
|
||
- Clipboard MIME types 和最大大小。
|
||
- RTX/NACK/FEC/TWCC 支持。
|
||
- Portal permission level 和恢复能力。
|
||
|
||
能力摘要使用稳定排序后计算 digest,创建会话时将 digest 写入票据,避免协商过程被替换。
|
||
|
||
## 8. 会话创建
|
||
|
||
### 8.1 请求
|
||
|
||
`CreateSessionRequest` 至少包含:
|
||
|
||
```text
|
||
request_id
|
||
session_type
|
||
target_user
|
||
requested_permissions
|
||
display_selection
|
||
quality_profile
|
||
zero_copy_policy: required_end_to_end | compatibility
|
||
client_capabilities_digest
|
||
```
|
||
|
||
`required_end_to_end` 是 Linux 图形会话默认值。Agent 在捕获/缩放/编码端、native-video helper 在解码/呈现端分别发送带 generation 的 `EndpointMemoryPath`;SessionController 只有在两端均为经过验证的 `zero_copy` 后才进入 Connected。任一端报告 `gpu_copy`、`cpu_upload`、`software` 或 `opaque_gpu_path` 都使协商失败。`compatibility` 必须由用户显式选择,不能由远端或自动重连自行降低策略。
|
||
|
||
```text
|
||
EndpointMemoryPath:
|
||
endpoint: agent_encode | client_render
|
||
generation
|
||
status: zero_copy | gpu_copy | cpu_upload | software | opaque_gpu_path
|
||
adapter_identity
|
||
capture_device_identity
|
||
surface_type: dmabuf | d3d11 | cpu | opaque
|
||
cpu_map_count
|
||
cross_adapter_copy_count
|
||
verification_source
|
||
```
|
||
|
||
切换显示器、GPU、编码器、decoder 或分辨率会递增 generation,并使旧验证立即失效。两端必须重新报告;计数非零或验证信息缺失时不能进入严格 Connected 状态。
|
||
|
||
agentd 完成设备权限、目标用户、会话存在性和并发限制校验后,签发短期单次使用的 Session Ticket。
|
||
|
||
### 8.2 Ticket
|
||
|
||
Ticket 绑定:
|
||
|
||
- client public key hash。
|
||
- session ID/type/target UID。
|
||
- permission bitset 和 authorization epoch。
|
||
- issue/expiry time。
|
||
- 一次性 nonce。
|
||
- capability digest。
|
||
|
||
Session 进程只接受 agentd 签名且未过期的 Ticket。Ticket 使用一次后进入短期 replay cache。
|
||
|
||
### 8.3 生命周期
|
||
|
||
```text
|
||
CREATED -> AUTHORIZING -> NEGOTIATING -> CONNECTED
|
||
-> DEGRADED
|
||
-> RECONNECTING
|
||
-> CLOSING -> CLOSED
|
||
failures ----------------> FAILED
|
||
```
|
||
|
||
所有状态变化包含 reason code、可重试标记和用户安全展示文本 ID。不得仅发送任意字符串错误。
|
||
|
||
### 8.4 多显示器布局
|
||
|
||
显示拓扑是独立、带版本的安全边界:
|
||
|
||
```text
|
||
DisplayLayout:
|
||
generation
|
||
displays[]:
|
||
stable_id: 16 bytes
|
||
rect: x, y, width, height
|
||
scale: numerator / denominator
|
||
is_primary
|
||
|
||
DisplaySelection:
|
||
layout_generation
|
||
mode: single | all | custom
|
||
display_ids[]
|
||
```
|
||
|
||
坐标使用物理桌面像素,主屏原点固定为 `(0, 0)`;位于主屏左侧或上方的显示器使用负坐标。布局必须恰好有一个主屏,并拒绝空布局、重复 ID、零尺寸、无效缩放、坐标端点溢出和累计像素数溢出。自定义选择拒绝空集合、重复/未知 ID 以及 generation 不匹配。
|
||
|
||
显示器热插拔、位置、旋转、DPI、分辨率或主屏变化都会发布新 generation。输入、捕获选择、分辨率请求和零拷贝报告必须绑定同一 generation;收到旧 generation 时丢弃请求并重新同步,不能按数组序号猜测显示器身份。
|
||
|
||
Windows RDP helper 把验证后的本地布局映射为 Display Control Dynamic Monitor Layout。Linux Agent 把同一选择模型映射到 Portal/PipeWire 或 XRandR 捕获源,但不会修改 Linux 实体显示模式。
|
||
|
||
### 8.5 Windows Headless Compatibility Contract
|
||
|
||
Windows Headless compatibility 模式允许无硬件 GPU 主机使用可用的 Desktop Duplication 或 IDD 输出、WARP 或 CPU 编码,但必须显式报告 software 和 degraded。能力结果至少包含 capture_backend、adapter_type、surface_type、encoder_backend、hardware_encoder_verified、hardware_pipeline_verified、hardware_path_verified、cpu_map_count、max_width、max_height、max_fps 和 concurrent_sessions。`hardware_encoder_verified` 只代表真实短样本编码成功;DDA staging/readback 仍有 CPU map,因此该路径的 `hardware_pipeline_verified` 和 `hardware_path_verified` 必须为 false。若请求硬性要求 4K/120 或硬件编码,而本地只有 software 路径,Agent 返回 HARDWARE_ENCODER_UNAVAILABLE,不得静默降级。
|
||
|
||
Windows Headless 媒体契约固定为 SDR 8-bit:video codec 为 H.264/AVC、H.265/HEVC 或 AV1,输入格式为 NV12,audio codec 为 Opus。IDD 不提供本项目的 HDR10/10-bit 能力,不得出现 P010、Main10 或 HDR metadata。
|
||
|
||
Windows Headless 视频使用 Hysteria2 不可靠 datagram,允许丢失。缺少一个分片时丢弃整个帧;过期帧不重传;首帧非 IDR、sequence 中断、discontinuity、解码失败或关键帧损坏时,经独立可靠控制流发送带 session ID 的 `request_keyframe`。Client 以 250 ms 最小间隔合并请求,Agent 以容量 1 的队列合并请求并通过 `CODECAPI_AVEncVideoForceKeyFrame` 请求编码器输出 IDR。收到新 codec config 和 IDR 后才能恢复普通帧。
|
||
|
||
本地 encoded-ring descriptor 只能经 `\\.\pipe\RemoteDesk\...` 命名空间交付。Pipe DACL 只授权当前用户 SID,拒绝远程客户端并启用首实例保护;双方从进程环境取得同一个随机 32 字节 base64url bootstrap,Agent 为每个连接发送 32 字节随机 nonce,Go bridge 返回 `HMAC-SHA256(key, "RemoteDesk Windows Agent pipe auth v1\\0" || nonce)`。bootstrap 不进入命令行或 JSON,双方读取后从各自环境删除;认证和 hello 共用有界握手时限。仅 `authenticated: true` 的连接可作为生产 descriptor 来源,ring 路径、session ID、generation、owner PID 和 geometry 必须全部校验后才能映射。
|
||
|
||
RDA1 音频 datagram 使用固定 40 字节 little-endian header:magic/version/reserved/header_bytes、stream ID、generation、sequence、PTS、duration_ms 和 flags,后接一个不超过 4 KiB 的独立 Opus packet。reserved 必须为 0,当前仅定义 discontinuity flag,duration 仅允许 10/20/40/60 ms;未知 flags、零 identity、空 payload 和超限 packet 必须在进入 jitter buffer 前拒绝。音频 packet 不依赖 RDV1 frame ID,不因视频分片缺失而等待或丢弃。
|
||
|
||
输入控制使用独立可靠高优先级流,带 input_seq 和 layout_generation。输入发送不等待视频帧、编码完成、媒体 ACK 或音频播放。文件和剪贴板使用独立可靠低优先级流,不能阻塞输入、音频或视频。
|
||
|
||
音视频共享 session clock/QPC 映射和 generation,但独立采集、编码、队列和发送。客户端以音频播放时钟同步视频,视频落后时丢弃旧帧,不为追求完整帧率而增加播放延迟。
|
||
|
||
Hysteria2 音频包使用独立的 RDA1 datagram,不进入视频分片重组器。固定 40 字节头包含 stream、generation、sequence、100ns media PTS、duration(10/20/40/60 ms)和 flags,Opus payload 上限为 4 KiB。音频包独立丢弃或播放,视频缺片不能阻塞音频,音频丢包也不能阻塞视频。Windows compatibility runtime 已使用独立 worker 完成 WASAPI loopback、48 kHz 双声道 20 ms Opus、独立 mmap ring、Go RDA1 发送,以及 viewer 有界 jitter/FEC/PLC/WASAPI playback 的源码接线;统一 session-start QPC、长时间漂移校正和真实设备验收仍是发布前条件。
|
||
|
||
## 9. WebRTC 信令
|
||
|
||
- Offer/Answer 和 ICE candidate 放入 Protobuf 信令消息。
|
||
- SDP 大小和 candidate 数量有限制。
|
||
- DTLS fingerprint 必须与 Session Ticket/握手身份绑定。
|
||
- ICE 优先直连,并接受 Allocator 签发的短期 TURN credential。
|
||
- Allocator 返回 `DIRECT/SINGLE_EDGE/DUAL_EDGE` 候选路径、Client/Agent POP 和路径 generation。
|
||
- 智能优选可以让两个端点使用不同 relay candidate,经供应商骨干连接两个 POP。
|
||
- TURN credential 绑定 POP、session/allocation、有效期和配额,不能替代 Session Ticket。
|
||
- 边缘 Gateway 只路由已签名请求,Agent 仍是最终授权方。
|
||
- 网络接口变化可以触发 ICE restart;失败后重建 PeerConnection。
|
||
- 路径变化使用递增 generation,旧 POP 探测和 credential 结果不得覆盖新选择。
|
||
|
||
当前 Edge HTTP 信令边界使用域隔离的 `EdgeNegotiationEnvelopeV1`,只允许已被 Agent 接受且仍有效的 Session Intent 建立 mailbox。send/poll 都由对应 Client 或 Agent Ed25519 key 签名;签名覆盖 request ID、session ID、端点角色、操作、sequence、generation、消息类型、payload、nonce、签发和过期时间。send sequence 从 1 开始并按端点严格递增;共享 generation 从 1 开始,只有 `restart` 可精确加一;poll 的 sequence 是已消费的 peer cursor。服务端拒绝过期、重放、跨会话、跨角色、乱序、错误 generation 和超限消息,只返回 peer 产生的消息。
|
||
|
||
资源上限为 SDP 128 KiB、单 candidate 4 KiB、每端点每 generation 128 candidates、每会话 512 消息、单次 poll 64 条且 payload 最多 192 KiB、最多 1024 活跃 mailbox 和总 payload 8 MiB。该 mailbox 只完成控制信令转发;DTLS fingerprint、TURN credential 和最终媒体连接仍必须由端点 WebRTC 层验证。
|
||
|
||
### 9.1 Edge Path Messages
|
||
|
||
边缘路径协商增加:
|
||
|
||
```text
|
||
PathProbeRequest(candidate_pops, token, expires_at)
|
||
PathProbeReport(endpoint, pop, udp/tcp/tls metrics, token)
|
||
BackboneMetric(pop_a, pop_b, rtt/loss/jitter, provider_signature)
|
||
PathSelection(mode, client_pop, agent_pop, transport, generation)
|
||
RelayCredential(pop, endpoint, allocation_id, expires_at, quota)
|
||
PathSwitchRequest(old_generation, candidates, reason)
|
||
```
|
||
|
||
端点探测只代表最后一公里;POP 间骨干指标必须来自受信控制面或供应商测量,不能由客户端任意声明。`PathSelection` 不授予会话权限,必须与有效 Session Ticket 同时使用。
|
||
|
||
## 10. DataChannel
|
||
|
||
| Channel | ordered | retransmit | 优先级 |
|
||
|---|---:|---:|---:|
|
||
| control | 是 | 无限/连接级超时 | 最高 |
|
||
| key_button | 是 | 有限时间可靠 | 最高 |
|
||
| pointer_motion | 否 | 0 或极低 | 高 |
|
||
| terminal | 是 | 可靠 | 高 |
|
||
| clipboard | 是 | 可靠 | 低 |
|
||
| file | 是 | 可靠 | 最低 |
|
||
|
||
键盘按键与鼠标按钮不能与高频移动共用不可靠 channel。SCTP buffered amount 达到高水位时,文件和剪贴板生产者暂停。
|
||
|
||
## 11. 输入协议
|
||
|
||
键盘事件:
|
||
|
||
```text
|
||
event_id
|
||
hid_usage
|
||
pressed
|
||
modifiers
|
||
layout_id
|
||
unicode_text optional
|
||
```
|
||
|
||
鼠标事件:
|
||
|
||
```text
|
||
event_id
|
||
mode absolute|relative
|
||
display_id
|
||
topology_version
|
||
x/y or dx/dy
|
||
buttons
|
||
wheel_x/wheel_y
|
||
```
|
||
|
||
绝对坐标使用固定整数范围归一化,避免浮点差异。显示器拓扑版本不匹配时丢弃位置事件并请求最新拓扑。重连、失焦和断开时交换完整按键状态,释放可能卡住的键。
|
||
|
||
## 12. 质量控制协议
|
||
|
||
分辨率控制使用可靠、有序的 Control DataChannel。客户端先读取 `DisplayCapabilities`:
|
||
|
||
```text
|
||
display_ids/topology_version
|
||
source_width/source_height
|
||
min_width/min_height
|
||
max_width/max_height
|
||
max_pixels
|
||
width_alignment/height_alignment
|
||
dynamic_reconfigure
|
||
supported_modes: follow_window | source_native | fixed
|
||
```
|
||
|
||
客户端发送 `ResolutionRequest`,Agent 返回 `ResolutionApplied` 或结构化错误:
|
||
|
||
```text
|
||
request_id
|
||
generation
|
||
mode
|
||
target_width/target_height optional
|
||
adaptive_downscale
|
||
topology_version
|
||
|
||
ResolutionApplied:
|
||
request_id/generation
|
||
source_width/source_height
|
||
encoded_width/encoded_height
|
||
reason: requested | network_downscale | capability_clamp | source_changed
|
||
```
|
||
|
||
宽高在进入媒体管线前校验并按编码器约束对齐,不能由 Agent 静默放大到超出用户目标。旧 generation 的请求和确认均忽略。Linux 的请求只改变编码尺寸,不代表修改 Wayland/Xorg 的实体显示模式。RDP 使用相同的客户端状态模型,但请求由 RDP helper 映射到 Display Control,而不是发给 Linux Agent。
|
||
|
||
Agent 周期性发送 `QualityReport`:
|
||
|
||
```text
|
||
rtt_ms
|
||
loss_fraction
|
||
jitter_ms
|
||
gcc_target_bps
|
||
nack_rate
|
||
pli_count
|
||
encode_time_ms
|
||
send_queue_ms
|
||
current_ladder
|
||
source_width/source_height
|
||
encoded_width/encoded_height
|
||
resolution_generation
|
||
capture_memory_path
|
||
encode_memory_path
|
||
client_render_memory_path
|
||
zero_copy_verified
|
||
```
|
||
|
||
客户端返回 decode/render time、dropped frames 和 DataChannel buffer。质量档位切换消息带 generation,旧 generation 的异步结果忽略。媒体自适应细节见 `network-adaptation.md`。
|
||
|
||
## 13. 剪贴板
|
||
|
||
- Offer/Request/Data 三阶段,不收到 Request 不发送内容。
|
||
- 文本第一版只支持 UTF-8 plain text。
|
||
- MIME type allowlist 和单项大小限制。
|
||
- 内容带 content hash,重复内容不循环同步。
|
||
- 本地/远端 sequence 防止双方互相回写。
|
||
- 权限撤销、锁屏或会话结束立即清理缓冲。
|
||
|
||
协议 minor 15 将文本剪贴板接入 X11 桌面会话。`begin_direct_web_rtc` 和 `open_desktop` 新增默认关闭的 `clipboard_read`、`clipboard_write`,分别表示 Client 可读取被控端剪贴板、可写入被控端剪贴板;agentd 必须再次对照配对授权检查方向,并只透明转发结构化消息,root 进程不得读取内容。发送方先用单调非零 sequence、UTF-8 字节数和小写 SHA-256 发送 `DesktopClipboardOffer`,接收方验证 32 KiB 上限后以同 sequence 请求,发送方才返回无 padding 的 canonical Base64 `DesktopClipboardData`。接收方将 Data 与当前待处理 Offer 的 sequence、长度和摘要绑定,拒绝无效 UTF-8、NUL、非规范 Base64、CR/CRLF 未规范化或摘要不匹配的内容。双方记录最近应用/发出的摘要,避免系统剪贴板所有权变化造成回写循环;只保留最新待请求内容。minor 14 及更早客户端缺少协商字段时两方向均为 `false`。当前源码实现 X11 与 Windows `CF_UNICODETEXT`,Wayland Portal 剪贴板尚未接入。
|
||
|
||
## 14. 文件传输
|
||
|
||
```text
|
||
FileOffer -> Accept(path policy) -> Chunk* -> Complete(hash)
|
||
-> Reject/Cancel
|
||
```
|
||
|
||
- 文件名是名称,不允许携带绝对路径或 `..`。
|
||
- Agent 生成目标临时路径,完成哈希校验后原子改名。
|
||
- Chunk 使用固定最大大小和 offset,支持已确认范围恢复。
|
||
- 总大小、并发数、速率和磁盘剩余空间均有限制。
|
||
- 软链接、特殊文件和设备节点默认拒绝。
|
||
- Critical 网络状态暂停数据块但保留控制消息。
|
||
|
||
当前文件协议 minor 4 的上传请求包含由 Client 对 target user、相对路径、声明大小和整文件 SHA-256 派生的稳定 transfer ID。Agent 不信任 Client 提供 offset;降权 file helper 只打开目标目录中该 ID 对应的普通 partial 文件,读取实际长度并哈希已有前缀,再通过 `FileReady.offset` 返回。中断时保留 partial,续传只接收剩余字节;达到声明大小但整文件 SHA-256 不符时删除 partial,成功时才原子替换目标。
|
||
|
||
协议 minor 5 增加 X11 桌面兼容传输。`OpenDesktop` 绑定授权普通用户、最大宽高和 1..30 FPS;会话中的 `DesktopResize` 可把最大宽高重配为 200..8192。Client 合并连续窗口变化后发送最终尺寸,Agent 在当前帧收到 `DesktopFrameAck` 后才按新上限捕获下一帧,不修改 Xorg 的实体显示模式。每帧先发送 `DesktopFrameStart`,再发送严格递增的 45 KiB `DesktopFrameChunk`,最后用 `DesktopFrameComplete` 携带未压缩 BGRA 的 SHA-256。Client 只有在分块数量、压缩长度、解压后精确字节数和 SHA-256 全部匹配且窗口成功呈现后才发送 `DesktopFrameAck`;Agent 在收到该 ACK 前不捕获下一帧。`DesktopPing/DesktopPong` 在当前 WSS 或 Edge relay 会话内测量实际往返时间,不依赖两端时钟。单帧最多 33,177,600 像素、128 MiB 未压缩数据和 64 MiB 压缩数据,解压读取使用精确上限防止压缩炸弹。
|
||
|
||
桌面输入使用 `DesktopInput`,包含绝对指针、五键鼠标、水平/垂直滚轮、X11 keysym 按下/释放和 `release_all`。Agent 根据当前用户 X server 的 keyboard mapping 将 keysym 转为 keycode,客户端不能直接注入平台相关 X keycode。桌面断开、窗口失焦和退出都会释放 Agent 跟踪的按键状态。
|
||
|
||
协议 minor 6 为 X11 兼容链路增加短时会话续接。首次 `OpenDesktop` 不携带令牌,用户会话进程生成 32 字节随机 URL-safe Base64 令牌,并在 `DesktopOpened` 返回;agentd 从已完成 Ed25519 认证的 `ClientGrant` 注入客户端指纹,远程请求不能自行指定该绑定。WSS 或 Edge relay 异常断开时立即释放 XTEST 输入状态,丢弃未完成帧和 ACK,但保留 X11 连接、递增帧序列和自适应 pacer 15 秒。Client 按 1、2、4 秒有界重连,重新执行证书固定和 Client 身份认证,Edge 路径重新申请一次性 relay 票据,再以同一用户和令牌调用 `OpenDesktop`。只有令牌、客户端指纹、目标用户和有效期全部匹配才恢复;成功后令牌立即旋转,旧令牌不可重放。Windows Client 还把最新令牌保存到当前用户 Credential Manager,并按 Agent TLS 证书指纹和 Linux 用户隔离;helper 进程异常退出后,在 15 秒租约内以相同身份重新启动可读取令牌继续会话。正常关闭、服务端关闭或恢复拒绝会删除该记录,令牌不进入 argv、浏览器状态或普通文件。恢复从新的完整帧边界开始,不重放断线期间的输入、帧分块或 ACK。命令解析、状态校验、采集或输入注入失败也统一释放 XTEST 输入,但不建立恢复租约;正常 `Close` 同样不创建租约。
|
||
|
||
协议 minor 7 在 `DesktopFrameMetadata` 增加实际 `compression_level`。X11 兼容链路从 zlib level 1 启动:连续两个慢呈现 ACK 且最近编码耗时不超过当前帧预算三分之一时,逐级提高压缩至最多 6;连续两帧编码耗时超过帧预算一半时逐级降低;连续 60 个在帧预算内完成的 ACK 后逐级回到 level 1。等级和帧率控制器都使用真实编码计时与呈现 ACK,状态随 15 秒恢复租约保留。每帧诊断同时报告等级、压缩后/未压缩字节比例和编码耗时,客户端仍按元数据长度上限、精确解压长度与 SHA-256 完整性验证后才呈现。
|
||
|
||
协议 minor 8 为桌面打开命令增加签名 Intent 的 `edge_session_id` 和 Agentd 到用户会话的显式 `webrtc_h264` 协商位,并增加 `DesktopH264Start/Chunk/Complete/Unavailable` 内部事件。Agentd 只有在 session ID、过期时间、目标用户和已认证 Client 指纹全部匹配时才单次消费 WebRTC sender,直连请求不能取走 Edge sender。每个 Annex-B access unit 独立声明尺寸、时长、关键帧、编码耗时、编码器和分块数;Agentd 按 16 MiB AU 上限、45 KiB 分块、严格顺序、canonical base64、精确长度及 SHA-256 验证后才发送。
|
||
|
||
协议 minor 9 增加显式 `DesktopVideoMode`、`DesktopVideoModeChanged` 和 `DesktopH264FrameAck`。Client 只有在实际呈现第一张原生 H.264 帧后才请求 `webrtc_h264` 模式;用户会话会等待当前 zlib 帧的呈现 ACK,在完整帧边界确认切换。确认后每次捕获只发送 H.264,不再编码或传输完整 zlib 帧,并等待对应 sequence 的 H.264 呈现 ACK 后才捕获下一帧。Agentd 只向 Client 转发 H.264 Start 元数据用于 sequence 绑定、采集和编码遥测,Chunk/Complete 仍仅在 root/user IPC 内使用。RTP 序号中断会立即产生恢复事件并切回 zlib,同时继续监听关键帧以便重新进入原生模式;客户端解码失败、媒体邮箱积压、轨道结束、sender/encoder 失败或三秒内未收到 H.264 ACK 时也显式回退。服务端确认回退后从下一张完整帧恢复兼容传输。zlib ACK 有五秒绝对上限,错误类型或旧 sequence 的 ACK 不推进采集节奏。
|
||
|
||
协议 minor 10 为 X11 桌面输入增加 `pointer_delta`。Windows helper 只有在用户启用相对鼠标并成功抓取当前窗口光标后才读取 Raw Input;同一事件循环批次的位移会合并,保留小数余量,并将每轴单消息限制为 `-4096..4096`。零位移、越界值和未知字段由 Agent 拒绝。X11 用户会话使用 XTEST `root=None` 的相对移动语义,不把相对量误当成缩放后的绝对坐标。`Ctrl+Alt+Home` 在本地切换捕获;失焦、重连、全屏切换和退出都会释放远端按键/按钮,失焦、重连和退出还会解除光标抓取并丢弃未发送位移。
|
||
|
||
协议 minor 11 为 `DesktopFrameEncoding` 增加 `webrtc_h264`,用于没有 zlib 原始帧回退的 Wayland 严格媒体会话。Agent 只有在认证后的 Portal 返回单个有效 PipeWire stream、DMABUF 到 VA surface/H.264 管线产生首个 IDR、EIS Sender 完成 seat/device/keymap 协商且 Agentd 已绑定对应 WebRTC sender 后才发送 `DesktopOpened`。每个 H.264 sequence 由实际呈现后的 `DesktopH264FrameAck` 推进;`DesktopResize` 在上一帧 ACK 后先停止旧管线,再重建管线并等待新 IDR。绝对输入必须匹配 Portal stream 与 EIS region 的 `mapping_id`;相对指针、五键、滚轮和键盘通过同一 EIS emulation 生命周期提交。严格管线不允许退回 CPU BGRA,协商或客户端硬解失败会结束该 Wayland 会话。
|
||
|
||
协议 minor 12 为 `webrtc_h264` 会话启用与 X11 相同的轮换恢复令牌语义。意外断线立即释放全部 EIS 输入并销毁旧编码器,但用户会话进程最多保留 Portal、PipeWire remote 和 EIS 连接 15 秒;租约同时绑定 Agentd 注入的已认证 Client 指纹和目标 Linux 用户。Client 仅对 minor 12 及以上持久化该令牌。恢复连接必须先建立并授权新的 WebRTC sender,提交相同绑定的令牌后 Agent 才消费租约、重建严格编码器、等待新 IDR 并以连续递增的 sequence 返回 `DesktopOpened(resumed=true)`。令牌在每次恢复时轮换;错误令牌、不同 Client/用户、显式关闭、ACK 超时、输入/编码错误或租约到期都会拒绝恢复并显式关闭 Portal。
|
||
|
||
协议 minor 13 增加认证 WSS 内的直连 WebRTC 信令。Client 先发送绑定目标用户、尺寸和帧率的 `begin_direct_web_rtc`,Agent 完成现有桌面权限校验后返回 ready;后续 `direct_web_rtc_signal` 只接受严格有界的 offer、answer、ICE candidate 和 `ice_end`。双方只有在 PeerConnection connected、本地 `ice_end` 已发送且远端 `ice_end` 已消费后才离开信令阶段,从而保证后续 `OpenDesktop` 不会与残留 ICE 消息混用。`OpenDesktop` 的用户、尺寸和帧率必须与授权阶段完全一致,Client 指纹仍由 Agent 注入。直连仅使用 host candidates,不依赖 Edge/TURN;失败时 Client 发送 `abort_direct_web_rtc`,Agent 返回一次 `direct_web_rtc_unavailable` 后才允许 `OpenDesktop(webrtc_h264=false)` 进入 X11 zlib。Wayland 不接受该软件回退。成功时 Agent 直接把本次 PeerConnection 的 H.264 sender 交给用户会话代理,并在桌面结束或任一代理错误后显式关闭 ICE/DTLS/SRTP。
|
||
|
||
协议 minor 14 为 `begin_direct_web_rtc` 和 `open_desktop` 增加默认关闭的 `opus_audio`。Client 只有在对端 minor 不低于 14 且本次 WebRTC 媒体会话存在时才发送 `true`;minor 13 及更早 JSON 缺少该字段时反序列化为 `false`。直连授权把音频选择与 Client 指纹、用户、尺寸和帧率一同绑定,失败回退时视频和音频都必须关闭;Edge 路径也只在请求显式开启且取到绑定当前 session/user/Client 的媒体 sender 后向用户会话注入 `true`。未协商音频时用户会话不启动系统输出捕获,agentd 不向 Client 转发任何 Opus packet/unavailable 事件。协商后 Linux 用户会话仅捕获 Pulse/PipeWire 的默认输出 monitor(或经严格名称校验的管理员覆盖),编码为 48 kHz 双声道、20 ms、96 kbps Opus;每包限制 4 KiB,序号、时长、编码耗时和 canonical Base64 在 root/user IPC 边界重新校验。音频 sender 或 Windows 本地解码/播放失败只关闭音频,不中断 H.264 或 zlib 视频。
|
||
|
||
X11 用户会话把请求的 1..30 FPS 视为上限,并使用“帧写入 IPC 完成到客户端成功呈现 ACK”的真实反馈调整捕获节奏。连续两帧 ACK 超过当前帧预算两倍或 250 ms 时将目标速率降为当前值的约 75%,最低 1 FPS;连续 30 帧在预算内时每次恢复 1 FPS,最高不超过请求值。档位变化从新的完整周期开始,输入、Ping/Pong 和关闭消息不受帧计时器限制;丢失、错误序列或未呈现的帧不会被当作快速样本。
|
||
|
||
客户端主动退出时先发送 `release_all` 和 `Close`。用户会话进程完成 XTEST 按键/按钮释放后返回 `DesktopClosed { reason: "client_closed" }`,system Agent 最多等待 2 秒并转发该终止事件;控制端在收到关闭握手或超时后关闭 WebSocket。窗口仅保留 `Ctrl+Alt+F` 全屏切换,其他键盘事件直接进入远端输入链路。
|
||
|
||
下载请求携带本地隐藏 partial 的实际长度。Agent 按远端文件真实大小拒绝越界 offset,返回同一 offset、完整大小和 SHA-256,并只发送剩余区间。Client 在追加前重新读取并哈希已有前缀,完成后校验前缀与新增数据组成的整文件哈希;不匹配时删除 partial,匹配后才原子改名。partial 路径不允许替代已存在的最终目标。
|
||
|
||
## 15. 终端协议
|
||
|
||
终端消息包括:
|
||
|
||
- PTY output bytes。
|
||
- Client input bytes。
|
||
- Resize(cols, rows, pixel width/height optional)。
|
||
- Signal(INT/TERM/HUP allowlist)。
|
||
- Exit(code/signal/core flag)。
|
||
- Resume(last received sequence)。
|
||
|
||
协议传输字节而非命令字符串,Agent 不拼接 shell 命令。输出环形缓冲区有固定上限,超过后返回 `OUTPUT_GAP`,客户端明确提示丢失历史输出。
|
||
|
||
## 16. 重连与幂等
|
||
|
||
- Create/Close/Transfer 等请求携带 request ID,Agent 维护短期幂等缓存。
|
||
- 会话租约由 heartbeat 延长,断线后进入有限 grace period。
|
||
- 重连请求携带 session ID、resume token 和各 channel 最后确认 sequence。
|
||
- 输入事件默认不跨断线重放;终端/文件按各自序号恢复。
|
||
- 恢复成功后请求视频 IDR 并重新同步显示器、权限和输入状态。
|
||
|
||
## 17. 错误模型
|
||
|
||
错误分为:
|
||
|
||
```text
|
||
AUTH_* 配对、证书、票据、权限
|
||
SESSION_* 类型、状态、并发、用户会话
|
||
CAPABILITY_* 编码、Portal、显示器、输入
|
||
MEDIA_* 捕获、编码、WebRTC、解码
|
||
TERMINAL_* PAM、PTY、Shell、退出
|
||
FILE_* 路径、空间、哈希、权限
|
||
NETWORK_* 超时、重连、协商
|
||
INTERNAL_* 非预期内部错误
|
||
```
|
||
|
||
错误包含稳定 code、retryable、retry delay 和 correlation ID。内部路径、堆栈和密钥信息不发送给远端。
|
||
|
||
## 18. 资源限制
|
||
|
||
实现前锁定默认上限:
|
||
|
||
- 未认证连接数和每 IP 配对尝试。
|
||
- 每设备并发桌面/终端会话。
|
||
- Protobuf message、SDP、candidate 和字符串长度。
|
||
- 分辨率宽高、总像素、像素率和运行时重配频率。
|
||
- 剪贴板、文件、终端输出缓存和日志大小。
|
||
- DataChannel buffered amount。
|
||
- 会话创建、Portal 授权、协商和重连超时。
|
||
|
||
上限由安全配置决定,远端请求只能降低,不能提高系统硬限制。
|
||
|
||
## 19. 测试
|
||
|
||
- Protobuf golden vectors 与 round-trip。
|
||
- 主/次版本兼容和 unknown fields。
|
||
- 状态机非法转换的 property tests。
|
||
- 重复 request、过期 Ticket 和 replay。
|
||
- 截断、超长、乱序和未知消息 fuzz。
|
||
- 输入断线后的卡键恢复。
|
||
- 文件路径穿越、磁盘不足和 hash mismatch。
|
||
- DataChannel 拥塞时输入优先级。
|
||
- 协议日志脱敏测试。
|
||
# Windows Agent software fallback
|
||
|
||
Windows Agent advertises `dxgi-desktop-duplication-strict` as its preferred capture backend. A
|
||
controller may explicitly opt in to the lower-performance fallback by sending one JSON line:
|
||
|
||
```json
|
||
{"kind":"open_desktop","allow_software_fallback":true,"frames_per_second":10}
|
||
```
|
||
|
||
The fallback is never selected implicitly. RDWF v1 uses a 24-byte little-endian header. RDWF v2
|
||
uses a 40-byte header followed immediately by the zlib payload:
|
||
|
||
| Offset | Size | Value |
|
||
| --- | ---: | --- |
|
||
| 0 | 4 | ASCII `RDWF` |
|
||
| 4 | 1 | Protocol version (`2`; readers also accept `1`) |
|
||
| 5 | 1 | Codec (`1` = BGRA8 + zlib) |
|
||
| 6 | 2 | Reserved (`0`) |
|
||
| 8 | 4 | Width |
|
||
| 12 | 4 | Height |
|
||
| 16 | 4 | Uncompressed BGRA byte length |
|
||
| 20 | 4 | Compressed payload byte length |
|
||
| 24 | 8 | GDI capture latency in microseconds (v2) |
|
||
| 32 | 8 | zlib encode latency in microseconds (v2) |
|
||
|
||
BGRA rows are top-down and tightly packed at four bytes per pixel. The Agent caps the fallback at
|
||
15 FPS, a 16384-pixel dimension, and 256 MiB for both raw and compressed frames.
|
||
|
||
The Windows Agent viewer validates the header and lengths before allocating, inflates the zlib
|
||
payload, converts BGRA8 to the local softbuffer surface, and reports decode/presentation timing in
|
||
the session diagnostics endpoint. The control service starts it through
|
||
`POST /api/v1/windows-agent/desktop/launch` and exposes diagnostics at
|
||
`GET /api/v1/windows-agent/session/{session_id}`.
|