Files
rdp-proxy/workers/rdp-worker/README.md
T
2026-04-28 22:29:50 +03:00

485 lines
19 KiB
Markdown

# RDP Worker
Active C++ RDP Adapter worker for the Remote Access Platform.
Current test Docker deployment:
```text
rap-rdp-worker:stage5-2-download
```
This image is deployed on the canonical test Docker host
`docker-test` / `192.168.200.61` for Stage 5.2 proof. The accepted
ordered-region RDP path remains protected; the current focus is finishing
server-to-client file download desktop UI proof, not new platform expansion.
## Scope
- standalone C++ worker service
- Redis-backed worker registration, heartbeat, assignment consumption, lease renewal, and worker event publishing
- FreeRDP-based server-side RDP connection runtime
- persistent remote session process that stays connected when the client detaches
- reattach and takeover-aware assignment updates without recreating the server-side RDP session
- terminate and failure reporting back into the backend control plane
## Reproducible build environments
### Devcontainer
The repository now includes a worker-focused devcontainer in [`.devcontainer/devcontainer.json`](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\.devcontainer\devcontainer.json) and [`.devcontainer/Dockerfile`](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\.devcontainer\Dockerfile).
Environment assumptions:
- Debian `12.10-slim`
- CMake from Debian 12 packages
- Ninja from Debian 12 packages
- GCC/G++ from `build-essential`
- FreeRDP headers/libs from `freerdp2-dev`
- WinPR headers/libs from `libwinpr2-dev`
- no extra Redis client library is required because the worker uses its own socket-based RESP client
The devcontainer runs:
```sh
cmake --preset dev --directory workers/rdp-worker
```
after creation, so configure success is part of the expected environment contract.
### Docker image build
The worker Docker build is defined in [Dockerfile](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\workers\rdp-worker\Dockerfile).
Environment assumptions:
- Debian `12.10-slim`
- `build-essential`
- `cmake`
- `ninja-build`
- `pkg-config`
- `freerdp2-dev`
- `libwinpr2-dev`
The image uses [CMakePresets.json](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\workers\rdp-worker\CMakePresets.json) and installs the final binary to the deterministic path:
```text
/usr/local/bin/rdp-worker
```
## Exact build commands
### Local native build
From the repository root:
```powershell
cmake --preset dev --directory workers/rdp-worker
cmake --build --preset dev --directory workers/rdp-worker
```
Expected binary path:
```text
workers/rdp-worker/build/rdp-worker
```
### Docker build
From the repository root:
```powershell
docker build --tag rap-rdp-worker:dev --file workers/rdp-worker/Dockerfile workers/rdp-worker
docker run --rm --entrypoint /bin/sh rap-rdp-worker:dev -lc "test -x /usr/local/bin/rdp-worker"
```
Expected result:
- `docker build` completes successfully
- the second command exits with code `0`
- the runtime binary path inside the image is `/usr/local/bin/rdp-worker`
## Runtime configuration
Environment variables:
- `RDP_WORKER_ID`
- `RDP_WORKER_REDIS_HOST`
- `RDP_WORKER_REDIS_PORT`
- `RDP_WORKER_REDIS_PASSWORD`
- `RDP_WORKER_REDIS_DB`
- `RDP_WORKER_HEARTBEAT_INTERVAL_SECONDS`
- `RDP_WORKER_LEASE_RENEW_INTERVAL_SECONDS`
- `RDP_WORKER_ASSIGNMENT_POLL_INTERVAL_SECONDS`
- `RDP_WORKER_CAPABILITIES`
- `RDP_WORKER_INSECURE_SKIP_VERIFY`
- `RDP_WORKER_DATA_PLANE_ENABLED`
- `RDP_WORKER_DATA_PLANE_LISTEN_HOST`
- `RDP_WORKER_DATA_PLANE_LISTEN_PORT`
- `RDP_WORKER_DATA_PLANE_PUBLIC_KEY_FILE`
- `RDP_WORKER_DATA_PLANE_PUBLIC_KEY_PEM`
- `RDP_WORKER_DATA_PLANE_TLS_CERT_FILE`
- `RDP_WORKER_DATA_PLANE_TLS_KEY_FILE`
- `RDP_WORKER_RDPGFX_ENABLED`
## Data Plane v1 Direct WSS Boundary
Stage DP-1C adds an optional direct worker WSS endpoint at
`/rap/v1/data-plane`. It is disabled by default and does not change Windows
client behavior by itself; the existing backend gateway remains the active
fallback path unless the backend advertises a data-capable direct candidate.
When enabled, the endpoint:
- requires TLS certificate and private key files
- validates the backend-issued RS256 `data_plane_token` with a public key
- verifies token scope for session, attachment, user, organization, worker,
resource, allowed channels, expiry, audience, and `jti`
- binds only to an already-running `SessionRuntime`
- rejects token `jti` replay with a bounded in-memory TTL cache
- rejects missing runtime, wrong worker, expired token, invalid signature, and
malformed token cases without creating a new RDP session
- accepts the existing JSON realtime envelopes for `input`, `control`,
`clipboard`, and `file_upload`
- emits the existing JSON session state, render frame, clipboard, and file
upload progress events
- emits `session.frame` as DP-2 binary WebSocket frames when the client
requests `render_transport=binary_v1`; JSON/base64 render remains available
for backend gateway fallback/debug compatibility
- drains direct input before Redis fallback input, keeps the direct inbound
queue bounded, and coalesces stale mouse moves
- keeps render latest-frame-only and droppable on the direct WSS writer
- tags direct inbound envelopes with token-bound session/attachment/user/org
claims so old attachments after takeover cannot keep controlling the runtime
Required environment:
```text
RDP_WORKER_DATA_PLANE_ENABLED=true
RDP_WORKER_DATA_PLANE_LISTEN_HOST=0.0.0.0
RDP_WORKER_DATA_PLANE_LISTEN_PORT=8443
RDP_WORKER_DATA_PLANE_PUBLIC_KEY_FILE=/path/to/data-plane-public.pem
RDP_WORKER_DATA_PLANE_TLS_CERT_FILE=/path/to/cert.pem
RDP_WORKER_DATA_PLANE_TLS_KEY_FILE=/path/to/key.pem
```
Production direct worker WSS trust is coordinated by the backend candidate
metadata described in `docs/architecture/DIRECT_WORKER_TLS_PKI.md`.
- Smoke/dev can use self-signed certificates only when the backend advertises
the candidate as `smoke_only=true` and the Windows client has the explicit
smoke override enabled.
- Production workers must use certificates valid for their advertised direct
WSS hostname/IP subject alternative names.
- Production direct candidates should be advertised by the backend only with
`DATA_PLANE_DIRECT_WORKER_TLS_TRUST_MODE=public_ca` or `platform_ca`.
The image also installs a token validation smoke helper:
```sh
rdp-worker-dataplane-token-probe --token <jwt> --public-key-file <public.pem> --worker-id <worker_id>
rdp-worker-dataplane-bind-probe --scenario valid
rdp-worker-dataplane-bind-probe --scenario wrong-attachment
rdp-worker-dataplane-bind-probe --scenario channels-too-broad
```
Run the endpoint validation smoke from the repository root after building
`rap-rdp-worker:dp1c-hardened` on the test Docker host:
```powershell
pwsh -ExecutionPolicy Bypass -File scripts/smoke/data-plane-v1c-smoke.ps1
```
Stage DP-2.1 removes the internal base64 encode/decode hop from the direct
render path. FreeRDP frame capture now carries raw BGRA bytes through the worker
runtime to the direct binary render sink, and direct WSS sends those bytes as
`RAP2` binary WebSocket frames. Compatibility JSON/base64 `session_frame` events
are still generated for the backend-gateway fallback boundary only. Backend
gateway fallback remains supported and must stay enabled.
Stage DP-3A adds direct binary color mode selection. `full_color` sends the raw
BGRA frame unchanged. `grayscale` converts the direct binary frame to grayscale
inside the worker direct render sink, preserves BGRA32 output, and does not
modify the backend-gateway JSON/base64 fallback path. Direct frame diagnostics
include `requested_color_mode`, `applied_color_mode`,
`grayscale_conversion_applied`, `raw_frame_bytes_before`,
`raw_frame_bytes_after`, `binary_direct_bytes`, and `conversion_time_ms`.
## RDPGFX Gated Smoke Mode
`RDP_WORKER_RDPGFX_ENABLED=true` enables an experimental RDPGFX advertisement
path for target compatibility testing only.
Default behavior:
- RDPGFX is disabled.
- Classic GDI region-first rendering remains the safe production/dev path.
- Direct binary WSS and backend gateway fallback continue to work without this
flag.
When the flag is enabled, the worker logs:
- `rdp.gfx config requested=true`
- `rdp.gfx channel_subscription ...`
- `rdp.gfx channel_connected ...` if the target opens the RDPGFX channel
- `rdp.gfx pipeline_init_success` or `rdp.gfx pipeline_init_failed`
- `rdp.gfx surface_event ...` when FreeRDP surface callbacks are available
- RDPGFX counters in `rdp.perf callback_summary`
The current live smoke target resets the connection when RDPGFX is advertised,
so this flag must not be enabled for normal operation. See
`artifacts/rdp-perf4-report.md`.
## Cursor Adapter Boundary
RDP-A4 adds a dedicated cursor boundary inside the C++ RDP Adapter.
The worker installs FreeRDP pointer callbacks and normalizes them into
`session_cursor_updated` events. Direct worker WSS exposes those events as
`cursor.update` envelopes. Cursor is scheduled as latest-only/droppable and does
not wait behind binary display frames. Backend gateway fallback remains
compatible with the same worker event payload.
The current implementation publishes cursor metadata:
- update kind
- sequence
- desktop width/height
- x/y position
- visibility
- shape changed flag
- cache index
- hotspot
- cursor width/height
- XOR bpp
- mask byte size
- system cursor type
Smoke-proven image:
```text
rap-rdp-worker:rdp-a4-cursor-adapter
```
Probe:
```powershell
docker -H ssh://docker-test run --rm rap-rdp-worker:rdp-a4-cursor-adapter /usr/local/bin/rdp-worker-cursor-adapter-probe
```
Expected result:
```text
cursor_adapter_probe ok
```
See `artifacts/rdp-a4-cursor-adapter-report.md`.
## GDI Repaint Cadence Hardening
RDP-Perf-5A keeps the default classic GDI region-first path and improves its
runtime cadence without changing backend contracts or session lifecycle.
The worker now:
- drains already-signaled FreeRDP event handles in a bounded loop
- rate-limits no-change detector logs
- renews worker leases outside the hot render/input loop
- publishes region and interactive render frames at a 33 ms interval
- keeps full-frame fallback at a 100 ms interval
- keeps direct worker WSS binary render and backend gateway JSON/base64 fallback
compatible
Smoke-proven image:
```text
rap-rdp-worker:rdp-perf5a-repaint-cadence
```
Report:
```text
artifacts/rdp-perf5a-report.md
```
Manual UX validation after subsequent adapter work showed that keyboard, mouse,
idle repaint, and general interaction are usable enough for the current MVP
baseline, but small redraw artifacts remain and are the next hardening target.
## Direct Attach Baseline And Region Repair
After P1 manual visual smoke, the current accepted baseline image is:
```text
rap-rdp-worker:rdp-p1-region-order2
```
This baseline keeps the classic GDI region-first path and adds two important
visual correctness safeguards:
- direct attach baseline full-frame capture so a newly attached client does not
start from a black or incomplete framebuffer
- throttled full-frame repair after detected region loss so dropped dirty
regions do not leave persistent holes
The direct worker WSS writer treats attach-baseline frames as non-droppable
reliable events. Normal display updates remain region-first and droppable where
safe. Full frames are reserved for baseline/attach/recovery/fallback repair, not
normal rendering.
Probes verified during the P1 baseline freeze:
```powershell
docker -H ssh://docker-test run --rm rap-rdp-worker:rdp-p1-region-order2 rdp-worker-graphics-adapter-probe
docker -H ssh://docker-test run --rm rap-rdp-worker:rdp-p1-region-order2 rdp-worker-cursor-adapter-probe
docker -H ssh://docker-test run --rm rap-rdp-worker:rdp-p1-region-order2 rdp-worker-service-adapter-protocol-probe
docker -H ssh://docker-test run --rm rap-rdp-worker:rdp-p1-region-order2 rdp-worker-dataplane-bind-probe --scenario valid
```
Expected results:
- `graphics_adapter_probe ok`
- `cursor_adapter_probe ok`
- service adapter channel list printed without error
- `PASS scenario=valid`
Known remaining visual limitation:
- drag/release repaint is usable but not polished; drag behaves like an older
RDP client on a weak link by moving a frame rather than continuously
repainting the full window.
## Certificate Verification Policy
- `strict` is the default per-resource mode and keeps normal FreeRDP certificate validation enabled
- `ignore` is a saved per-resource mode coming from backend session assignment and enables `FreeRDP_IgnoreCertificate` only for that connection
- `RDP_WORKER_INSECURE_SKIP_VERIFY=true` remains available only as a smoke/debug fallback when assignment data does not include an explicit mode; it is not the production configuration path
## Resource metadata contract
The worker currently expects target connection data inside `resources.metadata`:
```json
{
"rdp_host": "10.0.0.10",
"rdp_port": 3389,
"username": "Administrator",
"password": "secret",
"domain": ""
}
```
`address` is used as a fallback host when `rdp_host` is absent.
## Current limitations
- rendering is implemented as a classic GDI region-first BGRA path with direct
binary WSS support and baseline/region-loss repair; encoded graphics, codecs,
tiles, and RDPGFX production mode are not accepted yet
- clipboard is text-only; image/HTML/RTF/binary clipboard formats are
intentionally blocked
- file upload is implemented through controlled worker storage and restricted
drive visibility; server-to-client download is build-proven through the
restricted `RAP_Transfers\ToClient` drop zone, with live runtime acceptance
still pending
- audio, printer, webcam, multi-monitor, and advanced graphics pipeline
optimizations are intentionally not implemented yet
- worker-to-backend communication is currently Redis-only; no dedicated worker API exists yet
- connection secrets are still sourced from resource metadata for this minimal end-to-end proof and are not yet integrated with encrypted secret storage
- FreeRDP session loop remains focused on lifecycle correctness and the
region-first adapter path; remaining redraw artifacts must be fixed before
starting another graphics feature
- the Docker/native build can now be reproduced in the defined environment, but a full real-target smoke run still depends on a reachable RDP host and a machine where Docker can run
- `RDP_WORKER_INSECURE_SKIP_VERIFY=true` is only a smoke/debug fallback and should not be used instead of the per-resource certificate verification policy
## Verification levels
- local native: expected on machines that match the documented Debian/devcontainer package set
- container-proven: the Dockerfile is the canonical reproducible worker build environment and a successful `docker build` proves configure + compile + install
- CI-defined: [`.github/workflows/build.yml`](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\.github\workflows\build.yml) is prepared to build backend and worker image and verify the installed worker binary path
- smoke-path: see [scripts/smoke/README.md](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\scripts\smoke\README.md) for what is and is not proven yet
## Troubleshooting
- If `pkg-config` cannot find `freerdp2` or `winpr2`, verify that the development packages are installed and that `pkg-config --list-all` includes both modules.
- If `cmake --preset dev --directory workers/rdp-worker` fails, confirm that the environment matches the Debian 12 package set from the Dockerfile or devcontainer.
- If `docker build` succeeds but runtime startup fails, verify Redis connectivity and worker environment variables.
- If `freerdp_connect` fails, confirm that the seeded metadata contains `rdp_host`, `rdp_port`, `username`, `password`, and optional `domain`.
- If the worker registers but never receives assignments, inspect Redis keys `worker:registration:<worker_id>` and `worker:control:<worker_id>`.
## Clipboard Text Boundary
The worker receives `clipboard` control envelopes from the backend session
gateway only after backend policy checks have passed. It enforces
`assignment.policy.clipboard_mode` again before touching the FreeRDP runtime
boundary, so UI or gateway mistakes do not become the only clipboard guard.
Supported policy values are `disabled`, `client_to_server`,
`server_to_client`, and `bidirectional`. The default is `disabled`.
Current implementation is text-only. It deliberately rejects images, HTML,
RTF, binary payloads, and file-like clipboard data by never accepting those
formats in the worker envelope contract.
Stage 4.1 integrates the FreeRDP `cliprdr` virtual channel for text clipboard.
The worker advertises `CF_UNICODETEXT`, requests server `CF_UNICODETEXT` data,
and converts UTF-16LE clipboard payloads to UTF-8 before publishing normalized
`session_clipboard_text` events to the backend.
Current limitation: only text is supported. Images, HTML, RTF, file lists,
binary formats, and file-like clipboard payloads remain intentionally blocked.
## File Upload Boundary
Stage 5.1.1 supports client-to-server upload into a restricted per-session
directory that is exposed to the remote Windows session through FreeRDP drive
redirection. It does not expose a remote Windows filesystem browser, arbitrary
paths, shared worker folders, or server-to-client download.
The worker receives `file_upload` control envelopes only after backend gateway
policy checks pass, then enforces `assignment.policy.file_transfer_mode` again
before writing any bytes. Supported policy values are `disabled`,
`client_to_server`, `server_to_client`, and `bidirectional`; only
`client_to_server` and `bidirectional` allow upload in this stage. Default is
`disabled`.
Uploaded files are finalized under:
```text
${RDP_WORKER_TRANSFER_ROOT:-/tmp/rap-rdp-worker-transfers}/<session_id>/visible/<sanitized_file_name>
```
Only this `visible` directory is mapped to the remote RDP session as the
restricted drive `RAP_Transfers`. The worker passes only that path to FreeRDP's
RDPDR drive-redirection channel; it never maps `${RDP_WORKER_TRANSFER_ROOT}`,
the session parent directory, or arbitrary client-provided paths.
Safety constraints in the worker:
- maximum file size is 25 MiB
- maximum chunk size is 256 KiB
- file names must be simple names only, with no path separators, drive
prefixes, `..`, or empty names
- existing target files are never overwritten silently
- chunks must arrive in order with the expected offset
- final FNV-1a content hash must match when the client supplies one
- files are stored only; the worker never executes uploaded content
- upload temp files are kept inside the same restricted `visible` directory and
renamed into place only after validation completes
- the `visible` directory is cleaned up on detach, and the full per-session
transfer directory is removed on termination or session failure
Stage 5.2 adds a server-to-client download path using only the
restricted `ToClient` directory inside the existing `RAP_Transfers` drive. A
remote user copies a regular file into `RAP_Transfers\ToClient`; the worker
detects stable files in that directory only, publishes `file_download.available`,
and streams chunks after client `file_download.start` / `file_download.ack`.
The worker still does not expose a general remote filesystem browser, arbitrary
paths, shared worker folders, SMB/WebDAV, or a Windows agent. Core download
transport is runtime-proven for direct worker WSS and backend gateway fallback;
lifecycle blocking is runtime-proven for detach, old-controller takeover, and
worker failure. Full Stage 5.2 acceptance still needs manual desktop UI proof.
Runtime proof report:
- `artifacts/stage5-2-file-download-runtime-report.md`