Files
mike f8ead215d8
build-windows / build-hello-agent-x64 (push) Successful in 5m41s
Initial commit: hello-agent — headless RustDesk-protocol-compatible Windows agent
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>
2026-05-08 16:29:31 +02:00

414 lines
12 KiB
C++

// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ics/c-adding-an-application-rule-edge-traversal
/********************************************************************
Copyright (C) Microsoft. All Rights Reserved.
Abstract:
This C++ file includes sample code that adds a firewall rule with
EdgeTraversalOptions (one of the EdgeTraversalOptions values).
********************************************************************/
#include "pch.h"
#include <windows.h>
#include <stdio.h>
#include <netfw.h>
#include <strsafe.h>
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "oleaut32.lib")
#define STRING_BUFFER_SIZE 500
// Forward declarations
HRESULT WFCOMInitialize(INetFwPolicy2** ppNetFwPolicy2);
void WFCOMCleanup(INetFwPolicy2* pNetFwPolicy2);
HRESULT RemoveFirewallRule(
__in INetFwPolicy2* pNetFwPolicy2,
__in LPWSTR exeName);
HRESULT AddFirewallRuleWithEdgeTraversal(__in INetFwPolicy2* pNetFwPolicy2,
__in bool in,
__in LPWSTR exeName,
__in LPWSTR exeFile);
bool AddFirewallRule(bool add, LPWSTR exeName, LPWSTR exeFile)
{
bool result = false;
HRESULT hrComInit = S_OK;
HRESULT hr = S_OK;
INetFwPolicy2* pNetFwPolicy2 = NULL;
// Initialize COM.
hrComInit = CoInitializeEx(
0,
COINIT_APARTMENTTHREADED
);
// Ignore RPC_E_CHANGED_MODE; this just means that COM has already been
// initialized with a different mode. Since we don't care what the mode is,
// we'll just use the existing mode.
if (hrComInit != RPC_E_CHANGED_MODE)
{
if (FAILED(hrComInit))
{
WcaLog(LOGMSG_STANDARD, "CoInitializeEx failed: 0x%08lx\n", hrComInit);
goto Cleanup;
}
}
// Retrieve INetFwPolicy2
hr = WFCOMInitialize(&pNetFwPolicy2);
if (FAILED(hr))
{
goto Cleanup;
}
if (add) {
// Add firewall rule with EdgeTraversalOption=DeferApp (Windows7+) if available
// else add with Edge=True (Vista and Server 2008).
hr = AddFirewallRuleWithEdgeTraversal(pNetFwPolicy2, true, exeName, exeFile);
hr = AddFirewallRuleWithEdgeTraversal(pNetFwPolicy2, false, exeName, exeFile);
}
else {
hr = RemoveFirewallRule(pNetFwPolicy2, exeName);
}
result = SUCCEEDED(hr);
Cleanup:
// Release INetFwPolicy2
WFCOMCleanup(pNetFwPolicy2);
// Uninitialize COM.
if (SUCCEEDED(hrComInit))
{
CoUninitialize();
}
return result;
}
BSTR MakeRuleName(__in LPWSTR exeName)
{
WCHAR pwszTemp[STRING_BUFFER_SIZE] = L"";
HRESULT hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"%ls Service", exeName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to compose a resource identifier string: 0x%08lx\n", hr);
return NULL;
}
return SysAllocString(pwszTemp);
}
HRESULT RemoveFirewallRule(
__in INetFwPolicy2* pNetFwPolicy2,
__in LPWSTR exeName)
{
HRESULT hr = S_OK;
INetFwRules* pNetFwRules = NULL;
WCHAR pwszTemp[STRING_BUFFER_SIZE] = L"";
BSTR RuleName = NULL;
RuleName = MakeRuleName(exeName);
if (NULL == RuleName)
{
WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n");
goto Cleanup;
}
hr = pNetFwPolicy2->get_Rules(&pNetFwRules);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to retrieve firewall rules collection : 0x%08lx\n", hr);
goto Cleanup;
}
// We need to "Remove()" twice, because both "in" and "out" rules are added?
// There's no remarks for this case https://learn.microsoft.com/en-us/windows/win32/api/netfw/nf-netfw-inetfwrules-remove
hr = pNetFwRules->Remove(RuleName);
hr = pNetFwRules->Remove(RuleName);
if (FAILED(hr)) {
WcaLog(LOGMSG_STANDARD, "Failed to remove firewall rule \"%ls\" : 0x%08lx\n", exeName, hr);
}
else {
WcaLog(LOGMSG_STANDARD, "Firewall rule \"%ls\" is removed\n", exeName);
}
Cleanup:
SysFreeString(RuleName);
if (pNetFwRules != NULL)
{
pNetFwRules->Release();
}
return hr;
}
// Add firewall rule with EdgeTraversalOption=DeferApp (Windows7+) if available
// else add with Edge=True (Vista and Server 2008).
HRESULT AddFirewallRuleWithEdgeTraversal(
__in INetFwPolicy2* pNetFwPolicy2,
__in bool in,
__in LPWSTR exeName,
__in LPWSTR exeFile)
{
HRESULT hr = S_OK;
INetFwRules* pNetFwRules = NULL;
INetFwRule* pNetFwRule = NULL;
INetFwRule2* pNetFwRule2 = NULL;
WCHAR pwszTemp[STRING_BUFFER_SIZE] = L"";
BSTR RuleName = NULL;
BSTR RuleGroupName = NULL;
BSTR RuleDescription = NULL;
BSTR RuleAppPath = NULL;
long CurrentProfilesBitMask = 0;
// For localization purposes, the rule name, description, and group can be
// provided as indirect strings. These indirect strings can be defined in an rc file.
// Examples of the indirect string definitions in the rc file -
// 127 "EdgeTraversalOptions Sample Application"
// 128 "Allow inbound TCP traffic to application EdgeTraversalOptions.exe"
// 129 "Allow EdgeTraversalOptions.exe to receive inbound traffic for TCP protocol
// from remote machines located within your network as well as from
// the Internet (i.e from outside of your Edge device like Firewall or NAT"
// Examples of using indirect strings -
// hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"@EdgeTraversalOptions.exe,-128");
RuleName = MakeRuleName(exeName);
if (NULL == RuleName)
{
WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n");
goto Cleanup;
}
// Examples of using indirect strings -
// hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"@EdgeTraversalOptions.exe,-127");
hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, exeName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to compose a resource identifier string: 0x%08lx\n", hr);
goto Cleanup;
}
RuleGroupName = SysAllocString(pwszTemp); // Used for grouping together multiple rules
if (NULL == RuleGroupName)
{
WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n");
goto Cleanup;
}
// Examples of using indirect strings -
// hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"@EdgeTraversalOptions.exe,-129");
hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"Allow %ls to receive \
inbound traffic from remote machines located within your network as well as \
from the Internet", exeName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to compose a resource identifier string: 0x%08lx\n", hr);
goto Cleanup;
}
RuleDescription = SysAllocString(pwszTemp);
if (NULL == RuleDescription)
{
WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n");
goto Cleanup;
}
RuleAppPath = SysAllocString(exeFile);
if (NULL == RuleAppPath)
{
WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n");
goto Cleanup;
}
hr = pNetFwPolicy2->get_Rules(&pNetFwRules);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to retrieve firewall rules collection : 0x%08lx\n", hr);
goto Cleanup;
}
hr = CoCreateInstance(
__uuidof(NetFwRule), //CLSID of the class whose object is to be created
NULL,
CLSCTX_INPROC_SERVER,
__uuidof(INetFwRule), // Identifier of the Interface used for communicating with the object
(void**)&pNetFwRule);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "CoCreateInstance for INetFwRule failed: 0x%08lx\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_Name(RuleName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Name failed with error: 0x %x.\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_Grouping(RuleGroupName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Grouping failed with error: 0x %x.\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_Description(RuleDescription);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Description failed with error: 0x %x.\n", hr);
goto Cleanup;
}
// If you want the rule to avoid public, you can refer to
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ics/c-adding-an-outbound-rule
CurrentProfilesBitMask = NET_FW_PROFILE2_ALL;
hr = pNetFwRule->put_Direction(in ? NET_FW_RULE_DIR_IN : NET_FW_RULE_DIR_OUT);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Direction failed with error: 0x %x.\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_Action(NET_FW_ACTION_ALLOW);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Action failed with error: 0x %x.\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_ApplicationName(RuleAppPath);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_ApplicationName failed with error: 0x %x.\n", hr);
goto Cleanup;
}
//hr = pNetFwRule->put_Protocol(6); // TCP
//if (FAILED(hr))
//{
// WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Protocol failed with error: 0x %x.\n", hr);
// goto Cleanup;
//}
hr = pNetFwRule->put_Profiles(CurrentProfilesBitMask);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Profiles failed with error: 0x %x.\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_Enabled(VARIANT_TRUE);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Enabled failed with error: 0x %x.\n", hr);
goto Cleanup;
}
if (in) {
// Check if INetFwRule2 interface is available (i.e Windows7+)
// If supported, then use EdgeTraversalOptions
// Else use the EdgeTraversal boolean flag.
if (SUCCEEDED(pNetFwRule->QueryInterface(__uuidof(INetFwRule2), (void**)&pNetFwRule2)))
{
hr = pNetFwRule2->put_EdgeTraversalOptions(NET_FW_EDGE_TRAVERSAL_TYPE_DEFER_TO_APP);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_EdgeTraversalOptions failed with error: 0x %x.\n", hr);
goto Cleanup;
}
}
else
{
hr = pNetFwRule->put_EdgeTraversal(VARIANT_TRUE);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_EdgeTraversal failed with error: 0x %x.\n", hr);
goto Cleanup;
}
}
}
hr = pNetFwRules->Add(pNetFwRule);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to add firewall rule to the firewall rules collection : 0x%08lx\n", hr);
goto Cleanup;
}
WcaLog(LOGMSG_STANDARD, "Successfully added firewall rule !\n");
Cleanup:
SysFreeString(RuleName);
SysFreeString(RuleGroupName);
SysFreeString(RuleDescription);
SysFreeString(RuleAppPath);
if (pNetFwRule2 != NULL)
{
pNetFwRule2->Release();
}
if (pNetFwRule != NULL)
{
pNetFwRule->Release();
}
if (pNetFwRules != NULL)
{
pNetFwRules->Release();
}
return hr;
}
// Instantiate INetFwPolicy2
HRESULT WFCOMInitialize(INetFwPolicy2** ppNetFwPolicy2)
{
HRESULT hr = S_OK;
hr = CoCreateInstance(
__uuidof(NetFwPolicy2),
NULL,
CLSCTX_INPROC_SERVER,
__uuidof(INetFwPolicy2),
(void**)ppNetFwPolicy2);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "CoCreateInstance for INetFwPolicy2 failed: 0x%08lx\n", hr);
goto Cleanup;
}
Cleanup:
return hr;
}
// Release INetFwPolicy2
void WFCOMCleanup(INetFwPolicy2* pNetFwPolicy2)
{
// Release the INetFwPolicy2 object (Vista+)
if (pNetFwPolicy2 != NULL)
{
pNetFwPolicy2->Release();
}
}