485 lines
19 KiB
Markdown
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`
|