354 lines
18 KiB
Markdown
354 lines
18 KiB
Markdown
# 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 "hello-agent", binPath ends in --service
|
|
│
|
|
hello-agent.exe --service # Session 0, LocalSystem
|
|
│
|
|
├── unattended_password::rotate_and_report (background thread)
|
|
│ └─ POSTs per-boot password to <api-server>/api/unattended-password
|
|
│ with retry until ack — races rendezvous registration
|
|
│
|
|
└──> 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
|
|
│ └─ stamps `agent_name` / `agent_version` / `inventory`
|
|
│ into each /api/sysinfo payload (re-uploads when the
|
|
│ inventory collector below transitions empty → ready)
|
|
│ └─ signs every request with the device's Ed25519 sk
|
|
│ (same key rendezvous registers via RegisterPk).
|
|
│ The server's first valid sig flips that peer to
|
|
│ `managed=1` and unsigned posts get 401 from then on.
|
|
│ Spec: rustdesk-server/docs/AGENT-API-AUTH.md
|
|
├── inventory::collect_inventory (background thread)
|
|
│ └─ PowerShell + WMI + wlanapi + ipify → `INVENTORY` global
|
|
│ consumed by hbbs_http::sync above; one-shot, no retry
|
|
│
|
|
│ 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 (~2400 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
|
|
├── ci/ provision scripts for the Gitea Windows-build /
|
|
│ Linux-signing self-hosted runners
|
|
├── .gitea/workflows/ Gitea CI
|
|
└── README.md
|
|
```
|
|
|
|
The vendored source has a handful of local divergences from upstream.
|
|
Most patch sites carry an inline `hello-agent` breadcrumb (search for
|
|
`grep -rn "hello-agent" vendor/rustdesk/` in a tree where the patches
|
|
have been applied) but a couple of one-token edits like
|
|
`pub mod custom_server` don't, so the list below is the authoritative
|
|
inventory — keep it in sync when adding new patches.
|
|
|
|
1. **Module visibility** —
|
|
[`vendor/rustdesk/src/lib.rs`](vendor/rustdesk/src/lib.rs):
|
|
* `mod custom_server` → `pub mod custom_server` so hello-agent's
|
|
`config_import` can call the deploy-blob decoder.
|
|
* `mod ui_cm_interface` → `pub mod ui_cm_interface` so the headless
|
|
`--cm` process can plug a `MessageBoxW`-based `InvokeUiCM` into
|
|
upstream's connection-manager IPC loop and inherit file-transfer,
|
|
chat, and clipboard handling rather than re-implementing them.
|
|
2. **Build shape** — [`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 cadence** lowered 15s → 1s so device-online status in
|
|
the admin UI reacts faster:
|
|
[`libs/hbb_common/src/config.rs`](vendor/rustdesk/libs/hbb_common/src/config.rs)
|
|
(`REG_INTERVAL`, UDP rendezvous re-register) and
|
|
[`src/hbbs_http/sync.rs`](vendor/rustdesk/src/hbbs_http/sync.rs)
|
|
(`TIME_HEARTBEAT`, HTTP `/api/heartbeat`).
|
|
4. **Sysinfo upload extensions** —
|
|
[`libs/hbb_common/src/config.rs`](vendor/rustdesk/libs/hbb_common/src/config.rs)
|
|
adds three opt-in `RwLock<String>` globals — `AGENT_NAME`,
|
|
`AGENT_VERSION`, `INVENTORY` — that hello-agent populates at startup
|
|
(rebrand identity) and asynchronously (CMDB inventory).
|
|
[`src/hbbs_http/sync.rs`](vendor/rustdesk/src/hbbs_http/sync.rs)
|
|
reads them when each `/api/sysinfo` payload is constructed and
|
|
tracks `had_inventory` on the `InfoUploaded` state so the loop
|
|
re-uploads when `INVENTORY` transitions empty → populated (the
|
|
collector is async and routinely loses the race against the first
|
|
sysinfo tick).
|
|
5. **Documentation / branding URLs** retargeted from `rustdesk.com` to
|
|
`cstudio.ch/hello-agent/`:
|
|
[`libs/hbb_common/src/config.rs`](vendor/rustdesk/libs/hbb_common/src/config.rs)
|
|
(`LINK_DOCS_HOME`, `LINK_DOCS_X11_REQUIRED`),
|
|
[`src/client.rs`](vendor/rustdesk/src/client.rs) (`SCRAP_X11_REF_URL`,
|
|
login-screen help link). Plus author / copyright strings in
|
|
[`Cargo.toml`](vendor/rustdesk/Cargo.toml) (`LegalCopyright`) and
|
|
[`src/main.rs`](vendor/rustdesk/src/main.rs) (`.author(...)`).
|
|
Cosmetic, but they show through in the Windows EXE metadata and
|
|
in-app error dialogs.
|
|
6. **Signed agent API** — every `POST /api/heartbeat`,
|
|
`POST /api/sysinfo`, and `POST /api/unattended-password` carries
|
|
two extra headers (`X-RD-Device-Id`,
|
|
`X-RD-Signature: v1.<ts>.<base64-ed25519-sig>`) so the server can
|
|
bind the request to the device's existing rendezvous keypair
|
|
instead of trusting the `id` + `uuid` body fields. Without this
|
|
patch, anyone who knows a peer's id and uuid can inject inventory,
|
|
heartbeats, and unattended-access passwords for it. Three patch
|
|
sites in the vendor tree (plus one in the hello-agent crate):
|
|
* New file
|
|
[`src/hbbs_http/sign.rs`](vendor/rustdesk/src/hbbs_http/sign.rs) —
|
|
the signer (`build_signed_headers`, `path_from_url`). Reads
|
|
`Config::get_key_pair()` and `Config::get_id()`; uses the
|
|
re-exported `hbb_common::sodiumoxide`.
|
|
* [`src/hbbs_http.rs`](vendor/rustdesk/src/hbbs_http.rs) — adds
|
|
`pub mod sign;` next to the existing module declarations.
|
|
* [`src/common.rs`](vendor/rustdesk/src/common.rs) — the
|
|
`post_request_` and `parse_simple_header` header-string parsers
|
|
now accept a `\n`-separated list of `Name: Value` lines so we
|
|
can pass both signing headers in one call. Old single-pair
|
|
callers parse identically — there's no newline to split on.
|
|
* [`src/hbbs_http/sync.rs`](vendor/rustdesk/src/hbbs_http/sync.rs)
|
|
call sites (the sysinfo POST around the sysinfo-version
|
|
comparison block, and the heartbeat POST a few dozen lines
|
|
later) — both build a signed-headers string via
|
|
`crate::hbbs_http::sign::build_signed_headers("POST",
|
|
&path_from_url(&url), body.as_bytes()).unwrap_or_default()`
|
|
and pass it to `post_request` instead of `""`.
|
|
|
|
And in the hello-agent crate proper (not the vendor tree, no
|
|
re-sync concern):
|
|
* [`src/unattended_password.rs`](src/unattended_password.rs) —
|
|
`try_report` also signs its `POST /api/unattended-password`
|
|
via `librustdesk::hbbs_http::sign::build_signed_headers`.
|
|
|
|
Matching server side: see rustdesk-server's
|
|
[`docs/AGENT-API-AUTH.md`](https://github.com/cstudio-ch/rustdesk-server/blob/pro-features/docs/AGENT-API-AUTH.md)
|
|
for the wire format and verification flow.
|
|
|
|
## 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 local divergences enumerated in the
|
|
[Repo layout](#repo-layout) section above (that list is the source
|
|
of truth — most patches carry a `hello-agent` breadcrumb in a
|
|
nearby comment, but a couple of one-token edits like
|
|
`pub mod custom_server` do not). The reliable recipe is to diff
|
|
against the upstream rev you rsync'd from before you do the rsync,
|
|
stash the patch hunks, then re-apply them on top of the new tree.
|
|
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 `hello-agent.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\hello-agent"
|
|
```
|
|
|
|
…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 hello-agent` → `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 `"hello-agent"` 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%\hello-agent\` |
|
|
| Service-mode config / logs | `…\LocalService\AppData\Roaming\RustDesk\`| `…\LocalService\AppData\Roaming\hello-agent\` |
|
|
| Identity file (id + keypair) | `RustDesk.toml` | `hello-agent.toml` |
|
|
| IPC pipe namespace | `\\.\pipe\RustDesk\query…` | `\\.\pipe\hello-agent\query…` |
|
|
| Windows service name | `RustDesk` | `hello-agent` |
|
|
| 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 = "hello-agent"`)
|
|
— 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%\hello-agent\log\install\` (or `…\uninstall\`) |
|
|
| `--service` | LocalSystem (mirrored) | `%SystemRoot%\ServiceProfiles\LocalService\AppData\Roaming\hello-agent\log\service\` |
|
|
| `--server` (worker) | LocalSystem (mirrored) | `%SystemRoot%\ServiceProfiles\LocalService\AppData\Roaming\hello-agent\log\server\` |
|
|
| no flags (dev mode) | calling user | `%APPDATA%\hello-agent\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 `hello-agent` 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.
|
|
- ✅ CMDB asset inventory — BIOS serial, manufacturer, model, AD domain,
|
|
OS edition + release, CPU, RAM, disks, BitLocker recovery key,
|
|
network interfaces, public IP, current + nearby Wi-Fi networks.
|
|
Collected once at startup ([`src/inventory.rs`](src/inventory.rs),
|
|
[`src/wifi_native.rs`](src/wifi_native.rs)) and merged into the
|
|
`/api/sysinfo` payload under the `inventory` key, where the
|
|
rustdesk-server admin UI's per-device detail page reads it.
|
|
- ✅ Unattended-access password — rotated once per service start
|
|
([`src/unattended_password.rs`](src/unattended_password.rs)) and
|
|
reported to rustdesk-server's `/api/unattended-password` so the admin
|
|
UI can show the current per-boot password for headless /
|
|
no-user-logged-in support sessions.
|
|
- ✅ Authenticode code signing in CI — separate Linux signing job runs
|
|
`osslsigncode` against the cStudio CA, with pre-/post-sign SHA-256
|
|
audit hashes that catch a tampered transit between build and sign
|
|
runners ([`.gitea/workflows/build-windows.yml`](.gitea/workflows/build-windows.yml)).
|
|
- ⏳ Linux / macOS (out of scope for v0)
|
|
- ⏳ Multiple simultaneous interactive users (only one can receive the
|
|
approval popup at a time — the one in the `WTSActive` session)
|