# 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 产生的消息。 Agent Presence 注册、续期和注销使用域隔离的 `AgentPresenceProofV1`。Agent 首次启动自动生成并保护 Ed25519 设备身份,不要求用户提供密钥文件;Device ID 从设备公钥稳定派生。签名覆盖 Device ID、公钥、region、gateway、connection ID、register/unregister 动作、注册租约到期时间、一次性 nonce 和签发时间。Edge 在修改路由前验证身份绑定、最长 30 秒时钟偏差和 nonce 未使用,因此共享 Presence token 只能作为 API 访问凭据,不能单独创建、覆盖或删除设备路由。Agent signal poll/ack 继续使用独立的设备签名证明和一次性 nonce。 资源上限为 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 剪贴板尚未接入。 协议 minor 16 在 Ed25519 challenge 签名成功后增加强制 TOTP 步骤。Agent 发送 `TotpRequired { digits: 6, period_seconds: 30 }`,Client 必须在 30 秒内返回 `VerifyTotp`,随后 Agent 才能消费一次性配对码或返回 `Authenticated`。Agent 不保存客户端提交的动态码,对失败来源执行限流,并按已验证 Client 公钥拒绝同一时间步重放。未配置 TOTP 的 agentd 拒绝启动;Edge relay 仅透明转发这段固定证书的 TLS/WSS 流量,不参与 TOTP 校验。 ## 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 Android transport After mandatory TOTP authentication, Windows Agent protocol minor 3 advertises `supported_media_transports: ["webrtc", "plain_stream"]`. Android requests WebRTC first: ```json {"kind":"open_desktop","capture_mode":"compatibility","media_transport":"webrtc","frames_per_second":30} ``` The Agent starts bounded Windows H.264 capture and returns `webrtc_media_ready` with the desktop dimensions, ICE server list, and negotiation timeout. Android is the sole offerer; the Agent is the answerer. Both sides exchange one JSON object per TCP line: ```json {"kind":"webrtc_signal","signal_kind":"offer|answer|ice_candidate|ice_end","payload":"..."} ``` SDP is capped at 128 KiB and ICE candidates at 4 KiB by the shared WebRTC boundary. Both endpoints must signal candidate completion. The Agent returns `desktop_opened` only after ICE/DTLS connects and both `remotedesk-control` and `remotedesk-pointer` DataChannels arrive. H.264 uses WebRTC RTP, RTCP feedback, NACK/RTX and DTLS-SRTP. Reliable ordered keyboard, button, text, keyframe, and close commands use `remotedesk-control`; pointer moves use an unordered, short-lived channel. Android's native WebRTC stack owns RTP, jitter handling, H.264 decode, and Flutter Texture rendering. The Windows Agent optionally reads at most eight validated STUN/TURN entries from `REMOTEDESK_WINDOWS_ICE_SERVERS`. It passes the same session credentials to Android only after TOTP succeeds. With no configured entries, both peers use host candidates for LAN/VPN routing. If WebRTC negotiation or Windows H.264 capture is unavailable, the Agent returns `webrtc_media_unavailable` and Android requests `media_transport: "plain_stream"` on the same authenticated TCP connection. 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 this fallback at 30 FPS, a 16384-pixel dimension, and 256 MiB for raw/compressed frames; Android applies the tighter 33,177,600-pixel and 64 MiB compressed-frame limits before inflating in a worker isolate. WebRTC media and DataChannels are encrypted, but the direct TCP signaling channel has no TLS or host identity. TOTP alone does not prevent a signaling MITM, so this mode remains limited to a trusted LAN, private VPN, or authenticated AnyTLS tunnel.