Compare commits

...

17 Commits

Author SHA1 Message Date
rustdesk
c19698fa52 fix: route WebRTC ICE through rendezvous paths 2026-05-18 19:01:22 +08:00
rustdesk
1040df0399 fix: update CLI for current client APIs 2026-05-17 15:43:48 +08:00
rustdesk
03cbf609f6 feat: race WebRTC as a direct transport enhancement 2026-05-17 15:30:50 +08:00
rustdesk
0e0ec5a551 feat: route WebRTC ICE on controlled side 2026-05-17 15:24:27 +08:00
rustdesk
c50e7d078d feat: support trickle ICE in WebRTCStream 2026-05-17 15:18:53 +08:00
rustdesk
3d6b06e854 feat: add rendezvous WebRTC signaling fields 2026-05-17 15:15:12 +08:00
RustDesk
472c4fc03a --deploy, reuse the device token (#15035)
* --deploy, reuse the device token

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix review

* no id validation in deploy, so to keep the same behavior in udp register
pk

* Fix collapsed toolbar drag preview sizing

* Revert "Fix collapsed toolbar drag preview sizing"

This reverts commit 66e39abb74.

* remove too many logs

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-16 14:41:34 +08:00
rustdesk
9f8f726f12 fix compile 2026-05-15 17:30:59 +08:00
flusheDData
701a9c6cdc New terms added (#15036)
* Update es.rs

New terms added

* Update es.rs

New terms added

* Update Spanish translations for various strings

* Fix typo in Spanish translation for TLS fallback

* Add Spanish translations for various UI elements

* Update es.rs

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-05-15 15:31:25 +08:00
Alex Rijckaert
0d40cf2101 Update Dutch translations (#15024)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-05-14 16:43:40 +08:00
rustdesk
dd265dadd7 update hbb_common 2026-05-13 18:08:08 +08:00
Alex Rijckaert
fe5a8cb2ad Update Dutch translation (#14984) 2026-05-13 14:59:48 +08:00
John Fowler
b6caa1a7b2 hu.rs update (#14983)
Translate a new string.
2026-05-13 14:59:29 +08:00
fufesou
55c9707639 fix(msi): check install folder, remove files when uninstall (#15011)
* fix(msi): check install folder, remove files when uninstall

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): harden install folder normalization cleanup

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): better file attributes

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(mis): Simple refactor

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): avoid path-based attribute changes in cleanup

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): custom action, unset flag read before del

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-12 16:24:50 +08:00
Yan Wang
d8808baa83 Allow macOS monitor switching in privacy mode (#15004)
Co-authored-by: Codex <codex@openai.local>
2026-05-11 12:58:49 +08:00
fufesou
1978020d27 fix(custom-client): desktop, incoming only, touch drag (#14928)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-11 12:58:32 +08:00
fufesou
0e4b91b8d7 Harden os password (terminal windows and headless linux) anti brute force (#14985)
* fix(windows): terminal, preauth bruteforce

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(linux): headless, preauth bruteforce

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(linux): headless, OS login, minimal fix

Signed-off-by: fufesou <linlong1266@gmail.com>

* Terminal session, click-only

Signed-off-by: fufesou <linlong1266@gmail.com>

* Simple refactor, logs

Signed-off-by: fufesou <linlong1266@gmail.com>

* harden os password, better scoped failure set

Signed-off-by: fufesou <linlong1266@gmail.com>

* harden os password, ip failure count

Signed-off-by: fufesou <linlong1266@gmail.com>

* Check prelogin before starting cm

Signed-off-by: fufesou <linlong1266@gmail.com>

* Isolate terminal OS login failure tracking

Terminal OS login no longer reads or updates the default RustDesk
per-IP failure bucket. It now uses only the OS credential policy, while
RustDesk password attempts keep using the existing LOGIN_FAILURES[0]
bucket.

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-11 12:58:01 +08:00
24 changed files with 1395 additions and 279 deletions

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" }
hbb_common = { path = "libs/hbb_common", features = ["webrtc"] }
serde_derive = "1.0"
serde = "1.0"
serde_json = "1.0"

View File

@@ -16,6 +16,12 @@ 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;
@@ -684,8 +690,9 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Lock after session end'))));
}
final privacyModeState = PrivacyModeState.find(id);
if (pi.isSupportMultiDisplay &&
PrivacyModeState.find(id).isEmpty &&
(privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
final value =
@@ -776,7 +783,8 @@ List<TToggleMenu> toolbarPrivacyMode(
onChanged: enabled
? (value) {
if (value == null) return;
if (ffiModel.pi.currentDisplay != 0 &&
if (!allowDisplaySwitchInPrivacyMode(pi) &&
ffiModel.pi.currentDisplay != 0 &&
ffiModel.pi.currentDisplay != kAllDisplayValue) {
msgBox(
sessionId,

View File

@@ -376,7 +376,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
}
toolbarItems.add(Obx(() {
if (PrivacyModeState.find(widget.id).isEmpty &&
if ((PrivacyModeState.find(widget.id).isEmpty ||
allowDisplaySwitchInPrivacyMode(pi)) &&
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: !(bind.isIncomingOnly() && isInHomePage()) &&
showMaximize
onTap: !isIncomingHomePage && showMaximize
? () {
final current = DateTime.now().millisecondsSinceEpoch;
final elapsed = current - _lastClickTime;
@@ -610,7 +610,7 @@ class _DesktopTabState extends State<DesktopTab>
.then((value) => stateGlobal.setMaximized(value));
}
}
: null,
: (isIncomingHomePage ? () {} : null), // Keep tap recognizer for Windows touch.
onPanStart: (_) => startDragging(isMainWindow),
onPanCancel: () {
// We want to disable dragging of the tab area in the tab bar.

View File

@@ -31,17 +31,17 @@ LExit:
return WcaFinalize(er);
}
// Helper function to safely delete a file or directory using handle-based deletion.
// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
// Helper function to safely delete a file using handle-based deletion.
// Directories are refused after opening the handle.
BOOL SafeDeleteItem(LPCWSTR fullPath)
{
// Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
// Open the file/directory with delete and attribute-read access plus 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,
DELETE | FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
NULL,
OPEN_EXISTING,
@@ -55,6 +55,21 @@ 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;
@@ -77,98 +92,74 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
return result;
}
// Helper function to recursively delete a directory's contents with detailed logging.
void RecursiveDelete(LPCWSTR path)
BOOL PathEndsWithSlash(LPCWSTR path)
{
// Ensure the path is not empty or null.
if (path == NULL || path[0] == L'\0')
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))
{
return;
}
// Extra safety: never operate directly on a root path.
if (PathIsRootW(path))
DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY;
if (writableAttributes == 0)
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
writableAttributes = FILE_ATTRIBUTE_NORMAL;
}
if (SetFileAttributesW(fullPath, writableAttributes))
{
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath);
return;
}
// 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;
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;
}
WIN32_FIND_DATAW findData;
HANDLE hFind = FindFirstFileW(searchPath, &findData);
if (hFind == INVALID_HANDLE_VALUE)
DWORD attributes = GetFileAttributesW(fullPath);
if (attributes == INVALID_FILE_ATTRIBUTES)
{
// 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;
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND)
{
return TRUE;
}
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error);
return FALSE;
}
do
if (attributes & FILE_ATTRIBUTE_DIRECTORY)
{
// Skip '.' and '..' directories.
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
{
continue;
}
// 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;
}
// 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, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
WcaLog(LOGMSG_STANDARD, "Runtime cleanup skipped directory '%ls'.", fullPath);
return FALSE;
}
FindClose(hFind);
ClearReadOnlyAttribute(fullPath, attributes);
WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath);
return SafeDeleteItem(fullPath);
}
// See `Package.wxs` for the sequence of this custom action.
@@ -178,13 +169,13 @@ void RecursiveDelete(LPCWSTR path)
// 2. RemoveExistingProducts
// ├─ TerminateProcesses
// ├─ TryStopDeleteService
// ├─ RemoveInstallFolder - <-- Here
// ├─ RemoveRuntimeGeneratedFiles - <-- Here
// └─ RemoveFiles
// 3. InstallValidate
// 4. InstallFiles
// 5. InstallExecute
// 6. InstallFinalize
UINT __stdcall RemoveInstallFolder(
UINT __stdcall RemoveRuntimeGeneratedFiles(
__in MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
@@ -194,7 +185,7 @@ UINT __stdcall RemoveInstallFolder(
LPWSTR pwz = NULL;
LPWSTR pwzData = NULL;
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles");
ExitOnFailure(hr, "Failed to initialize");
hr = WcaGetProperty(L"CustomActionData", &pwzData);
@@ -202,24 +193,20 @@ UINT __stdcall RemoveInstallFolder(
pwz = pwzData;
hr = WcaReadStringFromCaData(&pwz, &installFolder);
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz);
if (installFolder == NULL || installFolder[0] == L'\0') {
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup.");
goto LExit;
}
if (PathIsRootW(installFolder)) {
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder);
goto LExit;
}
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.
WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder);
DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe");
LExit:
ReleaseStr(pwzData);

View File

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

View File

@@ -16,8 +16,15 @@
<!-- 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" />
<!-- 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" />
<!-- 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;)" />
<!-- 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="RemoveInstallFolder.SetParam" Return="check" Property="RemoveInstallFolder" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="RemoveRuntimeGeneratedFiles.SetParam" Return="check" Property="RemoveRuntimeGeneratedFiles" 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="RemoveInstallFolder" Before="RemoveFiles"/>
<Custom Action="RemoveInstallFolder.SetParam" Before="RemoveInstallFolder"/>
<Custom Action="TryStopDeleteService" Before="RemoveInstallFolder.SetParam" />
<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="TryStopDeleteService.SetParam" Before="TryStopDeleteService" />
<Custom Action="RemoveFirewallRules" Before="RemoveFiles"/>
<Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/>
<Custom Action="UninstallPrinter" Before="RemoveInstallFolder" Condition="VersionNT &gt;= 603" />
<Custom Action="UninstallPrinter" Before="RemoveRuntimeGeneratedFiles" Condition="VersionNT &gt;= 603" />
<Custom Action="TerminateProcesses" Before="RemoveInstallFolder"/>
<Custom Action="TerminateProcesses" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
<Custom Action="TerminateBrokers" Before="RemoveInstallFolder"/>
<Custom Action="TerminateBrokers" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateBrokers.SetParam" Before="TerminateBrokers"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveInstallFolder"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveRuntimeGeneratedFiles"/>
<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="RemoveInstallFolder" DllEntry="RemoveInstallFolder" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveRuntimeGeneratedFiles" DllEntry="RemoveRuntimeGeneratedFiles" 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,12 +23,13 @@ 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="2" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="5" Condition="NOT WIXUI_DONTVALIDATEPATH" />
</UI>
<UIRef Id="UI_MyInstallDialog" />
@@ -64,9 +65,16 @@ 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" />
<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;" />
<!-- 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="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::{SinkExt, StreamExt},
futures::StreamExt,
log,
message_proto::*,
protobuf::Message as _,
@@ -46,6 +46,7 @@ impl Session {
false,
None,
None,
None,
);
session
}
@@ -53,7 +54,7 @@ impl Session {
#[async_trait]
impl Interface for Session {
fn get_login_config_handler(&self) -> Arc<RwLock<LoginConfigHandler>> {
fn get_lch(&self) -> Arc<RwLock<LoginConfigHandler>> {
return self.lc.clone();
}
@@ -61,14 +62,20 @@ impl Interface for Session {
match msgtype {
"input-password" => {
self.sender
.send(Data::Login((self.password.clone(), true)))
.send(Data::Login((
String::new(),
String::new(),
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((password, true));
let login_data =
Data::Login((String::new(), String::new(), password, true));
self.sender.send(login_data).ok();
}
Err(e) => {
@@ -93,6 +100,8 @@ 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={}",
@@ -137,8 +146,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)) => {
log::info!("direct: {}", direct);
Ok(((mut stream, _direct, _secure, _kcp, _typ), direct)) => {
log::info!("direct: {:?}", direct);
// rpassword::prompt_password("Input anything to exit").ok();
loop {
tokio::select! {

View File

@@ -65,11 +65,12 @@ use hbb_common::{
self,
net::UdpSocket,
sync::{
mpsc::{unbounded_channel, UnboundedReceiver},
mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver},
oneshot,
},
time::{interval, Duration, Instant},
},
webrtc::WebRTCStream,
AddrMangle, ResultType, Stream,
};
pub use helper::*;
@@ -330,6 +331,19 @@ 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(),
@@ -338,6 +352,8 @@ impl Client {
interface.clone(),
udp.clone(),
Some(stop_udp_tx),
ipv6,
webrtc_offerer,
rendezvous_server.clone(),
servers.clone(),
contained,
@@ -355,6 +371,8 @@ impl Client {
interface,
(None, None),
None,
None,
None,
rendezvous_server,
servers,
contained,
@@ -366,6 +384,67 @@ impl Client {
}
}
fn is_expected_webrtc_ice_candidate(ice: &IceCandidate, session_key: &str) -> bool {
!session_key.is_empty() && ice.session_key == session_key && !ice.candidate.is_empty()
}
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::<()>();
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 {
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 Self::is_expected_webrtc_ice_candidate(&ice, &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,
@@ -374,6 +453,8 @@ 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,
@@ -446,14 +527,20 @@ 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 = if crate::get_ipv6_punch_enabled() {
if let Some((socket, addr)) = crate::get_ipv6_socket().await {
(Some(socket), Some(addr))
} else {
(None, None)
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()
}
}
} else {
(None, None)
String::new()
};
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" };
@@ -467,9 +554,16 @@ 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()
});
for i in 1..=3 {
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();
'punch_attempts: for i in 1..=3 {
log::info!(
"#{} {} punch attempt with {}, id: {}",
i,
@@ -479,9 +573,20 @@ impl Client {
);
socket.send(&msg_out).await?;
// below timeout should not bigger than hbbs's connection timeout.
if let Some(msg_in) =
crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 3000)).await
{
let attempt_deadline = Instant::now() + Duration::from_millis((i * 3000) as u64);
loop {
let remaining = attempt_deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
break;
}
let timeout_ms = remaining
.as_millis()
.clamp(1, u64::MAX as u128) as u64;
let Some(msg_in) =
crate::get_next_nonkeyexchange_msg(&mut socket, Some(timeout_ms)).await
else {
break;
};
match msg_in.union {
Some(rendezvous_message::Union::PunchHoleResponse(ph)) => {
if ph.socket_addr.is_empty() {
@@ -510,6 +615,7 @@ 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 {
@@ -528,7 +634,7 @@ impl Client {
}
}
log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr);
break;
break 'punch_attempts;
}
}
Some(rendezvous_message::Union::RelayResponse(rr)) => {
@@ -549,6 +655,38 @@ 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,
@@ -564,30 +702,81 @@ 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", pk, kcp, typ),
(conn, typ == "IPv6" || typ == "WebRTC", pk, kcp, typ),
(feedback, rendezvous_server),
false,
));
}
Some(rendezvous_message::Union::IceCandidate(ice)) => {
if Self::is_expected_webrtc_ice_candidate(&ice, &webrtc_session_key) {
pending_webrtc_ice.push(ice.candidate);
} else {
log::debug!(
"dropping ICE candidate for unexpected WebRTC session key {}",
ice.session_key,
);
}
}
_ => {
log::error!("Unexpected protobuf msg received: {:?}", msg_in);
}
}
}
}
drop(socket);
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);
}
if peer_addr.port() == 0 {
bail!("Failed to connect via rendezvous server");
}
@@ -621,6 +810,8 @@ impl Client {
interface,
udp.0,
ipv6.0,
webrtc_for_connect,
webrtc_bridge_stop,
punch_type,
)
.await?,
@@ -647,6 +838,8 @@ 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,
@@ -705,11 +898,23 @@ 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() {
@@ -4120,8 +4325,22 @@ pub mod peer_online {
#[cfg(test)]
mod tests {
use crate::client::Client;
use hbb_common::rendezvous_proto::IceCandidate;
use hbb_common::tokio;
#[test]
fn accepts_webrtc_ice_by_session_key_only() {
let ice = IceCandidate {
session_key: "session-a".to_owned(),
candidate: "candidate-json".to_owned(),
..Default::default()
};
assert!(Client::is_expected_webrtc_ice_candidate(&ice, "session-a"));
assert!(!Client::is_expected_webrtc_ice_candidate(&ice, "session-b"));
}
#[tokio::test]
async fn test_query_onlines() {
super::query_online_states(

View File

@@ -627,6 +627,98 @@ 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

@@ -312,6 +312,7 @@ 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")))]
@@ -929,6 +930,10 @@ 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) => {
@@ -1737,6 +1742,13 @@ 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")

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", ""),
("Connect via relay", "Conectar a través de 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", ""),
("The verification code is incorrect or has expired", "El código de verificación es incorrecto o ha caducado"),
("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", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("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"),
("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", ""),
("Hide Toolbar", ""),
("Show Toolbar", "Mostrar herramientas"),
("Hide Toolbar", "Ocultar herramientas"),
("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", ""),
("Light Theme", "Tema claro"),
("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", ""),
("Unpin Toolbar", ""),
("Pin Toolbar", "Anclar herramientas"),
("Unpin Toolbar", "Desanclar herramientas"),
("Recording", "Grabando"),
("Directory", "Directorio"),
("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"),
("Automatically record outgoing sessions", ""),
("Automatically record outgoing sessions", "Grabación automática de sesiones salientes"),
("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", ""),
("Prompt", "Solicitud"),
("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", ""),
("Volume up", "Bajar volumen"),
("Volume down", "Subir volumen"),
("Apps", "Aplicaciones"),
("Volume up", "Subir volumen"),
("Volume down", "Bajar 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", ""),
("Accessible devices", "Dispositivos accesibles"),
("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", ""),
("Enable IPv6 P2P connection", ""),
("Enable UDP hole punching", ""),
("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"),
("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 ...", ""),
("Show my cursor", ""),
("Preparing for installation ...", "Preparando instlación..."),
("Show my cursor", "Mostrar mi 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", ""),
("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", ""),
("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"),
("Continue with {}", "Continuar con {}"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("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"),
].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", ""),
("Enable privacy mode", "Adatvédelmi mód aktiválása"),
].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", ""),
("Enable privacy mode", "Privacymodus inschakelen"),
].iter().cloned().collect();
}

View File

@@ -38,49 +38,68 @@ fn main() {
if !common::global_init() {
return;
}
use clap::App;
use clap::{Arg, ArgAction, Command};
use hbb_common::log;
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")
let matches = Command::new("rustdesk")
.version(crate::VERSION)
.author("Purslane Ltd<info@rustdesk.com>")
.about("RustDesk command line tool")
.args_from_usage(&args)
.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"),
)
.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.value_of("port-forward") {
let options: Vec<String> = p.split(":").map(|x| x.to_owned()).collect();
if let Some(p) = matches.get_one::<String>("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 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 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 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.value_of("key").unwrap_or("").to_owned();
let key = matches
.get_one::<String>("key")
.map(String::as_str)
.unwrap_or("")
.to_owned();
let token = LocalConfig::get_option("access_token");
cli::start_one_port_forward(
options[0].clone(),
@@ -90,13 +109,17 @@ fn main() {
key,
token,
);
} else if let Some(p) = matches.value_of("connect") {
} else if let Some(p) = matches.get_one::<String>("connect") {
common::test_rendezvous_server();
common::test_nat_type();
let key = matches.value_of("key").unwrap_or("").to_owned();
let key = matches
.get_one::<String>("key")
.map(String::as_str)
.unwrap_or("")
.to_owned();
let token = LocalConfig::get_option("access_token");
cli::connect_test(p, key, token);
} else if let Some(p) = matches.value_of("server") {
} else if matches.get_flag("server") {
log::info!("id={}", hbb_common::config::Config::get_id());
crate::start_server(true, false);
}

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_DESKTOP_XSESSION_FAILED, LOGIN_MSG_PASSWORD_WRONG,
};
use hbb_common::{
allow_err, bail, log,
@@ -94,6 +94,49 @@ 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() {
@@ -136,14 +179,21 @@ pub fn try_start_desktop(_username: &str, _passsword: &str) -> String {
}
}
Err(e) => {
log::error!("Failed to start xsession {}", e);
LOGIN_MSG_DESKTOP_XSESSION_FAILED.to_owned()
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()
}
}
}
}
fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bool)> {
fn try_start_x_session(username: &str, password: &str) -> Result<(String, bool), XSessionStartError> {
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() {
@@ -161,7 +211,9 @@ fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bo
desktop_manager.is_running(),
))
} else {
bail!(crate::client::LOGIN_MSG_DESKTOP_NOT_INITED);
Err(XSessionStartError::env(
crate::client::LOGIN_MSG_DESKTOP_NOT_INITED.to_owned(),
))
}
}
@@ -247,10 +299,15 @@ impl DesktopManager {
self.is_child_running.load(Ordering::SeqCst)
}
fn try_start_x_session(&mut self, username: &str, password: &str) -> ResultType<()> {
fn try_start_x_session(
&mut self,
username: &str,
password: &str,
) -> Result<(), XSessionStartError> {
match get_user_by_name(username) {
Some(userinfo) => {
let mut client = pam::Client::with_password(&pam_get_service_name())?;
let mut client = pam::Client::with_password(&pam_get_service_name())
.map_err(|e| XSessionStartError::env(format!("failed to init pam client, {}", e)))?;
client
.conversation_mut()
.set_credentials(username, password);
@@ -267,17 +324,24 @@ impl DesktopManager {
Ok(())
}
Err(e) => {
bail!("failed to start x session, {}", e);
Err(XSessionStartError::env(format!(
"failed to start x session, {}",
e
)))
}
}
}
Err(e) => {
bail!("failed to check user pass for {}, {}", username, e);
Err(_e) => {
Err(XSessionStartError::auth(
XSESSION_AUTH_FAILURE_DETAIL.to_owned(),
))
}
}
}
None => {
bail!("failed to get userinfo of {}", username);
Err(XSessionStartError::auth(
XSESSION_AUTH_FAILURE_DETAIL.to_owned(),
))
}
}
}

View File

@@ -1,4 +1,5 @@
use std::{
collections::HashMap,
net::SocketAddr,
sync::{
atomic::{AtomicBool, Ordering},
@@ -21,8 +22,13 @@ use hbb_common::{
rendezvous_proto::*,
sleep,
socket_client::{self, connect_tcp, is_ipv4, new_direct_udp_for, new_udp_for},
tokio::{self, select, sync::Mutex, time::interval},
tokio::{
self, select,
sync::{mpsc, Mutex},
time::interval,
},
udp::FramedSocket,
webrtc::WebRTCStream,
AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr,
};
@@ -32,15 +38,41 @@ use crate::{
};
type Message = RendezvousMessage;
type RendezvousSender = mpsc::UnboundedSender<Message>;
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 {
@@ -48,6 +80,7 @@ pub struct RendezvousMediator {
host: String,
host_prefix: String,
keep_alive: i32,
rz_sender: RendezvousSender,
}
impl RendezvousMediator {
@@ -158,11 +191,13 @@ 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));
@@ -222,10 +257,21 @@ 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);
@@ -289,10 +335,22 @@ 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");
}
@@ -323,6 +381,17 @@ impl RendezvousMediator {
allow_err!(rz.handle_intranet(fla, server).await);
});
}
Some(rendezvous_message::Union::IceCandidate(ice)) => {
let tx = WEBRTC_ICE_TXS.lock().await.get(&ice.session_key).cloned();
if let Some(tx) = tx {
let _ = tx.send(ice.candidate);
} else {
log::debug!(
"dropping ICE candidate for unknown WebRTC session key {}",
ice.session_key
);
}
}
Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
let v0 = Config::get_rendezvous_servers();
Config::set_option(
@@ -345,11 +414,13 @@ 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;
@@ -377,6 +448,9 @@ 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;
@@ -428,6 +502,7 @@ impl RendezvousMediator {
rr.secure,
false,
Default::default(),
String::new(),
rr.control_permissions.clone().into_option(),
)
.await
@@ -442,6 +517,7 @@ 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);
@@ -460,6 +536,7 @@ impl RendezvousMediator {
socket_addr: socket_addr.into(),
version: crate::VERSION.to_owned(),
socket_addr_v6,
webrtc_sdp_answer,
..Default::default()
};
if initiate {
@@ -527,6 +604,7 @@ impl RendezvousMediator {
true,
true,
socket_addr_v6,
String::new(),
fla.control_permissions.into_option(),
)
.await
@@ -569,6 +647,81 @@ impl RendezvousMediator {
Ok(())
}
async fn spawn_webrtc_answerer(
&self,
ph: &PunchHole,
force_relay: bool,
server: ServerPtr,
peer_addr: SocketAddr,
control_permissions: Option<ControlPermissions>,
) -> ResultType<String> {
let mut stream =
WebRTCStream::new(&ph.webrtc_sdp_offer, force_relay, CONNECT_TIMEOUT).await?;
let answer = stream.get_local_endpoint().await?;
let session_key = stream.session_key().to_owned();
let return_route = ph.socket_addr.clone();
let (remote_ice_tx, mut remote_ice_rx) = mpsc::unbounded_channel::<String>();
WEBRTC_ICE_TXS
.lock()
.await
.insert(session_key.clone(), 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 socket_addr = return_route.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 {
socket_addr: socket_addr.clone(),
session_key: session_key_for_ice.clone(),
candidate,
..Default::default()
});
let _ = sender.send(msg);
}
});
}
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(&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;
@@ -580,7 +733,23 @@ 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.into_option();
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,
relay,
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()
};
if peer_addr_v6.port() > 0 && !relay {
socket_addr_v6 = start_ipv6(
peer_addr_v6,
@@ -607,6 +776,7 @@ impl RendezvousMediator {
true,
true,
socket_addr_v6.clone(),
webrtc_sdp_answer.clone(),
control_permissions,
)
.await;
@@ -620,6 +790,7 @@ 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 {
@@ -678,6 +849,21 @@ 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,6 +67,7 @@ pub mod input_service {
}
mod connection;
mod login_failure_check;
pub mod display_service;
#[cfg(windows)]
pub mod portable_service;

View File

@@ -1,3 +1,8 @@
#[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;
@@ -82,6 +87,9 @@ 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;
@@ -94,6 +102,32 @@ 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();
@@ -1497,6 +1531,9 @@ 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;
}
@@ -2376,33 +2413,6 @@ 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) {
@@ -2420,8 +2430,43 @@ 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")))]
self.try_start_cm_ipc();
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;
}
}
#[cfg(not(target_os = "linux"))]
let err_msg = "".to_owned();
@@ -2433,6 +2478,18 @@ 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;
}
@@ -2461,17 +2518,16 @@ impl Connection {
crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y"
&& is_logon();
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)
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")
@@ -2493,6 +2549,14 @@ 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(
@@ -2506,7 +2570,7 @@ impl Connection {
return true;
}
if !self.validate_password(allow_logon_screen_password) {
self.update_failure(failure, false, 0);
self.update_failure_with_scope(failure, false, 0, FailureScope::Default);
self.check_update_temporary_password(false);
if err_msg.is_empty() {
self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG)
@@ -2519,7 +2583,7 @@ impl Connection {
.await;
}
} else {
self.update_failure(failure, true, 0);
self.update_failure_with_scope(failure, true, 0, FailureScope::Default);
if err_msg.is_empty() {
#[cfg(target_os = "linux")]
self.linux_headless_handle.wait_desktop_cm_ready().await;
@@ -3484,16 +3548,16 @@ impl Connection {
self.terminal_user_token = Some(TerminalUserToken::SelfUser);
None
} else {
Some("The user is not an administrator.")
Some(TERMINAL_OS_LOGIN_FAILED_MSG)
}
}
Ok(Err(e)) => {
log::error!("Failed to check if the user is an administrator: {}", e);
Some("Failed to check if the user is an administrator.")
Some(TERMINAL_OS_LOGIN_FAILED_MSG)
}
Err(e) => {
log::error!("Failed to get logon user token: {}", e);
Some("Incorrect username or password.")
Some(TERMINAL_OS_LOGIN_FAILED_MSG)
}
}
}
@@ -3529,6 +3593,146 @@ 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
@@ -3555,18 +3759,37 @@ impl Connection {
Some((p64, p56, p48))
}
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;
}
cur
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);
}
return;
}
let map_mutex = &LOGIN_FAILURES[i];
if remove {
if failure.0 != 0 {
@@ -3587,14 +3810,15 @@ 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, bump(cur, time));
m.insert(key, Self::bump_failure_entry(cur, time));
}
// Update full IP: bump from the *original* passed-in failure
m.insert(self.ip.clone(), bump(failure, 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));
} else {
// Update full IP: bump from the *original* passed-in failure
// Re-read the full IP bucket in case another failed attempt updated it.
let mut m = map_mutex.lock().unwrap();
m.insert(self.ip.clone(), bump(failure, 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));
}
}
@@ -3634,8 +3858,50 @@ 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 {
@@ -5219,6 +5485,8 @@ pub enum AlarmAuditType {
// MultipleLoginsAttemptsWithinOneMinute = 4,
// MultipleLoginsAttemptsWithinOneHour = 5,
ExceedIPv6PrefixAttempts = 6,
TerminalOsLoginBackoff = 7,
TerminalOsLoginConcurrency = 8,
}
pub enum FileAuditType {

View File

@@ -0,0 +1,231 @@
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);
}
}