Compare commits

..

6 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
66 changed files with 577 additions and 1186 deletions

View file

@ -47,7 +47,7 @@ screencapturekit = ["cpal/screencapturekit"]
[dependencies] [dependencies]
async-trait = "0.1" async-trait = "0.1"
scrap = { path = "libs/scrap", features = ["wayland"] } 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_derive = "1.0"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"

View file

@ -142,10 +142,6 @@ const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
const String kOptionCodecPreference = "codec-preference"; const String kOptionCodecPreference = "codec-preference";
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left"; const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right"; const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
const String kOptionRemoteMenubarEdge = "remote-menubar-edge";
const String kOptionRemoteMenubarFraction = "remote-menubar-frac";
const String kOptionAllowMultiEdgeToolbarDock =
"allow-multi-edge-toolbar-dock";
const String kOptionHideAbTagsPanel = "hideAbTagsPanel"; const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
const String kOptionRemoteMenubarState = "remoteMenubarState"; const String kOptionRemoteMenubarState = "remoteMenubarState";
const String kOptionPeerSorting = "peer-sorting"; const String kOptionPeerSorting = "peer-sorting";

View file

@ -488,16 +488,6 @@ class _GeneralState extends State<_General> {
_OptionCheckBox(context, 'Confirm before closing multiple tabs', _OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs, kOptionEnableConfirmClosingTabs,
isServer: false), isServer: false),
if (!bind.isIncomingOnly())
_OptionCheckBox(
context,
'allow-remote-toolbar-docking-any-edge',
kOptionAllowMultiEdgeToolbarDock,
isServer: false,
update: (_) {
reloadAllWindows();
},
),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
if (!isWeb) wallpaper(), if (!isWeb) wallpaper(),
if (!isWeb && !bind.isIncomingOnly()) ...[ if (!isWeb && !bind.isIncomingOnly()) ...[

View file

@ -28,220 +28,6 @@ import './kb_layout_type_chooser.dart';
import 'package:flutter_hbb/utils/scale.dart'; import 'package:flutter_hbb/utils/scale.dart';
import 'package:flutter_hbb/common/widgets/custom_scale_base.dart'; import 'package:flutter_hbb/common/widgets/custom_scale_base.dart';
enum _ToolbarEdge { top, right, bottom, left }
_ToolbarEdge _parseToolbarEdge(String? s) {
switch (s) {
case 'right':
return _ToolbarEdge.right;
case 'bottom':
return _ToolbarEdge.bottom;
case 'left':
return _ToolbarEdge.left;
default:
return _ToolbarEdge.top;
}
}
String _toolbarEdgeToString(_ToolbarEdge e) {
switch (e) {
case _ToolbarEdge.top:
return 'top';
case _ToolbarEdge.right:
return 'right';
case _ToolbarEdge.bottom:
return 'bottom';
case _ToolbarEdge.left:
return 'left';
}
}
bool _isHorizontalEdge(_ToolbarEdge e) =>
e == _ToolbarEdge.top || e == _ToolbarEdge.bottom;
const _legacyRemoteMenubarDragX = 'remote-menubar-drag-x';
double _clampToolbarFraction(double fraction, double left, double right) {
if (fraction < left) fraction = left;
if (fraction > right) fraction = right;
return fraction;
}
Size _toolbarSizeForEdge(_ToolbarEdge edge, Size? measured) {
final isHorizontal = _isHorizontalEdge(edge);
final fallback = isHorizontal ? const Size(360, 40) : const Size(40, 360);
final size = measured ?? fallback;
final long = size.longestSide;
final short = size.shortestSide;
return Size(isHorizontal ? long : short, isHorizontal ? short : long);
}
Offset _toolbarOffsetForEdge({
required _ToolbarEdge edge,
required double fraction,
required Size parentSize,
required Size toolbarSize,
}) {
final xTravel = parentSize.width - toolbarSize.width;
final yTravel = parentSize.height - toolbarSize.height;
switch (edge) {
case _ToolbarEdge.top:
return Offset(xTravel * fraction, 0);
case _ToolbarEdge.bottom:
return Offset(xTravel * fraction, yTravel);
case _ToolbarEdge.left:
return Offset(0, yTravel * fraction);
case _ToolbarEdge.right:
return Offset(xTravel, yTravel * fraction);
}
}
double _fractionForAlignedDrag({
required double cursor,
required double grabOffset,
required double parentExtent,
required double toolbarExtent,
required double left,
required double right,
}) {
final travelExtent = parentExtent - toolbarExtent;
if (travelExtent <= 0) {
return _clampToolbarFraction(0.5, left, right);
}
return _clampToolbarFraction(
(cursor - grabOffset) / travelExtent, left, right);
}
({double left, double right}) _fractionBoundsForEdge(
_ToolbarEdge edge,
double left,
double right,
) {
return _isHorizontalEdge(edge)
? (left: left, right: right)
: (left: 0, right: 1);
}
String _toolbarRawFraction({
required bool multiEdgeEnabled,
required _ToolbarEdge edge,
required String? savedFraction,
required String? legacyFraction,
}) {
if (!multiEdgeEnabled) {
return (legacyFraction != null && legacyFraction.isNotEmpty)
? legacyFraction
: '0.5';
}
if (savedFraction != null && savedFraction.isNotEmpty) {
return savedFraction;
}
if (edge == _ToolbarEdge.top &&
legacyFraction != null &&
legacyFraction.isNotEmpty) {
return legacyFraction;
}
return '0.5';
}
// Returns the alignment for the wrapper Align that positions the entire
// toolbar against the given edge at the given fraction along that edge.
// Alignment uses [-1, 1] coordinates (0 = center).
Alignment _alignmentForEdge(_ToolbarEdge edge, double fraction) {
final f = fraction * 2 - 1;
switch (edge) {
case _ToolbarEdge.top:
return Alignment(f, -1);
case _ToolbarEdge.bottom:
return Alignment(f, 1);
case _ToolbarEdge.left:
return Alignment(-1, f);
case _ToolbarEdge.right:
return Alignment(1, f);
}
}
// The drag handle hangs off the side of the toolbar facing away from the
// docked edge, so the icons themselves sit flush against that edge.
BorderRadius _collapseHandleBorderRadius(_ToolbarEdge edge) {
const r = Radius.circular(5);
switch (edge) {
case _ToolbarEdge.top:
return const BorderRadius.vertical(bottom: r);
case _ToolbarEdge.bottom:
return const BorderRadius.vertical(top: r);
case _ToolbarEdge.left:
return const BorderRadius.horizontal(right: r);
case _ToolbarEdge.right:
return const BorderRadius.horizontal(left: r);
}
}
int _monitorMenuQuarterTurns(_ToolbarEdge edge) {
switch (edge) {
case _ToolbarEdge.left:
return 1;
case _ToolbarEdge.right:
return 3;
case _ToolbarEdge.top:
case _ToolbarEdge.bottom:
return 0;
}
}
IconData _toolbarCollapseIcon(_ToolbarEdge edge, bool isCollapsed) {
switch (edge) {
case _ToolbarEdge.top:
return isCollapsed ? Icons.expand_more : Icons.expand_less;
case _ToolbarEdge.bottom:
return isCollapsed ? Icons.expand_less : Icons.expand_more;
case _ToolbarEdge.left:
return isCollapsed ? Icons.chevron_right : Icons.chevron_left;
case _ToolbarEdge.right:
return isCollapsed ? Icons.chevron_left : Icons.chevron_right;
}
}
class _ToolbarDockingOptions {
_ToolbarDockingOptions({
required this.edge,
required this.fraction,
required this.multiEdgeEnabled,
});
_ToolbarEdge edge;
double fraction;
bool multiEdgeEnabled;
}
final _toolbarDockingOptionsBySession = <String, _ToolbarDockingOptions>{};
String _toolbarDockingCacheKey(SessionID sessionId) => sessionId.toString();
_ToolbarDockingOptions? _cachedToolbarDockingOptions(SessionID sessionId) =>
_toolbarDockingOptionsBySession[_toolbarDockingCacheKey(sessionId)];
void _cacheToolbarDockingOptions({
required SessionID sessionId,
required _ToolbarEdge edge,
required double fraction,
required bool multiEdgeEnabled,
}) {
final key = _toolbarDockingCacheKey(sessionId);
final cached = _toolbarDockingOptionsBySession[key];
if (cached == null) {
_toolbarDockingOptionsBySession[key] = _ToolbarDockingOptions(
edge: edge,
fraction: fraction,
multiEdgeEnabled: multiEdgeEnabled,
);
return;
}
cached.edge = edge;
cached.fraction = fraction;
cached.multiEdgeEnabled = multiEdgeEnabled;
}
class ToolbarState { class ToolbarState {
late RxBool _pin; late RxBool _pin;
@ -464,26 +250,8 @@ class RemoteToolbar extends StatefulWidget {
class _RemoteToolbarState extends State<RemoteToolbar> { class _RemoteToolbarState extends State<RemoteToolbar> {
late Debouncer<int> _debouncerHide; late Debouncer<int> _debouncerHide;
bool _isCursorOverImage = false; bool _isCursorOverImage = false;
final _fraction = 0.5.obs; final _fractionX = 0.5.obs;
final _edge = _ToolbarEdge.top.obs;
final _dragging = false.obs; final _dragging = false.obs;
// Live drag preview: where the toolbar would dock if the user dropped now.
final _previewEdge = Rxn<_ToolbarEdge>();
final _previewFraction = Rxn<double>();
// Measured size of the live toolbar, so the preview ghost matches reality
// (collapsed handle vs expanded toolbar). Updated after every layout pass.
final _toolbarSize = Rxn<Size>();
final _toolbarKey = GlobalKey(debugLabel: 'remote_toolbar_root');
// When false (default), the toolbar stays on the top edge and the drag
// handle just slides it horizontally preserving long-standing UX while
// still fixing the bug where dragging only moved the handle. When true,
// the user has opted into multi-edge docking with nearest-edge snap.
// Kept in sync after settings-triggered rebuilds.
final _multiEdgeEnabled = false.obs;
final _dockingOptionsInitialized = false.obs;
bool _pendingDockingOptionSync = false;
int _dockingOptionSyncSerial = 0;
int _dragEpoch = 0;
int get windowId => stateGlobal.windowId; int get windowId => stateGlobal.windowId;
@ -505,144 +273,16 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
void _minimize() async => void _minimize() async =>
await WindowController.fromWindowId(windowId).minimize(); await WindowController.fromWindowId(windowId).minimize();
Future<void> _syncDockingOptions({required bool force}) async {
final syncSerial = ++_dockingOptionSyncSerial;
if (_dragging.isTrue) {
_deferDockingOptionsSync();
return;
}
final dragEpoch = _dragEpoch;
// Use the canonical helper so the option's documented default semantics
// apply (allow-* prefix => default false). Keeping it raw-string would
// diverge from how _OptionCheckBox displays the same key.
final multiEdgeEnabled =
mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock);
final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId);
if (cached == null && pi.isSet.isFalse) {
return;
}
final hadDockingOptions = cached != null;
final wasMultiEdgeEnabled =
cached?.multiEdgeEnabled ?? _multiEdgeEnabled.value;
if (!force &&
hadDockingOptions &&
wasMultiEdgeEnabled == multiEdgeEnabled) {
_pendingDockingOptionSync = false;
return;
}
final savedFraction = await bind.sessionGetOption(
sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarFraction);
// Backward compat: legacy horizontal-only position.
final legacyFraction = await bind.sessionGetOption(
sessionId: widget.ffi.sessionId, arg: _legacyRemoteMenubarDragX);
if (!mounted || syncSerial != _dockingOptionSyncSerial) return;
var nextEdge = _edge.value;
var savedFractionForNextEdge = savedFraction;
var keepCurrentPosition = false;
if (!multiEdgeEnabled) {
nextEdge = _ToolbarEdge.top;
} else if (force || wasMultiEdgeEnabled || cached == null) {
final edgeStr = await bind.sessionGetOption(
sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarEdge);
if (!mounted || syncSerial != _dockingOptionSyncSerial) return;
nextEdge = _parseToolbarEdge(edgeStr);
} else {
// The setting changed from top-only to multi-edge while this toolbar is
// already visible. Keep its current position instead of jumping to the
// last saved multi-edge dock.
nextEdge = cached.edge;
savedFractionForNextEdge = cached.fraction.toString();
keepCurrentPosition = true;
}
final rawFraction = _toolbarRawFraction(
multiEdgeEnabled: multiEdgeEnabled,
edge: nextEdge,
savedFraction: savedFractionForNextEdge,
legacyFraction: legacyFraction,
);
// Clamp to the saved drag-bound contract so a corrupted or out-of-range
// saved value can't bypass it until the user drags again.
final dragLeft = double.tryParse(
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft)) ??
0.0;
final dragRight = double.tryParse(
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight)) ??
1.0;
final fractionBounds =
_fractionBoundsForEdge(nextEdge, dragLeft, dragRight);
final nextFraction = (double.tryParse(rawFraction) ?? 0.5)
.clamp(fractionBounds.left, fractionBounds.right)
.toDouble();
if (!mounted || syncSerial != _dockingOptionSyncSerial) return;
if (_dragging.isTrue || dragEpoch != _dragEpoch) {
_deferDockingOptionsSync();
return;
}
_edge.value = nextEdge;
_fraction.value = nextFraction;
_multiEdgeEnabled.value = multiEdgeEnabled;
_dockingOptionsInitialized.value = true;
_cacheToolbarDockingOptions(
sessionId: widget.ffi.sessionId,
edge: nextEdge,
fraction: nextFraction,
multiEdgeEnabled: multiEdgeEnabled,
);
_pendingDockingOptionSync = false;
if (!multiEdgeEnabled || keepCurrentPosition) {
bind.sessionPeerOption(
sessionId: widget.ffi.sessionId,
name: kOptionRemoteMenubarEdge,
value: _toolbarEdgeToString(nextEdge),
);
bind.sessionPeerOption(
sessionId: widget.ffi.sessionId,
name: kOptionRemoteMenubarFraction,
value: nextFraction.toString(),
);
}
}
void _deferDockingOptionsSync() {
_pendingDockingOptionSync = true;
if (_dragging.isFalse) {
_syncDockingOptionsAfterDragIfNeeded();
}
}
void _markToolbarDragEpoch() {
++_dragEpoch;
}
void _syncDockingOptionsAfterDragIfNeeded() {
if (!_pendingDockingOptionSync) return;
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _syncDockingOptions(force: false);
});
}
@override @override
initState() { initState() {
super.initState(); super.initState();
final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId);
final multiEdgeEnabled =
mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock);
final shouldResetToTop =
cached != null && cached.multiEdgeEnabled && !multiEdgeEnabled;
if (cached != null && !shouldResetToTop) {
_edge.value = cached.edge;
_fraction.value = cached.fraction;
_multiEdgeEnabled.value = multiEdgeEnabled;
_dockingOptionsInitialized.value = true;
}
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await _syncDockingOptions(force: cached == null || shouldResetToTop); _fractionX.value = double.tryParse(await bind.sessionGetOption(
sessionId: widget.ffi.sessionId,
arg: 'remote-menubar-drag-x') ??
'0.5') ??
0.5;
// Initialize toolbar states (collapse, hide) from session options // Initialize toolbar states (collapse, hide) from session options
widget.state.init(widget.ffi.sessionId); widget.state.init(widget.ffi.sessionId);
}); });
@ -663,14 +303,6 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
}); });
} }
@override
void didUpdateWidget(covariant RemoteToolbar oldWidget) {
super.didUpdateWidget(oldWidget);
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _syncDockingOptions(force: false);
});
}
_debouncerHideProc(int v) { _debouncerHideProc(int v) {
if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) { if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) {
collapse.value = true; collapse.value = true;
@ -679,130 +311,64 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
@override @override
dispose() { dispose() {
++_dockingOptionSyncSerial;
widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
super.dispose(); super.dispose();
widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
// Wait for initialization to complete to prevent flickering // Wait for initialization to complete to prevent flickering
if (!widget.state.initialized.value || if (!widget.state.initialized.value) {
!_dockingOptionsInitialized.value) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
// If toolbar is hidden, return empty widget // If toolbar is hidden, return empty widget
if (hide.value) { if (hide.value) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final edge = _edge.value; return Align(
final isHorizontal = _isHorizontalEdge(edge); alignment: Alignment.topCenter,
child: collapse.isFalse
// Measure the live toolbar after every layout so the preview ghost can ? _buildToolbar(context)
// match its actual footprint (collapsed handle vs expanded toolbar). : _buildDraggableCollapse(context),
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_dragging.isTrue) return;
final ro = _toolbarKey.currentContext?.findRenderObject();
if (ro is RenderBox && ro.hasSize) {
final s = ro.size;
if (_toolbarSize.value != s) _toolbarSize.value = s;
}
});
final toolbar = Align(
alignment: _alignmentForEdge(edge, _fraction.value),
child: KeyedSubtree(
key: _toolbarKey,
child: collapse.isFalse
? _buildToolbar(context, edge, isHorizontal)
: _buildDraggableCollapse(context, edge, isHorizontal),
),
);
// Always return the Stack even when not dragging so the toolbar's
// position in the Element tree stays stable. Wrapping/unwrapping it
// mid-drag was killing the Draggable's gesture state.
return Stack(
fit: StackFit.expand,
children: [
IgnorePointer(
child: Obx(() {
final pe = _previewEdge.value;
final pf = _previewFraction.value;
if (!_dragging.isTrue || pe == null || pf == null) {
return const SizedBox.shrink();
}
return _buildDragPreview(context, pe, pf, _toolbarSize.value);
}),
),
toolbar,
],
); );
}); });
} }
Widget _buildDragPreview(BuildContext context, _ToolbarEdge edge, Widget _buildDraggableCollapse(BuildContext context) {
double fraction, Size? measured) {
final color = Theme.of(context).colorScheme.primary;
// Use the measured live toolbar size so collapsed vs expanded looks
// right. The current orientation may differ from the preview orientation
// (e.g. dragging a top-docked toolbar toward the left edge), so swap the
// long/short axes when previewing a different orientation.
final previewSize = _toolbarSizeForEdge(edge, measured);
return Align(
alignment: _alignmentForEdge(edge, fraction),
child: Container(
width: previewSize.width,
height: previewSize.height,
decoration: BoxDecoration(
color: color.withOpacity(0.10),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withOpacity(0.55), width: 1.5),
),
),
);
}
Widget _buildDraggableCollapse(
BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
return Obx(() { return Obx(() {
if (collapse.isFalse && _dragging.isFalse) { if (collapse.isFalse && _dragging.isFalse) {
triggerAutoHide(); triggerAutoHide();
} }
final borderRadius = _collapseHandleBorderRadius(edge); final borderRadius = BorderRadius.vertical(
return Offstage( bottom: Radius.circular(5),
offstage: _dragging.isTrue, );
child: Material( return Align(
elevation: _ToolbarTheme.elevation, alignment: FractionalOffset(_fractionX.value, 0),
shadowColor: MyTheme.color(context).shadow, child: Offstage(
borderRadius: borderRadius, offstage: _dragging.isTrue,
child: _DraggableShowHide( child: Material(
id: widget.id, elevation: _ToolbarTheme.elevation,
sessionId: widget.ffi.sessionId, shadowColor: MyTheme.color(context).shadow,
dragging: _dragging,
fraction: _fraction,
edge: _edge,
previewEdge: _previewEdge,
previewFraction: _previewFraction,
toolbarSize: _toolbarSize,
markDragEpoch: _markToolbarDragEpoch,
syncDockingOptionsAfterDragIfNeeded:
_syncDockingOptionsAfterDragIfNeeded,
isHorizontal: isHorizontal,
multiEdgeEnabled: _multiEdgeEnabled.value,
toolbarState: widget.state,
setFullscreen: _setFullscreen,
setMinimize: _minimize,
borderRadius: borderRadius, borderRadius: borderRadius,
child: _DraggableShowHide(
id: widget.id,
sessionId: widget.ffi.sessionId,
dragging: _dragging,
fractionX: _fractionX,
toolbarState: widget.state,
setFullscreen: _setFullscreen,
setMinimize: _minimize,
borderRadius: borderRadius,
),
), ),
), ),
); );
}); });
} }
Widget _buildToolbar( Widget _buildToolbar(BuildContext context) {
BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
final List<Widget> toolbarItems = []; final List<Widget> toolbarItems = [];
toolbarItems.add(_PinMenu(state: widget.state)); toolbarItems.add(_PinMenu(state: widget.state));
if (!isWebDesktop) { if (!isWebDesktop) {
@ -816,7 +382,6 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
return _MonitorMenu( return _MonitorMenu(
id: widget.id, id: widget.id,
ffi: widget.ffi, ffi: widget.ffi,
edge: edge,
setRemoteState: widget.setRemoteState); setRemoteState: widget.setRemoteState);
} else { } else {
return Offstage(); return Offstage();
@ -842,53 +407,37 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
if (!isWeb) toolbarItems.add(_RecordMenu()); if (!isWeb) toolbarItems.add(_RecordMenu());
toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0)); final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0));
// innerAxis: how the toolbar icons themselves flow. return Column(
// outerAxis: how the toolbar block and the handle stack against each other
// (perpendicular to the dock edge, so the handle hangs off the interior face).
final innerAxis = isHorizontal ? Axis.horizontal : Axis.vertical;
final outerAxis = isHorizontal ? Axis.vertical : Axis.horizontal;
final spacer = isHorizontal
? SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
: SizedBox(height: _ToolbarTheme.buttonHMargin * 2);
final toolbarMaterial = Material(
elevation: _ToolbarTheme.elevation,
shadowColor: MyTheme.color(context).shadow,
borderRadius: toolbarBorderRadius,
color: Theme.of(context)
.menuBarTheme
.style
?.backgroundColor
?.resolve(MaterialState.values.toSet()),
child: SingleChildScrollView(
scrollDirection: innerAxis,
child: Theme(
data: themeData(),
child: _ToolbarTheme.borderWrapper(
context,
Flex(
direction: innerAxis,
mainAxisSize: MainAxisSize.min,
children: [
spacer,
...toolbarItems,
spacer,
],
),
toolbarBorderRadius),
),
),
);
final handle = _buildDraggableCollapse(context, edge, isHorizontal);
// The handle hangs off the interior face of the toolbar (away from the
// docked edge), centered along that face by the Flex's default cross-axis
// alignment, so the icons themselves sit flush against the docked edge.
final children = (edge == _ToolbarEdge.top || edge == _ToolbarEdge.left)
? [toolbarMaterial, handle]
: [handle, toolbarMaterial];
return Flex(
direction: outerAxis,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: children, children: [
Material(
elevation: _ToolbarTheme.elevation,
shadowColor: MyTheme.color(context).shadow,
borderRadius: toolbarBorderRadius,
color: Theme.of(context)
.menuBarTheme
.style
?.backgroundColor
?.resolve(MaterialState.values.toSet()),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Theme(
data: themeData(),
child: _ToolbarTheme.borderWrapper(
context,
Row(
children: [
SizedBox(width: _ToolbarTheme.buttonHMargin * 2),
...toolbarItems,
SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
],
),
toolbarBorderRadius),
),
),
),
_buildDraggableCollapse(context),
],
); );
} }
@ -967,13 +516,11 @@ class _MobileActionMenu extends StatelessWidget {
class _MonitorMenu extends StatelessWidget { class _MonitorMenu extends StatelessWidget {
final String id; final String id;
final FFI ffi; final FFI ffi;
final _ToolbarEdge edge;
final Function(VoidCallback) setRemoteState; final Function(VoidCallback) setRemoteState;
const _MonitorMenu({ const _MonitorMenu({
Key? key, Key? key,
required this.id, required this.id,
required this.ffi, required this.ffi,
required this.edge,
required this.setRemoteState, required this.setRemoteState,
}) : super(key: key); }) : super(key: key);
@ -984,17 +531,9 @@ class _MonitorMenu extends StatelessWidget {
!isWeb && ffi.ffiModel.pi.isSupportMultiDisplay; !isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => showMonitorsToolbar
final child = showMonitorsToolbar ? buildMultiMonitorMenu(context)
? buildMultiMonitorMenu(context) : Obx(() => buildMonitorMenu(context));
: Obx(() => buildMonitorMenu(context));
final quarterTurns = _monitorMenuQuarterTurns(edge);
if (quarterTurns == 0) return child;
return RotatedBox(
quarterTurns: quarterTurns,
child: child,
);
}
Widget buildMonitorMenu(BuildContext context) { Widget buildMonitorMenu(BuildContext context) {
final width = SimpleWrapper<double>(0); final width = SimpleWrapper<double>(0);
@ -1126,8 +665,7 @@ class _MonitorMenu extends StatelessWidget {
} }
final scale = _ToolbarTheme.buttonSize / rect.height * 0.75; final scale = _ToolbarTheme.buttonSize / rect.height * 0.75;
final height = rect.height * scale; final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5;
final startY = (_ToolbarTheme.buttonSize - height) * 0.5;
final startX = startY; final startX = startY;
final children = <Widget>[]; final children = <Widget>[];
@ -1170,7 +708,7 @@ class _MonitorMenu extends StatelessWidget {
width.value = rect.width * scale + startX * 2; width.value = rect.width * scale + startX * 2;
return SizedBox( return SizedBox(
width: width.value, width: width.value,
height: height + startY * 2, height: rect.height * scale + startY * 2,
child: Stack( child: Stack(
children: children, children: children,
), ),
@ -2981,18 +2519,7 @@ class RdoMenuButton<T> extends StatelessWidget {
class _DraggableShowHide extends StatefulWidget { class _DraggableShowHide extends StatefulWidget {
final String id; final String id;
final SessionID sessionId; final SessionID sessionId;
final RxDouble fraction; final RxDouble fractionX;
final Rx<_ToolbarEdge> edge;
final Rxn<_ToolbarEdge> previewEdge;
final Rxn<double> previewFraction;
final Rxn<Size> toolbarSize;
final VoidCallback markDragEpoch;
final VoidCallback syncDockingOptionsAfterDragIfNeeded;
final bool isHorizontal;
// Whether multi-edge docking is enabled for this session (toggled in
// Settings -> Other). When false, the drag handle slides the toolbar
// horizontally on the top edge and never switches edges.
final bool multiEdgeEnabled;
final RxBool dragging; final RxBool dragging;
final ToolbarState toolbarState; final ToolbarState toolbarState;
final BorderRadius borderRadius; final BorderRadius borderRadius;
@ -3004,15 +2531,7 @@ class _DraggableShowHide extends StatefulWidget {
Key? key, Key? key,
required this.id, required this.id,
required this.sessionId, required this.sessionId,
required this.fraction, required this.fractionX,
required this.edge,
required this.previewEdge,
required this.previewFraction,
required this.toolbarSize,
required this.markDragEpoch,
required this.syncDockingOptionsAfterDragIfNeeded,
required this.isHorizontal,
required this.multiEdgeEnabled,
required this.dragging, required this.dragging,
required this.toolbarState, required this.toolbarState,
required this.setFullscreen, required this.setFullscreen,
@ -3025,12 +2544,10 @@ class _DraggableShowHide extends StatefulWidget {
} }
class _DraggableShowHideState extends State<_DraggableShowHide> { class _DraggableShowHideState extends State<_DraggableShowHide> {
Offset position = Offset.zero;
Size size = Size.zero;
double left = 0.0; double left = 0.0;
double right = 1.0; double right = 1.0;
Offset? _lastPointerDown;
Offset? _dragGrabOffset;
double? _dragLongAxisGrabOffset;
Size? _dragToolbarSize;
RxBool get collapse => widget.toolbarState.collapse; RxBool get collapse => widget.toolbarState.collapse;
@ -3056,174 +2573,41 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
} }
} }
// Bias applied to the currently-previewed edge so a drag hovering between
// two edges doesn't flicker. Only relevant when multi-edge is enabled.
static const double _switchHysteresisPx = 50.0;
_ToolbarEdge _nearestToolbarEdge(Offset cursor, Size mediaSize) {
if (!widget.multiEdgeEnabled) return widget.edge.value;
double rawDist(_ToolbarEdge e) {
switch (e) {
case _ToolbarEdge.top:
return cursor.dy;
case _ToolbarEdge.bottom:
return mediaSize.height - cursor.dy;
case _ToolbarEdge.left:
return cursor.dx;
case _ToolbarEdge.right:
return mediaSize.width - cursor.dx;
}
}
final previewed = widget.previewEdge.value;
var winner = widget.edge.value;
var best = double.infinity;
for (final e in _ToolbarEdge.values) {
final biased =
e == previewed ? rawDist(e) - _switchHysteresisPx : rawDist(e);
if (biased < best) {
best = biased;
winner = e;
}
}
return winner;
}
void _ensureDragGrabOffset(Offset cursor) {
if (_dragGrabOffset != null) return;
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
final toolbarSize =
_toolbarSizeForEdge(widget.edge.value, widget.toolbarSize.value);
_dragToolbarSize = toolbarSize;
final toolbarOffset = _toolbarOffsetForEdge(
edge: widget.edge.value,
fraction: widget.fraction.value,
parentSize: mediaSize,
toolbarSize: toolbarSize,
);
_dragGrabOffset = cursor - toolbarOffset;
_dragLongAxisGrabOffset = _isHorizontalEdge(widget.edge.value)
? _dragGrabOffset?.dx
: _dragGrabOffset?.dy;
}
double _dragGrabOffsetForEdge(_ToolbarEdge edge, Size toolbarSize) {
final offset = _dragLongAxisGrabOffset ?? 0;
final extent =
_isHorizontalEdge(edge) ? toolbarSize.width : toolbarSize.height;
return _clampToolbarFraction(offset, 0, extent);
}
void _updatePreview(Offset cursor) {
_ensureDragGrabOffset(cursor);
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
final winner = _nearestToolbarEdge(cursor, mediaSize);
widget.previewEdge.value = winner;
final toolbarSize = _toolbarSizeForEdge(winner, _dragToolbarSize);
final grabOffset = _dragGrabOffsetForEdge(winner, toolbarSize);
final double frac;
if (winner == _ToolbarEdge.top || winner == _ToolbarEdge.bottom) {
frac = _fractionForAlignedDrag(
cursor: cursor.dx,
grabOffset: grabOffset,
parentExtent: mediaSize.width,
toolbarExtent: toolbarSize.width,
left: left,
right: right,
);
} else {
final fractionBounds = _fractionBoundsForEdge(winner, left, right);
frac = _fractionForAlignedDrag(
cursor: cursor.dy,
grabOffset: grabOffset,
parentExtent: mediaSize.height,
toolbarExtent: toolbarSize.height,
left: fractionBounds.left,
right: fractionBounds.right,
);
}
widget.previewFraction.value = frac;
}
void _resetDragTracking() {
_lastPointerDown = null;
_dragGrabOffset = null;
_dragLongAxisGrabOffset = null;
_dragToolbarSize = null;
}
void _commitPreview() {
final newEdge = widget.previewEdge.value;
final frac = widget.previewFraction.value;
widget.previewEdge.value = null;
widget.previewFraction.value = null;
widget.dragging.value = false;
widget.markDragEpoch();
_resetDragTracking();
widget.syncDockingOptionsAfterDragIfNeeded();
if (newEdge == null || frac == null) return;
widget.edge.value = newEdge;
widget.fraction.value = frac;
_cacheToolbarDockingOptions(
sessionId: widget.sessionId,
edge: newEdge,
fraction: frac,
multiEdgeEnabled: widget.multiEdgeEnabled,
);
bind.sessionPeerOption(
sessionId: widget.sessionId,
name: kOptionRemoteMenubarEdge,
value: _toolbarEdgeToString(newEdge),
);
bind.sessionPeerOption(
sessionId: widget.sessionId,
name: kOptionRemoteMenubarFraction,
value: frac.toString(),
);
if (widget.multiEdgeEnabled) {
return;
}
bind.sessionPeerOption(
sessionId: widget.sessionId,
name: _legacyRemoteMenubarDragX,
value: frac.toString(),
);
}
Widget _buildDraggable(BuildContext context) { Widget _buildDraggable(BuildContext context) {
return Listener( return Draggable(
onPointerDown: (event) => _lastPointerDown = event.position, axis: Axis.horizontal,
child: Draggable( child: Icon(
// When multi-edge docking is off the toolbar stays on the top edge, Icons.drag_indicator,
// so lock the feedback to horizontal motion otherwise the handle size: 20,
// floats away from the top while dragging and the toolbar looks color: MyTheme.color(context).drag_indicator,
// unmoored. When multi-edge is on we need 2D drag for snap-to-edge.
axis: widget.multiEdgeEnabled ? null : Axis.horizontal,
child: Icon(
widget.isHorizontal ? Icons.drag_indicator : Icons.drag_handle,
size: 20,
color: MyTheme.color(context).drag_indicator,
),
feedback: widget,
onDragStarted: () {
widget.markDragEpoch();
final pointerDown = _lastPointerDown;
if (pointerDown != null) {
_ensureDragGrabOffset(pointerDown);
}
widget.dragging.value = true;
// Seed the preview at the current docked edge/fraction so something
// shows the instant the drag begins, before the first onDragUpdate.
widget.previewEdge.value = widget.edge.value;
widget.previewFraction.value = widget.fraction.value;
},
onDragUpdate: (details) {
_updatePreview(details.globalPosition);
},
onDragEnd: (_) => _commitPreview(),
), ),
feedback: widget,
onDragStarted: (() {
final RenderObject? renderObj = context.findRenderObject();
if (renderObj != null) {
final RenderBox renderBox = renderObj as RenderBox;
size = renderBox.size;
position = renderBox.localToGlobal(Offset.zero);
}
widget.dragging.value = true;
}),
onDragEnd: (details) {
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
widget.fractionX.value +=
(details.offset.dx - position.dx) / (mediaSize.width - size.width);
if (widget.fractionX.value < left) {
widget.fractionX.value = left;
}
if (widget.fractionX.value > right) {
widget.fractionX.value = right;
}
bind.sessionPeerOption(
sessionId: widget.sessionId,
name: 'remote-menubar-drag-x',
value: widget.fractionX.value.toString(),
);
widget.dragging.value = false;
},
); );
} }
@ -3253,9 +2637,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
); );
} }
final axis = widget.isHorizontal ? Axis.horizontal : Axis.vertical; final child = Row(
final child = Flex(
direction: axis,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildDraggable(context), _buildDraggable(context),
@ -3296,7 +2678,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
message: translate( message: translate(
collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'), collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'),
child: Icon( child: Icon(
_toolbarCollapseIcon(widget.edge.value, collapse.isTrue), collapse.isFalse ? Icons.expand_less : Icons.expand_more,
size: iconSize, size: iconSize,
), ),
))), ))),
@ -3338,8 +2720,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
borderRadius: widget.borderRadius, borderRadius: widget.borderRadius,
), ),
child: SizedBox( child: SizedBox(
height: widget.isHorizontal ? 20 : null, height: 20,
width: widget.isHorizontal ? null : 20,
child: child, child: child,
), ),
), ),

