Compare commits

..

22 Commits

Author SHA1 Message Date
fufesou
cdfd986cb9 Merge branch 'master' into terminal-utf8-and-reconnect 2026-05-07 12:13:44 +08:00
fufesou
e47e5b38b6 fix(terminal): update hbb_common
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-07 12:12:37 +08:00
fufesou
6a53757f68 fix(terminal): comments utf-8 chunk accumulator
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-03 11:37:01 +08:00
fufesou
5a65d45244 fix(terminal): comments
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-03 09:53:56 +08:00
fufesou
e99829d709 fix(terminal): update hbb_common
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-30 21:43:38 +08:00
fufesou
071c6b1c12 fix(terminal): flag, retry output
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 23:48:55 +08:00
fufesou
26a356d0f5 fix(terminal): reconnect, refactor
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 23:11:13 +08:00
fufesou
929a4e78ba fix(terminal): env en_US.UTF-8
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 22:30:39 +08:00
fufesou
18479129a2 fix: cap terminal reconnect replay output
- split reconnect replay backlog into capped chunks
  - mark terminal data replay chunks for client-side suppression
  - avoid using open-message text to suppress xterm replies
  - reuse default terminal padding value
  - remove misleading Enter-key normalization PR link

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 22:12:26 +08:00
fufesou
b516dfb15b fix(terminal): reconnect suppress next output
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 20:54:04 +08:00
fufesou
d5568d9188 fix(terminal): windows&macos, charset utf-8
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 10:11:25 +08:00
fufesou
1745bab204 fix(terminal): schedule frame before flushing buffered output
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 20:46:47 +08:00
fufesou
0eff404323 fix(terminal): remove invalid test
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 20:31:37 +08:00
fufesou
67b5484ded fix(terminal): avoid reconnect stalls and delayed layout writes
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 20:21:15 +08:00
fufesou
268827ef64 fix(terminal): merge reconnect backlog into replay output
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 20:11:18 +08:00
fufesou
c4542b4a5d fix(terminal): close terminal window on disconnect dialog
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 19:47:57 +08:00
fufesou
59f3060a04 fix(terminal): dialog, close window
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 18:40:49 +08:00
fufesou
0a1500a72a fix(terminal): reconnect, error handling
1. Terminal shows "^[[1;1R^[[2;2R^[[>0;0;0c"
2. NaN

```
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Converting object to an encodable object failed: NaN
...
```

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 17:58:51 +08:00
rustdesk
4f5c7db70a fix ios enter: https://github.com/rustdesk/rustdesk/issues/14907 2026-04-26 09:08:11 +08:00
fufesou
0112167029 fix(terminal): subtract with overflow
```
thread '<unnamed>' panicked at src\server\terminal_service.rs:476:17:
attempt to subtract with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'tokio-runtime-worker' panicked at src\server\terminal_service.rs:1576:50:
called `Result::unwrap()` on an `Err` value: PoisonError { .. }
[2026-04-25T07:17:34Z ERROR librustdesk::server::service] Failed to join thread for service ts_9badd3fe-2411-4996-9f40-93c979009edd, Any { .. }
```

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-25 15:46:25 +08:00
rustdesk
0d77482a64 Fix terminal auto-reconnect freeze: reconnect resumes terminal output, while multi-tab reconnect avoids restoring duplicate tabs for terminals that are already open. 2026-04-25 01:07:00 +08:00
rustdesk
ca3ef2a1c3 fix: handle incomplete UTF-8 sequences in terminal output, rework on https://github.com/rustdesk/rustdesk/pull/14736 2026-04-25 00:44:23 +08:00
35 changed files with 549 additions and 5940 deletions

4
Cargo.lock generated
View File

@@ -5996,8 +5996,8 @@ dependencies = [
[[package]]
name = "parity-tokio-ipc"
version = "0.7.3-6"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#d0ae39bffe5d5a3e8d82a1b6bcb1ca5a9b2f1c01"
version = "0.7.3-5"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291"
dependencies = [
"futures",
"libc",

View File

@@ -47,7 +47,7 @@ screencapturekit = ["cpal/screencapturekit"]
[dependencies]
async-trait = "0.1"
scrap = { path = "libs/scrap", features = ["wayland"] }
hbb_common = { path = "libs/hbb_common", features = ["webrtc"] }
hbb_common = { path = "libs/hbb_common" }
serde_derive = "1.0"
serde = "1.0"
serde_json = "1.0"

View File

@@ -16,12 +16,6 @@ import 'package:get/get.dart';
bool isEditOsPassword = false;
// macOS privacy mode blacks out all online displays, so switching the remote
// display does not weaken the local privacy protection.
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) {
return pi.platform == kPeerPlatformMacOS;
}
class TTextMenu {
final Widget child;
final VoidCallback? onPressed;
@@ -690,9 +684,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Lock after session end'))));
}
final privacyModeState = PrivacyModeState.find(id);
if (pi.isSupportMultiDisplay &&
(privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) &&
PrivacyModeState.find(id).isEmpty &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
final value =
@@ -783,8 +776,7 @@ List<TToggleMenu> toolbarPrivacyMode(
onChanged: enabled
? (value) {
if (value == null) return;
if (!allowDisplaySwitchInPrivacyMode(pi) &&
ffiModel.pi.currentDisplay != 0 &&
if (ffiModel.pi.currentDisplay != 0 &&
ffiModel.pi.currentDisplay != kAllDisplayValue) {
msgBox(
sessionId,

View File

@@ -376,8 +376,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
}
toolbarItems.add(Obx(() {
if ((PrivacyModeState.find(widget.id).isEmpty ||
allowDisplaySwitchInPrivacyMode(pi)) &&
if (PrivacyModeState.find(widget.id).isEmpty &&
pi.displaysCount.value > 1) {
return _MonitorMenu(
id: widget.id,

View File

@@ -593,13 +593,13 @@ class _DesktopTabState extends State<DesktopTab>
}
Widget _buildBar() {
final isIncomingHomePage = bind.isIncomingOnly() && isInHomePage();
return Row(
children: [
Expanded(
child: GestureDetector(
// custom double tap handler
onTap: !isIncomingHomePage && showMaximize
onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
showMaximize
? () {
final current = DateTime.now().millisecondsSinceEpoch;
final elapsed = current - _lastClickTime;
@@ -610,7 +610,7 @@ class _DesktopTabState extends State<DesktopTab>
.then((value) => stateGlobal.setMaximized(value));
}
}
: (isIncomingHomePage ? () {} : null), // Keep tap recognizer for Windows touch.
: null,
onPanStart: (_) => startDragging(isMainWindow),
onPanCancel: () {
// We want to disable dragging of the tab area in the tab bar.

View File

@@ -391,30 +391,14 @@ class FileController {
await Future.delayed(Duration(milliseconds: 100));
final savedDir = (await bind.sessionGetPeerOption(
final dir = (await bind.sessionGetPeerOption(
sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
Future<bool> tryOpenReadyDirs() async {
final dirs = <String>{
if (directory.value.path.isNotEmpty) directory.value.path,
if (savedDir.isNotEmpty) savedDir,
options.value.home,
};
for (final dir in dirs) {
if (await _openDirectoryPath(dir, isBack: true)) {
return true;
}
}
return false;
}
var opened = await tryOpenReadyDirs();
openDirectory(dir.isEmpty ? options.value.home : dir);
await Future.delayed(Duration(seconds: 1));
if (!opened) {
// The peer may become ready during the reconnect delay, so retry the
// same candidates instead of only retrying the default home directory.
await tryOpenReadyDirs();
if (directory.value.path.isEmpty) {
openDirectory(options.value.home);
}
}
@@ -445,23 +429,19 @@ class FileController {
});
}
Future<bool> refresh() async {
// "." can be both a refresh command and a real remote directory path.
// Refresh must bypass openDirectory's command dispatch to avoid recursion.
return await _openDirectoryPath(directory.value.path, isBack: true);
Future<void> refresh() async {
await openDirectory(directory.value.path);
}
Future<bool> openDirectory(String path, {bool isBack = false}) async {
if (!isBack && path == ".") {
return await refresh();
Future<void> openDirectory(String path, {bool isBack = false}) async {
if (path == ".") {
refresh();
return;
}
if (!isBack && path == "..") {
return await _goToParentDirectory(isBack: isBack);
if (path == "..") {
goToParentDirectory();
return;
}
return await _openDirectoryPath(path, isBack: isBack);
}
Future<bool> _openDirectoryPath(String path, {bool isBack = false}) async {
if (!isBack) {
pushHistory();
}
@@ -478,10 +458,8 @@ class FileController {
final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
fd.format(isWindows, sort: sortBy.value);
directory.value = fd;
return true;
} catch (e) {
debugPrint("Failed to openDirectory $path: $e");
return false;
}
}
@@ -509,22 +487,19 @@ class FileController {
goBack();
return;
}
unawaited(_openDirectoryPath(path, isBack: true).then<void>((_) {}));
openDirectory(path, isBack: true);
}
void goToParentDirectory() {
unawaited(_goToParentDirectory().then<void>((_) {}));
}
Future<bool> _goToParentDirectory({bool isBack = false}) async {
final isWindows = options.value.isWindows;
final dirPath = directory.value.path;
var parent = PathUtil.dirname(dirPath, isWindows);
// specially for C:\, D:\, goto '/'
if (parent == dirPath && isWindows) {
return await _openDirectoryPath('/', isBack: isBack);
openDirectory('/');
return;
}
return await _openDirectoryPath(parent, isBack: isBack);
openDirectory(parent);
}
// TODO deprecated this

View File

@@ -31,17 +31,17 @@ LExit:
return WcaFinalize(er);
}
// Helper function to safely delete a file using handle-based deletion.
// Directories are refused after opening the handle.
// Helper function to safely delete a file or directory using handle-based deletion.
// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
BOOL SafeDeleteItem(LPCWSTR fullPath)
{
// Open the file/directory with delete and attribute-read access plus FILE_FLAG_OPEN_REPARSE_POINT
// Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
// to prevent following symlinks.
// Use shared access to allow deletion even when other processes have the file open.
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
HANDLE hFile = CreateFileW(
fullPath,
DELETE | FILE_READ_ATTRIBUTES,
DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
NULL,
OPEN_EXISTING,
@@ -55,21 +55,6 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
return FALSE;
}
BY_HANDLE_FILE_INFORMATION fileInfo;
if (FALSE == GetFileInformationByHandle(hFile, &fileInfo))
{
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to inspect '%ls'. Error: %lu", fullPath, GetLastError());
CloseHandle(hFile);
return FALSE;
}
if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Refusing to delete directory '%ls'.", fullPath);
CloseHandle(hFile);
return FALSE;
}
// Use SetFileInformationByHandle to mark for deletion.
// The file will be deleted when the handle is closed.
FILE_DISPOSITION_INFO dispInfo;
@@ -92,74 +77,98 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
return result;
}
BOOL PathEndsWithSlash(LPCWSTR path)
// Helper function to recursively delete a directory's contents with detailed logging.
void RecursiveDelete(LPCWSTR path)
{
size_t length = 0;
HRESULT hr = StringCchLengthW(path, MAX_PATH, &length);
if (FAILED(hr) || length == 0)
{
return FALSE;
}
WCHAR last = path[length - 1];
return last == L'\\' || last == L'/';
}
void ClearReadOnlyAttribute(LPCWSTR fullPath, DWORD attributes)
{
if (!(attributes & FILE_ATTRIBUTE_READONLY))
// Ensure the path is not empty or null.
if (path == NULL || path[0] == L'\0')
{
return;
}
DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY;
if (writableAttributes == 0)
// Extra safety: never operate directly on a root path.
if (PathIsRootW(path))
{
writableAttributes = FILE_ATTRIBUTE_NORMAL;
}
if (SetFileAttributesW(fullPath, writableAttributes))
{
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath);
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
return;
}
WcaLog(LOGMSG_STANDARD, "Runtime cleanup failed to clear read-only attribute for '%ls'. Error: %lu", fullPath, GetLastError());
}
BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName)
{
WCHAR fullPath[MAX_PATH];
LPCWSTR separator = PathEndsWithSlash(installFolder) ? L"" : L"\\";
HRESULT hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s%s%s", installFolder, separator, fileName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Runtime cleanup path is too long for '%ls'.", fileName);
return FALSE;
// MAX_PATH is enough here since the installer should not be using longer paths.
// No need to handle extended-length paths (\\?\) in this context.
WCHAR searchPath[MAX_PATH];
HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path);
if (FAILED(hr)) {
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path);
return;
}
DWORD attributes = GetFileAttributesW(fullPath);
if (attributes == INVALID_FILE_ATTRIBUTES)
WIN32_FIND_DATAW findData;
HANDLE hFind = FindFirstFileW(searchPath, &findData);
if (hFind == INVALID_HANDLE_VALUE)
{
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND)
// This can happen if the directory is empty or doesn't exist, which is not an error in our case.
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError());
return;
}
do
{
// Skip '.' and '..' directories.
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
{
return TRUE;
continue;
}
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error);
return FALSE;
}
// MAX_PATH is enough here since the installer should not be using longer paths.
// No need to handle extended-length paths (\\?\) in this context.
WCHAR fullPath[MAX_PATH];
hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName);
if (FAILED(hr)) {
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path);
continue;
}
if (attributes & FILE_ATTRIBUTE_DIRECTORY)
// Before acting, ensure the read-only attribute is not set.
if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
{
if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY))
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError());
}
}
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
// Check for reparse points (symlinks/junctions) to prevent directory traversal attacks.
// Do not follow reparse points, only remove the link itself.
if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath);
SafeDeleteItem(fullPath);
}
else
{
// Recursively delete directory contents first
RecursiveDelete(fullPath);
// Then delete the directory itself
SafeDeleteItem(fullPath);
}
}
else
{
// Delete file using safe handle-based deletion
SafeDeleteItem(fullPath);
}
} while (FindNextFileW(hFind, &findData) != 0);
DWORD lastError = GetLastError();
if (lastError != ERROR_NO_MORE_FILES)
{
WcaLog(LOGMSG_STANDARD, "Runtime cleanup skipped directory '%ls'.", fullPath);
return FALSE;
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
}
ClearReadOnlyAttribute(fullPath, attributes);
WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath);
return SafeDeleteItem(fullPath);
FindClose(hFind);
}
// See `Package.wxs` for the sequence of this custom action.
@@ -169,13 +178,13 @@ BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName)
// 2. RemoveExistingProducts
// ├─ TerminateProcesses
// ├─ TryStopDeleteService
// ├─ RemoveRuntimeGeneratedFiles - <-- Here
// ├─ RemoveInstallFolder - <-- Here
// └─ RemoveFiles
// 3. InstallValidate
// 4. InstallFiles
// 5. InstallExecute
// 6. InstallFinalize
UINT __stdcall RemoveRuntimeGeneratedFiles(
UINT __stdcall RemoveInstallFolder(
__in MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
@@ -185,7 +194,7 @@ UINT __stdcall RemoveRuntimeGeneratedFiles(
LPWSTR pwz = NULL;
LPWSTR pwzData = NULL;
hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles");
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
ExitOnFailure(hr, "Failed to initialize");
hr = WcaGetProperty(L"CustomActionData", &pwzData);
@@ -193,20 +202,24 @@ UINT __stdcall RemoveRuntimeGeneratedFiles(
pwz = pwzData;
hr = WcaReadStringFromCaData(&pwz, &installFolder);
ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz);
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
if (installFolder == NULL || installFolder[0] == L'\0') {
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup.");
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
goto LExit;
}
if (PathIsRootW(installFolder)) {
WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder);
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
goto LExit;
}
WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder);
DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe");
WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder);
RecursiveDelete(installFolder);
// The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories.
// We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer.
LExit:
ReleaseStr(pwzData);

View File

@@ -2,7 +2,7 @@ LIBRARY "CustomActions"
EXPORTS
CustomActionHello
RemoveRuntimeGeneratedFiles
RemoveInstallFolder
TerminateProcesses
AddFirewallRules
SetPropertyIsServiceRunning

View File

@@ -16,15 +16,8 @@
<!-- If a command line value was stored, restore it after the registry search has been performed -->
<SetProperty Action="RestoreSavedInstallFolderValue" Id="INSTALLFOLDER" Value="[SavedInstallFolderCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedInstallFolderCmdLineValue" />
<!-- Normalize INSTALLFOLDER from the command line or registry before assigning INSTALLFOLDER_INNER. -->
<!-- Case 1: already ends with \$(var.Product)\, keep it unchanged. -->
<SetProperty Action="SetInstallFolderInnerFromProductDir" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot;" />
<!-- Case 2: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
<SetProperty Action="SetInstallFolderInnerFromProductDirNoSlash" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;" />
<!-- Case 3: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
<SetProperty Action="SetInstallFolderInnerAppendProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- Case 4: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
<SetProperty Action="SetInstallFolderInnerAppendSlashProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND NOT INSTALLFOLDER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- If a command line value or registry value was set, update the main properties with the value -->
<SetProperty Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER" />
<!-- INSTALLFOLDER_INNER is defined for compatibility with previous versions of the installer. -->
<!-- Because we need to use INSTALLFOLDER as the command line argument. -->

View File

@@ -12,7 +12,7 @@
</Component>
</DirectoryRef>
<CustomAction Id="RemoveRuntimeGeneratedFiles.SetParam" Return="check" Property="RemoveRuntimeGeneratedFiles" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="RemoveInstallFolder.SetParam" Return="check" Property="RemoveInstallFolder" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="AddFirewallRules.SetParam" Return="check" Property="AddFirewallRules" Value="1[INSTALLFOLDER_INNER]$(var.Product).exe" />
<CustomAction Id="RemoveFirewallRules.SetParam" Return="check" Property="RemoveFirewallRules" Value="0[INSTALLFOLDER_INNER]$(var.Product).exe" />
<CustomAction Id="CreateStartService.SetParam" Return="check" Property="CreateStartService" Value="$(var.Product);&quot;[INSTALLFOLDER_INNER]$(var.Product).exe&quot; --service" />
@@ -77,21 +77,21 @@
<Custom Action="AddRegSoftwareSASGeneration" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)"/>
<Custom Action="RemoveRuntimeGeneratedFiles" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot; OR UPGRADINGPRODUCTCODE)"/>
<Custom Action="RemoveRuntimeGeneratedFiles.SetParam" Before="RemoveRuntimeGeneratedFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot; OR UPGRADINGPRODUCTCODE)"/>
<Custom Action="TryStopDeleteService" Before="RemoveRuntimeGeneratedFiles.SetParam" />
<Custom Action="RemoveInstallFolder" Before="RemoveFiles"/>
<Custom Action="RemoveInstallFolder.SetParam" Before="RemoveInstallFolder"/>
<Custom Action="TryStopDeleteService" Before="RemoveInstallFolder.SetParam" />
<Custom Action="TryStopDeleteService.SetParam" Before="TryStopDeleteService" />
<Custom Action="RemoveFirewallRules" Before="RemoveFiles"/>
<Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/>
<Custom Action="UninstallPrinter" Before="RemoveRuntimeGeneratedFiles" Condition="VersionNT &gt;= 603" />
<Custom Action="UninstallPrinter" Before="RemoveInstallFolder" Condition="VersionNT &gt;= 603" />
<Custom Action="TerminateProcesses" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateProcesses" Before="RemoveInstallFolder"/>
<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
<Custom Action="TerminateBrokers" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateBrokers" Before="RemoveInstallFolder"/>
<Custom Action="TerminateBrokers.SetParam" Before="TerminateBrokers"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveInstallFolder"/>
<Custom Action="RemoveAmyuniIdd.SetParam" Before="RemoveAmyuniIdd"/>
</InstallExecuteSequence>

View File

@@ -5,7 +5,7 @@
<Binary Id="Custom_Actions_Dll" SourceFile="$(var.CustomActions.TargetDir)$(var.CustomActions.TargetName).dll" />
<CustomAction Id="CustomActionHello" DllEntry="CustomActionHello" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveRuntimeGeneratedFiles" DllEntry="RemoveRuntimeGeneratedFiles" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveInstallFolder" DllEntry="RemoveInstallFolder" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TerminateProcesses" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TerminateBrokers" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="AddFirewallRules" DllEntry="AddFirewallRules" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>

View File

@@ -23,13 +23,12 @@ Patch dialog sequence:
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<?include ../Includes.wxi?>
<?foreach WIXUIARCH in X86;X64;A64 ?>
<Fragment>
<UI Id="UI_MyInstallDialog_$(WIXUIARCH)">
<Publish Dialog="LicenseAgreementDlg" Control="Print" Event="DoAction" Value="WixUIPrintEula_$(WIXUIARCH)" />
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="5" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="2" Condition="NOT WIXUI_DONTVALIDATEPATH" />
</UI>
<UIRef Id="UI_MyInstallDialog" />
@@ -65,16 +64,9 @@ Patch dialog sequence:
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg" Condition="LicenseAccepted = &quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg" />
<!-- Normalize INSTALLFOLDER_INNER before SetTargetPath and WixUIValidatePath run. -->
<!-- UI case 1: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\" Order="1" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;" />
<!-- UI case 2: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]$(var.Product)\" Order="2" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- UI case 3: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\$(var.Product)\" Order="3" Condition="INSTALLFOLDER_INNER AND NOT INSTALLFOLDER_INNER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="4" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="6" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="7" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MyInstallDirDlg" Order="1" Condition="NOT Installed" />

View File

