Initial commit: hello-agent — headless RustDesk-protocol-compatible Windows agent
build-windows / build-hello-agent-x64 (push) Successful in 5m41s
build-windows / build-hello-agent-x64 (push) Successful in 5m41s
A single-binary, Flutter-free remote-support agent that speaks the stock
RustDesk wire protocol. Designed for one-line MDM deployment against a
self-hosted rustdesk-server: a supporter using the unmodified rustdesk.exe
client connects, the controlled-side user gets a native Win32 approval
prompt, click Yes / No.
CLI surface
hello-agent.exe --install # register + start service
hello-agent.exe --uninstall # stop, delete, clean up
hello-agent.exe --config <BLOB> # admin-UI deploy string
hello-agent.exe --install --config <BLOB> # MDM one-liner
--config accepts both forms emitted by the rustdesk-server admin UI: the
reversed-base64 deploy string and the host=,key=,api=,relay= filename
form. Decoded via the upstream custom_server module, persisted via
hbb_common::config::Config::set_option.
Architecture
--service runs as a Session 0 LocalSystem service. It polls
WTSGetActiveConsoleSessionId and (re)spawns hello-agent.exe --server
into the active console session via librustdesk::platform::run_as_user,
handling the Session 0 → user-session token impersonation.
--server is the worker. It boots three concurrent components:
1. cm_popup: an IPC listener on the rustdesk `_cm` named pipe
2. librustdesk::start_server(true, false): the upstream protocol
stack — rendezvous mediator, NAT punch, IPC server, screen
capture, login validation, hbbs_http heartbeat / sysinfo sync
3. (implicit) ApproveMode::Click is pinned in config, so every
incoming connection routes through cm_popup
The popup mechanism reuses an existing upstream contract without any
patches to the protocol code: when a peer connects with no password,
Connection::start in the upstream code calls try_start_cm_ipc, which
ipc::connect-s the `_cm` pipe before falling back to spawning a Flutter
CM child. Since cm_popup is up first, step 1 succeeds; we read the
Data::Login{authorized:false} frame, show MessageBoxTimeoutW (Yes/No,
60s, top-most, system-modal), and reply Data::Authorize or Data::Close.
Source tree
src/main.rs CLI dispatcher + run_server() composition
src/cli.rs hand-rolled argv parser + unit tests
src/service.rs windows-service install/uninstall/dispatcher
src/config_import.rs --config blob decoding + persistence
src/cm_popup.rs _cm IPC listener + Win32 approval dialog
Vendoring
The upstream RustDesk crate is vendored under vendor/rustdesk/ — full
workspace including libs/{hbb_common, scrap, enigo, clipboard,
virtual_display, remote_printer}. This makes the build self-contained
(no submodules, no sibling-repo checkout in CI) and gives us freedom to
fork in a different direction later. Excluded from the vendor: .git,
target/, flutter/, appimage/, flatpak/, fastlane/, docs/, examples/,
ci/, build.py, Dockerfile, upstream README/CLAUDE/AGENTS/GEMINI.
One local divergence vs. upstream: vendor/rustdesk/src/lib.rs flips
`mod custom_server` → `pub mod custom_server` so config_import.rs can
call get_custom_server_from_string without going through the
ui_interface shim. Documented in README.md → "Re-syncing the vendored
copy".
CI
.gitea/workflows/build-windows.yml builds on a self-hosted Windows
runner with Rust 1.75, LLVM 15.0.6 (libclang for bindgen via libvpx-sys),
and a vcpkg cache. The vendored vcpkg.json drives x64-windows-static
deps. The workflow stages the resulting hello-agent.exe into
SignOutput\, reports authenticode signing status (warns on unsigned),
and uploads as artifact. ~15 min full build, faster on incremental.
Out of scope for this commit: Linux/macOS builds, code signing, MSI
packaging, coexistence with stock rustdesk on the same box (currently
shares the RustDesk APP_NAME and config dir).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
# hello-agent
|
||||
|
||||
A headless, RustDesk-protocol-compatible remote-support agent for Windows.
|
||||
|
||||
One self-contained binary, no Flutter UI. Designed for one-line MDM
|
||||
deployment against a self-hosted [rustdesk-server](https://github.com/rustdesk/rustdesk-server)
|
||||
(or the Pro/admin variant). A supporter using the stock `rustdesk.exe`
|
||||
client can connect; the controlled-side user gets a native approval
|
||||
prompt and clicks Yes / No.
|
||||
|
||||
## CLI
|
||||
|
||||
```
|
||||
hello-agent.exe --install # register + start service
|
||||
hello-agent.exe --uninstall # stop, delete, clean up
|
||||
hello-agent.exe --config <BLOB> # import admin-UI deploy string
|
||||
hello-agent.exe --install --config <BLOB> # MDM one-liner
|
||||
```
|
||||
|
||||
`--config` accepts both forms emitted by the rustdesk-server admin UI:
|
||||
|
||||
* the reversed-base64 deploy string (`0nI900VsFHZ…`)
|
||||
* the `host=server,key=…,api=…,relay=…` filename form
|
||||
|
||||
If `--config` is **omitted** and no prior install left a rendezvous
|
||||
configuration behind, hello-agent falls back to a built-in default
|
||||
pointing at the cybnet rustdesk-server:
|
||||
|
||||
```
|
||||
custom-rendezvous-server = rd.gamecom.ch
|
||||
api-server = https://rd.gamecom.ch
|
||||
relay-server = rd.gamecom.ch
|
||||
key = tcxma69cN3OWt25jQ75apSCtaZGIfDqIIP6yGNj3dgs=
|
||||
```
|
||||
|
||||
Operators who run their own rustdesk-server must pass `--config` with
|
||||
their deploy blob; defaults are only applied when the config slot is
|
||||
empty, so `--config` always wins.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
hello-agent.exe --install
|
||||
│
|
||||
└──> creates Windows service "HelloAgent", binPath ends in --service
|
||||
│
|
||||
hello-agent.exe --service # Session 0, LocalSystem
|
||||
│
|
||||
└──> spawns into the active console session as SYSTEM token:
|
||||
│
|
||||
hello-agent.exe --server # user session, SYSTEM token
|
||||
│
|
||||
├── default ipc listener (rustdesk core)
|
||||
├── RendezvousMediator ──> rustdesk-server registration + NAT
|
||||
├── hbbs_http::sync ──> /api/heartbeat + /api/sysinfo
|
||||
│
|
||||
│ at startup, --server proactively spawns (via WTSQueryUserToken
|
||||
│ + CreateProcessAsUserW with lpDesktop = winsta0\default —
|
||||
│ librustdesk's run_as_user uses lpDesktop=NULL which inherits
|
||||
│ the invisible Session 0 service desktop):
|
||||
▼
|
||||
hello-agent.exe --cm # user session, USER token,
|
||||
│ # winsta0\default desktop
|
||||
├── binds `_cm` IPC pipe (long-running — one child per session)
|
||||
├── reads Data::Login from parent's start_ipc
|
||||
├── shows MessageBoxW on the user's interactive desktop
|
||||
└── replies Data::Authorize / Data::Close (per peer), keeps listening
|
||||
```
|
||||
|
||||
The protocol stack (rendezvous, login validation, screen capture, input,
|
||||
relay) is the upstream `librustdesk` code, **vendored under
|
||||
[`vendor/rustdesk/`](vendor/rustdesk/)** for an independent build. This
|
||||
crate is the thin shell that gives us the new CLI surface, the Windows
|
||||
service shell, and the native approval popup that replaces the stock
|
||||
Flutter Connection Manager.
|
||||
|
||||
## Repo layout
|
||||
|
||||
```
|
||||
hello-agent/
|
||||
├── src/ hello-agent sources (~600 lines)
|
||||
├── vendor/rustdesk/ vendored RustDesk crate + workspace libs
|
||||
│ ├── Cargo.toml rustdesk's own workspace + package manifest
|
||||
│ ├── src/ librustdesk source
|
||||
│ └── libs/ hbb_common, scrap, enigo, clipboard, …
|
||||
├── Cargo.toml hello-agent package manifest, path-deps on vendor
|
||||
├── .gitea/workflows/ Gitea CI
|
||||
└── README.md
|
||||
```
|
||||
|
||||
The vendored source has a few local divergences from upstream — all
|
||||
documented inline at the patch site so they're easy to spot when
|
||||
re-syncing:
|
||||
|
||||
1. [`vendor/rustdesk/src/lib.rs`](vendor/rustdesk/src/lib.rs):
|
||||
`mod custom_server` → `pub mod custom_server` so hello-agent can call
|
||||
the deploy-blob decoder.
|
||||
2. [`vendor/rustdesk/Cargo.toml`](vendor/rustdesk/Cargo.toml):
|
||||
`[lib] crate-type` reduced from `["cdylib", "staticlib", "rlib"]` to
|
||||
`["rlib"]`. We statically link the rlib into hello-agent.exe; the
|
||||
cdylib link step (used by upstream for Flutter FFI) trips
|
||||
`LNK1169 multiply-defined symbols` from overlapping
|
||||
windows-targets/windows_x86_64_msvc versions and we don't need it.
|
||||
3. Heartbeat intervals lowered 15s → 1s so device-online status in the
|
||||
admin UI reacts faster:
|
||||
[`vendor/rustdesk/libs/hbb_common/src/config.rs`](vendor/rustdesk/libs/hbb_common/src/config.rs)
|
||||
(`REG_INTERVAL`, UDP rendezvous re-register) and
|
||||
[`vendor/rustdesk/src/hbbs_http/sync.rs`](vendor/rustdesk/src/hbbs_http/sync.rs)
|
||||
(`TIME_HEARTBEAT`, HTTP `/api/heartbeat`).
|
||||
|
||||
## Build
|
||||
|
||||
### Local (Windows)
|
||||
|
||||
```powershell
|
||||
$env:VCPKG_ROOT = "C:\vcpkg"
|
||||
cd vendor\rustdesk
|
||||
& "$env:VCPKG_ROOT\vcpkg" install --triplet x64-windows-static
|
||||
cd ..\..
|
||||
cargo build --release --bin hello-agent
|
||||
# → target\release\hello-agent.exe
|
||||
```
|
||||
|
||||
The first build is slow (~15 min) because cargo compiles the entire
|
||||
RustDesk crate plus its workspace libraries. Subsequent builds are
|
||||
incremental.
|
||||
|
||||
### CI
|
||||
|
||||
[`.gitea/workflows/build-windows.yml`](.gitea/workflows/build-windows.yml)
|
||||
builds on a self-hosted Windows runner. It checks out hello-agent
|
||||
(self-contained, no submodules), runs vcpkg against the vendored
|
||||
`vcpkg.json`, builds, and uploads `SignOutput\hello-agent-<version>-x86_64.exe`.
|
||||
|
||||
## Re-syncing the vendored copy
|
||||
|
||||
To pull updates from upstream RustDesk:
|
||||
|
||||
1. Sync the upstream rustdesk repo locally and `git submodule update --init` for `libs/hbb_common`.
|
||||
2. `rsync -a --delete --exclude=.git --exclude=target --exclude=flutter --exclude=appimage … upstream-rustdesk/ vendor/rustdesk/`
|
||||
3. Re-apply the one-line `pub mod custom_server` patch in
|
||||
[`vendor/rustdesk/src/lib.rs`](vendor/rustdesk/src/lib.rs).
|
||||
4. `cargo build --release --bin hello-agent` — fix any breakage from
|
||||
upstream API drift in our [src/](src/) modules.
|
||||
|
||||
## Stale keys / supporter "stuck on connecting"
|
||||
|
||||
The agent's identity (`id`) and `key_pair` live in `HelloAgent.toml`.
|
||||
They're generated once on first run, registered with the rendezvous
|
||||
server, and re-used forever after. **If the rendezvous server's cached
|
||||
entry and the agent's local keypair drift apart, the encrypted handshake
|
||||
silently fails on the supporter side** — the supporter's stock rustdesk
|
||||
client shows "Please wait for the remote side…" / similar, the agent log
|
||||
shows a `Connection opened` followed by ~30 seconds of nothing then
|
||||
`Peer close`, and the popup never fires (because no `LoginRequest` ever
|
||||
decrypts).
|
||||
|
||||
How to recognize it: agent log says `register_pk of rd due to key not
|
||||
confirmed` followed by `Generated new keypair for id:`, *and* the
|
||||
rustdesk-server admin UI already has a record for that agent id from
|
||||
prior runs.
|
||||
|
||||
How to recover:
|
||||
|
||||
1. Delete the device record for that agent id from the rustdesk-server
|
||||
admin UI's device list. The next agent heartbeat re-creates it with
|
||||
the current public key.
|
||||
2. Restart the supporter's stock rustdesk app (clears its in-process
|
||||
pubkey cache).
|
||||
3. Reconnect — the supporter now resolves the current pubkey, the
|
||||
handshake succeeds, the popup fires.
|
||||
|
||||
`hello-agent --uninstall` deliberately preserves the LocalService config
|
||||
dir so the agent keypair survives an uninstall→reinstall cycle. To force
|
||||
a fresh keypair, also run after `--uninstall`:
|
||||
|
||||
```
|
||||
rmdir /s /q "%SystemRoot%\ServiceProfiles\LocalService\AppData\Roaming\HelloAgent"
|
||||
```
|
||||
|
||||
…and then delete the device record from the admin UI as above.
|
||||
|
||||
## Verifying end-to-end
|
||||
|
||||
1. Install: `hello-agent.exe --install --config <BLOB>` from elevated PowerShell.
|
||||
2. Confirm: `sc query HelloAgent` → `RUNNING`.
|
||||
3. From another machine running stock `rustdesk.exe`, enter the agent's
|
||||
ID and click Connect.
|
||||
4. The agent's logged-in user sees `HelloAgent — Allow remote support?`.
|
||||
Click Yes; session opens, mouse/keyboard/screen all work.
|
||||
5. Uninstall: `hello-agent.exe --uninstall`. Confirm `sc query` returns 1060.
|
||||
|
||||
## Namespacing
|
||||
|
||||
`hbb_common` ships a single global, `APP_NAME`, that drives the location
|
||||
of every piece of on-disk state (config dir, log dir) and the prefix of
|
||||
every named pipe. Upstream defaults it to `"RustDesk"`. Hello-agent
|
||||
rewrites it to `"HelloAgent"` as the very first line of `main()` —
|
||||
identical to the write path the upstream Flutter build uses for OEM
|
||||
rebrands ([`read_custom_client`](vendor/rustdesk/src/common.rs)). Because
|
||||
`APP_NAME` is a `RwLock<String>` read lazily on first use, doing the
|
||||
write before any path code runs is enough to redirect *every* hbb_common
|
||||
consumer in the same process tree.
|
||||
|
||||
In practice that means:
|
||||
|
||||
| What | Stock rustdesk | hello-agent |
|
||||
| --------------------------------- | ----------------------------------------- | ------------------------------------------------- |
|
||||
| User-mode config / logs | `%APPDATA%\RustDesk\` | `%APPDATA%\HelloAgent\` |
|
||||
| Service-mode config / logs | `…\LocalService\AppData\Roaming\RustDesk\`| `…\LocalService\AppData\Roaming\HelloAgent\` |
|
||||
| Identity file (id + keypair) | `RustDesk.toml` | `HelloAgent.toml` |
|
||||
| IPC pipe namespace | `\\.\pipe\RustDesk\query…` | `\\.\pipe\HelloAgent\query…` |
|
||||
| Windows service name | `RustDesk` | `HelloAgent` |
|
||||
| Install dir | `%ProgramFiles%\RustDesk\` | `%ProgramFiles%\hello-agent\` |
|
||||
|
||||
The two binaries can therefore coexist on the same machine without
|
||||
clobbering each other's state. The override is set in
|
||||
[`src/main.rs`](src/main.rs) (`pub const APP_NAME: &str = "HelloAgent"`)
|
||||
— change it there if you ever need to re-brand.
|
||||
|
||||
## Where logs go
|
||||
|
||||
`hbb_common`'s logger writes per-mode rolling files under `<config_dir>/log/<mode>/`:
|
||||
|
||||
| Mode (CLI flag) | Effective user | Log dir |
|
||||
| --------------------- | ------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `--install` / `--uninstall` | calling user (must be admin) | `%APPDATA%\HelloAgent\log\install\` (or `…\uninstall\`) |
|
||||
| `--service` | LocalSystem (mirrored) | `%SystemRoot%\ServiceProfiles\LocalService\AppData\Roaming\HelloAgent\log\service\` |
|
||||
| `--server` (worker) | LocalSystem (mirrored) | `%SystemRoot%\ServiceProfiles\LocalService\AppData\Roaming\HelloAgent\log\server\` |
|
||||
| no flags (dev mode) | calling user | `%APPDATA%\HelloAgent\log\hello-agent\` |
|
||||
|
||||
The `cm_popup` module also writes a parallel diagnostic trace at
|
||||
`%TEMP%\hello-agent-cm.log` (kept around for debugging the IPC handshake;
|
||||
it duplicates info that's already in the main log).
|
||||
|
||||
## Status
|
||||
|
||||
- ✅ Windows x64 (physical console *and* RDP sessions — the agent picks
|
||||
whichever session the user is actively using)
|
||||
- ✅ Coexists with stock RustDesk on the same box — config dir, log dir,
|
||||
and named pipes are namespaced under `HelloAgent` rather than the
|
||||
upstream default of `RustDesk` (see [Namespacing](#namespacing) below).
|
||||
The only residual contention is the optional direct-server port
|
||||
(TCP 21118) and LAN-discovery port (UDP 21119); both default to off,
|
||||
so a vanilla install of each side can run simultaneously.
|
||||
- ⏳ Linux / macOS (out of scope for v0)
|
||||
- ⏳ Code signing (CI warns, doesn't sign)
|
||||
- ⏳ Multiple simultaneous interactive users (only one can receive the
|
||||
approval popup at a time — the one in the `WTSActive` session)
|
||||
Reference in New Issue
Block a user