View file

@ -346,7 +346,7 @@ class InputModel {
/// which runs per-engine, so each isolate registers its own handler tied /// which runs per-engine, so each isolate registers its own handler tied
/// to its own set of InputModels. /// to its own set of InputModels.
static void initSideButtonChannel() { static void initSideButtonChannel() {
if (!isLinux) return; if (!Platform.isLinux) return;
if (_sideButtonChannelInitialized) return; if (_sideButtonChannelInitialized) return;
_sideButtonChannelInitialized = true; _sideButtonChannelInitialized = true;

@ -1 +1 @@
Subproject commit 9043c15acc6d5b42b6c12ad284c16c1ec172f1f0 Subproject commit 5a78ec42303ca046d808742e911a156636a2432b

View file

@ -276,21 +276,12 @@ impl PipeWireRecorder {
// see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982
src.set_property("always-copy", &true)?; src.set_property("always-copy", &true)?;
// COSMIC/Wayland fix: insert videoconvert between pipewiresrc and appsink.
// xdg-desktop-portal-cosmic's modifier negotiation fails when the downstream
// format set is too narrow (appsink only accepts BGRx/RGBx), producing
// "no more output formats" / not-negotiated (-4). videoconvert accepts any
// system-memory video/x-raw format, widening negotiation so the portal can
// settle on a format it can deliver via its SHM path.
let convert = gst::ElementFactory::make("videoconvert", None)?;
let sink = gst::ElementFactory::make("appsink", None)?; let sink = gst::ElementFactory::make("appsink", None)?;
sink.set_property("drop", &true)?; sink.set_property("drop", &true)?;
sink.set_property("max-buffers", &1u32)?; sink.set_property("max-buffers", &1u32)?;
pipeline.add_many(&[&src, &convert, &sink])?; pipeline.add_many(&[&src, &sink])?;
src.link(&convert)?; src.link(&sink)?;
convert.link(&sink)?;
let appsink = sink let appsink = sink
.dynamic_cast::<AppSink>() .dynamic_cast::<AppSink>()

View file

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

View file

@ -65,11 +65,12 @@ use hbb_common::{
self, self,
net::UdpSocket, net::UdpSocket,
sync::{ sync::{
mpsc::{unbounded_channel, UnboundedReceiver}, mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver},
oneshot, oneshot,
}, },
time::{interval, Duration, Instant}, time::{interval, Duration, Instant},
}, },
webrtc::WebRTCStream,
AddrMangle, ResultType, Stream, AddrMangle, ResultType, Stream,
}; };
pub use helper::*; pub use helper::*;
@ -330,6 +331,19 @@ impl Client {
} else { } else {
(None, None) (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( let fut = Self::_start_inner(
peer.to_owned(), peer.to_owned(),
key.to_owned(), key.to_owned(),
@ -338,6 +352,8 @@ impl Client {
interface.clone(), interface.clone(),
udp.clone(), udp.clone(),
Some(stop_udp_tx), Some(stop_udp_tx),
ipv6,
webrtc_offerer,
rendezvous_server.clone(), rendezvous_server.clone(),
servers.clone(), servers.clone(),
contained, contained,
@ -355,6 +371,8 @@ impl Client {
interface, interface,
(None, None), (None, None),
None, None,
None,
None,
rendezvous_server, rendezvous_server,
servers, servers,
contained, 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( async fn _start_inner(
peer: String, peer: String,
key: String, key: String,
@ -374,6 +453,8 @@ impl Client {
interface: impl Interface, interface: impl Interface,
mut udp: (Option<Arc<UdpSocket>>, Option<Arc<Mutex<u16>>>), mut udp: (Option<Arc<UdpSocket>>, Option<Arc<Mutex<u16>>>),
stop_udp_tx: Option<oneshot::Sender<()>>, stop_udp_tx: Option<oneshot::Sender<()>>,
mut ipv6: Option<(Arc<UdpSocket>, bytes::Bytes)>,
mut webrtc_offerer: Option<WebRTCStream>,
mut rendezvous_server: String, mut rendezvous_server: String,
servers: Vec<String>, servers: Vec<String>,
contained: bool, contained: bool,
@ -446,14 +527,20 @@ impl Client {
// Stop UDP NAT test task if still running // Stop UDP NAT test task if still running
stop_udp_tx.map(|tx| tx.send(())); stop_udp_tx.map(|tx| tx.send(()));
let mut msg_out = RendezvousMessage::new(); let mut msg_out = RendezvousMessage::new();
let mut ipv6 = if crate::get_ipv6_punch_enabled() { let mut ipv6 = ipv6
if let Some((socket, addr)) = crate::get_ipv6_socket().await { .take()
(Some(socket), Some(addr)) .map(|(socket, addr)| (Some(socket), Some(addr)))
} else { .unwrap_or((None, None));
(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 { } else {
(None, None) String::new()
}; };
let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0); 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" }; let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" };
@ -467,9 +554,16 @@ impl Client {
udp_port: udp_nat_port as _, udp_port: udp_nat_port as _,
force_relay: interface.is_force_relay(), force_relay: interface.is_force_relay(),
socket_addr_v6: ipv6.1.unwrap_or_default(), socket_addr_v6: ipv6.1.unwrap_or_default(),
webrtc_sdp_offer: webrtc_sdp_offer.clone(),
..Default::default() ..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!( log::info!(
"#{} {} punch attempt with {}, id: {}", "#{} {} punch attempt with {}, id: {}",
i, i,
@ -479,9 +573,20 @@ impl Client {
); );
socket.send(&msg_out).await?; socket.send(&msg_out).await?;
// below timeout should not bigger than hbbs's connection timeout. // below timeout should not bigger than hbbs's connection timeout.
if let Some(msg_in) = let attempt_deadline = Instant::now() + Duration::from_millis((i * 3000) as u64);
crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 3000)).await 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 { match msg_in.union {
Some(rendezvous_message::Union::PunchHoleResponse(ph)) => { Some(rendezvous_message::Union::PunchHoleResponse(ph)) => {
if ph.socket_addr.is_empty() { if ph.socket_addr.is_empty() {
@ -510,6 +615,7 @@ impl Client {
relay_server = ph.relay_server; relay_server = ph.relay_server;
peer_addr = AddrMangle::decode(&ph.socket_addr); peer_addr = AddrMangle::decode(&ph.socket_addr);
feedback = ph.feedback; feedback = ph.feedback;
webrtc_sdp_answer = ph.webrtc_sdp_answer;
let s = udp.0.take(); let s = udp.0.take();
if ph.is_udp && s.is_some() { if ph.is_udp && s.is_some() {
if let Some(s) = s { if let Some(s) = s {
@ -528,7 +634,7 @@ impl Client {
} }
} }
log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr); log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr);
break; break 'punch_attempts;
} }
} }
Some(rendezvous_message::Union::RelayResponse(rr)) => { Some(rendezvous_message::Union::RelayResponse(rr)) => {
@ -549,6 +655,38 @@ impl Client {
} }
} }
signed_id_pk = rr.pk().into(); 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( let fut = Self::create_relay(
&peer, &peer,
rr.uuid, rr.uuid,
@ -564,30 +702,81 @@ impl Client {
} }
.boxed(), .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 // Run all connection attempts concurrently, return the first successful one
let (conn, kcp, typ) = match select_ok(connect_futures).await { let (conn, kcp, typ) = match select_ok(connect_futures).await {
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2), Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
Err(e) => (Err(e), None, ""), Err(e) => (Err(e), None, ""),
}; };
if let Some(stop) = webrtc_bridge_stop {
let _ = stop.send(());
}
let mut conn = conn?; let mut conn = conn?;
feedback = rr.feedback; feedback = rr.feedback;
log::info!("{:?} used to establish {typ} connection", start.elapsed()); log::info!("{:?} used to establish {typ} connection", start.elapsed());
let pk = let pk =
Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?; Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?;
return Ok(( return Ok((
(conn, typ == "IPv6", pk, kcp, typ), (conn, typ == "IPv6" || typ == "WebRTC", pk, kcp, typ),
(feedback, rendezvous_server), (feedback, rendezvous_server),
false, 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); 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 { if peer_addr.port() == 0 {
bail!("Failed to connect via rendezvous server"); bail!("Failed to connect via rendezvous server");
} }
@ -621,6 +810,8 @@ impl Client {
interface, interface,
udp.0, udp.0,
ipv6.0, ipv6.0,
webrtc_for_connect,
webrtc_bridge_stop,
punch_type, punch_type,
) )
.await?, .await?,
@ -647,6 +838,8 @@ impl Client {
interface: impl Interface, interface: impl Interface,
udp_socket_nat: Option<Arc<UdpSocket>>, udp_socket_nat: Option<Arc<UdpSocket>>,
udp_socket_v6: Option<Arc<UdpSocket>>, udp_socket_v6: Option<Arc<UdpSocket>>,
webrtc_offerer: Option<WebRTCStream>,
webrtc_bridge_stop: Option<oneshot::Sender<()>>,
punch_type: &str, punch_type: &str,
) -> ResultType<( ) -> ResultType<(
Stream, Stream,
@ -705,11 +898,23 @@ impl Client {
if let Some(udp_socket_v6) = udp_socket_v6 { if let Some(udp_socket_v6) = udp_socket_v6 {
connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6", connect_timeout).boxed()); 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 // Run all connection attempts concurrently, return the first successful one
let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await { let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await {
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2), Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
Err(e) => (Err(e), None, ""), Err(e) => (Err(e), None, ""),
}; };
if let Some(stop) = webrtc_bridge_stop {
let _ = stop.send(());
}
let mut direct = !conn.is_err(); let mut direct = !conn.is_err();
if interface.is_force_relay() || conn.is_err() { if interface.is_force_relay() || conn.is_err() {
@ -4120,8 +4325,22 @@ pub mod peer_online {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::client::Client;
use hbb_common::rendezvous_proto::IceCandidate;
use hbb_common::tokio; 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] #[tokio::test]
async fn test_query_onlines() { async fn test_query_onlines() {
super::query_online_states( super::query_online_states(

View file

@ -199,20 +199,6 @@ pub fn core_main() -> Option<Vec<String>> {
} }
std::thread::spawn(move || crate::start_server(false, no_server)); std::thread::spawn(move || crate::start_server(false, no_server));
} else { } else {
#[cfg(any(target_os = "linux", target_os = "macos"))]
// Root CLI management commands must talk to the user `--server` main IPC.
// Example: `sudo rustdesk --option custom-rendezvous-server` should query the
// user's IPC instead of root's `/tmp/<app>-0/ipc`; `connect()` still limits this
// routing to empty-postfix main IPC only.
let _user_main_ipc_scope = if crate::platform::is_installed()
&& is_root()
&& is_user_main_ipc_scope_cli_command(&args)
{
Some(crate::ipc::UserMainIpcScope::new())
} else {
None
};
#[cfg(windows)] #[cfg(windows)]
{ {
use crate::platform; use crate::platform;
@ -952,57 +938,6 @@ fn is_root() -> bool {
crate::platform::is_root() crate::platform::is_root()
} }
#[cfg(any(target_os = "linux", target_os = "macos", test))]
fn is_user_main_ipc_scope_cli_command(args: &[String]) -> bool {
matches!(
args.first().map(String::as_str),
Some("--password")
| Some("--set-unlock-pin")
| Some("--get-id")
| Some("--set-id")
| Some("--config")
| Some("--option")
| Some("--assign")
| Some("--deploy")
)
}
#[cfg(test)]
mod tests {
use super::*;
fn args(values: &[&str]) -> Vec<String> {
values.iter().map(|value| value.to_string()).collect()
}
#[test]
fn user_main_ipc_scope_cli_command_matches_management_commands_only() {
for command in [
"--password",
"--set-unlock-pin",
"--get-id",
"--set-id",
"--config",
"--option",
"--assign",
"--deploy",
] {
assert!(is_user_main_ipc_scope_cli_command(&args(&[command])));
}
for command in [
"--service",
"--server",
"--tray",
"--cm",
"--check-hwcodec-config",
"--connect",
] {
assert!(!is_user_main_ipc_scope_cli_command(&args(&[command])));
}
}
}
/// Check if the executable is a Quick Support version. /// Check if the executable is a Quick Support version.
/// Note: This function must be kept in sync with `libs/portable/src/main.rs`. /// Note: This function must be kept in sync with `libs/portable/src/main.rs`.
#[cfg(windows)] #[cfg(windows)]

View file

@ -33,25 +33,25 @@ use hbb_common::{
tokio_util::codec::Framed, tokio_util::codec::Framed,
ResultType, ResultType,
}; };
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_auth::authorize_service_scoped_ipc_connection;
#[cfg(windows)] #[cfg(windows)]
pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection; pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection;
#[cfg(windows)] #[cfg(windows)]
pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt; pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt;
#[cfg(windows)] #[cfg(windows)]
pub(crate) use ipc_auth::log_rejected_windows_ipc_connection; pub(crate) use ipc_auth::log_rejected_windows_ipc_connection;
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(target_os = "linux")]
use ipc_auth::{active_uid, authorize_service_scoped_ipc_connection}; 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)] #[cfg(windows)]
use ipc_auth::{ use ipc_auth::{
authorize_windows_main_ipc_connection, portable_service_listener_security_attributes, authorize_windows_main_ipc_connection, portable_service_listener_security_attributes,
should_allow_everyone_create_on_windows, should_allow_everyone_create_on_windows,
}; };
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub(crate) use ipc_auth::{
ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid,
log_rejected_uinput_connection, peer_uid_from_fd,
};
#[cfg(target_os = "linux")]
use ipc_fs::terminal_count_candidate_uids; use ipc_fs::terminal_count_candidate_uids;
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_fs::{ use ipc_fs::{
@ -63,8 +63,6 @@ use parity_tokio_ipc::{
}; };
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(any(target_os = "linux", target_os = "macos"))]
use std::cell::Cell;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -73,47 +71,12 @@ use std::{
// IPC actions here. // IPC actions here.
pub const IPC_ACTION_CLOSE: &str = "close"; pub const IPC_ACTION_CLOSE: &str = "close";
#[cfg(target_os = "windows")]
const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000; const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000;
#[cfg(target_os = "windows")]
pub(crate) const IPC_TOKEN_LEN: usize = 64; pub(crate) const IPC_TOKEN_LEN: usize = 64;
#[cfg(target_os = "windows")]
const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2; const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2;
#[cfg(target_os = "windows")]
const _: () = assert!(IPC_TOKEN_LEN % 2 == 0); const _: () = assert!(IPC_TOKEN_LEN % 2 == 0);
pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true); pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true);
#[cfg(any(target_os = "linux", target_os = "macos"))]
thread_local! {
static USE_USER_MAIN_IPC: Cell<bool> = Cell::new(false);
}
#[must_use = "bind this guard to a local variable to keep the IPC scope active"]
/// Thread-local guard for routing root main IPC to the active user on Linux/macOS.
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) struct UserMainIpcScope {
previous: bool,
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl UserMainIpcScope {
pub(crate) fn new() -> Self {
let previous = USE_USER_MAIN_IPC.with(|use_user_main| {
let previous = use_user_main.get();
use_user_main.set(true);
previous
});
Self { previous }
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl Drop for UserMainIpcScope {
fn drop(&mut self) {
USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.set(self.previous));
}
}
#[inline] #[inline]
pub async fn connect_service(ms_timeout: u64) -> ResultType<ConnectionTmpl<ConnClient>> { pub async fn connect_service(ms_timeout: u64) -> ResultType<ConnectionTmpl<ConnClient>> {
connect(ms_timeout, crate::POSTFIX_SERVICE).await connect(ms_timeout, crate::POSTFIX_SERVICE).await
@ -1149,7 +1112,11 @@ async fn handle(data: Data, stream: &mut Connection) {
}; };
} }
#[cfg(target_os = "windows")] 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> { pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
use hbb_common::rand::{rngs::OsRng, RngCore as _}; use hbb_common::rand::{rngs::OsRng, RngCore as _};
use std::fmt::Write as _; use std::fmt::Write as _;
@ -1170,7 +1137,6 @@ pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
Ok(token) Ok(token)
} }
#[cfg(target_os = "windows")]
pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool { pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool {
if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN { if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN {
return false; return false;
@ -1183,7 +1149,6 @@ pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> boo
== 0 == 0
} }
#[cfg(target_os = "windows")]
pub(crate) async fn portable_service_ipc_handshake_as_client<T>( pub(crate) async fn portable_service_ipc_handshake_as_client<T>(
stream: &mut ConnectionTmpl<T>, stream: &mut ConnectionTmpl<T>,
token: &str, token: &str,
@ -1208,7 +1173,6 @@ where
} }
} }
#[cfg(target_os = "windows")]
pub(crate) async fn portable_service_ipc_handshake_as_server<T, F>( pub(crate) async fn portable_service_ipc_handshake_as_server<T, F>(
stream: &mut ConnectionTmpl<T>, stream: &mut ConnectionTmpl<T>,
mut validate_token: F, mut validate_token: F,
@ -1245,103 +1209,6 @@ async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType<Connection
Ok(ConnectionTmpl::new(client)) Ok(ConnectionTmpl::new(client))
} }
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[inline]
fn select_server_uid_for_user_main_ipc(
server_uids: &[u32],
active_uid: Option<u32>,
prefer_root: bool,
) -> ResultType<u32> {
let mut server_uids = server_uids.to_vec();
server_uids.sort_unstable();
server_uids.dedup();
match server_uids.as_slice() {
[] => {
if let Some(uid) = active_uid {
// If no `--server` processes are found but the active user is identifiable,
// try the active user anyway because the main process may also listen on "" IPC.
return Ok(uid);
} else {
bail!("No --server process found for user main IPC")
}
}
[uid] => return Ok(*uid),
_ => {}
}
if prefer_root && server_uids.contains(&0) {
return Ok(0);
}
if let Some(active_uid) = active_uid.filter(|uid| server_uids.contains(uid)) {
return Ok(active_uid);
}
bail!("Multiple --server processes found for user main IPC");
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn running_server_uids_for_current_exe() -> ResultType<Vec<u32>> {
let current_exe = std::env::current_exe()?;
let current_exe_path = std::fs::canonicalize(&current_exe)?;
let current_pid = hbb_common::sysinfo::Pid::from_u32(std::process::id());
let mut sys = hbb_common::sysinfo::System::new();
sys.refresh_processes();
let mut server_uids = Vec::new();
for process in sys.processes().values() {
if process.pid() == current_pid {
continue;
}
if process.cmd().get(1).map_or(true, |arg| arg != "--server") {
continue;
}
let Ok(process_path) = std::fs::canonicalize(process.exe()) else {
continue;
};
if process_path != current_exe_path {
continue;
}
let Some(uid) = process.user_id().map(|uid| **uid as u32) else {
// Root CLI management commands need a stable matching `--server` target.
// If this key process races during enumeration, failing the command is clearer
// than silently skipping it; `--server` is not expected to exit frequently.
bail!("Failed to read --server process uid");
};
server_uids.push(uid);
}
Ok(server_uids)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn user_main_ipc_server_uid() -> ResultType<u32> {
let server_uids = running_server_uids_for_current_exe()?;
#[cfg(target_os = "linux")]
let prefer_root = crate::platform::linux::is_login_screen_wayland();
#[cfg(target_os = "macos")]
let prefer_root = false;
select_server_uid_for_user_main_ipc(&server_uids, active_uid(), prefer_root)
}
pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let use_user_main_ipc = USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.get());
let is_root_main_ipc =
unsafe { hbb_common::libc::geteuid() == 0 } && postfix.is_empty() && use_user_main_ipc;
if is_root_main_ipc {
let uid = user_main_ipc_server_uid()?;
let path = Config::ipc_path_for_uid(uid, postfix);
return connect_with_path(ms_timeout, &path).await;
}
let path = Config::ipc_path(postfix);
return connect_with_path(ms_timeout, &path).await;
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let path = Config::ipc_path(postfix);
connect_with_path(ms_timeout, &path).await
}
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub async fn connect_for_uid( pub async fn connect_for_uid(
ms_timeout: u64, ms_timeout: u64,
@ -2135,16 +2002,7 @@ mod test {
assert!(std::mem::size_of::<Data>() <= 120); assert!(std::mem::size_of::<Data>() <= 120);
} }
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(target_os = "linux")]
#[test]
fn test_service_ipc_path_is_shared_across_uids() {
assert_eq!(
Config::ipc_path_for_uid(0, crate::POSTFIX_SERVICE),
Config::ipc_path_for_uid(501, crate::POSTFIX_SERVICE)
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test] #[test]
fn test_ipc_path_differs_by_uid_for_cm() { fn test_ipc_path_differs_by_uid_for_cm() {
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 }; let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
@ -2163,46 +2021,4 @@ mod test {
Config::ipc_path_for_uid(other_uid, postfix) Config::ipc_path_for_uid(other_uid, postfix)
); );
} }
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_uses_active_uid_when_no_server_found() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[], Some(501), false).unwrap(),
501
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_uses_single_server_uid() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[501], None, false).unwrap(),
501
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_prefers_active_uid_with_multiple_servers() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[0, 501], Some(501), false).unwrap(),
501
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_prefers_root_on_wayland_login_screen() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[0, 501], Some(501), true).unwrap(),
0
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_fails_when_multiple_servers_are_ambiguous() {
assert!(select_server_uid_for_user_main_ipc(&[501, 502], None, false).is_err());
}
} }

View file

@ -607,30 +607,27 @@ pub(crate) fn log_rejected_windows_ipc_connection(
peer_session_id: Option<u32>, peer_session_id: Option<u32>,
expected_session_id: Option<u32>, expected_session_id: Option<u32>,
peer_is_system: Option<bool>, peer_is_system: Option<bool>,
peer_is_elevated: Option<bool>,
) { ) {
static LOG_THROTTLE: OnceLock<Mutex<UnauthorizedIpcLogThrottle>> = OnceLock::new(); static LOG_THROTTLE: OnceLock<Mutex<UnauthorizedIpcLogThrottle>> = OnceLock::new();
throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| {
if suppressed > 0 { if suppressed > 0 {
log::warn!( log::warn!(
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?} (suppressed {} similar events)", "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?} (suppressed {} similar events)",
postfix, postfix,
peer_pid, peer_pid,
peer_session_id, peer_session_id,
expected_session_id, expected_session_id,
peer_is_system, peer_is_system,
peer_is_elevated,
suppressed suppressed
); );
} else { } else {
log::warn!( log::warn!(
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?}", "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}",
postfix, postfix,
peer_pid, peer_pid,
peer_session_id, peer_session_id,
expected_session_id, expected_session_id,
peer_is_system, peer_is_system
peer_is_elevated
); );
} }
}); });
@ -658,14 +655,8 @@ pub(crate) fn authorize_service_scoped_ipc_connection(stream: &Connection, postf
#[cfg(windows)] #[cfg(windows)]
pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool { pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool {
let ( let (authorized, peer_pid, peer_session_id, server_session_id, peer_is_system) =
authorized, stream.server_authorization_status();
peer_pid,
peer_session_id,
server_session_id,
peer_is_system,
peer_is_elevated,
) = stream.server_authorization_status();
if !authorized { if !authorized {
log_rejected_windows_ipc_connection( log_rejected_windows_ipc_connection(
postfix, postfix,
@ -673,7 +664,6 @@ pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix
peer_session_id, peer_session_id,
server_session_id, server_session_id,
peer_is_system, peer_is_system,
peer_is_elevated,
); );
return false; return false;
} }
@ -786,14 +776,7 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
fn server_authorization_status( fn server_authorization_status(
&self, &self,
) -> ( ) -> (bool, Option<u32>, Option<u32>, Option<u32>, Option<bool>) {
bool,
Option<u32>,
Option<u32>,
Option<u32>,
Option<bool>,
Option<bool>,
) {
let peer_pid = self.peer_pid(); let peer_pid = self.peer_pid();
let server_session_id = crate::platform::windows::get_current_process_session_id(); let server_session_id = crate::platform::windows::get_current_process_session_id();
let peer_session_id = let peer_session_id =
@ -803,34 +786,20 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
let peer_is_system = peer_is_system_result let peer_is_system = peer_is_system_result
.as_ref() .as_ref()
.and_then(|r| r.as_ref().ok().copied()); .and_then(|r| r.as_ref().ok().copied());
let session_authorized = is_allowed_windows_session_scoped_peer( if server_session_id.is_none() && !peer_is_system.unwrap_or(false) {
peer_is_system.unwrap_or(false),
peer_session_id,
server_session_id,
);
let peer_is_elevated_result = if session_authorized {
None
} else {
peer_pid.map(|pid| crate::platform::windows::is_elevated(Some(pid)))
};
let peer_is_elevated = peer_is_elevated_result
.as_ref()
.and_then(|r| r.as_ref().ok().copied());
if server_session_id.is_none()
&& !peer_is_system.unwrap_or(false)
&& !peer_is_elevated.unwrap_or(false)
{
// When the server session id cannot be determined, the session-id allow-path is // When the server session id cannot be determined, the session-id allow-path is
// disabled and only privileged peers can be authorized. // disabled and only SYSTEM peers can be authorized.
log::debug!( log::debug!(
"IPC authorization: server session id unavailable; rejecting non-privileged peer, peer_pid={:?}, peer_session_id={:?}", "IPC authorization: server session id unavailable; rejecting non-SYSTEM peer, peer_pid={:?}, peer_session_id={:?}",
peer_pid, peer_pid,
peer_session_id peer_session_id
); );
} }
// Main IPC trusts same-session peers, LocalSystem, and elevated administrators. let authorized = is_allowed_windows_session_scoped_peer(
// Service-scoped IPC channels keep their own stricter authorization paths. peer_is_system.unwrap_or(false),
let authorized = session_authorized || peer_is_elevated.unwrap_or(false); peer_session_id,
server_session_id,
);
if !authorized { if !authorized {
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) { if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) {
log::debug!( log::debug!(
@ -839,13 +808,6 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
err err
); );
} }
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_elevated_result.as_ref()) {
log::debug!(
"Failed to determine whether peer process is elevated, pid={}, err={}",
pid,
err
);
}
} }
( (
authorized, authorized,
@ -853,7 +815,6 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
peer_session_id, peer_session_id,
server_session_id, server_session_id,
peer_is_system, peer_is_system,
peer_is_elevated,
) )
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "كلمة المرور مخفية"), ("password-hidden-tip", "كلمة المرور مخفية"),
("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."), ("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."),
("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"), ("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "永久密码已设置(已隐藏)"), ("password-hidden-tip", "永久密码已设置(已隐藏)"),
("preset-password-in-use-tip", "当前使用预设密码"), ("preset-password-in-use-tip", "当前使用预设密码"),
("Enable privacy mode", "允许隐私模式"), ("Enable privacy mode", "允许隐私模式"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."),
("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."),
("Enable privacy mode", "Datenschutzmodus aktivieren"), ("Enable privacy mode", "Datenschutzmodus aktivieren"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -274,6 +274,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"),
("password-hidden-tip", "Permanent password is set (hidden)."), ("password-hidden-tip", "Permanent password is set (hidden)."),
("preset-password-in-use-tip", "Preset password is currently in use."), ("preset-password-in-use-tip", "Preset password is currently in use."),
("allow-remote-toolbar-docking-any-edge", "Allow docking remote toolbar to any window edge"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "La contraseña permanente está ajustada a (oculta)."), ("password-hidden-tip", "La contraseña permanente está ajustada a (oculta)."),
("preset-password-in-use-tip", "Se está usando la contraseña predeterminada."), ("preset-password-in-use-tip", "Se está usando la contraseña predeterminada."),
("Enable privacy mode", "Habilitar modo privado"), ("Enable privacy mode", "Habilitar modo privado"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."),
("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."),
("Enable privacy mode", "Activer le mode de confidentialité"), ("Enable privacy mode", "Activer le mode de confidentialité"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -654,7 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accessible devices", "એક્સેસિબલ ઉપકરણો"), ("Accessible devices", "એક્સેસિબલ ઉપકરણો"),
("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"), ("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"),
("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"), ("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"),
("Use D3D rendering", ""),
("Printer", "પ્રિન્ટર"), ("Printer", "પ્રિન્ટર"),
("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."), ("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."),
("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."), ("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."),
@ -744,6 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."), ("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."),
("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."), ("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -654,7 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accessible devices", "सुलभ डिवाइस"), ("Accessible devices", "सुलभ डिवाइस"),
("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"), ("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"),
("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"), ("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"),
("Use D3D rendering", ""),
("Printer", "प्रिंटर"), ("Printer", "प्रिंटर"),
("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"), ("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"),
("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"), ("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"),
@ -743,7 +742,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "प्रदर्शित नाम"), ("Display Name", "प्रदर्शित नाम"),
("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"), ("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"),
("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"), ("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."),
("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), ("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", "Adatvédelmi mód aktiválása"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "È impostata una password permanente (nascosta)."), ("password-hidden-tip", "È impostata una password permanente (nascosta)."),
("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."),
("Enable privacy mode", "Abilita modalità privacy"), ("Enable privacy mode", "Abilita modalità privacy"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"),
("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

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

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -654,7 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"), ("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"),
("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"), ("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"),
("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"), ("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"),
("Use D3D rendering", ""),
("Printer", "പ്രിന്റർ"), ("Printer", "പ്രിന്റർ"),
("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."), ("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."),
("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."), ("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."),
@ -743,7 +742,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "ഡിസ്‌പ്ലേ പേര്"), ("Display Name", "ഡിസ്‌പ്ലേ പേര്"),
("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."), ("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."),
("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."), ("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."),
("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."),
("Enable privacy mode", "Privacymodus inschakelen"), ("Enable privacy mode", "Privacymodus inschakelen"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."),
("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -740,10 +740,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"), ("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"),
("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"), ("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"),
("Continue with {}", "Continuar com {}"), ("Continue with {}", "Continuar com {}"),
("Display Name", "Nome de Exibição"), ("Display Name", ""),
("password-hidden-tip", "A senha permanente está definida como (oculta)."), ("password-hidden-tip", ""),
("preset-password-in-use-tip", "A senha predefinida está sendo usada."), ("preset-password-in-use-tip", ""),
("Enable privacy mode", "Habilitar modo de privacidade"), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -540,7 +540,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."), ("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."),
("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"), ("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"),
("Check for software update on startup", "Verifică actualizări la pornire"), ("Check for software update on startup", "Verifică actualizări la pornire"),
("upgrade_rustdesk_server_pro_to_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."), ("upgrade_rustdesk_server_pro_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."),
("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), ("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."),
("Filter by intersection", "Filtrează prin intersecție"), ("Filter by intersection", "Filtrează prin intersecție"),
("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"), ("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"),
@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."), ("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."),
("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."), ("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."),
("preset-password-in-use-tip", "Установленный пароль сейчас используется."), ("preset-password-in-use-tip", "Установленный пароль сейчас используется."),
("Enable privacy mode", "Использовать режим конфиденциальности"), ("Enable privacy mode", "Использовать режим конфиденциальности"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Parola gizli"), ("password-hidden-tip", "Parola gizli"),
("preset-password-in-use-tip", "Önceden ayarlanmış parola kullanılıyor"), ("preset-password-in-use-tip", "Önceden ayarlanmış parola kullanılıyor"),
("Enable privacy mode", "Gizlilik modunu etkinleştir"), ("Enable privacy mode", "Gizlilik modunu etkinleştir"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "固定密碼已設定(已隱藏)"), ("password-hidden-tip", "固定密碼已設定(已隱藏)"),
("preset-password-in-use-tip", "目前正在使用預設密碼"), ("preset-password-in-use-tip", "目前正在使用預設密碼"),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("Enable privacy mode", ""), ("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View file

@ -38,49 +38,68 @@ fn main() {
if !common::global_init() { if !common::global_init() {
return; return;
} }
use clap::App; use clap::{Arg, ArgAction, Command};
use hbb_common::log; use hbb_common::log;
let args = format!( let matches = Command::new("rustdesk")
"-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) .version(crate::VERSION)
.author("Purslane Ltd<info@rustdesk.com>") .author("Purslane Ltd<info@rustdesk.com>")
.about("RustDesk command line tool") .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(); .get_matches();
use hbb_common::{config::LocalConfig, env_logger::*}; use hbb_common::{config::LocalConfig, env_logger::*};
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
if let Some(p) = matches.value_of("port-forward") { if let Some(p) = matches.get_one::<String>("port-forward") {
let options: Vec<String> = p.split(":").map(|x| x.to_owned()).collect(); let options: Vec<String> = p.split(':').map(|x| x.to_owned()).collect();
if options.len() < 3 { if options.len() < 3 {
log::error!("Wrong port-forward options"); log::error!("Wrong port-forward options");
return; return;
} }
let mut port = 0; let port = match options[1].parse::<i32>() {
if let Ok(v) = options[1].parse::<i32>() { Ok(v) => v,
port = v; Err(_) => {
} else { log::error!("Wrong local-port");
log::error!("Wrong local-port"); return;
return; }
} };
let mut remote_port = 0; let remote_port = match options[2].parse::<i32>() {
if let Ok(v) = options[2].parse::<i32>() { Ok(v) => v,
remote_port = v; Err(_) => {
} else { log::error!("Wrong remote-port");
log::error!("Wrong remote-port"); return;
return; }
} };
let mut remote_host = "localhost".to_owned(); let mut remote_host = "localhost".to_owned();
if options.len() > 3 { if options.len() > 3 {
remote_host = options[3].clone(); remote_host = options[3].clone();
} }
common::test_rendezvous_server(); common::test_rendezvous_server();
common::test_nat_type(); 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"); let token = LocalConfig::get_option("access_token");
cli::start_one_port_forward( cli::start_one_port_forward(
options[0].clone(), options[0].clone(),
@ -90,13 +109,17 @@ fn main() {
key, key,
token, 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_rendezvous_server();
common::test_nat_type(); 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"); let token = LocalConfig::get_option("access_token");
cli::connect_test(p, key, 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()); log::info!("id={}", hbb_common::config::Config::get_id());
crate::start_server(true, false); crate::start_server(true, false);
} }

View file

@ -614,7 +614,6 @@ fn authorize_service_scoped_ipc_connection(
peer_session_id, peer_session_id,
expected_active_session_id, expected_active_session_id,
peer_is_system, peer_is_system,
None,
); );
return false; return false;
} }

View file

@ -1,4 +1,5 @@
use std::{ use std::{
collections::HashMap,
net::SocketAddr, net::SocketAddr,
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
@ -21,8 +22,13 @@ use hbb_common::{
rendezvous_proto::*, rendezvous_proto::*,
sleep, sleep,
socket_client::{self, connect_tcp, is_ipv4, new_direct_udp_for, new_udp_for}, 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, udp::FramedSocket,
webrtc::WebRTCStream,
AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr, AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr,
}; };
@ -32,11 +38,13 @@ use crate::{
}; };
type Message = RendezvousMessage; type Message = RendezvousMessage;
type RendezvousSender = mpsc::UnboundedSender<Message>;
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref SOLVING_PK_MISMATCH: Mutex<String> = Default::default(); 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_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 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 SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false);
@ -72,6 +80,7 @@ pub struct RendezvousMediator {
host: String, host: String,
host_prefix: String, host_prefix: String,
keep_alive: i32, keep_alive: i32,
rz_sender: RendezvousSender,
} }
impl RendezvousMediator { impl RendezvousMediator {
@ -182,11 +191,13 @@ impl RendezvousMediator {
let host = check_port(&host, RENDEZVOUS_PORT); let host = check_port(&host, RENDEZVOUS_PORT);
log::info!("start udp: {host}"); log::info!("start udp: {host}");
let (mut socket, mut addr) = new_udp_for(&host, CONNECT_TIMEOUT).await?; 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 { let mut rz = Self {
addr: addr.clone(), addr: addr.clone(),
host: host.clone(), host: host.clone(),
host_prefix: Self::get_host_prefix(&host), host_prefix: Self::get_host_prefix(&host),
keep_alive: crate::DEFAULT_KEEP_ALIVE, keep_alive: crate::DEFAULT_KEEP_ALIVE,
rz_sender,
}; };
let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT)); let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT));
@ -246,6 +257,9 @@ impl RendezvousMediator {
}, },
} }
}, },
Some(msg_out) = rz_out_rx.recv() => {
Sink::Framed(&mut socket, &addr).send(&msg_out).await?;
},
_ = timer.tick() => { _ = timer.tick() => {
if SHOULD_EXIT.load(Ordering::SeqCst) { if SHOULD_EXIT.load(Ordering::SeqCst) {
break; break;
@ -367,6 +381,17 @@ impl RendezvousMediator {
allow_err!(rz.handle_intranet(fla, server).await); 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)) => { Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
let v0 = Config::get_rendezvous_servers(); let v0 = Config::get_rendezvous_servers();
Config::set_option( Config::set_option(
@ -389,11 +414,13 @@ impl RendezvousMediator {
let mut conn = connect_tcp(host.clone(), CONNECT_TIMEOUT).await?; let mut conn = connect_tcp(host.clone(), CONNECT_TIMEOUT).await?;
let key = crate::get_key(true).await; let key = crate::get_key(true).await;
crate::secure_tcp(&mut conn, &key).await?; crate::secure_tcp(&mut conn, &key).await?;
let (rz_sender, mut rz_out_rx) = mpsc::unbounded_channel::<Message>();
let mut rz = Self { let mut rz = Self {
addr: conn.local_addr().into_target_addr()?, addr: conn.local_addr().into_target_addr()?,
host: host.clone(), host: host.clone(),
host_prefix: Self::get_host_prefix(&host), host_prefix: Self::get_host_prefix(&host),
keep_alive: crate::DEFAULT_KEEP_ALIVE, keep_alive: crate::DEFAULT_KEEP_ALIVE,
rz_sender,
}; };
let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT)); let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT));
let mut last_register_sent: Option<Instant> = None; let mut last_register_sent: Option<Instant> = None;
@ -421,6 +448,9 @@ impl RendezvousMediator {
let msg = Message::parse_from_bytes(&bytes)?; let msg = Message::parse_from_bytes(&bytes)?;
rz.handle_resp(msg.union, Sink::Stream(&mut conn), &server, &mut update_latency).await? 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() => { _ = timer.tick() => {
if SHOULD_EXIT.load(Ordering::SeqCst) { if SHOULD_EXIT.load(Ordering::SeqCst) {
break; break;
@ -472,6 +502,7 @@ impl RendezvousMediator {
rr.secure, rr.secure,
false, false,
Default::default(), Default::default(),
String::new(),
rr.control_permissions.clone().into_option(), rr.control_permissions.clone().into_option(),
) )
.await .await
@ -486,6 +517,7 @@ impl RendezvousMediator {
secure: bool, secure: bool,
initiate: bool, initiate: bool,
socket_addr_v6: bytes::Bytes, socket_addr_v6: bytes::Bytes,
webrtc_sdp_answer: String,
control_permissions: Option<ControlPermissions>, control_permissions: Option<ControlPermissions>,
) -> ResultType<()> { ) -> ResultType<()> {
let peer_addr = AddrMangle::decode(&socket_addr); let peer_addr = AddrMangle::decode(&socket_addr);
@ -504,6 +536,7 @@ impl RendezvousMediator {
socket_addr: socket_addr.into(), socket_addr: socket_addr.into(),
version: crate::VERSION.to_owned(), version: crate::VERSION.to_owned(),
socket_addr_v6, socket_addr_v6,
webrtc_sdp_answer,
..Default::default() ..Default::default()
}; };
if initiate { if initiate {
@ -571,6 +604,7 @@ impl RendezvousMediator {
true, true,
true, true,
socket_addr_v6, socket_addr_v6,
String::new(),
fla.control_permissions.into_option(), fla.control_permissions.into_option(),
) )
.await .await
@ -613,6 +647,81 @@ impl RendezvousMediator {
Ok(()) 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<()> { async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> {
let mut peer_addr = AddrMangle::decode(&ph.socket_addr); let mut peer_addr = AddrMangle::decode(&ph.socket_addr);
let last = *LAST_MSG.lock().await; let last = *LAST_MSG.lock().await;
@ -624,7 +733,23 @@ impl RendezvousMediator {
let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6); let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6);
let relay = use_ws() || Config::is_proxy() || ph.force_relay; let relay = use_ws() || Config::is_proxy() || ph.force_relay;
let mut socket_addr_v6 = Default::default(); 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 { if peer_addr_v6.port() > 0 && !relay {
socket_addr_v6 = start_ipv6( socket_addr_v6 = start_ipv6(
peer_addr_v6, peer_addr_v6,
@ -651,6 +776,7 @@ impl RendezvousMediator {
true, true,
true, true,
socket_addr_v6.clone(), socket_addr_v6.clone(),
webrtc_sdp_answer.clone(),
control_permissions, control_permissions,
) )
.await; .await;
@ -664,6 +790,7 @@ impl RendezvousMediator {
nat_type: nat_type.into(), nat_type: nat_type.into(),
version: crate::VERSION.to_owned(), version: crate::VERSION.to_owned(),
socket_addr_v6, socket_addr_v6,
webrtc_sdp_answer,
..Default::default() ..Default::default()
}; };
if ph.udp_port > 0 { if ph.udp_port > 0 {