@@ -3,7 +3,7 @@ use async_trait::async_trait;
use hbb_common::{
config::PeerConfig,
config::READ_TIMEOUT,
futures::StreamExt,
futures::{SinkExt, StreamExt},
log,
message_proto::*,
protobuf::Message as _,
@@ -46,7 +46,6 @@ impl Session {
false,
None,
None,
None,
);
session
}
@@ -54,7 +53,7 @@ impl Session {
#[async_trait]
impl Interface for Session {
fn get_lch(&self) -> Arc<RwLock<LoginConfigHandler>> {
fn get_login_config_handler(&self) -> Arc<RwLock<LoginConfigHandler>> {
return self.lc.clone();
}
@@ -62,20 +61,14 @@ impl Interface for Session {
match msgtype {
"input-password" => {
self.sender
.send(Data::Login((
String::new(),
String::new(),
self.password.clone(),
true,
)))
.send(Data::Login((self.password.clone(), true)))
.ok();
}
"re-input-password" => {
log::error!("{}: {}", title, text);
match rpassword::prompt_password("Enter password: ") {
Ok(password) => {
let login_data =
Data::Login((String::new(), String::new(), password, true));
let login_data = Data::Login((password, true));
self.sender.send(login_data).ok();
}
Err(e) => {
@@ -100,8 +93,6 @@ impl Interface for Session {
self.lc.write().unwrap().handle_peer_info(&pi);
}
fn set_multiple_windows_session(&self, _sessions: Vec<WindowsSession>) {}
async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) {
log::info!(
"password={}",
@@ -146,8 +137,8 @@ pub async fn connect_test(id: &str, key: String, token: String) {
Err(err) => {
log::error!("Failed to connect {}: {}", &id, err);
}
Ok(((mut stream, _direct, _secure, _kcp, _typ), direct)) => {
log::info!("direct: {:?}", direct);
Ok((mut stream, direct)) => {
log::info!("direct: {}", direct);
// rpassword::prompt_password("Input anything to exit").ok();
loop {
tokio::select! {

View File

@@ -65,12 +65,11 @@ use hbb_common::{
self,
net::UdpSocket,
sync::{
mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver},
mpsc::{unbounded_channel, UnboundedReceiver},
oneshot,
},
time::{interval, Duration, Instant},
},
webrtc::WebRTCStream,
AddrMangle, ResultType, Stream,
};
pub use helper::*;
@@ -331,19 +330,6 @@ impl Client {
} else {
(None, None)
};
let ipv6 = if crate::get_ipv6_punch_enabled() {
crate::get_ipv6_socket().await
} else {
None
};
let webrtc_offerer =
match WebRTCStream::new("", interface.is_force_relay(), CONNECT_TIMEOUT).await {
Ok(stream) => Some(stream),
Err(err) => {
log::warn!("webrtc offerer setup failed: {}", err);
None
}
};
let fut = Self::_start_inner(
peer.to_owned(),
key.to_owned(),
@@ -352,8 +338,6 @@ impl Client {
interface.clone(),
udp.clone(),
Some(stop_udp_tx),
ipv6,
webrtc_offerer,
rendezvous_server.clone(),
servers.clone(),
contained,
@@ -371,8 +355,6 @@ impl Client {
interface,
(None, None),
None,
None,
None,
rendezvous_server,
servers,
contained,
@@ -384,68 +366,6 @@ impl Client {
}
}
fn spawn_webrtc_ice_bridge(
mut socket: Stream,
mut local_ice_rx: Option<UnboundedReceiver<String>>,
webrtc: WebRTCStream,
peer: String,
session_key: String,
) -> oneshot::Sender<()> {
let (stop_tx, mut stop_rx) = oneshot::channel::<()>();
let my_id = Config::get_id();
tokio::spawn(async move {
loop {
match stop_rx.try_recv() {
Ok(_) | Err(tokio::sync::oneshot::error::TryRecvError::Closed) => break,
Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {}
}
if let Some(rx) = local_ice_rx.as_mut() {
loop {
match rx.try_recv() {
Ok(candidate) => {
let mut msg = RendezvousMessage::new();
msg.set_ice_candidate(IceCandidate {
from_id: my_id.clone(),
to_id: peer.clone(),
session_key: session_key.clone(),
candidate,
..Default::default()
});
if let Err(err) = socket.send(&msg).await {
log::warn!("failed to send WebRTC ICE candidate: {}", err);
return;
}
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
local_ice_rx = None;
break;
}
}
}
}
if let Some(msg_in) =
crate::get_next_nonkeyexchange_msg(&mut socket, Some(100)).await
{
if let Some(rendezvous_message::Union::IceCandidate(ice)) = msg_in.union {
if ice.from_id == peer
&& ice.to_id == my_id
&& ice.session_key == session_key
{
if let Err(err) = webrtc.add_remote_ice_candidate(&ice.candidate).await
{
log::warn!("failed to add WebRTC ICE candidate: {}", err);
}
}
}
}
}
});
stop_tx
}
async fn _start_inner(
peer: String,
key: String,
@@ -454,8 +374,6 @@ impl Client {
interface: impl Interface,
mut udp: (Option<Arc<UdpSocket>>, Option<Arc<Mutex<u16>>>),
stop_udp_tx: Option<oneshot::Sender<()>>,
mut ipv6: Option<(Arc<UdpSocket>, bytes::Bytes)>,
mut webrtc_offerer: Option<WebRTCStream>,
mut rendezvous_server: String,
servers: Vec<String>,
contained: bool,
@@ -528,20 +446,14 @@ impl Client {
// Stop UDP NAT test task if still running
stop_udp_tx.map(|tx| tx.send(()));
let mut msg_out = RendezvousMessage::new();
let mut ipv6 = ipv6
.take()
.map(|(socket, addr)| (Some(socket), Some(addr)))
.unwrap_or((None, None));
let webrtc_sdp_offer = if let Some(webrtc) = webrtc_offerer.as_ref() {
match webrtc.get_local_endpoint().await {
Ok(endpoint) => endpoint,
Err(err) => {
log::warn!("failed to read local WebRTC offer: {}", err);
String::new()
}
let mut ipv6 = if crate::get_ipv6_punch_enabled() {
if let Some((socket, addr)) = crate::get_ipv6_socket().await {
(Some(socket), Some(addr))
} else {
(None, None)
}
} else {
String::new()
(None, None)
};
let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0);
let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" };
@@ -555,15 +467,8 @@ impl Client {
udp_port: udp_nat_port as _,
force_relay: interface.is_force_relay(),
socket_addr_v6: ipv6.1.unwrap_or_default(),
webrtc_sdp_offer: webrtc_sdp_offer.clone(),
..Default::default()
});
let webrtc_session_key = webrtc_offerer
.as_ref()
.map(|webrtc| webrtc.session_key().to_owned())
.unwrap_or_default();
let mut webrtc_sdp_answer = String::new();
let mut pending_webrtc_ice = Vec::<String>::new();
for i in 1..=3 {
log::info!(
"#{} {} punch attempt with {}, id: {}",
@@ -605,7 +510,6 @@ impl Client {
relay_server = ph.relay_server;
peer_addr = AddrMangle::decode(&ph.socket_addr);
feedback = ph.feedback;
webrtc_sdp_answer = ph.webrtc_sdp_answer;
let s = udp.0.take();
if ph.is_udp && s.is_some() {
if let Some(s) = s {
@@ -645,38 +549,6 @@ impl Client {
}
}
signed_id_pk = rr.pk().into();
let mut webrtc_bridge_stop = None;
let mut webrtc_for_connect = None;
if !rr.webrtc_sdp_answer.is_empty() {
if let Some(webrtc) = webrtc_offerer.take() {
if let Err(err) =
webrtc.set_remote_endpoint(&rr.webrtc_sdp_answer).await
{
log::warn!("failed to set WebRTC relay answer: {}", err);
} else {
for candidate in pending_webrtc_ice.drain(..) {
if let Err(err) =
webrtc.add_remote_ice_candidate(&candidate).await
{
log::warn!(
"failed to add buffered WebRTC ICE candidate: {}",
err
);
}
}
let session_key = webrtc.session_key().to_owned();
let local_ice_rx = webrtc.take_local_ice_rx();
webrtc_bridge_stop = Some(Self::spawn_webrtc_ice_bridge(
socket,
local_ice_rx,
webrtc.clone(),
peer.clone(),
session_key,
));
webrtc_for_connect = Some(webrtc);
}
}
}
let fut = Self::create_relay(
&peer,
rr.uuid,
@@ -692,86 +564,30 @@ impl Client {
}
.boxed(),
);
if let Some(mut webrtc) = webrtc_for_connect {
connect_futures.push(
async move {
webrtc.wait_connected(CONNECT_TIMEOUT).await?;
Ok((Stream::WebRTC(webrtc), None, "WebRTC"))
}
.boxed(),
);
}
// Run all connection attempts concurrently, return the first successful one
let (conn, kcp, typ) = match select_ok(connect_futures).await {
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
Err(e) => (Err(e), None, ""),
};
if let Some(stop) = webrtc_bridge_stop {
let _ = stop.send(());
}
let mut conn = conn?;
feedback = rr.feedback;
log::info!("{:?} used to establish {typ} connection", start.elapsed());
let pk =
Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?;
return Ok((
(conn, typ == "IPv6" || typ == "WebRTC", pk, kcp, typ),
(conn, typ == "IPv6", pk, kcp, typ),
(feedback, rendezvous_server),
false,
));
}
Some(rendezvous_message::Union::IceCandidate(ice)) => {
if !webrtc_session_key.is_empty()
&& ice.from_id == peer
&& ice.to_id == Config::get_id()
&& ice.session_key == webrtc_session_key
{
pending_webrtc_ice.push(ice.candidate);
} else {
log::debug!(
"dropping ICE candidate for unexpected WebRTC session from {} key {}",
ice.from_id,
ice.session_key
);
}
}
_ => {
log::error!("Unexpected protobuf msg received: {:?}", msg_in);
}
}
}
}
let mut webrtc_bridge_stop = None;
let mut webrtc_for_connect = None;
if !webrtc_sdp_answer.is_empty() {
if let Some(webrtc) = webrtc_offerer.take() {
if let Err(err) = webrtc.set_remote_endpoint(&webrtc_sdp_answer).await {
log::warn!("failed to set WebRTC answer: {}", err);
drop(socket);
} else {
for candidate in pending_webrtc_ice.drain(..) {
if let Err(err) = webrtc.add_remote_ice_candidate(&candidate).await {
log::warn!("failed to add buffered WebRTC ICE candidate: {}", err);
}
}
let session_key = webrtc.session_key().to_owned();
let local_ice_rx = webrtc.take_local_ice_rx();
webrtc_bridge_stop = Some(Self::spawn_webrtc_ice_bridge(
socket,
local_ice_rx,
webrtc.clone(),
peer.clone(),
session_key,
));
webrtc_for_connect = Some(webrtc);
}
} else {
drop(socket);
}
} else {
drop(socket);
}
drop(socket);
if peer_addr.port() == 0 {
bail!("Failed to connect via rendezvous server");
}
@@ -805,8 +621,6 @@ impl Client {
interface,
udp.0,
ipv6.0,
webrtc_for_connect,
webrtc_bridge_stop,
punch_type,
)
.await?,
@@ -833,8 +647,6 @@ impl Client {
interface: impl Interface,
udp_socket_nat: Option<Arc<UdpSocket>>,
udp_socket_v6: Option<Arc<UdpSocket>>,
webrtc_offerer: Option<WebRTCStream>,
webrtc_bridge_stop: Option<oneshot::Sender<()>>,
punch_type: &str,
) -> ResultType<(
Stream,
@@ -893,23 +705,11 @@ impl Client {
if let Some(udp_socket_v6) = udp_socket_v6 {
connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6", connect_timeout).boxed());
}
if let Some(mut webrtc) = webrtc_offerer {
connect_futures.push(
async move {
webrtc.wait_connected(connect_timeout).await?;
Ok((Stream::WebRTC(webrtc), None, "WebRTC"))
}
.boxed(),
);
}
// Run all connection attempts concurrently, return the first successful one
let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await {
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
Err(e) => (Err(e), None, ""),
};
if let Some(stop) = webrtc_bridge_stop {
let _ = stop.send(());
}
let mut direct = !conn.is_err();
if interface.is_force_relay() || conn.is_err() {

View File

@@ -146,13 +146,7 @@ pub fn core_main() -> Option<Vec<String>> {
crate::portable_service::client::set_quick_support(_is_quick_support);
}
let mut log_name = "".to_owned();
// Keep portable-service logs under a stable directory name.
let has_portable_service_shmem_arg = args
.iter()
.any(|arg| arg.starts_with("--portable-service-shmem-name="));
if has_portable_service_shmem_arg {
log_name = "portable-service".to_owned();
} else if args.len() > 0 && args[0].starts_with("--") {
if args.len() > 0 && args[0].starts_with("--") {
let name = args[0].replace("--", "");
if !name.is_empty() {
log_name = name;
@@ -627,98 +621,6 @@ pub fn core_main() -> Option<Vec<String>> {
println!("Installation and administrative privileges required!");
}
return None;
} else if args[0] == "--deploy" {
if config::Config::no_register_device() {
println!("Cannot deploy an unregistrable device!");
} else if crate::platform::is_installed() && is_root() {
let max = args.len() - 1;
let pos = args.iter().position(|x| x == "--token").unwrap_or(max);
if pos >= max {
println!("--token is required!");
return None;
}
let token = args[pos + 1].to_owned();
let get_value = |c: &str| {
let pos = args.iter().position(|x| x == c).unwrap_or(max);
if pos < max {
Some(args[pos + 1].to_owned())
} else {
None
}
};
let new_id = get_value("--id");
let local_id = crate::ipc::get_id();
let id_to_deploy = new_id.clone().unwrap_or_else(|| local_id.clone());
let uuid = crate::encode64(hbb_common::get_uuid());
let pk = crate::encode64(
hbb_common::config::Config::get_key_pair().1,
);
let body = serde_json::json!({
"id": id_to_deploy,
"uuid": uuid,
"pk": pk,
});
let header = "Authorization: Bearer ".to_owned() + &token;
let url = crate::ui_interface::get_api_server() + "/api/devices/deploy";
match crate::post_request_sync(url, body.to_string(), &header) {
Err(err) => {
println!("Request failed: {}", err);
std::process::exit(1);
}
Ok(text) => {
let parsed: serde_json::Value =
serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
let result = parsed["result"].as_str().unwrap_or("");
match result {
"OK" => {
if let Some(ref new_id) = new_id {
if *new_id != local_id {
if let Err(err) =
crate::ipc::set_config("id", new_id.clone())
{
println!(
"Failed to persist deployed id locally: {}",
err
);
std::process::exit(1);
}
}
}
if let Err(err) = crate::ipc::notify_deployed() {
log::warn!("Failed to notify deployed state: {}", err);
}
println!("Device deployed.");
}
"NOT_ENABLED" => {
println!("Server does not require deployment.");
std::process::exit(3);
}
"INVALID_INPUT" => {
println!("Invalid input.");
std::process::exit(5);
}
"ID_TAKEN" => {
println!(
"Id `{}` is already used by another machine on the server.",
id_to_deploy
);
std::process::exit(6);
}
_ => {
if text.is_empty() {
println!("Unknown response.");
} else {
println!("{}", text);
}
std::process::exit(1);
}
}
}
}
} else {
println!("Installation and administrative privileges required!");
}
return None;
} else if args[0] == "--check-hwcodec-config" {
#[cfg(feature = "hwcodec")]
crate::ipc::hwcodec_process();

View File

@@ -1,28 +1,33 @@
#[path = "ipc/auth.rs"]
mod ipc_auth;
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[path = "ipc/fs.rs"]
mod ipc_fs;
use crate::{
common::CheckTestNatType,
privacy_mode::PrivacyModeState,
ui_interface::{get_local_option, set_local_option},
};
use bytes::Bytes;
use parity_tokio_ipc::{
Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes,
};
use serde_derive::{Deserialize, Serialize};
use std::{
collections::HashMap,
sync::atomic::{AtomicBool, Ordering},
};
#[cfg(not(windows))]
use std::{fs::File, io::prelude::*};
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::plugin::ipc::Plugin;
use crate::{
common::{is_server, CheckTestNatType},
privacy_mode,
privacy_mode::PrivacyModeState,
rendezvous_mediator::RendezvousMediator,
ui_interface::{get_local_option, set_local_option},
};
use bytes::Bytes;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use clipboard::ClipboardFile;
#[cfg(target_os = "linux")]
use hbb_common::anyhow;
use hbb_common::{
allow_err, bail, bytes,
bytes_codec::BytesCodec,
config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2},
config::{
self,
keys::{self, OPTION_ALLOW_WEBSOCKET},
Config, Config2,
},
futures::StreamExt as _,
futures_util::sink::SinkExt,
log, password_security as password, timeout,
@@ -33,55 +38,13 @@ use hbb_common::{
tokio_util::codec::Framed,
ResultType,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_auth::authorize_service_scoped_ipc_connection;
#[cfg(windows)]
pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection;
#[cfg(windows)]
pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt;
#[cfg(windows)]
pub(crate) use ipc_auth::log_rejected_windows_ipc_connection;
#[cfg(target_os = "linux")]
pub(crate) use ipc_auth::{
active_uid, ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid,
log_rejected_uinput_connection, peer_uid_from_fd,
};
#[cfg(windows)]
use ipc_auth::{
authorize_windows_main_ipc_connection, portable_service_listener_security_attributes,
should_allow_everyone_create_on_windows,
};
#[cfg(target_os = "linux")]
use ipc_fs::terminal_count_candidate_uids;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_fs::{
check_pid, ensure_secure_ipc_parent_dir, scrub_secure_ipc_parent_dir,
should_scrub_parent_entries_after_check_pid, write_pid,
};
use parity_tokio_ipc::{
Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes,
};
use serde_derive::{Deserialize, Serialize};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::os::unix::fs::PermissionsExt;
use std::{
collections::HashMap,
sync::atomic::{AtomicBool, Ordering},
};
use crate::{common::is_server, privacy_mode, rendezvous_mediator::RendezvousMediator};
// IPC actions here.
pub const IPC_ACTION_CLOSE: &str = "close";
const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000;
pub(crate) const IPC_TOKEN_LEN: usize = 64;
const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2;
const _: () = assert!(IPC_TOKEN_LEN % 2 == 0);
pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true);
#[inline]
pub async fn connect_service(ms_timeout: u64) -> ResultType<ConnectionTmpl<ConnClient>> {
connect(ms_timeout, crate::POSTFIX_SERVICE).await
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum FS {
@@ -244,8 +207,6 @@ pub enum DataControl {
pub enum DataPortableService {
Ping,
Pong,
AuthToken(String),
AuthResult(bool),
ConnCount(Option<usize>),
Mouse((Vec<u8>, i32, String, u32, bool, bool)),
Pointer((Vec<u8>, i32)),
@@ -312,7 +273,6 @@ pub enum Data {
ClipboardNonFile(Option<(String, Vec<ClipboardNonFile>)>),
PrivacyModeState((i32, PrivacyModeState, String)),
TestRendezvousServer,
Deployed,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Keyboard(DataKeyboard),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -451,22 +411,6 @@ pub async fn start(postfix: &str) -> ResultType<()> {
Ok(stream) => {
let mut stream = Connection::new(stream);
let postfix = postfix.to_owned();
#[cfg(any(target_os = "linux", target_os = "macos"))]
if config::is_service_ipc_postfix(&postfix) {
if !authorize_service_scoped_ipc_connection(&stream, &postfix) {
continue;
}
}
#[cfg(windows)]
if postfix.is_empty() {
// Windows main IPC (`postfix == ""`) is authorized here.
// Other security-sensitive channels use dedicated authorization paths:
// - `_portable_service`: portable-service listener + handshake policy
// - service-scoped postfixes: service-specific listener/authorization
if !authorize_windows_main_ipc_connection(&stream, &postfix) {
continue;
}
}
tokio::spawn(async move {
loop {
match stream.next().await {
@@ -475,48 +419,9 @@ pub async fn start(postfix: &str) -> ResultType<()> {
break;
}
Ok(Some(data)) => {
// On Linux/macOS, the protected `_service` channel is used only for
// syncing config between root service and the active user process.
//
// NOTE: `is_service_ipc_postfix()` also includes `_uinput_*`, but those
// channels are handled by the dedicated uinput listener/protocol in
// `src/server/uinput.rs` and therefore do not share this Data enum
// allowlist. The SyncConfig allowlist here is intentionally scoped to the
// `_service` channel only.
//
// Keep this explicit branch to avoid policy drift between `_service` and
// uinput IPC paths while still minimizing exposed message surface here.
#[cfg(any(target_os = "linux", target_os = "macos"))]
if postfix == crate::POSTFIX_SERVICE {
if matches!(&data, Data::SyncConfig(_)) {
handle(data, &mut stream).await;
} else {
log::warn!(
"Rejected non-sync data on protected _service IPC channel: postfix={}, data_kind={:?}, peer_uid={:?}",
postfix,
std::mem::discriminant(&data),
stream.peer_uid()
);
// Close the connection to avoid keeping a protected channel
// alive while repeatedly receiving invalid traffic.
break;
}
continue;
}
handle(data, &mut stream).await;
}
Ok(None) => {
// `Ok(None)` means a complete frame arrived but did not
// deserialize into `Data`. Peer close/reset is returned as
// `Err` by `ConnectionTmpl::next()`. Keep the historical
// ignore behavior except on the protected `_service` channel.
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
if postfix == crate::POSTFIX_SERVICE {
break;
}
}
}
_ => {}
}
}
});
@@ -531,77 +436,20 @@ pub async fn start(postfix: &str) -> ResultType<()> {
pub async fn new_listener(postfix: &str) -> ResultType<Incoming> {
let path = Config::ipc_path(postfix);
#[cfg(any(target_os = "linux", target_os = "macos"))]
let should_scrub_parent_entries = ensure_secure_ipc_parent_dir(&path, postfix)?;
#[cfg(any(target_os = "linux", target_os = "macos"))]
let existing_listener_alive = check_pid(postfix).await;
#[cfg(any(target_os = "linux", target_os = "macos"))]
if should_scrub_parent_entries_after_check_pid(
should_scrub_parent_entries,
existing_listener_alive,
) {
scrub_secure_ipc_parent_dir(&path, postfix)?;
}
#[cfg(not(any(windows, target_os = "android", target_os = "ios")))]
check_pid(postfix).await;
let mut endpoint = Endpoint::new(path.clone());
let security_attrs = {
#[cfg(windows)]
{
if postfix == "_portable_service" {
portable_service_listener_security_attributes()
} else if should_allow_everyone_create_on_windows(postfix) {
SecurityAttributes::allow_everyone_create()
} else {
Ok(SecurityAttributes::empty())
}
}
#[cfg(not(windows))]
{
SecurityAttributes::allow_everyone_create()
}
};
match security_attrs {
match SecurityAttributes::allow_everyone_create() {
Ok(attr) => endpoint.set_security_attributes(attr),
Err(err) => {
log::error!("Failed to set ipc{} security: {}", postfix, err);
#[cfg(windows)]
if postfix == "_portable_service" {
// Fail closed for `_portable_service` when SDDL construction fails.
// This endpoint is security-critical and must not start with default ACLs.
return Err(err.into());
}
}
Err(err) => log::error!("Failed to set ipc{} security: {}", postfix, err),
};
match endpoint.incoming() {
Ok(incoming) => {
if postfix == crate::POSTFIX_SERVICE {
log::info!("Started protected ipc service server: postfix={}", postfix);
} else {
log::info!("Started ipc{} server at path: {}", postfix, &path);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
log::info!("Started ipc{} server at path: {}", postfix, &path);
#[cfg(not(windows))]
{
// NOTE: On Linux/macOS, some IPC sockets are intentionally world-connectable
// (0666) so the active (non-root) user process can connect. Authorization is
// enforced at accept-time for these channels, and the protected `_service`
// channel is further restricted by an explicit message allowlist (SyncConfig
// only).
let socket_mode = if config::is_service_ipc_postfix(postfix) {
0o0666
} else {
0o0600
};
if let Err(err) =
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(socket_mode))
{
log::error!(
"Failed to set permissions on ipc{} socket at path {}: {}",
postfix,
&path,
err
);
std::fs::remove_file(&path).ok();
return Err(err.into());
}
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok();
write_pid(postfix);
}
Ok(incoming)
@@ -930,10 +778,6 @@ async fn handle(data: Data, stream: &mut Connection) {
Data::TestRendezvousServer => {
crate::test_rendezvous_server();
}
Data::Deployed => {
crate::rendezvous_mediator::NEEDS_DEPLOY.store(false, Ordering::SeqCst);
crate::rendezvous_mediator::RendezvousMediator::restart();
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::SwitchSidesRequest(id) => {
@@ -1109,116 +953,15 @@ async fn handle(data: Data, stream: &mut Connection) {
);
}
_ => {}
};
}
}
pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
let path = Config::ipc_path(postfix);
connect_with_path(ms_timeout, &path).await
}
pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
use hbb_common::rand::{rngs::OsRng, RngCore as _};
use std::fmt::Write as _;
let mut random_bytes = [0u8; IPC_TOKEN_RANDOM_BYTES];
let mut rng = OsRng;
rng.try_fill_bytes(&mut random_bytes).map_err(|err| {
hbb_common::anyhow::anyhow!(
"failed to generate portable service ipc token from OsRng: {}",
err
)
})?;
let mut token = String::with_capacity(IPC_TOKEN_LEN);
for byte in random_bytes {
let _ = write!(token, "{:02x}", byte);
}
Ok(token)
}
pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool {
if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN {
return false;
}
expected
.as_bytes()
.iter()
.zip(candidate.as_bytes().iter())
.fold(0u8, |diff, (left, right)| diff | (*left ^ *right))
== 0
}
pub(crate) async fn portable_service_ipc_handshake_as_client<T>(
stream: &mut ConnectionTmpl<T>,
token: &str,
) -> ResultType<()>
where
T: AsyncRead + AsyncWrite + std::marker::Unpin,
{
stream
.send(&Data::DataPortableService(DataPortableService::AuthToken(
token.to_owned(),
)))
.await?;
match stream
.next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS)
.await?
{
Some(Data::DataPortableService(DataPortableService::AuthResult(true))) => Ok(()),
Some(Data::DataPortableService(DataPortableService::AuthResult(false))) => {
bail!("portable service ipc handshake was rejected by server")
}
Some(_) | None => bail!("portable service ipc handshake returned an unexpected response"),
}
}
pub(crate) async fn portable_service_ipc_handshake_as_server<T, F>(
stream: &mut ConnectionTmpl<T>,
mut validate_token: F,
) -> ResultType<()>
where
T: AsyncRead + AsyncWrite + std::marker::Unpin,
// Token validators must use `constant_time_ipc_token_eq` or an equivalent
// fixed-length comparison; this handshake is part of the privilege boundary.
F: FnMut(&str) -> bool,
{
let authorized = match stream
.next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS)
.await?
{
Some(Data::DataPortableService(DataPortableService::AuthToken(token))) => {
validate_token(&token)
}
Some(_) | None => false,
};
stream
.send(&Data::DataPortableService(DataPortableService::AuthResult(
authorized,
)))
.await?;
if !authorized {
bail!("portable service ipc handshake failed")
}
Ok(())
}
#[inline]
async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
let client = timeout(ms_timeout, Endpoint::connect(path)).await??;
let client = timeout(ms_timeout, Endpoint::connect(&path)).await??;
Ok(ConnectionTmpl::new(client))
}
#[cfg(target_os = "linux")]
pub async fn connect_for_uid(
ms_timeout: u64,
uid: u32,
postfix: &str,
) -> ResultType<ConnectionTmpl<ConnClient>> {
let path = Config::ipc_path_for_uid(uid, postfix);
connect_with_path(ms_timeout, &path).await
}
#[cfg(target_os = "linux")]
#[tokio::main(flavor = "current_thread")]
pub async fn start_pa() {
@@ -1296,6 +1039,54 @@ pub async fn start_pa() {
}
}
#[inline]
#[cfg(not(windows))]
fn get_pid_file(postfix: &str) -> String {
let path = Config::ipc_path(postfix);
format!("{}.pid", path)
}
#[cfg(not(any(windows, target_os = "android", target_os = "ios")))]
async fn check_pid(postfix: &str) {
let pid_file = get_pid_file(postfix);
if let Ok(mut file) = File::open(&pid_file) {
let mut content = String::new();
file.read_to_string(&mut content).ok();
let pid = content.parse::<usize>().unwrap_or(0);
if pid > 0 {
use hbb_common::sysinfo::System;
let mut sys = System::new();
sys.refresh_processes();
if let Some(p) = sys.process(pid.into()) {
if let Some(current) = sys.process((std::process::id() as usize).into()) {
if current.name() == p.name() {
// double check with connect
if connect(1000, postfix).await.is_ok() {
return;
}
}
}
}
}
}
// if not remove old ipc file, the new ipc creation will fail
// if we remove a ipc file, but the old ipc process is still running,
// new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive
std::fs::remove_file(&Config::ipc_path(postfix)).ok();
}
#[inline]
#[cfg(not(windows))]
fn write_pid(postfix: &str) {
let path = get_pid_file(postfix);
if let Ok(mut file) = File::create(&path) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok();
file.write_all(&std::process::id().to_string().into_bytes())
.ok();
}
}
pub struct ConnectionTmpl<T> {
inner: Framed<T, BytesCodec>,
}
@@ -1742,13 +1533,6 @@ pub async fn test_rendezvous_server() -> ResultType<()> {
Ok(())
}
#[tokio::main(flavor = "current_thread")]
pub async fn notify_deployed() -> ResultType<()> {
let mut c = connect(1000, "").await?;
c.send(&Data::Deployed).await?;
Ok(())
}
#[tokio::main(flavor = "current_thread")]
pub async fn send_url_scheme(url: String) -> ResultType<()> {
connect(1_000, "_url")
@@ -1766,10 +1550,9 @@ pub fn close_all_instances() -> ResultType<bool> {
}
}
#[cfg(windows)]
#[tokio::main(flavor = "current_thread")]
pub async fn connect_to_user_session(usid: Option<u32>) -> ResultType<()> {
let mut stream = crate::ipc::connect_service(1000).await?;
let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?;
timeout(1000, stream.send(&crate::ipc::Data::UserSid(usid))).await??;
Ok(())
}
@@ -1895,76 +1678,13 @@ pub async fn update_controlling_session_count(count: usize) -> ResultType<()> {
#[cfg(target_os = "linux")]
#[tokio::main(flavor = "current_thread")]
pub async fn get_terminal_session_count() -> ResultType<usize> {
let timeout_ms = 1_000;
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
let candidate_uids = terminal_count_candidate_uids(effective_uid);
let mut last_err: Option<anyhow::Error> = None;
for candidate_uid in candidate_uids {
let socket_path = Config::ipc_path_for_uid(candidate_uid, "");
let connect_result = timeout(timeout_ms, Endpoint::connect(&socket_path))
.await
.map_err(|err| {
anyhow::anyhow!(
"Timeout connecting to terminal ipc at {}: {}",
socket_path,
err
)
});
let connection = match connect_result {
Ok(Ok(connection)) => connection,
Ok(Err(err)) => {
last_err = Some(anyhow::anyhow!(
"Failed to connect to terminal ipc at {}: {}",
socket_path,
err
));
continue;
}
Err(err) => {
last_err = Some(err);
continue;
}
};
let mut ipc_conn = ConnectionTmpl::new(connection);
if let Err(err) = ipc_conn.send(&Data::TerminalSessionCount(0)).await {
last_err = Some(anyhow::anyhow!(
"Failed to request terminal session count via ipc at {}: {}",
socket_path,
err
));
continue;
}
match ipc_conn.next_timeout(timeout_ms).await {
Ok(Some(Data::TerminalSessionCount(session_count))) => {
return Ok(session_count);
}
Ok(None) => {
last_err = Some(anyhow::anyhow!(
"Invalid response when requesting terminal session count via ipc at {}",
socket_path
));
}
Ok(other) => {
last_err = Some(anyhow::anyhow!(
"Unexpected response when requesting terminal session count via ipc at {}: {:?}",
socket_path,
other.map(|v| std::mem::discriminant(&v))
));
}
Err(err) => {
last_err = Some(anyhow::anyhow!(
"Failed to read terminal session count via ipc at {}: {}",
socket_path,
err
));
}
}
}
if let Some(err) = last_err {
Err(err.into())
} else {
Ok(0)
let ms_timeout = 1_000;
let mut c = connect(ms_timeout, "").await?;
c.send(&Data::TerminalSessionCount(0)).await?;
if let Some(Data::TerminalSessionCount(c)) = c.next_timeout(ms_timeout).await? {
return Ok(c);
}
Ok(0)
}
async fn handle_wayland_screencast_restore_token(
@@ -1995,30 +1715,9 @@ pub async fn set_install_option(k: String, v: String) -> ResultType<()> {
#[cfg(test)]
mod test {
use super::*;
#[test]
fn verify_ffi_enum_data_size() {
println!("{}", std::mem::size_of::<Data>());
assert!(std::mem::size_of::<Data>() <= 120);
}
#[cfg(target_os = "linux")]
#[test]
fn test_ipc_path_differs_by_uid_for_cm() {
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
let other_uid = effective_uid.saturating_add(1);
let postfix = "_cm";
// Default connect path targets the current effective uid.
assert_eq!(
Config::ipc_path(postfix),
Config::ipc_path_for_uid(effective_uid, postfix)
);
// A different uid yields a different socket path - this is the root cause of the
// cross-user regression when root spawns a user process but still connects as uid 0.
assert_ne!(
Config::ipc_path(postfix),
Config::ipc_path_for_uid(other_uid, postfix)
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,951 +0,0 @@
#[cfg(target_os = "linux")]
use super::ipc_auth::active_uid;
use crate::ipc::{connect, Data};
use hbb_common::{config, log, ResultType};
use std::{
ffi::CString,
io::{Error, ErrorKind},
os::unix::ffi::OsStrExt,
path::Path,
};
struct FdGuard(i32);
impl Drop for FdGuard {
fn drop(&mut self) {
unsafe {
hbb_common::libc::close(self.0);
}
}
}
#[cfg(target_os = "linux")]
#[inline]
pub(crate) fn terminal_count_candidate_uids(effective_uid: u32) -> Vec<u32> {
if effective_uid != 0 {
return vec![effective_uid];
}
let mut candidates = Vec::with_capacity(2);
if let Some(uid) = active_uid().filter(|uid| *uid != 0) {
candidates.push(uid);
}
candidates.push(0);
candidates
}
#[inline]
fn expected_ipc_parent_mode(postfix: &str) -> u32 {
if config::is_service_ipc_postfix(postfix) {
0o0711
} else {
0o0700
}
}
fn open_ipc_parent_dir_fd(parent_c: &CString) -> std::io::Result<i32> {
let fd = unsafe {
hbb_common::libc::open(
parent_c.as_ptr(),
hbb_common::libc::O_RDONLY
| hbb_common::libc::O_DIRECTORY
| hbb_common::libc::O_CLOEXEC
| hbb_common::libc::O_NOFOLLOW,
)
};
if fd < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(fd)
}
}
// Remove one preexisting IPC artifact via an already-opened parent directory FD.
//
// Security intent:
// - Bind cleanup to the exact parent inode that passed O_NOFOLLOW + fstat checks.
// - Avoid path-based TOCTOU during scrub (e.g., parent path rename/swap race).
//
// Flow:
// 1) fstatat(..., AT_SYMLINK_NOFOLLOW) to inspect the target entry under parent_fd.
// 2) Decide file vs directory from st_mode.
// 3) unlinkat relative to parent_fd (AT_REMOVEDIR for directories).
//
// Error policy:
// - NotFound is treated as benign (already removed / raced away).
// - Other errors are surfaced explicitly.
fn remove_parent_entry_via_fd(
parent_fd: i32,
parent_dir: &Path,
entry_name: &str,
) -> ResultType<()> {
if entry_name.contains('/') {
return Err(Error::new(
ErrorKind::InvalidInput,
format!(
"invalid ipc parent entry name (contains '/'): parent={}, entry={}",
parent_dir.display(),
entry_name
),
)
.into());
}
let entry_c = CString::new(entry_name.as_bytes().to_vec()).map_err(|err| {
Error::new(
ErrorKind::InvalidInput,
format!(
"invalid ipc parent entry name: parent={}, entry={}, err={}",
parent_dir.display(),
entry_name,
err
),
)
})?;
let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() };
let stat_rc = unsafe {
hbb_common::libc::fstatat(
parent_fd,
entry_c.as_ptr(),
&mut stat,
hbb_common::libc::AT_SYMLINK_NOFOLLOW,
)
};
if stat_rc != 0 {
let err = std::io::Error::last_os_error();
if err.kind() == ErrorKind::NotFound {
return Ok(());
}
return Err(Error::new(
err.kind(),
format!(
"failed to stat preexisting ipc parent dir entry by fd: parent={}, entry={}, err={}",
parent_dir.display(),
entry_name,
err
),
)
.into());
}
let is_dir = (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t))
== hbb_common::libc::S_IFDIR;
let unlink_flags = if is_dir {
hbb_common::libc::AT_REMOVEDIR
} else {
0
};
let unlink_rc =
unsafe { hbb_common::libc::unlinkat(parent_fd, entry_c.as_ptr(), unlink_flags) };
if unlink_rc != 0 {
let err = std::io::Error::last_os_error();
if err.kind() == ErrorKind::NotFound {
return Ok(());
}
return Err(Error::new(
err.kind(),
format!(
"failed to remove preexisting ipc parent dir entry by fd: parent={}, entry={}, err={}",
parent_dir.display(),
entry_name,
err
),
)
.into());
}
Ok(())
}
fn scrub_preexisting_ipc_parent_entries(
parent_fd: i32,
parent_dir: &Path,
postfix: &str,
) -> ResultType<()> {
let ipc_basename = format!("ipc{}", postfix);
remove_parent_entry_via_fd(parent_fd, parent_dir, &ipc_basename)?;
remove_parent_entry_via_fd(parent_fd, parent_dir, &format!("{}.pid", ipc_basename))?;
Ok(())
}
fn remove_ipc_socket_via_secure_parent_fd(postfix: &str) -> ResultType<()> {
let path = config::Config::ipc_path(postfix);
let parent_dir = Path::new(&path)
.parent()
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?;
let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?;
let fd = match open_ipc_parent_dir_fd(&parent_c) {
Ok(fd) => fd,
Err(open_err) => {
if open_err.kind() == ErrorKind::NotFound {
return Ok(());
}
return Err(Error::new(
open_err.kind(),
format!(
"failed to open ipc parent dir for stale socket cleanup (no-follow): postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
open_err
),
)
.into());
}
};
let _fd_guard = FdGuard(fd);
remove_parent_entry_via_fd(fd, parent_dir, &format!("ipc{}", postfix))
}
// Purpose:
// - Harden the IPC parent directory before creating/listening socket files.
// - Prevent symlink/path-race abuse and reject unsafe owner/mode.
//
// Approach:
// - Open parent dir with O_NOFOLLOW/O_DIRECTORY and operate on that fd.
// - Validate inode type/owner/mode via fstat.
// - For protected service postfix, optionally adopt owner (root only), then scrub stale
// rustdesk IPC artifacts when directory trust boundary changed.
//
// Main steps:
// 1) Resolve parent path and open/create directory securely.
// 2) Verify directory inode type and owner uid.
// 3) Enforce expected mode via fchmod on opened fd.
// 4) Scrub stale IPC artifacts when owner/mode was unsafe before hardening.
//
// References:
// - open(2): O_NOFOLLOW/O_DIRECTORY/O_CLOEXEC
// https://man7.org/linux/man-pages/man2/open.2.html
// - fstat(2): verify file type/metadata on opened fd
// https://man7.org/linux/man-pages/man2/fstat.2.html
// - fchown(2): adopt ownership when running as root
// https://man7.org/linux/man-pages/man2/chown.2.html
// - fchmod(2): enforce exact mode on opened fd
// https://man7.org/linux/man-pages/man2/fchmod.2.html
pub(crate) fn ensure_secure_ipc_parent_dir(path: &str, postfix: &str) -> ResultType<bool> {
let parent_dir = Path::new(path)
.parent()
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?;
// Harden against common TOCTOU by opening the parent directory with O_NOFOLLOW (so the parent
// itself cannot be a symlink) and then operating on its FD (fstat/fchown/fchmod). This ensures
// we mutate the inode we opened, though it does not protect against symlinks in ancestor path
// components.
let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?;
let fd = match open_ipc_parent_dir_fd(&parent_c) {
Ok(fd) => fd,
Err(open_err) => {
// If the directory doesn't exist yet, create it with the expected mode. The parent
// dir is intended to be a single-level /tmp path, so mkdir is sufficient here.
if open_err.raw_os_error() == Some(hbb_common::libc::ENOENT) {
let expected_mode = expected_ipc_parent_mode(postfix);
let rc = unsafe {
hbb_common::libc::mkdir(
parent_c.as_ptr(),
expected_mode as hbb_common::libc::mode_t,
)
};
if rc != 0 {
let mkdir_err = std::io::Error::last_os_error();
// Handle a race where another process created the directory first.
if mkdir_err.raw_os_error() != Some(hbb_common::libc::EEXIST) {
return Err(Error::new(
mkdir_err.kind(),
format!(
"failed to mkdir ipc parent dir: postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
mkdir_err
),
)
.into());
}
}
match open_ipc_parent_dir_fd(&parent_c) {
Ok(fd) => fd,
Err(err) => {
return Err(Error::new(
err.kind(),
format!(
"failed to open ipc parent dir (no-follow): postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
err
),
)
.into());
}
}
} else {
return Err(Error::new(
open_err.kind(),
format!(
"failed to open ipc parent dir (no-follow): postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
open_err
),
)
.into());
}
}
};
let _fd_guard = FdGuard(fd);
let mut st: hbb_common::libc::stat = unsafe { std::mem::zeroed() };
if unsafe { hbb_common::libc::fstat(fd, &mut st as *mut _) } != 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!(
"failed to stat ipc parent dir: postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
os_err
),
)
.into());
}
let mode = st.st_mode as u32;
let is_dir = (mode & (hbb_common::libc::S_IFMT as u32)) == (hbb_common::libc::S_IFDIR as u32);
if !is_dir {
return Err(Error::new(
ErrorKind::PermissionDenied,
format!(
"ipc parent is not directory: postfix={}, parent={}",
postfix,
parent_dir.display()
),
)
.into());
}
let expected_uid = unsafe { hbb_common::libc::geteuid() as u32 };
let mut owner_uid = st.st_uid as u32;
let mut adopted_foreign_service_parent = false;
// Service-scoped IPC may be created by different privilege contexts historically.
// If running as root on protected service postfix, try adopting ownership first.
if owner_uid != expected_uid && expected_uid == 0 && config::is_service_ipc_postfix(postfix) {
let rc = unsafe {
hbb_common::libc::fchown(
fd,
expected_uid as hbb_common::libc::uid_t,
hbb_common::libc::gid_t::MAX,
)
};
if rc == 0 {
let mut st2: hbb_common::libc::stat = unsafe { std::mem::zeroed() };
if unsafe { hbb_common::libc::fstat(fd, &mut st2 as *mut _) } == 0 {
owner_uid = st2.st_uid as u32;
st = st2;
adopted_foreign_service_parent = true;
}
} else {
// Keep behavior unchanged; capture errno to ease diagnosing why chown failed.
let err = std::io::Error::last_os_error();
log::warn!(
"Failed to chown ipc parent dir, parent={}, postfix={}, expected_uid={}, rc={}, err={:?}",
parent_dir.display(),
postfix,
expected_uid,
rc,
err
);
}
}
if owner_uid != expected_uid {
return Err(Error::new(
ErrorKind::PermissionDenied,
format!(
"unsafe ipc parent owner, postfix={}, expected uid {expected_uid}, got {owner_uid}: {}",
postfix,
parent_dir.display()
),
)
.into());
}
let expected_mode = expected_ipc_parent_mode(postfix);
// Include special bits (setuid/setgid/sticky) to ensure the directory is hardened to the exact
// expected mode.
let current_mode = (st.st_mode as u32) & 0o7777;
let repaired_parent_mode = current_mode != expected_mode;
let had_untrusted_parent_mode = (current_mode & 0o022) != 0;
if repaired_parent_mode {
// Use fchmod on the opened fd to avoid path-race between check and chmod.
if unsafe { hbb_common::libc::fchmod(fd, expected_mode as hbb_common::libc::mode_t) } != 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!(
"failed to chmod ipc parent dir: postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
os_err
),
)
.into());
}
}
let should_scrub =
repaired_parent_mode || adopted_foreign_service_parent || had_untrusted_parent_mode;
Ok(should_scrub)
}
pub(crate) fn scrub_secure_ipc_parent_dir(path: &str, postfix: &str) -> ResultType<()> {
let parent_dir = Path::new(path)
.parent()
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?;
let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?;
let fd = open_ipc_parent_dir_fd(&parent_c).map_err(|err| {
Error::new(
err.kind(),
format!(
"failed to open ipc parent dir for scrub (no-follow): postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
err
),
)
})?;
let _fd_guard = FdGuard(fd);
scrub_preexisting_ipc_parent_entries(fd, parent_dir, postfix)
}
#[inline]
pub(crate) fn get_pid_file(postfix: &str) -> String {
let path = config::Config::ipc_path(postfix);
format!("{}.pid", path)
}
// Purpose:
// - Write current process pid to pid file without following attacker-controlled symlinks.
// - Ensure the pid file is a regular file owned by the opened inode path.
//
// Approach:
// - Use libc open/fstat/write syscalls (FFI) so flags and inode validation are explicit.
// - Open file with O_NOFOLLOW/O_CLOEXEC and verify S_IFREG with fstat before write.
// - Keep unsafe scopes minimal and check syscall return values immediately.
//
// Main steps:
// 1) Secure-open pid file (without truncation).
// 2) Validate opened inode is a regular file owned by current euid.
// 3) Enforce pid file mode to 0600 and truncate via ftruncate after validation.
// 4) Write process id bytes through fd.
//
// Why not plain std::fs::write?
// - std::fs helpers cannot enforce this exact open-time hardening sequence
// (especially "open with O_NOFOLLOW, then fstat the same opened inode").
//
// References:
// - open(2): O_NOFOLLOW/O_CLOEXEC/O_NONBLOCK
// https://man7.org/linux/man-pages/man2/open.2.html
// - fstat(2): verify file type on opened fd
// https://man7.org/linux/man-pages/man2/fstat.2.html
// - fchmod(2): enforce secure mode on reused pid file
// https://man7.org/linux/man-pages/man2/fchmod.2.html
// - ftruncate(2): truncate after validation
// https://man7.org/linux/man-pages/man2/ftruncate.2.html
// - write(2): write bytes via fd
// https://man7.org/linux/man-pages/man2/write.2.html
fn write_pid_file(path: &Path) -> ResultType<()> {
let path_c = CString::new(path.as_os_str().as_bytes().to_vec()).map_err(|err| {
Error::new(
ErrorKind::InvalidInput,
format!("invalid pid file path '{}': {}", path.display(), err),
)
})?;
let flags = hbb_common::libc::O_WRONLY
| hbb_common::libc::O_CREAT
| hbb_common::libc::O_CLOEXEC
| hbb_common::libc::O_NOFOLLOW
| hbb_common::libc::O_NONBLOCK;
let fd = unsafe { hbb_common::libc::open(path_c.as_ptr(), flags, 0o0600) };
if fd < 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!(
"failed to open pid file with no-follow '{}': {}",
path.display(),
os_err
),
)
.into());
}
let _fd_guard = FdGuard(fd);
let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() };
if unsafe { hbb_common::libc::fstat(fd, &mut stat) } != 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!("failed to stat pid file '{}': {}", path.display(), os_err),
)
.into());
}
if (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t))
!= (hbb_common::libc::S_IFREG as hbb_common::libc::mode_t)
{
return Err(Error::new(
ErrorKind::PermissionDenied,
format!("pid file path is not a regular file: '{}'", path.display()),
)
.into());
}
let expected_uid = unsafe { hbb_common::libc::geteuid() as u32 };
if stat.st_uid as u32 != expected_uid {
return Err(Error::new(
ErrorKind::PermissionDenied,
format!(
"pid file owner mismatch: expected uid {}, got {} for '{}'",
expected_uid,
stat.st_uid,
path.display()
),
)
.into());
}
if unsafe { hbb_common::libc::fchmod(fd, 0o600) } != 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!("failed to chmod pid file '{}': {}", path.display(), os_err),
)
.into());
}
if unsafe { hbb_common::libc::ftruncate(fd, 0) } != 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!(
"failed to truncate pid file '{}': {}",
path.display(),
os_err
),
)
.into());
}
let bytes = std::process::id().to_string();
let buf = bytes.as_bytes();
// `write(2)` is allowed to return a short write even for regular files.
// PID content is tiny and usually written in one shot, but we still loop
// until all bytes are persisted so this path is semantically correct.
let mut written = 0usize;
while written < buf.len() {
let rc = unsafe {
hbb_common::libc::write(
fd,
buf[written..].as_ptr() as *const hbb_common::libc::c_void,
buf.len() - written,
)
};
if rc < 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!("failed to write pid file '{}': {}", path.display(), os_err),
)
.into());
}
if rc == 0 {
return Err(Error::new(
ErrorKind::WriteZero,
format!(
"failed to write pid file '{}': write returned 0 bytes",
path.display()
),
)
.into());
}
written += rc as usize;
}
Ok(())
}
#[inline]
pub(crate) fn write_pid(postfix: &str) {
let path = std::path::PathBuf::from(get_pid_file(postfix));
if let Err(err) = write_pid_file(&path) {
log::warn!(
"Failed to write pid file for postfix '{}', path='{}', err={}",
postfix,
path.display(),
err
);
}
}
// Purpose:
// - Read pid file safely and avoid trusting symlink/non-regular files.
//
// Approach:
// - Use libc open/fstat/read syscalls (FFI) to control flags and inode checks.
// - Open path with O_NOFOLLOW, validate opened fd via fstat, then read and parse.
// - Keep unsafe scopes minimal and check syscall return values immediately.
//
// Main steps:
// 1) Secure-open pid file read-only.
// 2) Ensure fd points to regular file.
// 3) Read bytes and parse usize pid.
//
// References:
// - open(2): O_NOFOLLOW/O_CLOEXEC/O_NONBLOCK
// https://man7.org/linux/man-pages/man2/open.2.html
// - fstat(2): validate S_IFREG on opened fd
// https://man7.org/linux/man-pages/man2/fstat.2.html
// - read(2): read bytes via fd
// https://man7.org/linux/man-pages/man2/read.2.html
#[inline]
fn read_pid_file_secure(path: &Path) -> Option<usize> {
let path_c = CString::new(path.as_os_str().as_bytes().to_vec()).ok()?;
let flags = hbb_common::libc::O_RDONLY
| hbb_common::libc::O_CLOEXEC
| hbb_common::libc::O_NOFOLLOW
| hbb_common::libc::O_NONBLOCK;
let fd = unsafe { hbb_common::libc::open(path_c.as_ptr(), flags) };
if fd < 0 {
return None;
}
let _fd_guard = FdGuard(fd);
let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() };
if unsafe { hbb_common::libc::fstat(fd, &mut stat) } != 0 {
return None;
}
if (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t))
!= (hbb_common::libc::S_IFREG as hbb_common::libc::mode_t)
{
return None;
}
let mut buffer = [0u8; 64];
let read_len = unsafe {
hbb_common::libc::read(
fd,
buffer.as_mut_ptr() as *mut hbb_common::libc::c_void,
buffer.len(),
)
};
if read_len <= 0 {
return None;
}
let content = String::from_utf8_lossy(&buffer[..read_len as usize]).to_string();
content.trim().parse::<usize>().ok()
}
#[inline]
async fn probe_existing_listener(postfix: &str) -> bool {
let Ok(mut stream) = connect(1000, postfix).await else {
return false;
};
if postfix != crate::POSTFIX_SERVICE {
return true;
}
if stream.send(&Data::SyncConfig(None)).await.is_err() {
return false;
}
matches!(
stream.next_timeout(1000).await,
Ok(Some(Data::SyncConfig(Some(_))))
)
}
pub(crate) async fn check_pid(postfix: &str) -> bool {
let pid_file = std::path::PathBuf::from(get_pid_file(postfix));
if let Some(pid) = read_pid_file_secure(&pid_file) {
if pid > 0 {
let mut sys = hbb_common::sysinfo::System::new();
sys.refresh_processes();
if let Some(p) = sys.process(pid.into()) {
if let Some(current) = sys.process((std::process::id() as usize).into()) {
if current.name() == p.name() && probe_existing_listener(postfix).await {
return true;
}
}
}
}
}
if probe_existing_listener(postfix).await {
return true;
}
// if not remove old ipc file, the new ipc creation will fail
// if we remove a ipc file, but the old ipc process is still running,
// new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive
if let Err(err) = remove_ipc_socket_via_secure_parent_fd(postfix) {
log::debug!(
"Failed to remove stale ipc socket via secure parent fd: postfix={}, err={}",
postfix,
err
);
}
false
}
#[inline]
pub(crate) fn should_scrub_parent_entries_after_check_pid(
should_scrub_parent_entries: bool,
existing_listener_alive: bool,
) -> bool {
should_scrub_parent_entries && !existing_listener_alive
}
#[cfg(test)]
mod tests {
#[test]
fn test_write_pid_file_rejects_symlink() {
use std::os::unix::fs::symlink;
let unique = format!(
"rustdesk-ipc-pid-file-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let target = base.join("target_pid");
std::fs::write(&target, b"origin").unwrap();
let link = base.join("pid_link");
symlink(&target, &link).unwrap();
let res = super::write_pid_file(&link);
assert!(res.is_err());
assert_eq!(std::fs::read_to_string(&target).unwrap(), "origin");
std::fs::remove_file(&link).ok();
std::fs::remove_file(&target).ok();
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_ensure_secure_ipc_parent_dir_rejects_symlink_parent() {
use std::os::unix::fs::symlink;
let unique = format!(
"rustdesk-ipc-secure-dir-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
let real_dir = base.join("real");
let link_dir = base.join("link");
std::fs::create_dir_all(&real_dir).unwrap();
symlink(&real_dir, &link_dir).unwrap();
let ipc_path = link_dir.join("ipc_service");
let res =
super::ensure_secure_ipc_parent_dir(ipc_path.to_string_lossy().as_ref(), "_service");
assert!(res.is_err());
std::fs::remove_file(&link_dir).ok();
std::fs::remove_dir_all(&real_dir).ok();
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_ensure_secure_ipc_parent_dir_creates_parent_with_expected_mode() {
use std::os::unix::fs::PermissionsExt;
let unique = format!(
"rustdesk-ipc-secure-dir-create-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
// Intentionally choose a parent that does not exist to exercise the ENOENT -> mkdir branch.
let parent_dir = base.join("parent");
assert!(!parent_dir.exists());
let ipc_path = parent_dir.join("ipc");
let res = super::ensure_secure_ipc_parent_dir(ipc_path.to_string_lossy().as_ref(), "");
// Restrictive umask can make mkdir create a stricter initial mode. In that case
// ensure_secure_ipc_parent_dir repairs it with fchmod and may request a scrub.
res.unwrap();
let md = std::fs::metadata(&parent_dir).unwrap();
assert!(md.is_dir());
let mode = md.permissions().mode() & 0o777;
assert_eq!(mode, 0o0700);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_scrub_preexisting_ipc_parent_entries_only_removes_target_postfix_artifacts() {
use std::os::unix::ffi::OsStrExt;
let unique = format!(
"rustdesk-ipc-scrub-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let ipc_file = base.join("ipc_service");
let ipc_pid_file = base.join("ipc_service.pid");
let ipc_other_postfix_file = base.join("ipc_uinput_1");
let keep_file = base.join("keep.txt");
let keep_dir = base.join("keep_dir");
std::fs::write(&ipc_file, b"socket-placeholder").unwrap();
std::fs::write(&ipc_pid_file, b"1234").unwrap();
std::fs::write(&ipc_other_postfix_file, b"other-postfix").unwrap();
std::fs::write(&keep_file, b"keep").unwrap();
std::fs::create_dir_all(&keep_dir).unwrap();
let base_c = std::ffi::CString::new(base.as_os_str().as_bytes().to_vec()).unwrap();
let base_fd = super::open_ipc_parent_dir_fd(&base_c).unwrap();
let _base_guard = super::FdGuard(base_fd);
super::scrub_preexisting_ipc_parent_entries(base_fd, &base, "_service").unwrap();
assert!(!ipc_file.exists());
assert!(!ipc_pid_file.exists());
assert!(ipc_other_postfix_file.exists());
assert!(keep_file.exists());
assert!(keep_dir.exists());
std::fs::remove_file(&ipc_other_postfix_file).ok();
std::fs::remove_file(&keep_file).ok();
std::fs::remove_dir_all(&keep_dir).ok();
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_scrub_preexisting_ipc_parent_entries_should_bind_to_opened_inode_not_path() {
use std::os::unix::ffi::OsStrExt;
let unique = format!(
"rustdesk-ipc-scrub-fd-bind-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let trusted_parent = base.join("trusted_parent");
let trusted_parent_moved = base.join("trusted_parent_moved");
let attacker_parent = base.join("attacker_parent");
std::fs::create_dir_all(&trusted_parent).unwrap();
std::fs::create_dir_all(&attacker_parent).unwrap();
let trusted_ipc_file = trusted_parent.join("ipc_service");
let attacker_ipc_file = attacker_parent.join("ipc_service");
std::fs::write(&trusted_ipc_file, b"trusted").unwrap();
std::fs::write(&attacker_ipc_file, b"attacker").unwrap();
let trusted_parent_c =
std::ffi::CString::new(trusted_parent.as_os_str().as_bytes().to_vec()).unwrap();
let trusted_parent_fd = super::open_ipc_parent_dir_fd(&trusted_parent_c).unwrap();
let _trusted_parent_guard = super::FdGuard(trusted_parent_fd);
// Swap the path after the trusted inode has been opened.
std::fs::rename(&trusted_parent, &trusted_parent_moved).unwrap();
std::fs::rename(&attacker_parent, &trusted_parent).unwrap();
super::scrub_preexisting_ipc_parent_entries(trusted_parent_fd, &trusted_parent, "_service")
.unwrap();
// Expected secure behavior: scrub should target the inode that was opened before path swap.
assert!(
!trusted_parent_moved.join("ipc_service").exists(),
"trusted inode artifact should be removed even after path swap"
);
assert!(
trusted_parent.join("ipc_service").exists(),
"path-swapped attacker directory should not be scrubbed"
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_ensure_secure_ipc_parent_dir_keeps_service_artifacts_before_liveness_probe() {
use std::os::unix::fs::PermissionsExt;
let unique = format!(
"rustdesk-ipc-secure-dir-order-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let parent_dir = base.join("service_parent");
std::fs::create_dir_all(&parent_dir).unwrap();
// Trigger "had_untrusted_service_parent_mode".
std::fs::set_permissions(&parent_dir, std::fs::Permissions::from_mode(0o777)).unwrap();
let ipc_file = parent_dir.join("ipc_service");
let ipc_pid_file = parent_dir.join("ipc_service.pid");
std::fs::write(&ipc_file, b"socket-placeholder").unwrap();
std::fs::write(&ipc_pid_file, b"1234").unwrap();
let res =
super::ensure_secure_ipc_parent_dir(ipc_file.to_string_lossy().as_ref(), "_service");
assert_eq!(res.unwrap(), true);
// Parent hardening should run first; artifacts should stay until liveness probe completes.
assert!(ipc_file.exists(), "ipc socket marker should be preserved");
assert!(ipc_pid_file.exists(), "pid marker should be preserved");
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_ensure_secure_ipc_parent_dir_marks_non_service_mode_repair_for_scrub() {
use std::os::unix::fs::PermissionsExt;
let unique = format!(
"rustdesk-ipc-nonservice-mode-repair-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let parent_dir = base.join("non_service_parent");
std::fs::create_dir_all(&parent_dir).unwrap();
std::fs::set_permissions(&parent_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
let ipc_file = parent_dir.join("ipc");
std::fs::write(&ipc_file, b"socket-placeholder").unwrap();
let res = super::ensure_secure_ipc_parent_dir(ipc_file.to_string_lossy().as_ref(), "");
assert_eq!(res.unwrap(), true);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_should_scrub_parent_entries_after_check_pid_only_when_requested_and_not_alive() {
assert!(!super::should_scrub_parent_entries_after_check_pid(
false, false
));
assert!(!super::should_scrub_parent_entries_after_check_pid(
false, true
));
assert!(super::should_scrub_parent_entries_after_check_pid(
true, false
));
assert!(!super::should_scrub_parent_entries_after_check_pid(
true, true
));
}
}

View File

@@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Closed manually by the peer", "Cerrado manualmente por el par"),
("Enable remote configuration modification", "Habilitar modificación remota de configuración"),
("Run without install", "Ejecutar sin instalar"),
("Connect via relay", "Conectar a través de relay"),
("Connect via relay", ""),
("Always connect via relay", "Conéctese siempre a través de relay"),
("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"),
("Login", "Iniciar sesión"),
@@ -228,7 +228,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Username missed", "Olvidó su nombre de usuario"),
("Password missed", "Olvidó su contraseña"),
("Wrong credentials", "Credenciales incorrectas"),
("The verification code is incorrect or has expired", "El código de verificación es incorrecto o ha caducado"),
("The verification code is incorrect or has expired", ""),
("Edit Tag", "Editar tag"),
("Forget Password", "Olvidar contraseña"),
("Favorites", "Favoritos"),
@@ -302,8 +302,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"),
("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"),
("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"),
("Start on boot", "Iniciar al arrancar"),
("Start the screen sharing service on boot, requires special permissions", "Iniciar el servicio de pantalla compartida al arrancar, requiere permisos especiales"),
("Start on boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Conexión no disponible"),
("Legacy mode", "Modo heredado"),
("Map mode", "Modo mapa"),
@@ -326,8 +326,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Ratio", "Relación"),
("Image Quality", "Calidad de imagen"),
("Scroll Style", "Estilo de desplazamiento"),
("Show Toolbar", "Mostrar herramientas"),
("Hide Toolbar", "Ocultar herramientas"),
("Show Toolbar", ""),
("Hide Toolbar", ""),
("Direct Connection", "Conexión directa"),
("Relay Connection", "Conexión Relay"),
("Secure Connection", "Conexión segura"),
@@ -338,7 +338,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Security", "Seguridad"),
("Theme", "Tema"),
("Dark Theme", "Tema Oscuro"),
("Light Theme", "Tema claro"),
("Light Theme", ""),
("Dark", "Oscuro"),
("Light", "Claro"),
("Follow System", "Tema del sistema"),
@@ -355,12 +355,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Audio Input Device", "Dispositivo de entrada de audio"),
("Use IP Whitelisting", "Usar lista de IPs admitidas"),
("Network", "Red"),
("Pin Toolbar", "Anclar herramientas"),
("Unpin Toolbar", "Desanclar herramientas"),
("Pin Toolbar", ""),
("Unpin Toolbar", ""),
("Recording", "Grabando"),
("Directory", "Directorio"),
("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"),
("Automatically record outgoing sessions", "Grabación automática de sesiones salientes"),
("Automatically record outgoing sessions", ""),
("Change", "Cambiar"),
("Start session recording", "Comenzar grabación de sesión"),
("Stop session recording", "Detener grabación de sesión"),
@@ -368,7 +368,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable LAN discovery", "Habilitar descubrimiento de LAN"),
("Deny LAN discovery", "Denegar descubrimiento de LAN"),
("Write a message", "Escribir un mensaje"),
("Prompt", "Solicitud"),
("Prompt", ""),
("Please wait for confirmation of UAC...", "Por favor, espera confirmación de UAC"),
("elevated_foreground_window_tip", "La ventana actual del escritorio remoto necesita privilegios elevados para funcionar, así que no puedes usar ratón y teclado temporalmente. Puedes solicitar al usuario remoto que minimize la ventana actual o hacer clic en el botón de elevación de la ventana de gestión de conexión. Para evitar este problema, se recomienda instalar el programa en el dispositivo remto."),
("Disconnected", "Desconectado"),
@@ -616,9 +616,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("During service is on", "Mientras el servicio está activo"),
("Capture screen using DirectX", "Capturar pantalla con DirectX"),
("Back", "Atrás"),
("Apps", "Aplicaciones"),
("Volume up", "Subir volumen"),
("Volume down", "Bajar volumen"),
("Apps", ""),
("Volume up", "Bajar volumen"),
("Volume down", "Subir volumen"),
("Power", "Encendido"),
("Telegram bot", "Bot de Telegram"),
("enable-bot-tip", "Si activas esta característica puedes recibir código 2FA de tu bot. También puede funcionar como notificación de conexión."),
@@ -651,7 +651,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Actualizar portapapeles del cliente"),
("Untagged", "Sin itiquetar"),
("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"),
("Accessible devices", "Dispositivos accesibles"),
("Accessible devices", ""),
("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"),
("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."),
("Use D3D rendering", "Usar renderizado D3D"),
@@ -689,9 +689,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Use WebSocket", "Usar WebSocket"),
("Trackpad speed", "Velocidad de trackpad"),
("Default trackpad speed", "Velocidad predeterminada de trackpad"),
("Numeric one-time password", "Contraseña numérica de un solo uso"),
("Enable IPv6 P2P connection", "Habilitar conexión IPv6 P2P"),
("Enable UDP hole punching", "Habilitar perforación de agujero UDP"),
("Numeric one-time password", ""),
("Enable IPv6 P2P connection", ""),
("Enable UDP hole punching", ""),
("View camera", "Ver cámara"),
("Enable camera", "Habilitar cámara"),
("No cameras", "No hay cámaras"),
@@ -708,8 +708,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."),
("Supported only in the installed version.", "Soportado solo en la versión instalada."),
("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"),
("Preparing for installation ...", "Preparando instlación..."),
("Show my cursor", "Mostrar mi cursor"),
("Preparing for installation ...", ""),
("Show my cursor", ""),
("Scale custom", "Escala personalizada"),
("Custom scale slider", "Control deslizante de escala personalizada"),
("Decrease", "Disminuir"),
@@ -721,28 +721,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Mostrar joystick virtual"),
("Edit note", "Editar nota"),
("Alias", ""),
("ScrollEdge", "Desplazamiento de pantalla"),
("Allow insecure TLS fallback", "Permitir conexión TLS insegura de respaldo"),
("allow-insecure-tls-fallback-tip", "De forma predeterminada, RustDesk verifica el certificado de servidor para protocolos que usen TLS.\nCon esta opción habilitada, Rustdesk volverá al paso de omisión de verificación y procederá en caso de fallo de verificación."),
("Disable UDP", "Inhabilitar UDP"),
("disable-udp-tip", "Controla si se usa TCP solamente.\nCuando esta opción está activa, RustDesk no usará más el puerto UDP 21116, en su lugar se usará el TCP 21116."),
("server-oss-not-support-tip", "NOTA: El servidor RustDesk OSS no incluye esta característica."),
("input note here", "Introducir nota aquí"),
("note-at-conn-end-tip", "Pedir nota al finalizar la conexión"),
("Show terminal extra keys", "Mostrar teclas extra del terminal"),
("Relative mouse mode", "Modo de ratón relativo"),
("rel-mouse-not-supported-peer-tip", "El modo relativo de ratón no está soportado por el par."),
("rel-mouse-not-ready-tip", "El modo relativo de ratón aún no está preparado. Por favor, inténtalo de nuevo."),
("rel-mouse-lock-failed-tip", "Ha fallado el bloqueo del cursor. El modo relativo del ratón ha sido inhabilitado."),
("rel-mouse-exit-{}-tip", "Pulsa {} para salir."),
("rel-mouse-permission-lost-tip", "Permiso de teclado revocado. El modo relativo del ratón ha sido inhabilitado."),
("Changelog", "Registro de cambios"),
("keep-awake-during-outgoing-sessions-label", "Mantener la pantalla activa durante sesiones salientes"),
("keep-awake-during-incoming-sessions-label", "Mantener la pantalla activa durante sesiones entrantes"),
("ScrollEdge", ""),
("Allow insecure TLS fallback", ""),
("allow-insecure-tls-fallback-tip", ""),
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Continuar con {}"),
("Display Name", "Nombre de pantalla"),
("password-hidden-tip", "La contraseña permanente está ajustada a (oculta)."),
("preset-password-in-use-tip", "Se está usando la contraseña predeterminada."),
("Enable privacy mode", "Habilitar modo privado"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
].iter().cloned().collect();
}

View File

@@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Kijelző név"),
("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."),
("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."),
("Enable privacy mode", "Adatvédelmi mód aktiválása"),
("Enable privacy mode", ""),
].iter().cloned().collect();
}

View File

@@ -739,7 +739,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", "更新履歴"),
("keep-awake-during-outgoing-sessions-label", "送信セッション中は、画面のスリープを無効化する"),
("keep-awake-during-incoming-sessions-label", "受信セッション中は、画面のスリープを無効化する"),
("Continue with {}", "{} で続行する"),
("Continue with {}", "{}で続行する"),
("Display Name", "表示名"),
("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"),
("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"),

View File

@@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "표시 이름"),
("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."),
("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."),
("Enable privacy mode", "개인정보 보호 모드 사용함"),
("Enable privacy mode", ""),
].iter().cloned().collect();
}

View File

@@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Naam Weergeven"),
("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."),
("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."),
("Enable privacy mode", "Privacymodus inschakelen"),
("Enable privacy mode", ""),
].iter().cloned().collect();
}

View File

@@ -38,68 +38,49 @@ fn main() {
if !common::global_init() {
return;
}
use clap::{Arg, ArgAction, Command};
use clap::App;
use hbb_common::log;
let matches = Command::new("rustdesk")
let args = format!(
"-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]'
-c, --connect=[REMOTE_ID] 'test only'
-k, --key=[KEY] ''
-s, --server=[] 'Start server'",
);
let matches = App::new("rustdesk")
.version(crate::VERSION)
.author("Purslane Ltd<info@rustdesk.com>")
.about("RustDesk command line tool")
.arg(
Arg::new("port-forward")
.short('p')
.long("port-forward")
.value_name("PORT-FORWARD-OPTIONS")
.help("Format: remote-id:local-port:remote-port[:remote-host]"),
)
.arg(
Arg::new("connect")
.short('c')
.long("connect")
.value_name("REMOTE_ID")
.help("test only"),
)
.arg(Arg::new("key").short('k').long("key").value_name("KEY"))
.arg(
Arg::new("server")
.short('s')
.long("server")
.action(ArgAction::SetTrue)
.help("Start server"),
)
.args_from_usage(&args)
.get_matches();
use hbb_common::{config::LocalConfig, env_logger::*};
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
if let Some(p) = matches.get_one::<String>("port-forward") {
let options: Vec<String> = p.split(':').map(|x| x.to_owned()).collect();
if let Some(p) = matches.value_of("port-forward") {
let options: Vec<String> = p.split(":").map(|x| x.to_owned()).collect();
if options.len() < 3 {
log::error!("Wrong port-forward options");
return;
}
let port = match options[1].parse::<i32>() {
Ok(v) => v,
Err(_) => {
log::error!("Wrong local-port");
return;
}
};
let remote_port = match options[2].parse::<i32>() {
Ok(v) => v,
Err(_) => {
log::error!("Wrong remote-port");
return;
}
};
let mut port = 0;
if let Ok(v) = options[1].parse::<i32>() {
port = v;
} else {
log::error!("Wrong local-port");
return;
}
let mut remote_port = 0;
if let Ok(v) = options[2].parse::<i32>() {
remote_port = v;
} else {
log::error!("Wrong remote-port");
return;
}
let mut remote_host = "localhost".to_owned();
if options.len() > 3 {
remote_host = options[3].clone();
}
common::test_rendezvous_server();
common::test_nat_type();
let key = matches
.get_one::<String>("key")
.map(String::as_str)
.unwrap_or("")
.to_owned();
let key = matches.value_of("key").unwrap_or("").to_owned();
let token = LocalConfig::get_option("access_token");
cli::start_one_port_forward(
options[0].clone(),
@@ -109,17 +90,13 @@ fn main() {
key,
token,
);
} else if let Some(p) = matches.get_one::<String>("connect") {
} else if let Some(p) = matches.value_of("connect") {
common::test_rendezvous_server();
common::test_nat_type();
let key = matches
.get_one::<String>("key")
.map(String::as_str)
.unwrap_or("")
.to_owned();
let key = matches.value_of("key").unwrap_or("").to_owned();
let token = LocalConfig::get_option("access_token");
cli::connect_test(p, key, token);
} else if matches.get_flag("server") {
} else if let Some(p) = matches.value_of("server") {
log::info!("id={}", hbb_common::config::Config::get_id());
crate::start_server(true, false);
}

View File

@@ -29,12 +29,6 @@ use wallpaper;
pub const PA_SAMPLE_RATE: u32 = 48000;
static mut UNMODIFIED: bool = true;
#[derive(Clone, Debug)]
struct ActiveUserLookupCache {
uid: String,
username: String,
}
const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"];
const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"];
@@ -56,8 +50,6 @@ lazy_static::lazy_static! {
}
}
};
static ref ACTIVE_USER_LOOKUP_CACHE: std::sync::Mutex<Option<ActiveUserLookupCache>> =
std::sync::Mutex::new(None);
// https://github.com/rustdesk/rustdesk/issues/13705
// Check if `sudo -E` actually preserves environment.
//
@@ -90,27 +82,6 @@ lazy_static::lazy_static! {
};
}
#[inline]
fn update_active_user_lookup_cache(desktop: &Desktop) {
if let Ok(mut cache) = ACTIVE_USER_LOOKUP_CACHE.lock() {
if desktop.uid.is_empty() || desktop.username.is_empty() {
*cache = None;
} else {
*cache = Some(ActiveUserLookupCache {
uid: desktop.uid.clone(),
username: desktop.username.clone(),
});
}
}
}
#[inline]
fn get_active_user_id_name_from_cache() -> Option<(String, String)> {
let cache = ACTIVE_USER_LOOKUP_CACHE.lock().ok()?;
let entry = cache.as_ref()?;
Some((entry.uid.clone(), entry.username.clone()))
}
thread_local! {
// XDO context - created via libxdo-sys (which uses dynamic loading stub).
// If libxdo is not available, xdo will be null and xdo-based functions become no-ops.
@@ -818,7 +789,6 @@ pub fn start_os_service() {
let mut last_restart = Instant::now();
while running.load(Ordering::SeqCst) {
desktop.refresh();
update_active_user_lookup_cache(&desktop);
// Duplicate logic here with should_start_server
// Login wayland will try to start a headless --server.
@@ -891,29 +861,13 @@ pub fn start_os_service() {
}
#[inline]
/// Returns the cached active `(uid, username)` snapshot when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_user_id_name() -> (String, String) {
if let Some(id_name) = get_active_user_id_name_from_cache() {
return id_name;
}
let vec_id_name = get_values_of_seat0(&[1, 2]);
(vec_id_name[0].clone(), vec_id_name[1].clone())
}
#[inline]
/// Returns the cached active uid when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_userid() -> String {
if let Some((uid, _)) = get_active_user_id_name_from_cache() {
return uid;
}
get_values_of_seat0(&[1])[0].clone()
}
#[inline]
/// Returns the active uid from a fresh seat0 lookup, bypassing the service-loop cache.
pub fn get_active_userid_fresh() -> String {
get_values_of_seat0(&[1])[0].clone()
}
@@ -968,12 +922,7 @@ fn _get_display_manager() -> String {
}
#[inline]
/// Returns the cached active username when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_username() -> String {
if let Some((_, username)) = get_active_user_id_name_from_cache() {
return username;
}
get_values_of_seat0(&[2])[0].clone()
}

View File

@@ -2,7 +2,7 @@ use super::{linux::*, ResultType};
use crate::client::{
LOGIN_MSG_DESKTOP_NO_DESKTOP, LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER,
LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LOGIN_MSG_DESKTOP_XORG_NOT_FOUND,
LOGIN_MSG_DESKTOP_XSESSION_FAILED, LOGIN_MSG_PASSWORD_WRONG,
LOGIN_MSG_DESKTOP_XSESSION_FAILED,
};
use hbb_common::{
allow_err, bail, log,
@@ -94,49 +94,6 @@ fn detect_headless() -> Option<&'static str> {
None
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum XSessionStartErrorKind {
Auth,
Env,
}
const XSESSION_AUTH_FAILURE_DETAIL: &str = "authentication failed";
#[derive(Debug)]
struct XSessionStartError {
kind: XSessionStartErrorKind,
detail: String,
}
impl XSessionStartError {
fn auth(detail: String) -> Self {
Self {
kind: XSessionStartErrorKind::Auth,
detail,
}
}
fn env(detail: String) -> Self {
Self {
kind: XSessionStartErrorKind::Env,
detail,
}
}
}
impl std::fmt::Display for XSessionStartError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.detail)
}
}
fn map_xsession_start_error_to_login_msg(kind: XSessionStartErrorKind) -> &'static str {
match kind {
XSessionStartErrorKind::Auth => LOGIN_MSG_PASSWORD_WRONG,
XSessionStartErrorKind::Env => LOGIN_MSG_DESKTOP_XSESSION_FAILED,
}
}
pub fn try_start_desktop(_username: &str, _passsword: &str) -> String {
debug_assert!(crate::is_server());
if _username.is_empty() {
@@ -179,21 +136,14 @@ pub fn try_start_desktop(_username: &str, _passsword: &str) -> String {
}
}
Err(e) => {
match e.kind {
XSessionStartErrorKind::Auth => {
log::warn!("Failed to authenticate xsession user {}", e);
}
XSessionStartErrorKind::Env => {
log::error!("Failed to start xsession {}", e);
}
}
map_xsession_start_error_to_login_msg(e.kind).to_owned()
log::error!("Failed to start xsession {}", e);
LOGIN_MSG_DESKTOP_XSESSION_FAILED.to_owned()
}
}
}
}
fn try_start_x_session(username: &str, password: &str) -> Result<(String, bool), XSessionStartError> {
fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bool)> {
let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap();
if let Some(desktop_manager) = &mut (*desktop_manager) {
if let Some(seat0_username) = desktop_manager.get_supported_display_seat0_username() {
@@ -211,9 +161,7 @@ fn try_start_x_session(username: &str, password: &str) -> Result<(String, bool),
desktop_manager.is_running(),
))
} else {
Err(XSessionStartError::env(
crate::client::LOGIN_MSG_DESKTOP_NOT_INITED.to_owned(),
))
bail!(crate::client::LOGIN_MSG_DESKTOP_NOT_INITED);
}
}
@@ -299,15 +247,10 @@ impl DesktopManager {
self.is_child_running.load(Ordering::SeqCst)
}
fn try_start_x_session(
&mut self,
username: &str,
password: &str,
) -> Result<(), XSessionStartError> {
fn try_start_x_session(&mut self, username: &str, password: &str) -> ResultType<()> {
match get_user_by_name(username) {
Some(userinfo) => {
let mut client = pam::Client::with_password(&pam_get_service_name())
.map_err(|e| XSessionStartError::env(format!("failed to init pam client, {}", e)))?;
let mut client = pam::Client::with_password(&pam_get_service_name())?;
client
.conversation_mut()
.set_credentials(username, password);
@@ -324,24 +267,17 @@ impl DesktopManager {
Ok(())
}
Err(e) => {
Err(XSessionStartError::env(format!(
"failed to start x session, {}",
e
)))
bail!("failed to start x session, {}", e);
}
}
}
Err(_e) => {
Err(XSessionStartError::auth(
XSESSION_AUTH_FAILURE_DETAIL.to_owned(),
))
Err(e) => {
bail!("failed to check user pass for {}, {}", username, e);
}
}
}
None => {
Err(XSessionStartError::auth(
XSESSION_AUTH_FAILURE_DETAIL.to_owned(),
))
bail!("failed to get userinfo of {}", username);
}
}
}

View File

@@ -73,19 +73,10 @@ use winapi::{
};
use windows::Win32::{
Foundation::{CloseHandle as WinCloseHandle, HANDLE as WinHANDLE},
Security::{
GetTokenInformation as WinGetTokenInformation, IsWellKnownSid, TokenUser,
WinLocalSystemSid, TOKEN_QUERY as WIN_TOKEN_QUERY, TOKEN_USER,
},
System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
},
System::Threading::{
OpenProcess as WinOpenProcess, OpenProcessToken as WinOpenProcessToken,
QueryFullProcessImageNameW as WinQueryFullProcessImageNameW,
PROCESS_QUERY_LIMITED_INFORMATION as WIN_PROCESS_QUERY_LIMITED_INFORMATION,
},
};
use windows_service::{
define_windows_service,
@@ -97,14 +88,6 @@ use windows_service::{
};
use winreg::{enums::*, RegKey};
mod acl;
pub(crate) use acl::current_process_user_sid_string;
pub use acl::{
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
set_path_permission_for_portable_service_shmem_file,
validate_path_for_portable_service_shmem_dir,
};
pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window
pub const EXPLORER_EXE: &'static str = "explorer.exe";
pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW";
@@ -582,55 +565,6 @@ pub fn get_current_session_id(share_rdp: bool) -> DWORD {
unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) }
}
#[inline]
fn resolve_expected_active_session_id_for_service(session_id: u32) -> Option<u32> {
let share_rdp_enabled = is_share_rdp();
if get_available_sessions(false)
.iter()
.any(|e| e.sid == session_id)
{
return Some(session_id);
}
let current_active_session =
unsafe { get_current_session(if share_rdp_enabled { TRUE } else { FALSE }) };
if current_active_session == u32::MAX {
None
} else {
Some(current_active_session)
}
}
#[inline]
fn authorize_service_scoped_ipc_connection(
stream: &ipc::Connection,
expected_active_session_id: Option<u32>,
) -> bool {
let (authorized, peer_pid, peer_session_id, peer_is_system) =
stream.service_authorization_status_for_session(expected_active_session_id);
if !authorized {
ipc::log_rejected_windows_ipc_connection(
crate::POSTFIX_SERVICE,
peer_pid,
peer_session_id,
expected_active_session_id,
peer_is_system,
);
return false;
}
if let Err(err) =
ipc::ensure_peer_executable_matches_current_by_pid_opt(peer_pid, crate::POSTFIX_SERVICE)
{
log::warn!(
"Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}",
crate::POSTFIX_SERVICE,
peer_pid,
err
);
return false;
}
true
}
extern "system" {
fn BlockInput(v: BOOL) -> BOOL;
}
@@ -697,15 +631,6 @@ async fn run_service(_arguments: Vec<OsString>) -> ResultType<()> {
Ok(res) => match res {
Some(Ok(stream)) => {
let mut stream = ipc::Connection::new(stream);
// Keep IPC authorization consistent with the session we are currently serving.
// Recompute expected session right before authorization to avoid using a stale
// session_id after awaiting incoming.next().
let expected_active_session_id =
resolve_expected_active_session_id_for_service(session_id);
if !authorize_service_scoped_ipc_connection(&stream, expected_active_session_id)
{
continue;
}
if let Ok(Some(data)) = stream.next_timeout(1000).await {
match data {
ipc::Data::Close => {
@@ -1216,22 +1141,6 @@ pub fn get_active_user_home() -> Option<PathBuf> {
None
}
#[cfg(not(feature = "flutter"))]
#[inline]
pub fn portable_service_logon_helper_paths() -> Option<(PathBuf, PathBuf)> {
// Keep parity with history for now: derive LocalAppData from user profile path.
// If users report redirected/non-standard LocalAppData issues, switch to:
// `BaseDirs::new()?.data_local_dir()` for Known Folder-based resolution.
let user_dir = hbb_common::directories_next::UserDirs::new()?;
let dir = user_dir
.home_dir()
.join("AppData")
.join("Local")
.join("rustdesk-sciter");
let dst = dir.join("rustdesk.exe");
Some((dir, dst))
}
pub fn is_prelogin() -> bool {
let Some(username) = get_current_session_username() else {
return false;
@@ -2418,33 +2327,16 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
is_run_as_system,
crate::username(),
);
let mut arg_elevate = if is_setup {
let arg_elevate = if is_setup {
"--noinstall --elevate"
} else {
"--elevate"
}
.to_owned();
let mut arg_run_as_system = if is_setup {
};
let arg_run_as_system = if is_setup {
"--noinstall --run-as-system"
} else {
"--run-as-system"
}
.to_owned();
let shmem_name_from_args = crate::portable_service::portable_service_shmem_name_from_args();
if shmem_name_from_args.is_none() && crate::portable_service::has_portable_service_shmem_arg() {
log::error!("Invalid portable service shared memory argument, aborting elevation flow");
// This is a malformed bootstrap argument in a privilege-sensitive path.
// Keep fail-closed process termination here to avoid continuing elevation
// with inconsistent shared-memory contract.
std::process::exit(1);
}
if let Some(shmem_name) = shmem_name_from_args {
let shmem_arg = crate::portable_service::portable_service_shmem_arg(&shmem_name);
arg_elevate.push(' ');
arg_elevate.push_str(&shmem_arg);
arg_run_as_system.push(' ');
arg_run_as_system.push_str(&shmem_arg);
}
};
if is_root() {
if is_run_as_system {
log::info!("run portable service");
@@ -2455,7 +2347,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
Ok(elevated) => {
if elevated {
if !is_run_as_system {
if run_as_system(arg_run_as_system.as_str()).is_ok() {
if run_as_system(arg_run_as_system).is_ok() {
std::process::exit(0);
} else {
log::error!(
@@ -2466,7 +2358,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
}
} else {
if !is_elevate {
if let Ok(true) = elevate(arg_elevate.as_str()) {
if let Ok(true) = elevate(arg_elevate) {
std::process::exit(0);
} else {
log::error!("Failed to elevate, error {}", io::Error::last_os_error());
@@ -2524,115 +2416,6 @@ pub fn is_elevated(process_id: Option<DWORD>) -> ResultType<bool> {
}
}
#[inline]
unsafe fn read_token_user_buffer(token: WinHANDLE, subject: &str) -> ResultType<Vec<u8>> {
let mut token_user_size = 0u32;
let get_info_result = WinGetTokenInformation(token, TokenUser, None, 0, &mut token_user_size);
match get_info_result {
Ok(()) => {
if token_user_size == 0 {
bail!(
"Failed to get {} token user size: unexpected zero buffer size",
subject
);
}
}
Err(e) => {
// Allow expected size-probe failures if Windows still returns required size.
let is_insufficient_buffer =
e.code() == windows::core::HRESULT::from_win32(ERROR_INSUFFICIENT_BUFFER as u32);
let is_bad_length =
e.code() == windows::core::HRESULT::from_win32(ERROR_BAD_LENGTH as u32);
if (!is_insufficient_buffer && !is_bad_length) || token_user_size == 0 {
bail!("Failed to get {} token user size: {}", subject, e);
}
}
}
let mut buffer = vec![0u8; token_user_size as usize];
WinGetTokenInformation(
token,
TokenUser,
Some(buffer.as_mut_ptr() as *mut core::ffi::c_void),
token_user_size,
&mut token_user_size,
)
.map_err(|e| anyhow!("Failed to get {} token user: {}", subject, e))?;
let min_size = std::mem::size_of::<TOKEN_USER>();
if buffer.len() < min_size {
bail!(
"Failed to parse {} token user: buffer too small (got {}, need >= {})",
subject,
buffer.len(),
min_size
);
}
Ok(buffer)
}
/// Similar to `is_root()` / `is_local_system()` but for an arbitrary process.
///
/// Returns `true` if the target process is running as LocalSystem (SID: S-1-5-18).
///
/// TODO: After a few releases of real-world validation, consider replacing
/// the legacy `is_local_system()` with this implementation.
pub fn is_process_running_as_system(process_id: DWORD) -> ResultType<bool> {
unsafe {
let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)
.map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?;
let mut token = WinHANDLE::default();
let result = (|| -> ResultType<bool> {
WinOpenProcessToken(process, WIN_TOKEN_QUERY, &mut token)
.map_err(|e| anyhow!("Failed to open process {} token: {}", process_id, e))?;
let token_subject = format!("process {}", process_id);
let buffer = read_token_user_buffer(token, token_subject.as_str())?;
let token_user: TOKEN_USER =
std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER);
Ok(IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool())
})();
if !token.is_invalid() {
let _ = WinCloseHandle(token);
}
let _ = WinCloseHandle(process);
result
}
}
pub fn get_process_executable_path(process_id: DWORD) -> ResultType<PathBuf> {
const PROCESS_IMAGE_PATH_BUFFER_LEN: usize = 32 * 1024;
unsafe {
let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)
.map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?;
let result = (|| -> ResultType<PathBuf> {
let mut buffer = vec![0u16; PROCESS_IMAGE_PATH_BUFFER_LEN];
let mut length = PROCESS_IMAGE_PATH_BUFFER_LEN as u32;
WinQueryFullProcessImageNameW(
process,
windows::Win32::System::Threading::PROCESS_NAME_FORMAT(0),
windows::core::PWSTR(buffer.as_mut_ptr()),
&mut length,
)
.map_err(|e| anyhow!("Failed to query process {} image path: {}", process_id, e))?;
if length == 0 {
bail!(
"Failed to query process {} image path: empty result",
process_id
);
}
buffer.truncate(length as usize);
Ok(PathBuf::from(OsString::from_wide(&buffer)))
})();
let _ = WinCloseHandle(process);
result
}
}
pub fn is_foreground_window_elevated() -> ResultType<bool> {
unsafe {
let mut process_id: DWORD = 0;
@@ -2925,6 +2708,16 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) ->
return Ok(());
}
pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> {
std::process::Command::new("icacls")
.arg(dir.as_os_str())
.arg("/grant")
.arg(format!("*S-1-1-0:(OI)(CI){}", permission))
.arg("/T")
.spawn()?;
Ok(())
}
#[inline]
fn str_to_device_name(name: &str) -> [u16; 32] {
let mut device_name: Vec<u16> = wide_string(name);
@@ -4488,87 +4281,6 @@ pub(super) fn get_pids_with_first_arg_by_wmic<S1: AsRef<str>, S2: AsRef<str>>(
#[cfg(test)]
mod tests {
use super::*;
// Test-only reusable Win32 HANDLE RAII helper.
// If a future non-test path needs the same pattern, move it out of this test module.
//
// This struct is similar to `hbb_common::platform::windows::RAIIHandle`,
// but `RAIIHandle` depends on `WinApi` crate, while this `HandleGuard` only depends on `windows` crate.
struct HandleGuard(WinHANDLE);
impl HandleGuard {
#[inline]
fn new(handle: WinHANDLE) -> Self {
Self(handle)
}
#[inline]
fn get(&self) -> WinHANDLE {
self.0
}
}
impl Drop for HandleGuard {
fn drop(&mut self) {
unsafe {
if !self.0.is_invalid() {
let _ = WinCloseHandle(self.0);
}
}
}
}
#[test]
fn test_is_process_running_as_system_invalid_pid_errors() {
assert!(is_process_running_as_system(u32::MAX).is_err());
}
#[test]
fn test_is_process_running_as_system_matches_current_process_token_user() {
let pid = unsafe { windows::Win32::System::Threading::GetCurrentProcessId() };
let actual = is_process_running_as_system(pid).unwrap();
let expected = unsafe {
// Keep this test consistent: use only the `windows` crate APIs/types.
let process = HandleGuard::new(
WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
.expect("WinOpenProcess should succeed for current process"),
);
let mut token = WinHANDLE::default();
WinOpenProcessToken(process.get(), WIN_TOKEN_QUERY, &mut token)
.expect("WinOpenProcessToken should succeed for current process");
let token = HandleGuard::new(token);
let mut token_user_size = 0u32;
let _ = WinGetTokenInformation(token.get(), TokenUser, None, 0, &mut token_user_size);
assert_ne!(token_user_size, 0, "TokenUser size should be non-zero");
let mut buffer = vec![0u8; token_user_size as usize];
WinGetTokenInformation(
token.get(),
TokenUser,
Some(buffer.as_mut_ptr() as *mut core::ffi::c_void),
token_user_size,
&mut token_user_size,
)
.expect("WinGetTokenInformation(TokenUser) should succeed for current process");
let min_size = std::mem::size_of::<TOKEN_USER>();
assert!(
buffer.len() >= min_size,
"TokenUser buffer too small (got {}, need >= {})",
buffer.len(),
min_size
);
let token_user: TOKEN_USER =
std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER);
let expected = IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool();
expected
};
assert_eq!(actual, expected);
}
#[test]
fn test_uninstall_cert() {
println!("uninstall driver certs: {:?}", cert::uninstall_cert());

View File

@@ -1,903 +0,0 @@
// https://learn.microsoft.com/en-us/windows/win32/secgloss/security-glossary
use super::{read_token_user_buffer, wide_string, ResultType};
use hbb_common::{anyhow::anyhow, bail};
use std::{
fs, io,
os::windows::{ffi::OsStrExt, fs::MetadataExt},
path::Path,
};
use windows::{
core::{PCWSTR, PWSTR},
Win32::{
Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL},
Security::{
Authorization::{
ConvertSidToStringSidW, ConvertStringSidToSidW, GetNamedSecurityInfoW,
SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W, SET_ACCESS,
SE_FILE_OBJECT, TRUSTEE_IS_GROUP, TRUSTEE_IS_SID, TRUSTEE_IS_USER, TRUSTEE_W,
},
ACE_FLAGS, ACL, CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, NO_INHERITANCE,
OBJECT_INHERIT_ACE, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID,
TOKEN_QUERY, TOKEN_USER,
},
Storage::FileSystem::{FILE_ALL_ACCESS, FILE_GENERIC_WRITE},
System::Threading::{GetCurrentProcess, OpenProcessToken},
},
};
const FILE_ATTRIBUTE_REPARSE_POINT_U32: u32 = 0x400;
#[inline]
fn is_reparse_point(metadata: &fs::Metadata) -> bool {
(metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT_U32) != 0
}
fn apply_grant_sid_allow_ace_to_path(
path: &Path,
sid_ptr: *mut std::ffi::c_void,
access_mask: u32,
is_group: bool,
is_dir: bool,
) -> ResultType<()> {
// Merge mode: read existing DACL and append/replace ACE via SetEntriesInAclW.
// https://learn.microsoft.com/en-us/windows/win32/secauthz/modifying-the-acls-of-an-object-in-c--
let mut old_dacl: *mut ACL = std::ptr::null_mut();
let mut security_descriptor = PSECURITY_DESCRIPTOR::default();
let path_utf16: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let get_named_result = unsafe {
GetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(&mut old_dacl),
None,
&mut security_descriptor,
)
};
if get_named_result.0 != 0 {
bail!(
"GetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
get_named_result.0
);
}
let _sd_guard = LocalAllocGuard(security_descriptor.0);
let inherit_flags = if is_dir {
ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0)
} else {
NO_INHERITANCE
};
let explicit_access = [make_sid_trustee_entry(
sid_ptr,
access_mask,
inherit_flags,
is_group,
)];
let old_acl_option = if old_dacl.is_null() {
None
} else {
Some(old_dacl as *const ACL)
};
let mut new_acl: *mut ACL = std::ptr::null_mut();
let set_entries_result = unsafe {
SetEntriesInAclW(
Some(explicit_access.as_slice()),
old_acl_option,
&mut new_acl,
)
};
if set_entries_result.0 != 0 {
bail!(
"SetEntriesInAclW failed for '{}': win32_error={}",
path.display(),
set_entries_result.0
);
}
if new_acl.is_null() {
bail!(
"SetEntriesInAclW returned null ACL for '{}'",
path.display()
);
}
let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void);
let set_named_result = unsafe {
SetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(new_acl),
None,
)
};
if set_named_result.0 != 0 {
bail!(
"SetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
set_named_result.0
);
}
Ok(())
}
/// Grants `Everyone` on `dir` recursively for helper/runtime files that must be
/// readable/executable across user contexts.
///
/// `access_mask` is the Win32 file access mask to grant recursively.
pub fn set_path_permission(dir: &Path, access_mask: u32) -> ResultType<()> {
let metadata = fs::symlink_metadata(dir).map_err(|e| {
anyhow!(
"Failed to inspect ACL target directory '{}': {}",
dir.display(),
e
)
})?;
if is_reparse_point(&metadata) {
bail!(
"ACL target directory is a reparse point and is rejected: '{}'",
dir.display()
);
}
if !metadata.file_type().is_dir() {
bail!("ACL target is not a directory: '{}'", dir.display());
}
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0")?;
let mut stack = vec![dir.to_path_buf()];
while let Some(path) = stack.pop() {
let metadata = fs::symlink_metadata(&path)
.map_err(|e| anyhow!("Failed to inspect ACL target '{}': {}", path.display(), e))?;
if is_reparse_point(&metadata) {
continue;
}
let is_dir = metadata.file_type().is_dir();
apply_grant_sid_allow_ace_to_path(
&path,
everyone_sid.as_sid_ptr(),
access_mask,
true,
is_dir,
)?;
if !is_dir {
continue;
}
for entry in fs::read_dir(&path)
.map_err(|e| anyhow!("Failed to list ACL target dir '{}': {}", path.display(), e))?
{
let entry = entry.map_err(|e| {
anyhow!(
"Failed to read ACL target dir entry under '{}': {}",
path.display(),
e
)
})?;
stack.push(entry.path());
}
}
Ok(())
}
/// Returns the current process user SID as a standard SID string
/// (for example: `S-1-5-18`).
///
/// Source:
/// - Official SID-to-string API (`ConvertSidToStringSidW`):
/// https://learn.microsoft.com/en-us/windows/win32/api/sddl/nf-sddl-convertsidtostringsidw
pub(crate) fn current_process_user_sid_string() -> ResultType<String> {
let mut token = HANDLE::default();
let result = (|| -> ResultType<String> {
unsafe {
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token)
.map_err(|e| anyhow!("Failed to open current process token: {}", e))?;
}
let buffer = unsafe { read_token_user_buffer(token, "current process")? };
let token_user: TOKEN_USER =
unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER) };
if token_user.User.Sid.0.is_null() {
bail!("Token SID is null");
}
let mut sid_string_ptr = PWSTR::null();
unsafe {
ConvertSidToStringSidW(token_user.User.Sid, &mut sid_string_ptr).map_err(|e| {
anyhow!(
"ConvertSidToStringSidW failed for current process token SID: {}",
e
)
})?;
}
if sid_string_ptr.is_null() {
bail!("ConvertSidToStringSidW returned null SID string pointer");
}
let _sid_string_guard = LocalAllocGuard(sid_string_ptr.0 as *mut std::ffi::c_void);
unsafe {
sid_string_ptr
.to_string()
.map_err(|e| anyhow!("Failed to decode SID string as UTF-16: {}", e))
}
})();
if !token.is_invalid() {
unsafe {
let _ = CloseHandle(token);
}
}
result
}
/// Hardens ACLs for portable-service shared-memory path (directory or file).
///
/// Why:
/// - Shared memory used by portable service carries runtime control/data and must not inherit
/// broad/default ACLs.
/// - We explicitly grant only trusted principals and remove broad groups to reduce local
/// privilege-boundary bypass risk.
///
/// ACL policy applied via Win32 ACL APIs (`SetEntriesInAclW` + `SetNamedSecurityInfoW`):
/// - common (directory + file):
/// - `S-1-5-18` (LocalSystem): full control
/// - `S-1-5-32-544` (Built-in Administrators): full control
/// - `current_process_user_sid_string()` result: full control
/// - directory (`portable_service_shmem` parent):
/// - keep `Authenticated Users` directory-level write so other local accounts can
/// create their own runtime shmem files after account switching
/// - `FILE_GENERIC_WRITE + NO_INHERITANCE` means write/create on this directory itself;
/// it is intentionally not inherited by children.
/// Reference:
/// - File access rights:
/// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants
/// - ACE inheritance rules:
/// https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-inheritance-rules
/// - remove `Everyone` and `Users` grants
/// - file (`shared_memory*` flink):
/// - remove broad grants:
/// - `S-1-1-0` (Everyone)
/// - `S-1-5-11` (Authenticated Users)
/// - `S-1-5-32-545` (Users)
///
/// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
pub fn set_path_permission_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> {
set_path_permission_for_portable_service_shmem_impl(path, true)
}
#[inline]
pub fn validate_path_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> {
validate_portable_service_shmem_dir_target(path)
}
#[inline]
pub fn set_path_permission_for_portable_service_shmem_file(path: &Path) -> ResultType<()> {
set_path_permission_for_portable_service_shmem_impl(path, false)
}
#[derive(Debug)]
pub(super) struct LocalAllocGuard(*mut std::ffi::c_void);
impl LocalAllocGuard {
#[inline]
pub(super) fn as_sid_ptr(&self) -> *mut std::ffi::c_void {
self.0
}
}
impl Drop for LocalAllocGuard {
fn drop(&mut self) {
if self.0.is_null() {
return;
}
// Buffers returned by ConvertStringSidToSidW / SetEntriesInAclW /
// ConvertSidToStringSidW are LocalAlloc-owned and must be LocalFree'ed.
unsafe {
let _ = LocalFree(Some(HLOCAL(self.0)));
}
}
}
#[inline]
pub(super) fn sid_string_to_local_alloc_guard(sid: &str) -> ResultType<LocalAllocGuard> {
let sid_utf16 = wide_string(sid);
let mut sid_ptr = PSID::default();
unsafe {
ConvertStringSidToSidW(PCWSTR::from_raw(sid_utf16.as_ptr()), &mut sid_ptr)
.map_err(|e| anyhow!("ConvertStringSidToSidW failed for '{}': {}", sid, e))?;
}
if sid_ptr.0.is_null() {
bail!("ConvertStringSidToSidW returned null SID for '{}'", sid);
}
Ok(LocalAllocGuard(sid_ptr.0))
}
#[inline]
fn make_sid_trustee_entry(
sid_ptr: *mut std::ffi::c_void,
access_permissions: u32,
inheritance: ACE_FLAGS,
is_group: bool,
) -> EXPLICIT_ACCESS_W {
// `is_group` is explicitly provided by the caller from the concrete SID semantic
// (e.g. Administrators/Authenticated Users => group, LocalSystem/current user => user).
EXPLICIT_ACCESS_W {
grfAccessPermissions: access_permissions,
grfAccessMode: SET_ACCESS,
grfInheritance: inheritance,
Trustee: TRUSTEE_W {
pMultipleTrustee: std::ptr::null_mut(),
MultipleTrusteeOperation: Default::default(),
TrusteeForm: TRUSTEE_IS_SID,
TrusteeType: if is_group {
TRUSTEE_IS_GROUP
} else {
TRUSTEE_IS_USER
},
// SAFETY: With TrusteeForm=TRUSTEE_IS_SID, ptstrName is interpreted as PSID.
ptstrName: PWSTR::from_raw(sid_ptr as *mut u16),
},
}
}
fn validate_portable_service_shmem_dir_target(path: &Path) -> ResultType<()> {
let metadata = fs::symlink_metadata(path).map_err(|e| {
anyhow!(
"Failed to inspect portable service shared-memory ACL directory '{}': {}",
path.display(),
e
)
})?;
if is_reparse_point(&metadata) {
bail!(
"Portable service shared-memory ACL directory target is a reparse point and is rejected: '{}'",
path.display()
);
}
if !metadata.file_type().is_dir() {
bail!(
"Portable service shared-memory ACL target is not a directory: '{}'",
path.display()
);
}
Ok(())
}
fn set_path_permission_for_portable_service_shmem_impl(
path: &Path,
expect_dir: bool,
) -> ResultType<()> {
if expect_dir {
validate_portable_service_shmem_dir_target(path)?;
} else {
let metadata_result = fs::symlink_metadata(path);
match metadata_result {
Ok(metadata) => {
if metadata.file_type().is_dir() {
bail!(
"Portable service shared-memory ACL target is a directory, expected file-like path: '{}'",
path.display()
);
}
if is_reparse_point(&metadata) {
bail!(
"Portable service shared-memory ACL file target is a reparse point and is rejected: '{}'",
path.display()
);
}
}
Err(e)
if e.kind() == io::ErrorKind::NotFound
|| e.kind() == io::ErrorKind::PermissionDenied =>
{
// Keep going and let Win32 ACL APIs return the final OS error.
// `Path::exists()/is_file()` and metadata can collapse ACL-denied paths into
// a false "not found" signal under restricted directory ACLs.
}
Err(e) => {
bail!(
"Failed to inspect portable service shared-memory ACL target '{}': {}",
path.display(),
e
);
}
}
}
let user_sid = current_process_user_sid_string()?;
let local_system_sid = sid_string_to_local_alloc_guard("S-1-5-18")?;
let administrators_sid = sid_string_to_local_alloc_guard("S-1-5-32-544")?;
let current_user_sid = sid_string_to_local_alloc_guard(&user_sid)?;
let authenticated_users_sid = if expect_dir {
Some(sid_string_to_local_alloc_guard("S-1-5-11")?)
} else {
None
};
let inherit_flags = if expect_dir {
ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0)
} else {
NO_INHERITANCE
};
let mut entries = vec![
make_sid_trustee_entry(
local_system_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0,
inherit_flags,
false,
),
make_sid_trustee_entry(
administrators_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0,
inherit_flags,
true,
),
make_sid_trustee_entry(
current_user_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0,
inherit_flags,
false,
),
];
if let Some(auth_sid) = authenticated_users_sid.as_ref() {
// Keep the shared parent directory multi-user writable at directory level.
entries.push(make_sid_trustee_entry(
auth_sid.as_sid_ptr(),
FILE_GENERIC_WRITE.0,
NO_INHERITANCE,
true,
));
}
// Rebuild mode: build a fresh DACL (old ACL not merged) and apply as protected.
// This avoids carrying over broad legacy ACEs from inherited/default ACLs.
// Reference:
// - SetEntriesInAclW:
// https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setentriesinaclw
// - SetNamedSecurityInfoW (PROTECTED_DACL_SECURITY_INFORMATION):
// https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setnamedsecurityinfow
let mut new_acl: *mut ACL = std::ptr::null_mut();
let set_entries_result =
unsafe { SetEntriesInAclW(Some(entries.as_slice()), None, &mut new_acl) };
if set_entries_result.0 != 0 {
bail!(
"SetEntriesInAclW failed for '{}': win32_error={}",
path.display(),
set_entries_result.0
);
}
if new_acl.is_null() {
bail!(
"SetEntriesInAclW returned null ACL for '{}'",
path.display()
);
}
let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void);
let path_utf16: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let security_info = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION;
let set_named_result = unsafe {
SetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
security_info,
None,
None,
Some(new_acl),
None,
)
};
if set_named_result.0 != 0 {
bail!(
"SetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
set_named_result.0
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
current_process_user_sid_string, set_path_permission,
set_path_permission_for_portable_service_shmem_dir,
set_path_permission_for_portable_service_shmem_file, sid_string_to_local_alloc_guard,
LocalAllocGuard, ResultType,
};
use hbb_common::bail;
use std::{
fs,
os::windows::{ffi::OsStrExt, fs::symlink_dir, fs::symlink_file},
path::{Path, PathBuf},
};
use windows::{
core::PCWSTR,
Win32::{
Security::{
AclSizeInformation,
Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT},
EqualSid as WinEqualSid, GetAce, GetAclInformation, GetSecurityDescriptorControl,
ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_SIZE_INFORMATION,
DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SE_DACL_PROTECTED,
},
Storage::FileSystem::{
FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
},
},
};
const ACCESS_ALLOWED_ACE_TYPE_U8: u8 = 0;
fn unique_acl_test_path(prefix: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"rustdesk_acl_{}_{}_{}",
prefix,
std::process::id(),
hbb_common::rand::random::<u32>()
))
}
fn try_create_dir_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool {
match symlink_dir(target, link) {
Ok(()) => true,
Err(err) => {
eprintln!(
"skip {}: failed to create directory reparse point (symlink): {}",
test_name, err
);
false
}
}
}
fn try_create_file_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool {
match symlink_file(target, link) {
Ok(()) => true,
Err(err) => {
eprintln!(
"skip {}: failed to create file reparse point (symlink): {}",
test_name, err
);
false
}
}
}
fn get_file_dacl(path: &Path) -> ResultType<(*mut ACL, LocalAllocGuard)> {
let mut dacl: *mut ACL = std::ptr::null_mut();
let mut sd = PSECURITY_DESCRIPTOR::default();
let path_utf16: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let result = unsafe {
GetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(&mut dacl),
None,
&mut sd,
)
};
if result.0 != 0 {
bail!(
"GetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
result.0
);
}
if dacl.is_null() || sd.0.is_null() {
bail!("DACL/security descriptor missing for '{}'", path.display());
}
Ok((dacl, LocalAllocGuard(sd.0)))
}
fn has_allow_ace_with_mask(
dacl: *const ACL,
sid_ptr: *mut std::ffi::c_void,
mask: u32,
) -> bool {
let mut info = ACL_SIZE_INFORMATION::default();
if unsafe {
GetAclInformation(
dacl,
&mut info as *mut _ as *mut std::ffi::c_void,
std::mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
AclSizeInformation,
)
}
.is_err()
{
return false;
}
for index in 0..info.AceCount {
let mut ace_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
if unsafe { GetAce(dacl, index, &mut ace_ptr) }.is_err() || ace_ptr.is_null() {
continue;
}
let header = unsafe { &*(ace_ptr as *const ACE_HEADER) };
if header.AceType != ACCESS_ALLOWED_ACE_TYPE_U8 {
continue;
}
let allowed = unsafe { &*(ace_ptr as *const ACCESS_ALLOWED_ACE) };
let ace_sid = PSID((&allowed.SidStart as *const u32) as *mut std::ffi::c_void);
if unsafe { WinEqualSid(PSID(sid_ptr), ace_sid) }.is_ok()
&& (allowed.Mask & mask) == mask
{
return true;
}
}
false
}
fn has_any_allow_ace_for_sid(dacl: *const ACL, sid_ptr: *mut std::ffi::c_void) -> bool {
has_allow_ace_with_mask(dacl, sid_ptr, 0)
}
fn is_dacl_protected(sd: PSECURITY_DESCRIPTOR) -> bool {
let mut control: u16 = 0;
let mut revision: u32 = 0;
if unsafe { GetSecurityDescriptorControl(sd, &mut control, &mut revision) }.is_err() {
return false;
}
(control & SE_DACL_PROTECTED.0) != 0
}
#[test]
fn test_portable_service_shmem_dir_acl_policy() {
let dir = unique_acl_test_path("dir");
fs::create_dir_all(&dir).unwrap();
set_path_permission_for_portable_service_shmem_dir(&dir).unwrap();
let (dacl, sd_guard) = get_file_dacl(&dir).unwrap();
let current_user_sid =
sid_string_to_local_alloc_guard(&current_process_user_sid_string().unwrap()).unwrap();
let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap();
let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap();
let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap();
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap();
assert!(has_allow_ace_with_mask(
dacl,
system_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
admin_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
current_user_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
auth_users_sid.as_sid_ptr(),
FILE_GENERIC_WRITE.0
));
assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr()));
assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr()));
assert!(is_dacl_protected(PSECURITY_DESCRIPTOR(
sd_guard.as_sid_ptr()
)));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_portable_service_shmem_file_acl_policy() {
let dir = unique_acl_test_path("file");
fs::create_dir_all(&dir).unwrap();
let file = dir.join("shared_memory_portable_service_test");
fs::write(&file, b"x").unwrap();
set_path_permission_for_portable_service_shmem_file(&file).unwrap();
let (dacl, sd_guard) = get_file_dacl(&file).unwrap();
let current_user_sid =
sid_string_to_local_alloc_guard(&current_process_user_sid_string().unwrap()).unwrap();
let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap();
let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap();
let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap();
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap();
assert!(has_allow_ace_with_mask(
dacl,
system_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
admin_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
current_user_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(!has_any_allow_ace_for_sid(
dacl,
auth_users_sid.as_sid_ptr()
));
assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr()));
assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr()));
assert!(is_dacl_protected(PSECURITY_DESCRIPTOR(
sd_guard.as_sid_ptr()
)));
let _ = fs::remove_file(&file);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_set_path_permission_rx_applies_recursively() {
let root = unique_acl_test_path("set_path_permission");
let child_dir = root.join("child");
let child_file = child_dir.join("helper.exe");
fs::create_dir_all(&child_dir).unwrap();
fs::write(&child_file, b"x").unwrap();
if let Err(err) = set_path_permission(&root, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0) {
let text = err.to_string();
let _ = fs::remove_file(&child_file);
let _ = fs::remove_dir_all(&root);
if text.contains("win32_error=5") || text.contains("Access is denied") {
eprintln!(
"skip test_set_path_permission_rx_applies_recursively: insufficient WRITE_DAC in current environment: {}",
text
);
return;
}
panic!("set_path_permission failed unexpectedly: {}", text);
}
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
let rx_mask = FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0;
for target in [&root, &child_dir, &child_file] {
let (dacl, _sd_guard) = get_file_dacl(target).unwrap();
assert!(
has_allow_ace_with_mask(dacl, everyone_sid.as_sid_ptr(), rx_mask),
"Everyone RX grant missing on '{}'",
target.display()
);
}
let _ = fs::remove_file(&child_file);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn test_portable_service_shmem_dir_acl_rejects_file_target() {
let dir = unique_acl_test_path("dir_target_file");
fs::create_dir_all(&dir).unwrap();
let file = dir.join("target.txt");
fs::write(&file, b"x").unwrap();
let result = set_path_permission_for_portable_service_shmem_dir(&file);
assert!(result.is_err());
let _ = fs::remove_file(&file);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_portable_service_shmem_file_acl_rejects_dir_target() {
let dir = unique_acl_test_path("file_target_dir");
fs::create_dir_all(&dir).unwrap();
let result = set_path_permission_for_portable_service_shmem_file(&dir);
assert!(result.is_err());
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_portable_service_shmem_file_acl_rejects_missing_target() {
let path = unique_acl_test_path("missing").join("shared_memory_missing");
let result = set_path_permission_for_portable_service_shmem_file(&path);
assert!(result.is_err());
}
#[test]
fn test_set_path_permission_rejects_reparse_entrypoint() {
let root = unique_acl_test_path("reparse_entry");
let real_dir = root.join("real");
let link_dir = root.join("link");
fs::create_dir_all(&real_dir).unwrap();
if !try_create_dir_reparse_point(
&real_dir,
&link_dir,
"test_set_path_permission_rejects_reparse_entrypoint",
) {
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
return;
}
let result = set_path_permission(&link_dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0);
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
assert!(
text.contains("reparse point"),
"expected reparse-point rejection, got '{}'",
text
);
let _ = fs::remove_dir(&link_dir);
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn test_portable_service_shmem_dir_acl_rejects_reparse_target() {
let root = unique_acl_test_path("reparse_shmem_dir");
let real_dir = root.join("real");
let link_dir = root.join("link");
fs::create_dir_all(&real_dir).unwrap();
if !try_create_dir_reparse_point(
&real_dir,
&link_dir,
"test_portable_service_shmem_dir_acl_rejects_reparse_target",
) {
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
return;
}
let result = set_path_permission_for_portable_service_shmem_dir(&link_dir);
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
assert!(
text.contains("reparse point"),
"expected reparse-point rejection, got '{}'",
text
);
let _ = fs::remove_dir(&link_dir);
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn test_portable_service_shmem_file_acl_rejects_reparse_target() {
let root = unique_acl_test_path("reparse_shmem_file");
let real_file = root.join("real.txt");
let link_file = root.join("link.txt");
fs::create_dir_all(&root).unwrap();
fs::write(&real_file, b"x").unwrap();
if !try_create_file_reparse_point(
&real_file,
&link_file,
"test_portable_service_shmem_file_acl_rejects_reparse_target",
) {
let _ = fs::remove_file(&real_file);
let _ = fs::remove_dir_all(&root);
return;
}
let result = set_path_permission_for_portable_service_shmem_file(&link_file);
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
assert!(
text.contains("reparse point"),
"expected reparse-point rejection, got '{}'",
text
);
let _ = fs::remove_file(&link_file);
let _ = fs::remove_file(&real_file);
let _ = fs::remove_dir_all(&root);
}
}

View File

@@ -1,5 +1,4 @@
use std::{
collections::HashMap,
net::SocketAddr,
sync::{
atomic::{AtomicBool, Ordering},
@@ -22,13 +21,8 @@ use hbb_common::{
rendezvous_proto::*,
sleep,
socket_client::{self, connect_tcp, is_ipv4, new_direct_udp_for, new_udp_for},
tokio::{
self, select,
sync::{mpsc, Mutex},
time::interval,
},
tokio::{self, select, sync::Mutex, time::interval},
udp::FramedSocket,
webrtc::WebRTCStream,
AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr,
};
@@ -38,45 +32,15 @@ use crate::{
};
type Message = RendezvousMessage;
type RendezvousSender = mpsc::UnboundedSender<Message>;
fn webrtc_ice_key(peer_id: &str, session_key: &str) -> String {
format!("{}\n{}", peer_id, session_key)
}
lazy_static::lazy_static! {
static ref SOLVING_PK_MISMATCH: Mutex<String> = Default::default();
static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
static ref LAST_RELAY_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
static ref WEBRTC_ICE_TXS: Mutex<HashMap<String, mpsc::UnboundedSender<String>>> = Default::default();
}
static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false);
static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false);
pub(crate) static NEEDS_DEPLOY: AtomicBool = AtomicBool::new(false);
// register_pk retry interval (ms) when device is awaiting deployment
const DEPLOY_RETRY_INTERVAL: i64 = 30_000;
lazy_static::lazy_static! {
static ref LAST_NOT_DEPLOYED_REGISTER: Mutex<Option<Instant>> = Mutex::new(None);
}
// Single source of truth for the "awaiting deployment" backoff. The server has
// already told us this device is not in its db; until the operator runs
// `rustdesk --deploy --token <api_token>` there is no point re-running the
// register path more often than DEPLOY_RETRY_INTERVAL. Gating in the timer
// loops (rather than only inside register_pk) also avoids the
// last_register_sent / fails / latency / UDP-rebind churn the loop would
// otherwise spin on while no response ever comes back.
async fn deploy_register_throttled() -> bool {
if !NEEDS_DEPLOY.load(Ordering::SeqCst) {
return false;
}
LAST_NOT_DEPLOYED_REGISTER
.lock()
.await
.map(|t| (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL)
.unwrap_or(false)
}
#[derive(Clone)]
pub struct RendezvousMediator {
@@ -84,7 +48,6 @@ pub struct RendezvousMediator {
host: String,
host_prefix: String,
keep_alive: i32,
rz_sender: RendezvousSender,
}
impl RendezvousMediator {
@@ -195,13 +158,11 @@ impl RendezvousMediator {
let host = check_port(&host, RENDEZVOUS_PORT);
log::info!("start udp: {host}");
let (mut socket, mut addr) = new_udp_for(&host, CONNECT_TIMEOUT).await?;
let (rz_sender, mut rz_out_rx) = mpsc::unbounded_channel::<Message>();
let mut rz = Self {
addr: addr.clone(),
host: host.clone(),
host_prefix: Self::get_host_prefix(&host),
keep_alive: crate::DEFAULT_KEEP_ALIVE,
rz_sender,
};
let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT));
@@ -261,21 +222,10 @@ impl RendezvousMediator {
},
}
},
Some(msg_out) = rz_out_rx.recv() => {
Sink::Framed(&mut socket, &addr).send(&msg_out).await?;
},
_ = timer.tick() => {
if SHOULD_EXIT.load(Ordering::SeqCst) {
break;
}
// The server already told us this device is not deployed. Skip
// the whole register / fails / latency / UDP-rebind path until
// DEPLOY_RETRY_INTERVAL elapses, otherwise the loop spins every
// few seconds (log spam + misapplied network-recovery rebind)
// until the operator runs `rustdesk --deploy`.
if deploy_register_throttled().await {
continue;
}
let now = Some(Instant::now());
let expired = last_register_resp.map(|x| x.elapsed().as_millis() as i64 >= REG_INTERVAL).unwrap_or(true);
let timeout = last_register_sent.map(|x| x.elapsed().as_millis() as i64 >= reg_timeout).unwrap_or(false);
@@ -339,22 +289,10 @@ impl RendezvousMediator {
Config::set_key_confirmed(true);
Config::set_host_key_confirmed(&self.host_prefix, true);
*SOLVING_PK_MISMATCH.lock().await = "".to_owned();
NEEDS_DEPLOY.store(false, Ordering::SeqCst);
}
Ok(register_pk_response::Result::UUID_MISMATCH) => {
self.handle_uuid_mismatch(sink).await?;
}
Ok(register_pk_response::Result::NOT_DEPLOYED) => {
if !NEEDS_DEPLOY.load(Ordering::SeqCst) {
log::warn!("Server requires deployment. Run `rustdesk --deploy --token <api_token>` on this device.");
}
NEEDS_DEPLOY.store(true, Ordering::SeqCst);
// Clear key_confirmed so the UI reflects the truth: this device is
// not currently registered. Covers the case where an online device
// was deleted by an admin while running.
Config::set_key_confirmed(false);
Config::set_host_key_confirmed(&self.host_prefix, false);
}
_ => {
log::error!("unknown RegisterPkResponse");
}
@@ -385,22 +323,6 @@ impl RendezvousMediator {
allow_err!(rz.handle_intranet(fla, server).await);
});
}
Some(rendezvous_message::Union::IceCandidate(ice)) => {
if ice.to_id != Config::get_id() {
return Ok(());
}
let key = webrtc_ice_key(&ice.from_id, &ice.session_key);
let tx = WEBRTC_ICE_TXS.lock().await.get(&key).cloned();
if let Some(tx) = tx {
let _ = tx.send(ice.candidate);
} else {
log::debug!(
"dropping ICE candidate for unknown WebRTC session from {} key {}",
ice.from_id,
ice.session_key
);
}
}
Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
let v0 = Config::get_rendezvous_servers();
Config::set_option(
@@ -423,13 +345,11 @@ impl RendezvousMediator {
let mut conn = connect_tcp(host.clone(), CONNECT_TIMEOUT).await?;
let key = crate::get_key(true).await;
crate::secure_tcp(&mut conn, &key).await?;
let (rz_sender, mut rz_out_rx) = mpsc::unbounded_channel::<Message>();
let mut rz = Self {
addr: conn.local_addr().into_target_addr()?,
host: host.clone(),
host_prefix: Self::get_host_prefix(&host),
keep_alive: crate::DEFAULT_KEEP_ALIVE,
rz_sender,
};
let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT));
let mut last_register_sent: Option<Instant> = None;
@@ -457,9 +377,6 @@ impl RendezvousMediator {
let msg = Message::parse_from_bytes(&bytes)?;
rz.handle_resp(msg.union, Sink::Stream(&mut conn), &server, &mut update_latency).await?
}
Some(msg_out) = rz_out_rx.recv() => {
Sink::Stream(&mut conn).send(&msg_out).await?;
}
_ = timer.tick() => {
if SHOULD_EXIT.load(Ordering::SeqCst) {
break;
@@ -511,7 +428,6 @@ impl RendezvousMediator {
rr.secure,
false,
Default::default(),
String::new(),
rr.control_permissions.clone().into_option(),
)
.await
@@ -526,7 +442,6 @@ impl RendezvousMediator {
secure: bool,
initiate: bool,
socket_addr_v6: bytes::Bytes,
webrtc_sdp_answer: String,
control_permissions: Option<ControlPermissions>,
) -> ResultType<()> {
let peer_addr = AddrMangle::decode(&socket_addr);
@@ -545,7 +460,6 @@ impl RendezvousMediator {
socket_addr: socket_addr.into(),
version: crate::VERSION.to_owned(),
socket_addr_v6,
webrtc_sdp_answer,
..Default::default()
};
if initiate {
@@ -613,7 +527,6 @@ impl RendezvousMediator {
true,
true,
socket_addr_v6,
String::new(),
fla.control_permissions.into_option(),
)
.await
@@ -656,91 +569,6 @@ impl RendezvousMediator {
Ok(())
}
async fn spawn_webrtc_answerer(
&self,
ph: &PunchHole,
server: ServerPtr,
peer_addr: SocketAddr,
control_permissions: Option<ControlPermissions>,
) -> ResultType<String> {
if ph.requester_id.is_empty() {
log::warn!("WebRTC offer missing requester_id; falling back to existing transports");
return Ok(String::new());
}
let mut stream =
WebRTCStream::new(&ph.webrtc_sdp_offer, ph.force_relay, CONNECT_TIMEOUT).await?;
let answer = stream.get_local_endpoint().await?;
let session_key = stream.session_key().to_owned();
let peer_id = ph.requester_id.clone();
let (remote_ice_tx, mut remote_ice_rx) = mpsc::unbounded_channel::<String>();
WEBRTC_ICE_TXS
.lock()
.await
.insert(webrtc_ice_key(&peer_id, &session_key), remote_ice_tx);
let stream_for_remote_ice = stream.clone();
tokio::spawn(async move {
while let Some(candidate) = remote_ice_rx.recv().await {
if let Err(err) = stream_for_remote_ice.add_remote_ice_candidate(&candidate).await
{
log::warn!("failed to add remote WebRTC ICE candidate: {}", err);
}
}
});
if let Some(mut local_ice_rx) = stream.take_local_ice_rx() {
let sender = self.rz_sender.clone();
let my_id = Config::get_id();
let target_id = peer_id.clone();
let session_key_for_ice = session_key.clone();
tokio::spawn(async move {
while let Some(candidate) = local_ice_rx.recv().await {
let mut msg = Message::new();
msg.set_ice_candidate(IceCandidate {
from_id: my_id.clone(),
to_id: target_id.clone(),
session_key: session_key_for_ice.clone(),
candidate,
..Default::default()
});
let _ = sender.send(msg);
}
});
}
let peer_id_for_cleanup = peer_id.clone();
let session_key_for_cleanup = session_key.clone();
tokio::spawn(async move {
let result = stream.wait_connected(CONNECT_TIMEOUT).await;
WEBRTC_ICE_TXS
.lock()
.await
.remove(&webrtc_ice_key(
&peer_id_for_cleanup,
&session_key_for_cleanup,
));
if let Err(err) = result {
log::warn!("webrtc wait_connected failed: {}", err);
return;
}
if let Err(err) = crate::server::create_tcp_connection(
server,
Stream::WebRTC(stream),
peer_addr,
true,
control_permissions,
)
.await
{
log::warn!("failed to create WebRTC server connection: {}", err);
}
});
Ok(answer)
}
async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> {
let mut peer_addr = AddrMangle::decode(&ph.socket_addr);
let last = *LAST_MSG.lock().await;
@@ -752,22 +580,7 @@ impl RendezvousMediator {
let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6);
let relay = use_ws() || Config::is_proxy() || ph.force_relay;
let mut socket_addr_v6 = Default::default();
let control_permissions = ph.control_permissions.clone().into_option();
let webrtc_sdp_answer = if !ph.webrtc_sdp_offer.is_empty() {
self.spawn_webrtc_answerer(
&ph,
server.clone(),
peer_addr,
control_permissions.clone(),
)
.await
.unwrap_or_else(|err| {
log::warn!("failed to create WebRTC answer: {}", err);
String::new()
})
} else {
String::new()
};
let control_permissions = ph.control_permissions.into_option();
if peer_addr_v6.port() > 0 && !relay {
socket_addr_v6 = start_ipv6(
peer_addr_v6,
@@ -794,7 +607,6 @@ impl RendezvousMediator {
true,
true,
socket_addr_v6.clone(),
webrtc_sdp_answer.clone(),
control_permissions,
)
.await;
@@ -808,7 +620,6 @@ impl RendezvousMediator {
nat_type: nat_type.into(),
version: crate::VERSION.to_owned(),
socket_addr_v6,
webrtc_sdp_answer,
..Default::default()
};
if ph.udp_port > 0 {
@@ -867,21 +678,6 @@ impl RendezvousMediator {
}
async fn register_pk(&mut self, socket: Sink<'_>) -> ResultType<()> {
// Throttle register_pk when the device is awaiting deployment: server
// already told us we're not in its db; sending more often than every
// DEPLOY_RETRY_INTERVAL ms is wasted traffic until the operator runs
// `rustdesk --deploy --token <api_token>`.
if NEEDS_DEPLOY.load(Ordering::SeqCst) {
let mut last = LAST_NOT_DEPLOYED_REGISTER.lock().await;
if let Some(t) = *last {
if (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL {
return Ok(());
}
}
*last = Some(Instant::now());
} else {
*LAST_NOT_DEPLOYED_REGISTER.lock().await = None;
}
let mut msg_out = Message::new();
let pk = Config::get_key_pair().1;
let uuid = hbb_common::get_uuid();

View File

@@ -67,7 +67,6 @@ pub mod input_service {
}
mod connection;
mod login_failure_check;
pub mod display_service;
#[cfg(windows)]
pub mod portable_service;
@@ -732,7 +731,7 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option<tokio::sync::oneshot::Se
use hbb_common::sleep;
for i in 1..=tries {
sleep(i as f32 * CONFIG_SYNC_INTERVAL_SECS).await;
match crate::ipc::connect_service(1000).await {
match crate::ipc::connect(1000, "_service").await {
Ok(mut conn) => {
if !synced {
if conn.send(&Data::SyncConfig(None)).await.is_ok() {
@@ -773,12 +772,6 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option<tokio::sync::oneshot::Se
};
};
}
if !synced {
log::warn!(
"initial config sync from root failed, reconnecting to ipc_service"
);
continue;
}
}
loop {
@@ -795,7 +788,7 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option<tokio::sync::oneshot::Se
match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await {
Err(e) => {
log::error!("sync config to root failed: {}", e);
match crate::ipc::connect_service(1000).await {
match crate::ipc::connect(1000, "_service").await {
Ok(mut _conn) => {
conn = _conn;
log::info!("reconnected to ipc_service");

View File

@@ -1,8 +1,3 @@
#[cfg(target_os = "windows")]
use super::login_failure_check::try_acquire_os_credential_login_gate;
use super::login_failure_check::{
evaluate_os_credential_policy, record_os_credential_failure, FailureScope,
};
use super::{input_service::*, *};
#[cfg(feature = "unix-file-copy-paste")]
use crate::clipboard::try_empty_clipboard_files;
@@ -27,6 +22,8 @@ use crate::{
#[cfg(any(target_os = "android", target_os = "ios"))]
use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel};
use cidr_utils::cidr::IpCidr;
#[cfg(target_os = "linux")]
use hbb_common::platform::linux::run_cmds;
#[cfg(target_os = "android")]
use hbb_common::protobuf::EnumOrUnknown;
use hbb_common::{
@@ -87,9 +84,6 @@ lazy_static::lazy_static! {
static ref PENDING_SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default();
}
#[cfg(target_os = "windows")]
const TERMINAL_OS_LOGIN_FAILED_MSG: &str = "Incorrect username or password.";
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
@@ -102,32 +96,6 @@ fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
x == 0
}
#[cfg(target_os = "linux")]
fn should_check_linux_headless_os_auth_before_desktop_start(
is_headless_allowed: bool,
username: &str,
) -> bool {
is_headless_allowed
&& !username.trim().is_empty()
&& linux_desktop_manager::get_username().is_empty()
}
#[cfg(target_os = "linux")]
fn should_record_linux_headless_os_auth_failure(
is_headless_allowed: bool,
username: &str,
err_msg: &str,
) -> bool {
is_headless_allowed
&& !username.trim().is_empty()
&& err_msg == crate::client::LOGIN_MSG_PASSWORD_WRONG
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn should_use_terminal_os_login_scope(is_terminal: bool, os_login_username: &str) -> bool {
cfg!(target_os = "windows") && is_terminal && !os_login_username.trim().is_empty()
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
lazy_static::lazy_static! {
static ref WALLPAPER_REMOVER: Arc<Mutex<Option<WallPaperRemover>>> = Default::default();
@@ -1531,9 +1499,6 @@ impl Connection {
// Keep the connection alive so the client can continue with 2FA.
return true;
}
if let Some(keep_alive) = self.prepare_terminal_login_for_authorization().await {
return keep_alive;
}
if !self.connect_port_forward_if_needed().await {
return false;
}
@@ -2413,6 +2378,33 @@ impl Connection {
o.terminal_persistent.enum_value() == Ok(BoolOption::Yes);
}
self.terminal_service_id = terminal.service_id;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(msg) =
self.fill_terminal_user_token(&lr.os_login.username, &lr.os_login.password)
{
self.send_login_error(msg).await;
sleep(1.).await;
return false;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(is_user) =
terminal_service::is_service_specified_user(&self.terminal_service_id)
{
if let Some(user_token) = &self.terminal_user_token {
let has_service_token =
user_token.to_terminal_service_token().is_some();
if is_user != has_service_token {
// This occurs when the service id (in the configuration) is manually changed by the user, causing a mismatch in validation.
log::error!("Terminal service user mismatch detected. The service ID may have been manually changed in the configuration, causing validation to fail.");
// No need to translate the following message, because it is in an abnormal case.
self.send_login_error("Terminal service user mismatch detected.")
.await;
sleep(1.).await;
return false;
}
}
}
}
Some(login_request::Union::PortForward(mut pf)) => {
if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.control_permissions) {
@@ -2430,43 +2422,8 @@ impl Connection {
}
}
if !hbb_common::is_ip_str(&lr.username)
&& !hbb_common::is_domain_port_str(&lr.username)
&& lr.username != Config::get_id()
{
self.send_login_error(crate::client::LOGIN_MSG_OFFLINE)
.await;
return false;
}
#[cfg(target_os = "windows")]
if self.terminal
&& lr.os_login.username.trim().is_empty()
&& crate::platform::is_prelogin()
{
self.send_login_error(
"No active console user logged on, please connect and logon first.",
)
.await;
sleep(1.).await;
return false;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if !should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) {
self.try_start_cm_ipc();
}
#[cfg(target_os = "linux")]
if should_check_linux_headless_os_auth_before_desktop_start(
self.linux_headless_handle.is_headless_allowed,
&lr.os_login.username,
) {
let (_failure, res) = self.check_failure(0).await;
if !res {
return true;
}
}
self.try_start_cm_ipc();
#[cfg(not(target_os = "linux"))]
let err_msg = "".to_owned();
@@ -2478,18 +2435,6 @@ impl Connection {
// If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password.
if !err_msg.is_empty() && err_msg != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY
{
#[cfg(target_os = "linux")]
if should_record_linux_headless_os_auth_failure(
self.linux_headless_handle.is_headless_allowed,
&lr.os_login.username,
&err_msg,
) {
let (failure, res) = self.check_failure(0).await;
if !res {
return true;
}
self.update_failure(failure, false, 0);
}
self.send_login_error(err_msg).await;
return true;
}
@@ -2518,16 +2463,17 @@ impl Connection {
crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y"
&& is_logon();
if (password::approve_mode() == ApproveMode::Click && !allow_logon_screen_password)
if !hbb_common::is_ip_str(&lr.username)
&& !hbb_common::is_domain_port_str(&lr.username)
&& lr.username != Config::get_id()
{
self.send_login_error(crate::client::LOGIN_MSG_OFFLINE)
.await;
return false;
} else if (password::approve_mode() == ApproveMode::Click
&& !allow_logon_screen_password)
|| password::approve_mode() == ApproveMode::Both && !password::has_valid_password()
{
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) {
if let Some(keep_alive) = self.prepare_terminal_login_for_authorization().await
{
return keep_alive;
}
}
self.try_start_cm(lr.my_id, lr.my_name, false);
if hbb_common::get_version_number(&lr.version)
>= hbb_common::get_version_number("1.2.0")
@@ -2549,14 +2495,6 @@ impl Connection {
}
} else if lr.password.is_empty() {
if err_msg.is_empty() {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) {
if let Some(keep_alive) =
self.prepare_terminal_login_for_authorization().await
{
return keep_alive;
}
}
self.try_start_cm(lr.my_id, lr.my_name, false);
} else {
self.send_login_error(
@@ -2570,7 +2508,7 @@ impl Connection {
return true;
}
if !self.validate_password(allow_logon_screen_password) {
self.update_failure_with_scope(failure, false, 0, FailureScope::Default);
self.update_failure(failure, false, 0);
self.check_update_temporary_password(false);
if err_msg.is_empty() {
self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG)
@@ -2583,7 +2521,7 @@ impl Connection {
.await;
}
} else {
self.update_failure_with_scope(failure, true, 0, FailureScope::Default);
self.update_failure(failure, true, 0);
if err_msg.is_empty() {
#[cfg(target_os = "linux")]
self.linux_headless_handle.wait_desktop_cm_ready().await;
@@ -3548,16 +3486,16 @@ impl Connection {
self.terminal_user_token = Some(TerminalUserToken::SelfUser);
None
} else {
Some(TERMINAL_OS_LOGIN_FAILED_MSG)
Some("The user is not an administrator.")
}
}
Ok(Err(e)) => {
log::error!("Failed to check if the user is an administrator: {}", e);
Some(TERMINAL_OS_LOGIN_FAILED_MSG)
Some("Failed to check if the user is an administrator.")
}
Err(e) => {
log::error!("Failed to get logon user token: {}", e);
Some(TERMINAL_OS_LOGIN_FAILED_MSG)
Some("Incorrect username or password.")
}
}
}
@@ -3593,146 +3531,6 @@ impl Connection {
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
async fn prepare_terminal_login_for_authorization(&mut self) -> Option<bool> {
if !self.terminal || self.terminal_user_token.is_some() {
return None;
}
#[derive(Copy, Clone)]
enum TerminalAuthorizationMode {
OsLogin {
failure: ((i32, i32, i32), i32),
scope: FailureScope,
},
SessionUser,
}
let normalized_username = self.lr.os_login.username.trim().to_owned();
let auth_mode = if should_use_terminal_os_login_scope(self.terminal, &normalized_username) {
// Check failure state
let failure_scope = FailureScope::TerminalOsLogin;
let (failure, res) = self.check_failure_with_scope(0, failure_scope).await;
if !res {
log::warn!(
"OS credential login blocked by failure policy: ip={} conn_id={} scope={:?}",
self.ip,
self.inner.id(),
failure_scope
);
// Terminal OS login is sensitive. Close this connection instead of keeping it
// alive for retries on the same socket after a rate-limit block.
return Some(false);
}
TerminalAuthorizationMode::OsLogin {
failure,
scope: failure_scope,
}
} else {
TerminalAuthorizationMode::SessionUser
};
let is_terminal_os_login = matches!(auth_mode, TerminalAuthorizationMode::OsLogin { .. });
let failure_scope = match auth_mode {
TerminalAuthorizationMode::OsLogin { scope, .. } => scope,
TerminalAuthorizationMode::SessionUser => FailureScope::Default,
};
let username = normalized_username;
let password = self.lr.os_login.password.clone();
let terminal_login_error = {
#[cfg(target_os = "windows")]
{
// Concurrency gate for terminal OS login with credentials, to prevent brute-force attacks.
let _os_login_concurrency_guard = if is_terminal_os_login {
let guard = try_acquire_os_credential_login_gate();
if guard.is_err() {
log::warn!(
"OS credential login blocked by concurrency gate: ip={} conn_id={} scope={:?}",
self.ip,
self.inner.id(),
failure_scope
);
self.send_login_error("Please try 1 minute later").await;
sleep(1.).await;
Self::post_alarm_audit(
AlarmAuditType::TerminalOsLoginConcurrency,
json!({
"ip": self.ip,
"id": self.lr.my_id.clone(),
"name": self.lr.my_name.clone(),
}),
);
return Some(false);
}
guard.ok()
} else {
None
};
self.fill_terminal_user_token(&username, &password)
}
#[cfg(not(target_os = "windows"))]
{
self.fill_terminal_user_token(&username, &password)
}
};
if let Some(msg) = terminal_login_error {
if let TerminalAuthorizationMode::OsLogin { failure, scope } = auth_mode {
self.update_failure_with_scope(failure, false, 0, scope);
}
let auth_context = if is_terminal_os_login {
"OS credential login verification"
} else {
"Terminal session-user authorization"
};
log::warn!(
"{} failed: ip={} conn_id={} scope={:?} msg='{}'",
auth_context,
self.ip,
self.inner.id(),
failure_scope,
msg
);
self.send_login_error(msg).await;
sleep(1.).await;
return Some(false);
}
if let TerminalAuthorizationMode::OsLogin { failure, scope } = auth_mode {
self.update_failure_with_scope(failure, true, 0, scope);
}
if let Some(is_user) =
terminal_service::is_service_specified_user(&self.terminal_service_id)
{
if let Some(user_token) = &self.terminal_user_token {
let has_service_token = user_token.to_terminal_service_token().is_some();
if is_user != has_service_token {
log::error!(
"Terminal service user mismatch: ip={} conn_id={} service_is_user={} has_service_token={}. The service ID may have been manually changed in the configuration, causing validation to fail.",
self.ip,
self.inner.id(),
is_user,
has_service_token
);
// No need to translate the following message, because it is in an abnormal case.
self.send_login_error("Terminal service user mismatch detected.")
.await;
sleep(1.).await;
return Some(false);
}
}
}
if is_terminal_os_login {
self.try_start_cm_ipc();
}
None
}
#[cfg(any(target_os = "android", target_os = "ios"))]
async fn prepare_terminal_login_for_authorization(&mut self) -> Option<bool> {
None
}
// Try to parse connection IP as IPv6 address, returning /64, /56, and /48 prefixes.
// Parsing an IPv4 address just returns None.
// note: we specifically don't use hbb_common::is_ipv6_str to avoid divergence issues
@@ -3759,37 +3557,18 @@ impl Connection {
Some((p64, p56, p48))
}
fn bump_failure_entry(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) {
if cur.0 == time {
cur.1 += 1;
cur.2 += 1;
} else {
cur.0 = time;
cur.1 = 1;
cur.2 += 1;
}
cur
}
fn update_failure(&self, failure: ((i32, i32, i32), i32), remove: bool, i: usize) {
self.update_failure_with_scope(failure, remove, i, FailureScope::Default);
}
fn update_failure_with_scope(
&self,
(failure, time): ((i32, i32, i32), i32),
remove: bool,
i: usize,
scope: FailureScope,
) {
let os_credential_scope = matches!(scope, FailureScope::TerminalOsLogin);
if os_credential_scope {
if !remove {
record_os_credential_failure(scope);
fn update_failure(&self, (failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) {
fn bump(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) {
if cur.0 == time {
cur.1 += 1;
cur.2 += 1;
} else {
cur.0 = time;
cur.1 = 1;
cur.2 += 1;
}
return;
cur
}
let map_mutex = &LOGIN_FAILURES[i];
if remove {
if failure.0 != 0 {
@@ -3810,15 +3589,14 @@ impl Connection {
let mut m = map_mutex.lock().unwrap();
for key in [p64, p56, p48] {
let cur = m.get(&key).copied().unwrap_or((0, 0, 0));
m.insert(key, Self::bump_failure_entry(cur, time));
m.insert(key, bump(cur, time));
}
let current_ip = m.get(&self.ip).copied().unwrap_or((0, 0, 0));
m.insert(self.ip.clone(), Self::bump_failure_entry(current_ip, time));
// Update full IP: bump from the *original* passed-in failure
m.insert(self.ip.clone(), bump(failure, time));
} else {
// Re-read the full IP bucket in case another failed attempt updated it.
// Update full IP: bump from the *original* passed-in failure
let mut m = map_mutex.lock().unwrap();
let current_ip = m.get(&self.ip).copied().unwrap_or((0, 0, 0));
m.insert(self.ip.clone(), Self::bump_failure_entry(current_ip, time));
m.insert(self.ip.clone(), bump(failure, time));
}
}
@@ -3858,50 +3636,8 @@ impl Connection {
}
async fn check_failure(&mut self, i: usize) -> (((i32, i32, i32), i32), bool) {
self.check_failure_with_scope(i, FailureScope::Default)
.await
}
async fn check_failure_with_scope(
&mut self,
i: usize,
scope: FailureScope,
) -> (((i32, i32, i32), i32), bool) {
let time = (get_time() / 60_000) as i32;
if matches!(scope, FailureScope::TerminalOsLogin) {
let decision = evaluate_os_credential_policy(scope, get_time());
let res = if decision.allowed {
true
} else {
log::warn!(
"OS credential login blocked by policy: ip={} conn_id={} i={} msg='{}'",
self.ip,
self.inner.id(),
i,
decision.login_error.as_deref().unwrap_or("")
);
if let Some(login_error) = decision.login_error {
// Rare branch and currently temporary response copy; translation can be added later if needed.
self.send_login_error(login_error).await;
}
if let Some(audit) = decision.audit {
// For OS blocked/backoff events, we currently emit one alarm report per blocked attempt.
// TODO: Add unified cumulative/aggregation fields across alarm producers.
Self::post_alarm_audit(
audit,
json!({
"ip": self.ip,
"id": self.lr.my_id.clone(),
"name": self.lr.my_name.clone(),
}),
);
}
false
};
return (((0, 0, 0), time), res);
}
// IPv6 addresses are cheap to make so we check prefix/netblock as well
if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() {
if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p64, 64, 60).await {
@@ -5247,9 +4983,6 @@ pub fn remove_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool {
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
// IPC bootstrap summary:
// - Resolve target CM socket (headless/non-headless, optional UID-scoped path on Linux).
// - Start CM when missing, then bridge bidirectional messages between this task and CM IPC.
async fn start_ipc(
mut rx_to_cm: mpsc::UnboundedReceiver<ipc::Data>,
tx_from_cm: mpsc::UnboundedSender<ipc::Data>,
@@ -5264,19 +4997,10 @@ async fn start_ipc(
}
sleep(1.).await;
}
#[cfg(target_os = "linux")]
let headless_cm = crate::is_server()
&& crate::platform::is_headless_allowed()
&& linux_desktop_manager::is_headless();
#[cfg(not(target_os = "linux"))]
let headless_cm = false;
let mut stream = None;
if !headless_cm {
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
stream = Some(s);
}
}
if stream.is_none() {
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
stream = Some(s);
} else {
#[allow(unused_mut)]
#[allow(unused_assignments)]
let mut args = vec!["--cm"];
@@ -5286,123 +5010,75 @@ async fn start_ipc(
// Cm run as user, wait until desktop session is ready.
#[cfg(target_os = "linux")]
if headless_cm {
if crate::platform::is_headless_allowed() && linux_desktop_manager::is_headless() {
let mut username = linux_desktop_manager::get_username();
loop {
if !username.is_empty() {
break;
}
// `_rx_desktop_ready` is used as a wake-up signal from desktop/session state changes
// (for example wait_desktop_cm_ready paths). It is not itself a proof of CM readiness.
// TODO:
// When `_rx_desktop_ready` is closed, `recv()` returns
// `None` immediately and this loop may spin if `username` remains empty.
// Keep behavior unchanged for now; if field reports appear, handle `Ok(None)` by
// breaking/returning to avoid hot-looping.
let _res = timeout(1_000, _rx_desktop_ready.recv()).await;
username = linux_desktop_manager::get_username();
}
let uid = {
let username_for_cmd = username.clone();
let mut uid_cmd = hbb_common::tokio::process::Command::new("id");
// TODO:
// Keep current behavior for now to minimize change risk.
// If usernames starting with '-' are observed in the field, prefer:
// `id -u -- <username>` to avoid option-parsing ambiguity.
// Already verified that `id -u -- <username>` works as expected on macOS and Ubuntu 24.04.
uid_cmd.arg("-u").arg(&username_for_cmd).kill_on_drop(true);
let output = timeout(10_000, uid_cmd.output())
.await
.map_err(|_| anyhow!("Timed out querying uid for {}", username))?
.map_err(|e| anyhow!("Failed to run `id -u {}`: {}", username, e))?;
if !output.status.success() {
bail!("Failed to query uid for {}", username);
}
let output = String::from_utf8_lossy(&output.stdout);
let output = run_cmds(&format!("id -u {}", &username))?;
let output = output.trim();
if output.parse::<u32>().is_err() {
bail!("Invalid uid {}", output);
if output.is_empty() || !output.parse::<i32>().is_ok() {
bail!("Invalid username {}", &username);
}
output.to_string()
};
user = Some((uid, username));
args = vec!["--cm-no-ui"];
}
#[cfg(target_os = "linux")]
let cm_uid: Option<u32> = match &user {
Some((uid, _)) => Some(
uid.parse::<u32>()
.map_err(|_| anyhow!("Invalid uid {}", uid))?,
),
None => None,
};
#[cfg(target_os = "linux")]
if let Some(uid) = cm_uid {
if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await {
let run_done;
if crate::platform::is_root() {
let mut res = Ok(None);
for _ in 0..10 {
#[cfg(not(any(target_os = "linux")))]
{
log::debug!("Start cm");
res = crate::platform::run_as_user(args.clone());
}
#[cfg(target_os = "linux")]
{
log::debug!("Start cm");
res = crate::platform::run_as_user(
args.clone(),
user.clone(),
None::<(&str, &str)>,
);
}
if res.is_ok() {
break;
}
log::error!("Failed to run cm: {res:?}");
sleep(1.).await;
}
if let Some(task) = res? {
super::CHILD_PROCESS.lock().unwrap().push(task);
}
run_done = true;
} else {
run_done = false;
}
if !run_done {
log::debug!("Start cm");
super::CHILD_PROCESS
.lock()
.unwrap()
.push(crate::run_me(args)?);
}
for _ in 0..20 {
sleep(0.3).await;
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
stream = Some(s);
break;
}
}
if stream.is_none() {
let run_done;
if crate::platform::is_root() {
let mut res = Ok(None);
for _ in 0..10 {
#[cfg(not(any(target_os = "linux")))]
{
log::debug!("Start cm");
res = crate::platform::run_as_user(args.clone());
}
#[cfg(target_os = "linux")]
{
log::debug!("Start cm");
res = crate::platform::run_as_user(
args.clone(),
user.clone(),
None::<(&str, &str)>,
);
}
if res.is_ok() {
break;
}
log::error!("Failed to run cm: {res:?}");
sleep(1.).await;
}
if let Some(task) = res? {
super::CHILD_PROCESS.lock().unwrap().push(task);
}
run_done = true;
} else {
run_done = false;
}
if !run_done {
log::debug!("Start cm");
super::CHILD_PROCESS
.lock()
.unwrap()
.push(crate::run_me(args)?);
}
for _ in 0..20 {
sleep(0.3).await;
#[cfg(target_os = "linux")]
{
if let Some(uid) = cm_uid {
if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await {
stream = Some(s);
break;
}
continue;
}
}
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
stream = Some(s);
break;
}
}
bail!("Failed to connect to connection manager");
}
}
if stream.is_none() {
bail!("Failed to connect to connection manager");
}
let _res = tx_stream_ready.send(()).await;
let mut stream = stream.ok_or(anyhow!("none stream"))?;
@@ -5485,8 +5161,6 @@ pub enum AlarmAuditType {
// MultipleLoginsAttemptsWithinOneMinute = 4,
// MultipleLoginsAttemptsWithinOneHour = 5,
ExceedIPv6PrefixAttempts = 6,
TerminalOsLoginBackoff = 7,
TerminalOsLoginConcurrency = 8,
}
pub enum FileAuditType {

View File

@@ -1,231 +0,0 @@
use crate::AlarmAuditType;
use hbb_common::get_time;
#[cfg(target_os = "windows")]
use hbb_common::tokio::sync::{Mutex as TokioMutex, OwnedMutexGuard};
use std::sync::Mutex;
#[cfg(target_os = "windows")]
use std::sync::Arc;
const OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS: i64 = 120 * 60 * 1_000;
const OS_CREDENTIAL_LOGIN_BACKOFF_BASE_SECONDS: i64 = 15;
const OS_CREDENTIAL_LOGIN_BACKOFF_MAX_SECONDS: i64 = 30 * 60;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum FailureScope {
Default,
TerminalOsLogin,
}
pub(crate) struct OsCredentialPolicyDecision {
pub allowed: bool,
pub login_error: Option<String>,
pub audit: Option<AlarmAuditType>,
}
#[derive(Copy, Clone, Debug, Default)]
struct OsCredentialFailureState {
total_failures: i32,
backoff_until_ms: Option<i64>,
last_failure_ms: Option<i64>,
}
lazy_static::lazy_static! {
static ref OS_CREDENTIAL_LOGIN_FAILURE_STATE: Mutex<OsCredentialFailureState> =
Mutex::new(OsCredentialFailureState::default());
}
#[cfg(target_os = "windows")]
lazy_static::lazy_static! {
static ref OS_CREDENTIAL_LOGIN_MUTEX: Arc<TokioMutex<()>> = Arc::new(TokioMutex::new(()));
}
fn is_os_credential_scope(scope: FailureScope) -> bool {
matches!(scope, FailureScope::TerminalOsLogin)
}
fn state_for_os_credential_scope(
scope: FailureScope,
) -> Option<&'static Mutex<OsCredentialFailureState>> {
if is_os_credential_scope(scope) {
Some(&OS_CREDENTIAL_LOGIN_FAILURE_STATE)
} else {
None
}
}
fn backoff_audit_type_for_scope(scope: FailureScope) -> Option<AlarmAuditType> {
match scope {
FailureScope::TerminalOsLogin => Some(AlarmAuditType::TerminalOsLoginBackoff),
FailureScope::Default => None,
}
}
fn os_credential_login_backoff_seconds(total_failures: i32) -> i64 {
if total_failures <= 2 {
return 0;
}
let exp = (total_failures - 3).min(7);
let seconds = OS_CREDENTIAL_LOGIN_BACKOFF_BASE_SECONDS * (1_i64 << exp);
seconds.min(OS_CREDENTIAL_LOGIN_BACKOFF_MAX_SECONDS)
}
fn normalize_backoff(state: &mut OsCredentialFailureState, now_ms: i64) {
if let Some(until_ms) = state.backoff_until_ms {
if until_ms <= now_ms {
state.backoff_until_ms = None;
}
}
}
fn reset_totals_on_idle(state: &mut OsCredentialFailureState, now_ms: i64) {
if let Some(last_ms) = state.last_failure_ms {
if now_ms.saturating_sub(last_ms) >= OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS {
state.total_failures = 0;
state.backoff_until_ms = None;
state.last_failure_ms = None;
}
}
}
fn allow_decision() -> OsCredentialPolicyDecision {
OsCredentialPolicyDecision {
allowed: true,
login_error: None,
audit: None,
}
}
fn block_decision(
login_error: String,
alarm_type: Option<AlarmAuditType>,
) -> OsCredentialPolicyDecision {
OsCredentialPolicyDecision {
allowed: false,
login_error: Some(login_error),
audit: alarm_type,
}
}
pub(crate) fn evaluate_os_credential_policy(
scope: FailureScope,
now_ms: i64,
) -> OsCredentialPolicyDecision {
if !is_os_credential_scope(scope) {
return allow_decision();
}
let Some(state_mutex) = state_for_os_credential_scope(scope) else {
return allow_decision();
};
let mut state = state_mutex.lock().unwrap();
reset_totals_on_idle(&mut state, now_ms);
normalize_backoff(&mut state, now_ms);
if let Some(until_ms) = state.backoff_until_ms {
let remaining_ms = (until_ms - now_ms).max(0);
let remaining_seconds = ((remaining_ms + 999) / 1_000).max(1);
let seconds_label = if remaining_seconds == 1 {
"second"
} else {
"seconds"
};
block_decision(
format!(
"Please try again in {} {}.",
remaining_seconds, seconds_label
),
backoff_audit_type_for_scope(scope),
)
} else {
allow_decision()
}
}
pub(crate) fn record_os_credential_failure(scope: FailureScope) {
if !is_os_credential_scope(scope) {
return;
}
let Some(state_mutex) = state_for_os_credential_scope(scope) else {
return;
};
let mut state = state_mutex.lock().unwrap();
let now_ms = get_time();
reset_totals_on_idle(&mut state, now_ms);
normalize_backoff(&mut state, now_ms);
state.total_failures = state.total_failures.saturating_add(1);
state.last_failure_ms = Some(now_ms);
let backoff_seconds = os_credential_login_backoff_seconds(state.total_failures);
if backoff_seconds > 0 {
state.backoff_until_ms = Some(now_ms + backoff_seconds * 1_000);
}
}
#[cfg(target_os = "windows")]
pub(crate) fn try_acquire_os_credential_login_gate() -> Result<OwnedMutexGuard<()>, ()> {
OS_CREDENTIAL_LOGIN_MUTEX
.clone()
.try_lock_owned()
.map_err(|_| ())
}
#[cfg(test)]
mod tests {
use super::*;
static TEST_MUTEX: Mutex<()> = Mutex::new(());
fn clear_os_credential_failure_state(scope: FailureScope) {
if let Some(state_mutex) = state_for_os_credential_scope(scope) {
*state_mutex.lock().unwrap() = OsCredentialFailureState::default();
}
}
#[test]
fn os_credential_policy_prioritizes_backoff() {
let _guard = TEST_MUTEX.lock().unwrap();
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
let now_ms = get_time();
for _ in 0..3 {
record_os_credential_failure(FailureScope::TerminalOsLogin);
}
let decision = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms);
assert!(!decision.allowed);
assert!(decision.login_error.is_some());
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
}
#[test]
fn os_credential_policy_idle_window_resets_total_counter() {
let _guard = TEST_MUTEX.lock().unwrap();
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
for _ in 0..13 {
record_os_credential_failure(FailureScope::TerminalOsLogin);
}
let blocked = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, get_time());
assert!(!blocked.allowed);
let after_failures_ms = get_time();
let after_idle_ms = after_failures_ms + OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS + 1_000;
let allowed = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, after_idle_ms);
assert!(allowed.allowed);
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
}
#[test]
fn os_credential_policy_audits_every_backoff_block() {
let _guard = TEST_MUTEX.lock().unwrap();
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
for _ in 0..3 {
record_os_credential_failure(FailureScope::TerminalOsLogin);
}
let now_ms = get_time();
let first = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms);
let second = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms + 1_000);
assert!(!first.allowed);
assert!(!second.allowed);
assert!(first.audit.is_some());
assert!(second.audit.is_some());
clear_os_credential_failure_state(FailureScope::TerminalOsLogin);
}
}

View File

@@ -1,11 +1,3 @@
use crate::{
ipc::{self, new_listener, Connection, Data, DataPortableService, IPC_TOKEN_LEN},
platform::{
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
set_path_permission_for_portable_service_shmem_file,
validate_path_for_portable_service_shmem_dir,
},
};
use core::slice;
use hbb_common::{
allow_err,
@@ -23,26 +15,26 @@ use shared_memory::*;
use std::{
mem::size_of,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc, Mutex,
},
path::Path,
sync::{Arc, Mutex},
time::Duration,
};
use winapi::{
shared::minwindef::{BOOL, FALSE, TRUE},
um::winuser::{self, CURSORINFO, PCURSORINFO},
};
use windows::Win32::Storage::FileSystem::{FILE_GENERIC_EXECUTE, FILE_GENERIC_READ};
use crate::{
ipc::{self, new_listener, Connection, Data, DataPortableService},
platform::set_path_permission,
};
use super::video_qos;
const SIZE_COUNTER: usize = size_of::<i32>() * 2;
const FRAME_ALIGN: usize = 64;
const ADDR_IPC_TOKEN: usize = 0;
const ADDR_CURSOR_PARA: usize = ADDR_IPC_TOKEN + IPC_TOKEN_LEN;
const ADDR_CURSOR_PARA: usize = 0;
const ADDR_CURSOR_COUNTER: usize = ADDR_CURSOR_PARA + size_of::<CURSORINFO>();
const ADDR_CAPTURER_PARA: usize = ADDR_CURSOR_COUNTER + SIZE_COUNTER;
@@ -52,186 +44,12 @@ const ADDR_CAPTURE_FRAME_COUNTER: usize = ADDR_CAPTURE_WOULDBLOCK + size_of::<i3
const ADDR_CAPTURE_FRAME: usize =
(ADDR_CAPTURE_FRAME_COUNTER + SIZE_COUNTER + FRAME_ALIGN - 1) / FRAME_ALIGN * FRAME_ALIGN;
const MIN_RUNTIME_SHMEM_LEN: usize = ADDR_CAPTURE_FRAME + FRAME_ALIGN;
const IPC_SUFFIX: &str = "_portable_service";
pub const SHMEM_NAME: &str = "_portable_service";
pub const SHMEM_ARG_PREFIX: &str = "--portable-service-shmem-name=";
const SHMEM_PARENT_DIR: &str = "portable_service_shmem";
const SHMEM_NAME_MAX_LEN: usize = 64;
const MAX_NACK: usize = 3;
const PORTABLE_SERVICE_STARTUP_TIMEOUT: Duration = Duration::from_secs(15);
const MAX_DXGI_FAIL_TIME: usize = 5;
#[inline]
fn is_valid_portable_service_shmem_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= SHMEM_NAME_MAX_LEN
&& name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
}
#[inline]
pub fn portable_service_shmem_arg(name: &str) -> String {
format!("{SHMEM_ARG_PREFIX}{name}")
}
#[inline]
fn is_valid_portable_service_ipc_token(token: &str) -> bool {
token.len() == IPC_TOKEN_LEN
&& token
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
}
#[inline]
fn read_ipc_token_from_shmem(shmem: &SharedMemory) -> Option<String> {
if shmem.len() < ADDR_IPC_TOKEN + IPC_TOKEN_LEN {
log::error!(
"Portable service shared memory too small: len={}, need>={}",
shmem.len(),
ADDR_IPC_TOKEN + IPC_TOKEN_LEN
);
return None;
}
unsafe {
let ptr = shmem.as_ptr().add(ADDR_IPC_TOKEN);
let bytes = slice::from_raw_parts(ptr, IPC_TOKEN_LEN);
let end = bytes
.iter()
.position(|byte| *byte == 0)
.unwrap_or(IPC_TOKEN_LEN);
if end == 0 {
return None;
}
let token = std::str::from_utf8(&bytes[..end]).ok()?.to_owned();
if is_valid_portable_service_ipc_token(&token) {
Some(token)
} else {
None
}
}
}
#[inline]
fn validate_runtime_shmem_layout(shmem: &SharedMemory) -> ResultType<()> {
if shmem.len() < MIN_RUNTIME_SHMEM_LEN {
bail!(
"Portable service shared memory too small for runtime layout: len={}, need>={}",
shmem.len(),
MIN_RUNTIME_SHMEM_LEN
);
}
Ok(())
}
#[inline]
fn is_valid_capture_frame_length(shmem_len: usize, frame_len: usize) -> bool {
let frame_capacity = shmem_len.saturating_sub(ADDR_CAPTURE_FRAME);
frame_len > 0 && frame_len <= frame_capacity
}
#[inline]
fn shared_memory_flink_path_by_name(name: &str) -> ResultType<PathBuf> {
let mut dir = crate::platform::user_accessible_folder()?;
dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone());
dir = dir.join(SHMEM_PARENT_DIR);
Ok(dir.join(format!("shared_memory{}", name)))
}
#[inline]
fn remove_shared_memory_flink_once(name: &str, log_on_error: bool, log_context: &str) -> bool {
let flink = match shared_memory_flink_path_by_name(name) {
Ok(path) => path,
Err(err) => {
if log_on_error {
log::warn!(
"{} failed to resolve portable service shared-memory flink path for '{}': {}",
log_context,
name,
err
);
}
return false;
}
};
match std::fs::remove_file(&flink) {
Ok(()) => {
log::info!(
"{} removed portable service shared-memory flink artifact: {:?}",
log_context,
flink
);
true
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => true,
Err(err) => {
if log_on_error {
log::warn!(
"{} failed to remove portable service shared-memory flink artifact {:?}: {}",
log_context,
flink,
err
);
}
false
}
}
}
#[inline]
fn write_ipc_token_to_shmem(shmem: &SharedMemory, token: &str) -> ResultType<()> {
if !is_valid_portable_service_ipc_token(token) {
bail!("Invalid portable service ipc token");
}
shmem.write(ADDR_IPC_TOKEN, token.as_bytes());
Ok(())
}
#[inline]
fn clear_ipc_token_in_shmem(shmem: &SharedMemory) {
shmem.write(ADDR_IPC_TOKEN, &[0u8; IPC_TOKEN_LEN]);
}
#[inline]
fn portable_service_arg_value_candidate_from_arg<'a>(
arg: &'a str,
prefix: &str,
) -> Option<&'a str> {
let mut value = arg.strip_prefix(prefix)?;
value = value.trim_start();
value = value
.strip_prefix('"')
.or_else(|| value.strip_prefix('\''))
.unwrap_or(value);
value = value.split_whitespace().next().unwrap_or_default();
value = value.trim_matches(|c| c == '"' || c == '\'');
Some(value)
}
#[inline]
pub fn portable_service_shmem_name_from_args() -> Option<String> {
for arg in std::env::args() {
if let Some(value) = portable_service_arg_value_candidate_from_arg(&arg, SHMEM_ARG_PREFIX) {
if is_valid_portable_service_shmem_name(value) {
return Some(value.to_owned());
}
log::error!(
"Invalid portable service shared memory name argument: '{}'",
value
);
return None;
}
}
None
}
#[inline]
pub fn has_portable_service_shmem_arg() -> bool {
std::env::args().any(|arg| arg.starts_with(SHMEM_ARG_PREFIX))
}
pub struct SharedMemory {
inner: Shmem,
}
@@ -274,27 +92,7 @@ impl SharedMemory {
}
};
log::info!("Create shared memory, size: {}, flink: {}", size, flink);
if let Err(err) = set_path_permission_for_portable_service_shmem_file(Path::new(&flink)) {
// Release shmem handle first so best-effort flink cleanup has a chance to succeed.
drop(shmem);
match std::fs::remove_file(&flink) {
Ok(()) => {
log::info!(
"Create cleanup removed portable service shared-memory flink artifact: {}",
flink
);
}
Err(remove_err) if remove_err.kind() == std::io::ErrorKind::NotFound => {}
Err(remove_err) => {
log::warn!(
"Create cleanup failed to remove portable service shared-memory flink artifact {}: {}",
flink,
remove_err
);
}
}
return Err(err);
}
set_path_permission(Path::new(&flink), "F").ok();
Ok(SharedMemory { inner: shmem })
}
@@ -322,18 +120,9 @@ impl SharedMemory {
fn flink(name: String) -> ResultType<String> {
let mut dir = crate::platform::user_accessible_folder()?;
dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone());
dir = dir.join(SHMEM_PARENT_DIR);
let parent_created = !dir.exists();
if parent_created {
std::fs::create_dir_all(&dir)?;
}
if parent_created || crate::platform::is_root() {
// Harden parent ACL on first provisioning and periodically on SYSTEM path.
set_path_permission_for_portable_service_shmem_dir(&dir)?;
} else {
// Existing parents still need type/reparse validation. Non-SYSTEM callers may lack
// WRITE_DAC on a valid parent, so avoid rebuilding the ACL here.
validate_path_for_portable_service_shmem_dir(&dir)?;
if !dir.exists() {
std::fs::create_dir(&dir)?;
set_path_permission(&dir, "F").ok();
}
Ok(dir
.join(format!("shared_memory{}", name))
@@ -443,45 +232,16 @@ pub mod server {
lazy_static::lazy_static! {
static ref EXIT: Arc<Mutex<bool>> = Default::default();
static ref FORCE_EXIT_ARMED: AtomicBool = AtomicBool::new(false);
}
pub fn run_portable_service() {
let shmem_name = match portable_service_shmem_name_from_args() {
Some(name) => name,
None => {
if has_portable_service_shmem_arg() {
log::error!(
"Invalid portable service shared memory argument, aborting startup"
);
} else {
log::error!(
"Missing portable service shared memory argument, aborting startup"
);
}
return;
}
};
let shmem = match SharedMemory::open_existing(&shmem_name) {
let shmem = match SharedMemory::open_existing(SHMEM_NAME) {
Ok(shmem) => Arc::new(shmem),
Err(e) => {
log::error!("Failed to open existing shared memory: {:?}", e);
return;
}
};
if let Err(e) = validate_runtime_shmem_layout(shmem.as_ref()) {
log::error!("{}", e);
return;
}
let ipc_token = match read_ipc_token_from_shmem(shmem.as_ref()) {
Some(token) => token,
None => {
log::error!(
"Missing portable service ipc token in shared memory, aborting startup"
);
return;
}
};
let shmem1 = shmem.clone();
let shmem2 = shmem.clone();
let mut threads = vec![];
@@ -491,24 +251,17 @@ pub mod server {
threads.push(std::thread::spawn(|| {
run_capture(shmem2);
}));
threads.push(std::thread::spawn(move || {
run_ipc_client(ipc_token);
threads.push(std::thread::spawn(|| {
run_ipc_client();
}));
// Detached shutdown watchdog:
// - gives graceful shutdown/cleanup a short window
// - force-exits the process if workers are still stuck
std::thread::spawn(|| {
threads.push(std::thread::spawn(|| {
run_exit_check();
});
}));
let record_pos_handle = crate::input_service::try_start_record_cursor_pos();
// Arm forced-exit watchdog only for worker join phase.
// Once join phase completes, cleanup should not be interrupted by forced exit.
FORCE_EXIT_ARMED.store(true, Ordering::SeqCst);
for th in threads.drain(..) {
th.join().ok();
log::info!("thread joined");
}
FORCE_EXIT_ARMED.store(false, Ordering::SeqCst);
crate::input_service::try_stop_record_cursor_pos();
if let Some(handle) = record_pos_handle {
@@ -517,47 +270,16 @@ pub mod server {
Err(e) => log::error!("record_pos_handle join error {:?}", &e),
}
}
drop(shmem);
remove_shared_memory_flink_with_retry(&shmem_name);
}
fn run_exit_check() {
const FORCED_EXIT_DELAY: Duration = Duration::from_secs(3);
loop {
if EXIT.lock().unwrap().clone() {
break;
std::thread::sleep(Duration::from_millis(50));
std::process::exit(0);
}
std::thread::sleep(Duration::from_millis(50));
}
// Fallback only: normal shutdown path should complete and process should exit naturally.
// This forced exit is a last resort when worker threads are stuck and graceful teardown
// does not finish in time.
std::thread::sleep(FORCED_EXIT_DELAY);
if FORCE_EXIT_ARMED.load(Ordering::SeqCst) {
log::warn!(
"Portable service shutdown watchdog fallback triggered: forcing process exit after {:?}",
FORCED_EXIT_DELAY
);
std::process::exit(0);
}
}
fn remove_shared_memory_flink_with_retry(name: &str) {
const MAX_RETRY: usize = 20;
const RETRY_INTERVAL: Duration = Duration::from_millis(200);
for attempt in 0..MAX_RETRY {
let is_last_attempt = attempt + 1 == MAX_RETRY;
if remove_shared_memory_flink_once(name, is_last_attempt, "SYSTEM cleanup") {
return;
}
if !is_last_attempt {
std::thread::sleep(RETRY_INTERVAL);
}
}
log::warn!(
"SYSTEM cleanup failed to remove portable service shared-memory flink artifact '{}' after retry",
name
);
}
fn run_get_cursor_info(shmem: Arc<SharedMemory>) {
@@ -664,17 +386,6 @@ pub mod server {
match c.as_mut().map(|f| f.frame(spf)) {
Some(Ok(f)) => match f {
Frame::PixelBuffer(f) => {
let frame_capacity = shmem.len().saturating_sub(ADDR_CAPTURE_FRAME);
if f.data().len() > frame_capacity {
log::error!(
"Portable service capture frame exceeds shared memory capacity: frame_len={}, capacity={}, shmem_len={}",
f.data().len(),
frame_capacity,
shmem.len()
);
*EXIT.lock().unwrap() = true;
return;
}
utils::set_frame_info(
&shmem,
FrameInfo {
@@ -725,33 +436,17 @@ pub mod server {
}
#[tokio::main(flavor = "current_thread")]
async fn run_ipc_client(ipc_token: String) {
async fn run_ipc_client() {
use DataPortableService::*;
let postfix = IPC_SUFFIX;
match ipc::connect(1000, postfix).await {
Ok(mut stream) => {
if let Err(err) =
ipc::portable_service_ipc_handshake_as_client(&mut stream, &ipc_token).await
{
log::error!("portable service ipc handshake failed: {}", err);
*EXIT.lock().unwrap() = true;
return;
}
let mut timer =
crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1)));
let mut nack = 0;
loop {
if *EXIT.lock().unwrap() {
log::info!("Portable service EXIT signaled, closing ipc client loop");
stream
.send(&Data::DataPortableService(WillClose))
.await
.ok();
break;
}
tokio::select! {
res = stream.next() => {
match res {
@@ -831,11 +526,7 @@ pub mod client {
lazy_static::lazy_static! {
static ref RUNNING: Arc<Mutex<bool>> = Default::default();
static ref STARTING: Arc<Mutex<bool>> = Default::default();
static ref STARTING_TOKEN: AtomicU64 = AtomicU64::new(0);
static ref SHMEM: Arc<Mutex<Option<SharedMemory>>> = Default::default();
static ref SHMEM_RUNTIME_NAME: Arc<Mutex<Option<String>>> = Default::default();
static ref IPC_RUNTIME_TOKEN: Arc<Mutex<Option<String>>> = Default::default();
static ref SENDER : Mutex<mpsc::UnboundedSender<ipc::Data>> = Mutex::new(client::start_ipc_server());
static ref QUICK_SUPPORT: Arc<Mutex<bool>> = Default::default();
}
@@ -845,176 +536,12 @@ pub mod client {
Logon(String, String),
}
fn has_running_portable_service_process() -> bool {
let app_exe = format!("{}.exe", crate::get_app_name().to_lowercase());
!crate::platform::get_pids_of_process_with_first_arg(&app_exe, "--portable-service")
.is_empty()
}
#[inline]
fn next_portable_service_shmem_name() -> String {
format!(
"{}_{}_{:08x}",
crate::portable_service::SHMEM_NAME,
std::process::id(),
hbb_common::rand::random::<u32>()
)
}
#[inline]
fn set_runtime_ipc_token(token: String) {
*IPC_RUNTIME_TOKEN.lock().unwrap() = Some(token);
}
#[inline]
fn schedule_remove_runtime_shmem_flink_retry(name: String) {
std::thread::spawn(move || {
const MAX_RETRY: usize = 20;
const RETRY_INTERVAL: Duration = Duration::from_millis(200);
for _ in 0..MAX_RETRY {
std::thread::sleep(RETRY_INTERVAL);
if remove_shared_memory_flink_once(&name, false, "Client cleanup") {
return;
}
}
log::warn!(
"Failed to remove portable service shared-memory flink artifact '{}' after retry",
name
);
});
}
#[inline]
fn clear_runtime_shmem_state() {
let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap();
let mut shmem_lock = SHMEM.lock().unwrap();
if let Some(shmem) = shmem_lock.as_mut() {
clear_ipc_token_in_shmem(shmem);
}
*shmem_lock = None;
let runtime_name = SHMEM_RUNTIME_NAME.lock().unwrap().take();
*runtime_token = None;
drop(runtime_token);
drop(shmem_lock);
if let Some(name) = runtime_name.as_deref() {
if !remove_shared_memory_flink_once(name, true, "Client cleanup") {
schedule_remove_runtime_shmem_flink_retry(name.to_owned());
}
}
}
#[inline]
fn consume_runtime_ipc_token_if_match(candidate: &str) -> (bool, Option<String>) {
let mut token = IPC_RUNTIME_TOKEN.lock().unwrap();
if !token
.as_deref()
.is_some_and(|expected| ipc::constant_time_ipc_token_eq(expected, candidate))
{
return (false, None);
}
let mut shmem_lock = SHMEM.lock().unwrap();
let matched_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone();
*token = None;
if let Some(shmem) = shmem_lock.as_mut() {
clear_ipc_token_in_shmem(shmem);
}
(true, matched_shmem_name)
}
#[inline]
fn restore_runtime_ipc_token_after_failed_handshake(
token: &str,
expected_shmem_name: Option<&str>,
) {
let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap();
if let Some(current) = runtime_token.as_deref() {
if current != token {
log::debug!(
"Skip restoring portable service ipc token after handshake failure: runtime token has changed to a newer value"
);
return;
}
}
let mut shmem_lock = SHMEM.lock().unwrap();
let current_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone();
if current_shmem_name.as_deref() != expected_shmem_name {
if runtime_token.as_deref() == Some(token) {
*runtime_token = None;
}
log::debug!(
"Skip restoring portable service ipc token after handshake failure: shared-memory instance has changed"
);
return;
}
let shmem_write_error = if let Some(shmem) = shmem_lock.as_mut() {
write_ipc_token_to_shmem(shmem, token)
.err()
.map(|err| err.to_string())
} else {
Some("shared memory unavailable".to_owned())
};
if let Some(err) = shmem_write_error {
if runtime_token.as_deref() == Some(token) {
*runtime_token = None;
}
log::warn!(
"Failed to restore portable service ipc token after handshake failure: {}",
err
);
return;
}
*runtime_token = Some(token.to_owned());
}
#[inline]
fn schedule_starting_timeout_reset(launch_token: u64) {
std::thread::spawn(move || {
std::thread::sleep(PORTABLE_SERVICE_STARTUP_TIMEOUT);
let should_reset = {
// Guard against stale watchdogs from previous launches:
// only the watchdog that matches the latest STARTING_TOKEN may reset STARTING.
let current_token = STARTING_TOKEN.load(Ordering::SeqCst);
// Keep lock guards in explicit short scopes to make it obvious
// there is no nested lock ordering (and to avoid Copilot false positives).
let starting = { *STARTING.lock().unwrap() };
let running = { *RUNNING.lock().unwrap() };
current_token == launch_token && starting && !running
};
if should_reset {
log::warn!(
"Portable service startup timeout before IPC ready, reset STARTING state"
);
*STARTING.lock().unwrap() = false;
}
});
}
// Launch flow summary:
// 1) Prepare/reset runtime shared memory + IPC token.
// 2) Start helper process (direct or logon) with shmem argument.
// 3) Keep STARTING=true until IPC ping/pong marks RUNNING, or timeout watchdog resets it.
pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> {
log::info!("start portable service");
let launch_token = {
// Keep lock guards in explicit short scopes to make it obvious
// there is no nested lock ordering (and to avoid Copilot false positives).
let running = { *RUNNING.lock().unwrap() };
let mut starting = STARTING.lock().unwrap();
if *starting && !running && !has_running_portable_service_process() {
log::warn!(
"Detected stale portable service STARTING state without running process, reset it"
);
*starting = false;
}
if *starting || running {
bail!("already running");
}
*starting = true;
STARTING_TOKEN.fetch_add(1, Ordering::SeqCst) + 1
};
let start_result = (|| -> ResultType<()> {
clear_runtime_shmem_state();
let mut shmem_lock = SHMEM.lock().unwrap();
if RUNNING.lock().unwrap().clone() {
bail!("already running");
}
if SHMEM.lock().unwrap().is_none() {
let displays = scrap::Display::all()?;
if displays.is_empty() {
bail!("no display available!");
@@ -1031,153 +558,84 @@ pub mod client {
}
}
}
let shmem_size =
utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align).max(MIN_RUNTIME_SHMEM_LEN);
let shmem_name = next_portable_service_shmem_name();
if !is_valid_portable_service_shmem_name(&shmem_name) {
bail!("Generated invalid portable service shared memory name");
}
let ipc_token = ipc::generate_one_time_ipc_token()?;
let shmem_size = utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align);
// os error 112, no enough space
*shmem_lock = Some(crate::portable_service::SharedMemory::create(
&shmem_name,
*SHMEM.lock().unwrap() = Some(crate::portable_service::SharedMemory::create(
crate::portable_service::SHMEM_NAME,
shmem_size,
)?);
*SHMEM_RUNTIME_NAME.lock().unwrap() = Some(shmem_name);
shutdown_hooks::add_shutdown_hook(drop_portable_service_shared_memory);
let shmem_name = SHMEM_RUNTIME_NAME
.lock()
.unwrap()
.clone()
.ok_or_else(|| anyhow!("portable service shared memory name is unavailable"))?;
let init_token_result = if let Some(shmem) = shmem_lock.as_mut() {
unsafe {
libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _);
}
write_ipc_token_to_shmem(shmem, &ipc_token)
} else {
Ok(())
};
if let Err(e) = init_token_result {
drop(shmem_lock);
clear_runtime_shmem_state();
bail!(
"Failed to initialize portable service ipc token in shared memory: {}",
e
);
};
drop(shmem_lock);
set_runtime_ipc_token(ipc_token.clone());
let portable_service_arg = format!(
"--portable-service {}",
crate::portable_service::portable_service_shmem_arg(&shmem_name)
);
{
let _sender = SENDER.lock().unwrap();
}
match para {
StartPara::Direct => {
match crate::platform::run_background(
&std::env::current_exe()?.to_string_lossy().to_string(),
&portable_service_arg,
) {
Ok(true) => {}
Ok(false) => {
clear_runtime_shmem_state();
bail!("Failed to run portable service process");
}
Err(e) => {
clear_runtime_shmem_state();
bail!("Failed to run portable service process: {}", e);
}
}
}
StartPara::Logon(username, password) => {
#[allow(unused_mut)]
let mut exe = std::env::current_exe()?.to_string_lossy().to_string();
#[cfg(feature = "flutter")]
{
if let Some(dir) = Path::new(&exe).parent() {
if let Err(err) = set_path_permission(
Path::new(dir),
FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0,
) {
clear_runtime_shmem_state();
bail!("Failed to set permission of {:?}: {}", dir, err);
}
}
}
#[cfg(not(feature = "flutter"))]
if let Some((dir, dst)) =
crate::platform::windows::portable_service_logon_helper_paths()
{
let cleanup_helper_artifacts = || {
if Path::new(&exe) != dst {
std::fs::remove_file(&dst).ok();
}
std::fs::remove_dir(&dir).ok();
};
let mut use_logon_helper_exe = false;
if let Err(err) = std::fs::create_dir_all(&dir) {
log::warn!(
"Failed to create portable service logon helper dir {:?}: {}",
dir,
err
);
} else if let Err(err) = std::fs::copy(&exe, &dst) {
log::warn!(
"Failed to copy portable service logon helper binary from '{}' to {:?}: {}",
exe,
dst,
err
);
cleanup_helper_artifacts();
} else if !dst.exists() {
log::warn!(
"Portable service logon helper binary missing after copy: {:?}",
dst
);
cleanup_helper_artifacts();
} else if let Err(err) =
set_path_permission(&dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0)
{
log::warn!(
"Failed to set portable service logon helper path permission for {:?}: {}",
dir,
err
);
cleanup_helper_artifacts();
} else {
use_logon_helper_exe = true;
}
if use_logon_helper_exe {
exe = dst.to_string_lossy().to_string();
}
}
if let Err(e) = crate::platform::windows::create_process_with_logon(
username.as_str(),
password.as_str(),
&exe,
&portable_service_arg,
) {
clear_runtime_shmem_state();
bail!("Failed to run portable service process: {}", e);
}
}
}
schedule_starting_timeout_reset(launch_token);
Ok(())
})();
if start_result.is_err() {
*STARTING.lock().unwrap() = false;
}
start_result
if let Some(shmem) = SHMEM.lock().unwrap().as_mut() {
unsafe {
libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _);
}
}
match para {
StartPara::Direct => {
if let Err(e) = crate::platform::run_background(
&std::env::current_exe()?.to_string_lossy().to_string(),
"--portable-service",
) {
*SHMEM.lock().unwrap() = None;
bail!("Failed to run portable service process: {}", e);
}
}
StartPara::Logon(username, password) => {
#[allow(unused_mut)]
let mut exe = std::env::current_exe()?.to_string_lossy().to_string();
#[cfg(feature = "flutter")]
{
if let Some(dir) = Path::new(&exe).parent() {
if set_path_permission(Path::new(dir), "RX").is_err() {
*SHMEM.lock().unwrap() = None;
bail!("Failed to set permission of {:?}", dir);
}
}
}
#[cfg(not(feature = "flutter"))]
match hbb_common::directories_next::UserDirs::new() {
Some(user_dir) => {
let dir = user_dir
.home_dir()
.join("AppData")
.join("Local")
.join("rustdesk-sciter");
if std::fs::create_dir_all(&dir).is_ok() {
let dst = dir.join("rustdesk.exe");
if std::fs::copy(&exe, &dst).is_ok() {
if dst.exists() {
if set_path_permission(&dir, "RX").is_ok() {
exe = dst.to_string_lossy().to_string();
}
}
}
}
}
None => {}
}
if let Err(e) = crate::platform::windows::create_process_with_logon(
username.as_str(),
password.as_str(),
&exe,
"--portable-service",
) {
*SHMEM.lock().unwrap() = None;
bail!("Failed to run portable service process: {}", e);
}
}
}
let _sender = SENDER.lock().unwrap();
Ok(())
}
pub extern "C" fn drop_portable_service_shared_memory() {
// https://stackoverflow.com/questions/35980148/why-does-an-atexit-handler-panic-when-it-accesses-stdout
// Please make sure there is no print in the call stack
clear_runtime_shmem_state();
let mut lock = SHMEM.lock().unwrap();
if lock.is_some() {
*lock = None;
}
}
pub fn set_quick_support(v: bool) {
@@ -1197,11 +655,7 @@ pub mod client {
let mut option = SHMEM.lock().unwrap();
if let Some(shmem) = option.as_mut() {
unsafe {
libc::memset(
shmem.as_ptr().add(ADDR_CURSOR_PARA) as _,
0,
shmem.len().saturating_sub(ADDR_CURSOR_PARA) as _,
);
libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _);
}
utils::set_para(
shmem,
@@ -1248,19 +702,6 @@ pub mod client {
if utils::counter_ready(base.add(ADDR_CAPTURE_FRAME_COUNTER)) {
let frame_info_ptr = shmem.as_ptr().add(ADDR_CAPTURE_FRAME_INFO);
let frame_info = frame_info_ptr as *const FrameInfo;
let frame_len = (*frame_info).length;
if !is_valid_capture_frame_length(shmem.len(), frame_len) {
log::error!(
"Portable service frame length exceeds shared memory capacity: frame_len={}, shmem_len={}, frame_addr={}",
frame_len,
shmem.len(),
ADDR_CAPTURE_FRAME
);
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"invalid portable service frame length".to_string(),
));
}
if (*frame_info).width != self.width || (*frame_info).height != self.height {
log::info!(
"skip frame, ({},{}) != ({},{})",
@@ -1275,7 +716,7 @@ pub mod client {
));
}
let frame_ptr = base.add(ADDR_CAPTURE_FRAME);
let data = slice::from_raw_parts(frame_ptr, frame_len);
let data = slice::from_raw_parts(frame_ptr, (*frame_info).length);
Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
data,
self.width,
@@ -1337,49 +778,10 @@ pub mod client {
Some(result) = incoming.next() => {
match result {
Ok(stream) => {
let mut stream = Connection::new(stream);
if !ipc::authorize_windows_portable_service_ipc_connection(
&stream, postfix,
) {
continue;
}
let mut consumed_token: Option<String> = None;
let mut consumed_token_shmem_name: Option<String> = None;
let handshake_result =
ipc::portable_service_ipc_handshake_as_server(
&mut stream,
|token| {
let (matched, matched_shmem_name) =
consume_runtime_ipc_token_if_match(token);
if matched {
consumed_token = Some(token.to_owned());
consumed_token_shmem_name = matched_shmem_name;
true
} else {
false
}
},
)
.await;
if let Err(err) = handshake_result {
if let Some(token) = consumed_token.as_deref() {
restore_runtime_ipc_token_after_failed_handshake(
token,
consumed_token_shmem_name.as_deref(),
);
*STARTING.lock().unwrap() = false;
}
log::warn!(
"Rejected portable service ipc connection due to token handshake failure: postfix={}, err={}",
postfix,
err
);
continue;
}
log::info!("Got portable service ipc connection");
let rx_clone = rx.clone();
tokio::spawn(async move {
let mut stream = stream;
let mut stream = Connection::new(stream);
let postfix = postfix.to_owned();
let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1)));
let mut nack = 0;
@@ -1403,7 +805,6 @@ pub mod client {
Pong => {
nack = 0;
*RUNNING.lock().unwrap() = true;
*STARTING.lock().unwrap() = false;
},
ConnCount(None) => {
if !quick_support {
@@ -1440,7 +841,6 @@ pub mod client {
}
}
*RUNNING.lock().unwrap() = false;
*STARTING.lock().unwrap() = false;
});
}
Err(err) => {
@@ -1590,23 +990,3 @@ pub struct FrameInfo {
width: usize,
height: usize,
}
#[cfg(test)]
mod tests {
use super::{is_valid_capture_frame_length, ADDR_CAPTURE_FRAME};
#[test]
fn test_is_valid_capture_frame_length_rejects_zero_length() {
assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 1024, 0));
}
#[test]
fn test_is_valid_capture_frame_length_rejects_out_of_bounds_length() {
assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 17));
}
#[test]
fn test_is_valid_capture_frame_length_accepts_in_bounds_length() {
assert!(is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 16));
}
}

View File

@@ -185,13 +185,9 @@ pub mod client {
pub mod service {
use super::*;
use hbb_common::lazy_static;
#[cfg(target_os = "linux")]
use parity_tokio_ipc::Connection as RawIpcConnection;
use scrap::wayland::{
pipewire::RDP_SESSION_INFO, remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop,
};
#[cfg(target_os = "linux")]
use std::os::unix::io::AsRawFd;
use std::{collections::HashMap, sync::Mutex};
lazy_static::lazy_static! {
@@ -606,10 +602,7 @@ pub mod service {
}
DataKeyboard::KeyDown(enigo::Key::Raw(code)) => {
if *code < 8 {
log::error!(
"Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping",
code
);
log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code);
} else {
let down_event = InputEvent::new(EventType::KEY, *code - 8, 1);
allow_err!(keyboard.emit(&[down_event]));
@@ -617,10 +610,7 @@ pub mod service {
}
DataKeyboard::KeyUp(enigo::Key::Raw(code)) => {
if *code < 8 {
log::error!(
"Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping",
code
);
log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code);
} else {
let up_event = InputEvent::new(EventType::KEY, *code - 8, 0);
allow_err!(keyboard.emit(&[up_event]));
@@ -919,35 +909,6 @@ pub mod service {
});
}
#[cfg(target_os = "linux")]
fn authorize_uinput_peer(postfix: &str, stream: &RawIpcConnection) -> bool {
if !hbb_common::config::is_service_ipc_postfix(postfix) {
return true;
}
let peer_uid = ipc::peer_uid_from_fd(stream.as_raw_fd());
let active_uid = crate::platform::linux::get_active_userid_fresh()
.trim()
.parse::<u32>()
.ok();
let authorized =
peer_uid.is_some_and(|uid| ipc::is_allowed_service_peer_uid(uid, active_uid));
if !authorized {
crate::ipc::log_rejected_uinput_connection(postfix, peer_uid, active_uid);
return false;
}
if let Err(err) =
ipc::ensure_peer_executable_matches_current_by_fd(stream.as_raw_fd(), postfix)
{
log::warn!(
"Rejected connection on protected uinput ipc channel due to executable mismatch: postfix={}, err={}",
postfix,
err
);
return false;
}
true
}
/// Start uinput service.
async fn start_service<F: FnOnce(ipc::Connection) + Copy>(postfix: &str, handler: F) {
match new_listener(postfix).await {
@@ -955,10 +916,6 @@ pub mod service {
while let Some(result) = incoming.next().await {
match result {
Ok(stream) => {
#[cfg(target_os = "linux")]
if !authorize_uinput_peer(postfix, &stream) {
continue;
}
log::debug!("Got new connection of uinput ipc {}", postfix);
handler(Connection::new(stream));
}