f8ead215d8
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>
260 lines
5.2 KiB
Protocol Buffer
260 lines
5.2 KiB
Protocol Buffer
syntax = "proto3";
|
|
package hbb;
|
|
|
|
message RegisterPeer {
|
|
string id = 1;
|
|
int32 serial = 2;
|
|
}
|
|
|
|
enum ConnType {
|
|
DEFAULT_CONN = 0;
|
|
FILE_TRANSFER = 1;
|
|
PORT_FORWARD = 2;
|
|
RDP = 3;
|
|
VIEW_CAMERA = 4;
|
|
TERMINAL = 5;
|
|
}
|
|
|
|
message RegisterPeerResponse { bool request_pk = 2; }
|
|
|
|
message PunchHoleRequest {
|
|
string id = 1;
|
|
NatType nat_type = 2;
|
|
string licence_key = 3;
|
|
ConnType conn_type = 4;
|
|
string token = 5;
|
|
string version = 6;
|
|
int32 udp_port = 7;
|
|
bool force_relay = 8;
|
|
int32 upnp_port = 9;
|
|
bytes socket_addr_v6 = 10;
|
|
}
|
|
|
|
message ControlPermissions {
|
|
enum Permission {
|
|
keyboard = 0;
|
|
remote_printer = 1;
|
|
clipboard = 2;
|
|
file = 3;
|
|
audio = 4;
|
|
camera = 5;
|
|
terminal = 6;
|
|
tunnel = 7;
|
|
restart = 8;
|
|
recording = 9;
|
|
block_input = 10;
|
|
remote_modify = 11;
|
|
privacy_mode = 12;
|
|
}
|
|
uint64 permissions = 1;
|
|
}
|
|
|
|
message PunchHole {
|
|
bytes socket_addr = 1;
|
|
string relay_server = 2;
|
|
NatType nat_type = 3;
|
|
int32 udp_port = 4;
|
|
bool force_relay = 5;
|
|
int32 upnp_port = 6;
|
|
bytes socket_addr_v6 = 7;
|
|
ControlPermissions control_permissions = 8;
|
|
}
|
|
|
|
message TestNatRequest {
|
|
int32 serial = 1;
|
|
}
|
|
|
|
// per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative
|
|
message TestNatResponse {
|
|
int32 port = 1;
|
|
ConfigUpdate cu = 2; // for mobile
|
|
}
|
|
|
|
enum NatType {
|
|
UNKNOWN_NAT = 0;
|
|
ASYMMETRIC = 1;
|
|
SYMMETRIC = 2;
|
|
}
|
|
|
|
message PunchHoleSent {
|
|
bytes socket_addr = 1;
|
|
string id = 2;
|
|
string relay_server = 3;
|
|
NatType nat_type = 4;
|
|
string version = 5;
|
|
int32 upnp_port = 6;
|
|
bytes socket_addr_v6 = 7;
|
|
}
|
|
|
|
message RegisterPk {
|
|
string id = 1;
|
|
bytes uuid = 2;
|
|
bytes pk = 3;
|
|
string old_id = 4;
|
|
bool no_register_device = 5;
|
|
}
|
|
|
|
message RegisterPkResponse {
|
|
enum Result {
|
|
OK = 0;
|
|
UUID_MISMATCH = 2;
|
|
ID_EXISTS = 3;
|
|
TOO_FREQUENT = 4;
|
|
INVALID_ID_FORMAT = 5;
|
|
NOT_SUPPORT = 6;
|
|
SERVER_ERROR = 7;
|
|
}
|
|
Result result = 1;
|
|
int32 keep_alive = 2;
|
|
}
|
|
|
|
message PunchHoleResponse {
|
|
bytes socket_addr = 1;
|
|
bytes pk = 2;
|
|
enum Failure {
|
|
ID_NOT_EXIST = 0;
|
|
OFFLINE = 2;
|
|
LICENSE_MISMATCH = 3;
|
|
LICENSE_OVERUSE = 4;
|
|
}
|
|
Failure failure = 3;
|
|
string relay_server = 4;
|
|
oneof union {
|
|
NatType nat_type = 5;
|
|
bool is_local = 6;
|
|
}
|
|
string other_failure = 7;
|
|
int32 feedback = 8;
|
|
bool is_udp = 9;
|
|
int32 upnp_port = 10;
|
|
bytes socket_addr_v6 = 11;
|
|
}
|
|
|
|
message ConfigUpdate {
|
|
int32 serial = 1;
|
|
repeated string rendezvous_servers = 2;
|
|
}
|
|
|
|
message RequestRelay {
|
|
string id = 1;
|
|
string uuid = 2;
|
|
bytes socket_addr = 3;
|
|
string relay_server = 4;
|
|
bool secure = 5;
|
|
string licence_key = 6;
|
|
ConnType conn_type = 7;
|
|
string token = 8;
|
|
ControlPermissions control_permissions = 9;
|
|
}
|
|
|
|
message RelayResponse {
|
|
bytes socket_addr = 1;
|
|
string uuid = 2;
|
|
string relay_server = 3;
|
|
oneof union {
|
|
string id = 4;
|
|
bytes pk = 5;
|
|
}
|
|
string refuse_reason = 6;
|
|
string version = 7;
|
|
int32 feedback = 9;
|
|
bytes socket_addr_v6 = 10;
|
|
int32 upnp_port = 11;
|
|
}
|
|
|
|
message SoftwareUpdate { string url = 1; }
|
|
|
|
// if in same intranet, punch hole won't work both for udp and tcp,
|
|
// even some router has below connection error if we connect itself,
|
|
// { kind: Other, error: "could not resolve to any address" },
|
|
// so we request local address to connect.
|
|
message FetchLocalAddr {
|
|
bytes socket_addr = 1;
|
|
string relay_server = 2;
|
|
bytes socket_addr_v6 = 3;
|
|
ControlPermissions control_permissions = 4;
|
|
}
|
|
|
|
message LocalAddr {
|
|
bytes socket_addr = 1;
|
|
bytes local_addr = 2;
|
|
string relay_server = 3;
|
|
string id = 4;
|
|
string version = 5;
|
|
bytes socket_addr_v6 = 6;
|
|
}
|
|
|
|
message PeerDiscovery {
|
|
string cmd = 1;
|
|
string mac = 2;
|
|
string id = 3;
|
|
string username = 4;
|
|
string hostname = 5;
|
|
string platform = 6;
|
|
string misc = 7;
|
|
}
|
|
|
|
message OnlineRequest {
|
|
string id = 1;
|
|
repeated string peers = 2;
|
|
}
|
|
|
|
message OnlineResponse {
|
|
bytes states = 1;
|
|
}
|
|
|
|
message KeyExchange {
|
|
repeated bytes keys = 1;
|
|
}
|
|
|
|
message HealthCheck {
|
|
string token = 1;
|
|
}
|
|
|
|
message HeaderEntry {
|
|
string name = 1;
|
|
string value = 2;
|
|
}
|
|
|
|
message HttpProxyRequest {
|
|
string method = 1;
|
|
string path = 2;
|
|
repeated HeaderEntry headers = 3;
|
|
bytes body = 4;
|
|
}
|
|
|
|
message HttpProxyResponse {
|
|
int32 status = 1;
|
|
repeated HeaderEntry headers = 2;
|
|
bytes body = 3;
|
|
string error = 4;
|
|
}
|
|
|
|
message RendezvousMessage {
|
|
oneof union {
|
|
RegisterPeer register_peer = 6;
|
|
RegisterPeerResponse register_peer_response = 7;
|
|
PunchHoleRequest punch_hole_request = 8;
|
|
PunchHole punch_hole = 9;
|
|
PunchHoleSent punch_hole_sent = 10;
|
|
PunchHoleResponse punch_hole_response = 11;
|
|
FetchLocalAddr fetch_local_addr = 12;
|
|
LocalAddr local_addr = 13;
|
|
ConfigUpdate configure_update = 14;
|
|
RegisterPk register_pk = 15;
|
|
RegisterPkResponse register_pk_response = 16;
|
|
SoftwareUpdate software_update = 17;
|
|
RequestRelay request_relay = 18;
|
|
RelayResponse relay_response = 19;
|
|
TestNatRequest test_nat_request = 20;
|
|
TestNatResponse test_nat_response = 21;
|
|
PeerDiscovery peer_discovery = 22;
|
|
OnlineRequest online_request = 23;
|
|
OnlineResponse online_response = 24;
|
|
KeyExchange key_exchange = 25;
|
|
HealthCheck hc = 26;
|
|
HttpProxyRequest http_proxy_request = 27;
|
|
HttpProxyResponse http_proxy_response = 28;
|
|
}
|
|
}
|