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

View file

@ -142,10 +142,6 @@ const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
const String kOptionCodecPreference = "codec-preference";
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
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 kOptionRemoteMenubarState = "remoteMenubarState";
const String kOptionPeerSorting = "peer-sorting";

View file

@ -488,16 +488,6 @@ class _GeneralState extends State<_General> {
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs,
isServer: false),
if (!bind.isIncomingOnly())
_OptionCheckBox(
context,
'allow-remote-toolbar-docking-any-edge',
kOptionAllowMultiEdgeToolbarDock,
isServer: false,
update: (_) {
reloadAllWindows();
},
),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
if (!isWeb) wallpaper(),
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/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 {
late RxBool _pin;
@ -464,26 +250,8 @@ class RemoteToolbar extends StatefulWidget {
class _RemoteToolbarState extends State<RemoteToolbar> {
late Debouncer<int> _debouncerHide;
bool _isCursorOverImage = false;
final _fraction = 0.5.obs;
final _edge = _ToolbarEdge.top.obs;
final _fractionX = 0.5.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;
@ -505,144 +273,16 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
void _minimize() async =>
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
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 {
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
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) {
if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) {
collapse.value = true;
@ -679,130 +311,64 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
@override
dispose() {
++_dockingOptionSyncSerial;
widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
super.dispose();
widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
}
@override
Widget build(BuildContext context) {
return Obx(() {
// Wait for initialization to complete to prevent flickering
if (!widget.state.initialized.value ||
!_dockingOptionsInitialized.value) {
if (!widget.state.initialized.value) {
return const SizedBox.shrink();
}
// If toolbar is hidden, return empty widget
if (hide.value) {
return const SizedBox.shrink();
}
final edge = _edge.value;
final isHorizontal = _isHorizontalEdge(edge);
// Measure the live toolbar after every layout so the preview ghost can
// match its actual footprint (collapsed handle vs expanded toolbar).
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,
],
return Align(
alignment: Alignment.topCenter,
child: collapse.isFalse
? _buildToolbar(context)
: _buildDraggableCollapse(context),
);
});
}
Widget _buildDragPreview(BuildContext context, _ToolbarEdge edge,
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) {
Widget _buildDraggableCollapse(BuildContext context) {
return Obx(() {
if (collapse.isFalse && _dragging.isFalse) {
triggerAutoHide();
}
final borderRadius = _collapseHandleBorderRadius(edge);
return Offstage(
offstage: _dragging.isTrue,
child: Material(
elevation: _ToolbarTheme.elevation,
shadowColor: MyTheme.color(context).shadow,
borderRadius: borderRadius,
child: _DraggableShowHide(
id: widget.id,
sessionId: widget.ffi.sessionId,
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,
final borderRadius = BorderRadius.vertical(
bottom: Radius.circular(5),
);
return Align(
alignment: FractionalOffset(_fractionX.value, 0),
child: Offstage(
offstage: _dragging.isTrue,
child: Material(
elevation: _ToolbarTheme.elevation,
shadowColor: MyTheme.color(context).shadow,
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(
BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
Widget _buildToolbar(BuildContext context) {
final List<Widget> toolbarItems = [];
toolbarItems.add(_PinMenu(state: widget.state));
if (!isWebDesktop) {
@ -816,7 +382,6 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
return _MonitorMenu(
id: widget.id,
ffi: widget.ffi,
edge: edge,
setRemoteState: widget.setRemoteState);
} else {
return Offstage();
@ -842,53 +407,37 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
if (!isWeb) toolbarItems.add(_RecordMenu());
toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0));
// innerAxis: how the toolbar icons themselves flow.
// 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,
return Column(
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 {
final String id;
final FFI ffi;
final _ToolbarEdge edge;
final Function(VoidCallback) setRemoteState;
const _MonitorMenu({
Key? key,
required this.id,
required this.ffi,
required this.edge,
required this.setRemoteState,
}) : super(key: key);
@ -984,17 +531,9 @@ class _MonitorMenu extends StatelessWidget {
!isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
@override
Widget build(BuildContext context) {
final child = showMonitorsToolbar
? buildMultiMonitorMenu(context)
: Obx(() => buildMonitorMenu(context));
final quarterTurns = _monitorMenuQuarterTurns(edge);
if (quarterTurns == 0) return child;
return RotatedBox(
quarterTurns: quarterTurns,
child: child,
);
}
Widget build(BuildContext context) => showMonitorsToolbar
? buildMultiMonitorMenu(context)
: Obx(() => buildMonitorMenu(context));
Widget buildMonitorMenu(BuildContext context) {
final width = SimpleWrapper<double>(0);
@ -1126,8 +665,7 @@ class _MonitorMenu extends StatelessWidget {
}
final scale = _ToolbarTheme.buttonSize / rect.height * 0.75;
final height = rect.height * scale;
final startY = (_ToolbarTheme.buttonSize - height) * 0.5;
final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5;
final startX = startY;
final children = <Widget>[];
@ -1170,7 +708,7 @@ class _MonitorMenu extends StatelessWidget {
width.value = rect.width * scale + startX * 2;
return SizedBox(
width: width.value,
height: height + startY * 2,
height: rect.height * scale + startY * 2,
child: Stack(
children: children,
),
@ -2981,18 +2519,7 @@ class RdoMenuButton<T> extends StatelessWidget {
class _DraggableShowHide extends StatefulWidget {
final String id;
final SessionID sessionId;
final RxDouble fraction;
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 RxDouble fractionX;
final RxBool dragging;
final ToolbarState toolbarState;
final BorderRadius borderRadius;
@ -3004,15 +2531,7 @@ class _DraggableShowHide extends StatefulWidget {
Key? key,
required this.id,
required this.sessionId,
required this.fraction,
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.fractionX,
required this.dragging,
required this.toolbarState,
required this.setFullscreen,
@ -3025,12 +2544,10 @@ class _DraggableShowHide extends StatefulWidget {
}
class _DraggableShowHideState extends State<_DraggableShowHide> {
Offset position = Offset.zero;
Size size = Size.zero;
double left = 0.0;
double right = 1.0;
Offset? _lastPointerDown;
Offset? _dragGrabOffset;
double? _dragLongAxisGrabOffset;
Size? _dragToolbarSize;
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) {
return Listener(
onPointerDown: (event) => _lastPointerDown = event.position,
child: Draggable(
// When multi-edge docking is off the toolbar stays on the top edge,
// so lock the feedback to horizontal motion otherwise the handle
// floats away from the top while dragging and the toolbar looks
// 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(),
return Draggable(
axis: Axis.horizontal,
child: Icon(
Icons.drag_indicator,
size: 20,
color: MyTheme.color(context).drag_indicator,
),
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 = Flex(
direction: axis,
final child = Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildDraggable(context),
@ -3296,7 +2678,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
message: translate(
collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'),
child: Icon(
_toolbarCollapseIcon(widget.edge.value, collapse.isTrue),
collapse.isFalse ? Icons.expand_less : Icons.expand_more,
size: iconSize,
),
))),
@ -3338,8 +2720,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
borderRadius: widget.borderRadius,
),
child: SizedBox(
height: widget.isHorizontal ? 20 : null,
width: widget.isHorizontal ? null : 20,
height: 20,
child: child,
),
),

View file

@ -346,7 +346,7 @@ class InputModel {
/// which runs per-engine, so each isolate registers its own handler tied
/// to its own set of InputModels.
static void initSideButtonChannel() {
if (!isLinux) return;
if (!Platform.isLinux) return;
if (_sideButtonChannelInitialized) return;
_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
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)?;
sink.set_property("drop", &true)?;
sink.set_property("max-buffers", &1u32)?;
pipeline.add_many(&[&src, &convert, &sink])?;
src.link(&convert)?;
convert.link(&sink)?;
pipeline.add_many(&[&src, &sink])?;
src.link(&sink)?;
let appsink = sink
.dynamic_cast::<AppSink>()

View file

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

View file

@ -65,11 +65,12 @@ use hbb_common::{
self,
net::UdpSocket,
sync::{
mpsc::{unbounded_channel, UnboundedReceiver},
mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver},
oneshot,
},
time::{interval, Duration, Instant},
},
webrtc::WebRTCStream,
AddrMangle, ResultType, Stream,
};
pub use helper::*;
@ -330,6 +331,19 @@ impl Client {
} else {
(None, None)
};
let ipv6 = if crate::get_ipv6_punch_enabled() {
crate::get_ipv6_socket().await
} else {
None
};
let webrtc_offerer =
match WebRTCStream::new("", interface.is_force_relay(), CONNECT_TIMEOUT).await {
Ok(stream) => Some(stream),
Err(err) => {
log::warn!("webrtc offerer setup failed: {}", err);
None
}
};
let fut = Self::_start_inner(
peer.to_owned(),
key.to_owned(),
@ -338,6 +352,8 @@ impl Client {
interface.clone(),
udp.clone(),
Some(stop_udp_tx),
ipv6,
webrtc_offerer,
rendezvous_server.clone(),
servers.clone(),
contained,
@ -355,6 +371,8 @@ impl Client {
interface,
(None, None),
None,
None,
None,
rendezvous_server,
servers,
contained,
@ -366,6 +384,67 @@ impl Client {
}
}
fn is_expected_webrtc_ice_candidate(ice: &IceCandidate, session_key: &str) -> bool {
!session_key.is_empty() && ice.session_key == session_key && !ice.candidate.is_empty()
}
fn spawn_webrtc_ice_bridge(
mut socket: Stream,
mut local_ice_rx: Option<UnboundedReceiver<String>>,
webrtc: WebRTCStream,
peer: String,
session_key: String,
) -> oneshot::Sender<()> {
let (stop_tx, mut stop_rx) = oneshot::channel::<()>();
tokio::spawn(async move {
loop {
match stop_rx.try_recv() {
Ok(_) | Err(tokio::sync::oneshot::error::TryRecvError::Closed) => break,
Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {}
}
if let Some(rx) = local_ice_rx.as_mut() {
loop {
match rx.try_recv() {
Ok(candidate) => {
let mut msg = RendezvousMessage::new();
msg.set_ice_candidate(IceCandidate {
id: peer.clone(),
session_key: session_key.clone(),
candidate,
..Default::default()
});
if let Err(err) = socket.send(&msg).await {
log::warn!("failed to send WebRTC ICE candidate: {}", err);
return;
}
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
local_ice_rx = None;
break;
}
}
}
}
if let Some(msg_in) =
crate::get_next_nonkeyexchange_msg(&mut socket, Some(100)).await
{
if let Some(rendezvous_message::Union::IceCandidate(ice)) = msg_in.union {
if Self::is_expected_webrtc_ice_candidate(&ice, &session_key) {
if let Err(err) = webrtc.add_remote_ice_candidate(&ice.candidate).await
{
log::warn!("failed to add WebRTC ICE candidate: {}", err);
}
}
}
}
}
});
stop_tx
}
async fn _start_inner(
peer: String,
key: String,
@ -374,6 +453,8 @@ impl Client {
interface: impl Interface,
mut udp: (Option<Arc<UdpSocket>>, Option<Arc<Mutex<u16>>>),
stop_udp_tx: Option<oneshot::Sender<()>>,
mut ipv6: Option<(Arc<UdpSocket>, bytes::Bytes)>,
mut webrtc_offerer: Option<WebRTCStream>,
mut rendezvous_server: String,
servers: Vec<String>,
contained: bool,
@ -446,14 +527,20 @@ impl Client {
// Stop UDP NAT test task if still running
stop_udp_tx.map(|tx| tx.send(()));
let mut msg_out = RendezvousMessage::new();
let mut ipv6 = if crate::get_ipv6_punch_enabled() {
if let Some((socket, addr)) = crate::get_ipv6_socket().await {
(Some(socket), Some(addr))
} else {
(None, None)
let mut ipv6 = ipv6
.take()
.map(|(socket, addr)| (Some(socket), Some(addr)))
.unwrap_or((None, None));
let webrtc_sdp_offer = if let Some(webrtc) = webrtc_offerer.as_ref() {
match webrtc.get_local_endpoint().await {
Ok(endpoint) => endpoint,
Err(err) => {
log::warn!("failed to read local WebRTC offer: {}", err);
String::new()
}
}
} else {
(None, None)
String::new()
};
let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0);
let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" };
@ -467,9 +554,16 @@ impl Client {
udp_port: udp_nat_port as _,
force_relay: interface.is_force_relay(),
socket_addr_v6: ipv6.1.unwrap_or_default(),
webrtc_sdp_offer: webrtc_sdp_offer.clone(),
..Default::default()
});
for i in 1..=3 {
let webrtc_session_key = webrtc_offerer
.as_ref()
.map(|webrtc| webrtc.session_key().to_owned())
.unwrap_or_default();
let mut webrtc_sdp_answer = String::new();
let mut pending_webrtc_ice = Vec::<String>::new();
'punch_attempts: for i in 1..=3 {
log::info!(
"#{} {} punch attempt with {}, id: {}",
i,
@ -479,9 +573,20 @@ impl Client {
);
socket.send(&msg_out).await?;
// below timeout should not bigger than hbbs's connection timeout.
if let Some(msg_in) =
crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 3000)).await
{
let attempt_deadline = Instant::now() + Duration::from_millis((i * 3000) as u64);
loop {
let remaining = attempt_deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
break;
}
let timeout_ms = remaining
.as_millis()
.clamp(1, u64::MAX as u128) as u64;
let Some(msg_in) =
crate::get_next_nonkeyexchange_msg(&mut socket, Some(timeout_ms)).await
else {
break;
};
match msg_in.union {
Some(rendezvous_message::Union::PunchHoleResponse(ph)) => {
if ph.socket_addr.is_empty() {
@ -510,6 +615,7 @@ impl Client {
relay_server = ph.relay_server;
peer_addr = AddrMangle::decode(&ph.socket_addr);
feedback = ph.feedback;
webrtc_sdp_answer = ph.webrtc_sdp_answer;
let s = udp.0.take();
if ph.is_udp && s.is_some() {
if let Some(s) = s {
@ -528,7 +634,7 @@ impl Client {
}
}
log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr);
break;
break 'punch_attempts;
}
}
Some(rendezvous_message::Union::RelayResponse(rr)) => {
@ -549,6 +655,38 @@ impl Client {
}
}
signed_id_pk = rr.pk().into();
let mut webrtc_bridge_stop = None;
let mut webrtc_for_connect = None;
if !rr.webrtc_sdp_answer.is_empty() {
if let Some(webrtc) = webrtc_offerer.take() {
if let Err(err) =
webrtc.set_remote_endpoint(&rr.webrtc_sdp_answer).await
{
log::warn!("failed to set WebRTC relay answer: {}", err);
} else {
for candidate in pending_webrtc_ice.drain(..) {
if let Err(err) =
webrtc.add_remote_ice_candidate(&candidate).await
{
log::warn!(
"failed to add buffered WebRTC ICE candidate: {}",
err
);
}
}
let session_key = webrtc.session_key().to_owned();
let local_ice_rx = webrtc.take_local_ice_rx();
webrtc_bridge_stop = Some(Self::spawn_webrtc_ice_bridge(
socket,
local_ice_rx,
webrtc.clone(),
peer.clone(),
session_key,
));
webrtc_for_connect = Some(webrtc);
}
}
}
let fut = Self::create_relay(
&peer,
rr.uuid,
@ -564,30 +702,81 @@ impl Client {
}
.boxed(),
);
if let Some(mut webrtc) = webrtc_for_connect {
connect_futures.push(
async move {
webrtc.wait_connected(CONNECT_TIMEOUT).await?;
Ok((Stream::WebRTC(webrtc), None, "WebRTC"))
}
.boxed(),
);
}
// Run all connection attempts concurrently, return the first successful one
let (conn, kcp, typ) = match select_ok(connect_futures).await {
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
Err(e) => (Err(e), None, ""),
};
if let Some(stop) = webrtc_bridge_stop {
let _ = stop.send(());
}
let mut conn = conn?;
feedback = rr.feedback;
log::info!("{:?} used to establish {typ} connection", start.elapsed());
let pk =
Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?;
return Ok((
(conn, typ == "IPv6", pk, kcp, typ),
(conn, typ == "IPv6" || typ == "WebRTC", pk, kcp, typ),
(feedback, rendezvous_server),
false,
));
}
Some(rendezvous_message::Union::IceCandidate(ice)) => {
if Self::is_expected_webrtc_ice_candidate(&ice, &webrtc_session_key) {
pending_webrtc_ice.push(ice.candidate);
} else {
log::debug!(
"dropping ICE candidate for unexpected WebRTC session key {}",
ice.session_key,
);
}
}
_ => {
log::error!("Unexpected protobuf msg received: {:?}", msg_in);
}
}
}
}
drop(socket);
let mut webrtc_bridge_stop = None;
let mut webrtc_for_connect = None;
if !webrtc_sdp_answer.is_empty() {
if let Some(webrtc) = webrtc_offerer.take() {
if let Err(err) = webrtc.set_remote_endpoint(&webrtc_sdp_answer).await {
log::warn!("failed to set WebRTC answer: {}", err);
drop(socket);
} else {
for candidate in pending_webrtc_ice.drain(..) {
if let Err(err) = webrtc.add_remote_ice_candidate(&candidate).await {
log::warn!("failed to add buffered WebRTC ICE candidate: {}", err);
}
}
let session_key = webrtc.session_key().to_owned();
let local_ice_rx = webrtc.take_local_ice_rx();
webrtc_bridge_stop = Some(Self::spawn_webrtc_ice_bridge(
socket,
local_ice_rx,
webrtc.clone(),
peer.clone(),
session_key,
));
webrtc_for_connect = Some(webrtc);
}
} else {
drop(socket);
}
} else {
drop(socket);
}
if peer_addr.port() == 0 {
bail!("Failed to connect via rendezvous server");
}
@ -621,6 +810,8 @@ impl Client {
interface,
udp.0,
ipv6.0,
webrtc_for_connect,
webrtc_bridge_stop,
punch_type,
)
.await?,
@ -647,6 +838,8 @@ impl Client {
interface: impl Interface,
udp_socket_nat: Option<Arc<UdpSocket>>,
udp_socket_v6: Option<Arc<UdpSocket>>,
webrtc_offerer: Option<WebRTCStream>,
webrtc_bridge_stop: Option<oneshot::Sender<()>>,
punch_type: &str,
) -> ResultType<(
Stream,
@ -705,11 +898,23 @@ impl Client {
if let Some(udp_socket_v6) = udp_socket_v6 {
connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6", connect_timeout).boxed());
}
if let Some(mut webrtc) = webrtc_offerer {
connect_futures.push(
async move {
webrtc.wait_connected(connect_timeout).await?;
Ok((Stream::WebRTC(webrtc), None, "WebRTC"))
}
.boxed(),
);
}
// Run all connection attempts concurrently, return the first successful one
let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await {
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
Err(e) => (Err(e), None, ""),
};
if let Some(stop) = webrtc_bridge_stop {
let _ = stop.send(());
}
let mut direct = !conn.is_err();
if interface.is_force_relay() || conn.is_err() {
@ -4120,8 +4325,22 @@ pub mod peer_online {
#[cfg(test)]
mod tests {
use crate::client::Client;
use hbb_common::rendezvous_proto::IceCandidate;
use hbb_common::tokio;
#[test]
fn accepts_webrtc_ice_by_session_key_only() {
let ice = IceCandidate {
session_key: "session-a".to_owned(),
candidate: "candidate-json".to_owned(),
..Default::default()
};
assert!(Client::is_expected_webrtc_ice_candidate(&ice, "session-a"));
assert!(!Client::is_expected_webrtc_ice_candidate(&ice, "session-b"));
}
#[tokio::test]
async fn test_query_onlines() {
super::query_online_states(

View file

@ -199,20 +199,6 @@ pub fn core_main() -> Option<Vec<String>> {
}
std::thread::spawn(move || crate::start_server(false, no_server));
} 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)]
{
use crate::platform;
@ -952,57 +938,6 @@ fn is_root() -> bool {
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.
/// Note: This function must be kept in sync with `libs/portable/src/main.rs`.
#[cfg(windows)]

View file

@ -33,25 +33,25 @@ use hbb_common::{
tokio_util::codec::Framed,
ResultType,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_auth::authorize_service_scoped_ipc_connection;
#[cfg(windows)]
pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection;
#[cfg(windows)]
pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt;
#[cfg(windows)]
pub(crate) use ipc_auth::log_rejected_windows_ipc_connection;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_auth::{active_uid, authorize_service_scoped_ipc_connection};
#[cfg(target_os = "linux")]
pub(crate) use ipc_auth::{
active_uid, ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid,
log_rejected_uinput_connection, peer_uid_from_fd,
};
#[cfg(windows)]
use ipc_auth::{
authorize_windows_main_ipc_connection, portable_service_listener_security_attributes,
should_allow_everyone_create_on_windows,
};
#[cfg(target_os = "linux")]
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;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_fs::{
@ -63,8 +63,6 @@ use parity_tokio_ipc::{
};
use serde_derive::{Deserialize, Serialize};
#[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::{
collections::HashMap,
@ -73,47 +71,12 @@ use std::{
// IPC actions here.
pub const IPC_ACTION_CLOSE: &str = "close";
#[cfg(target_os = "windows")]
const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000;
#[cfg(target_os = "windows")]
pub(crate) const IPC_TOKEN_LEN: usize = 64;
#[cfg(target_os = "windows")]
const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2;
#[cfg(target_os = "windows")]
const _: () = assert!(IPC_TOKEN_LEN % 2 == 0);
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]
pub async fn connect_service(ms_timeout: u64) -> ResultType<ConnectionTmpl<ConnClient>> {
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> {
use hbb_common::rand::{rngs::OsRng, RngCore as _};
use std::fmt::Write as _;
@ -1170,7 +1137,6 @@ pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
Ok(token)
}
#[cfg(target_os = "windows")]
pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool {
if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN {
return false;
@ -1183,7 +1149,6 @@ pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> boo
== 0
}
#[cfg(target_os = "windows")]
pub(crate) async fn portable_service_ipc_handshake_as_client<T>(
stream: &mut ConnectionTmpl<T>,
token: &str,
@ -1208,7 +1173,6 @@ where
}
}
#[cfg(target_os = "windows")]
pub(crate) async fn portable_service_ipc_handshake_as_server<T, F>(
stream: &mut ConnectionTmpl<T>,
mut validate_token: F,
@ -1245,103 +1209,6 @@ async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType<Connection
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")]
pub async fn connect_for_uid(
ms_timeout: u64,
@ -2135,16 +2002,7 @@ mod test {
assert!(std::mem::size_of::<Data>() <= 120);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[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"))]
#[cfg(target_os = "linux")]
#[test]
fn test_ipc_path_differs_by_uid_for_cm() {
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
@ -2163,46 +2021,4 @@ mod test {
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>,
expected_session_id: Option<u32>,
peer_is_system: Option<bool>,
peer_is_elevated: Option<bool>,
) {
static LOG_THROTTLE: OnceLock<Mutex<UnauthorizedIpcLogThrottle>> = OnceLock::new();
throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| {
if suppressed > 0 {
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,
peer_pid,
peer_session_id,
expected_session_id,
peer_is_system,
peer_is_elevated,
suppressed
);
} else {
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,
peer_pid,
peer_session_id,
expected_session_id,
peer_is_system,
peer_is_elevated
peer_is_system
);
}
});
@ -658,14 +655,8 @@ pub(crate) fn authorize_service_scoped_ipc_connection(stream: &Connection, postf
#[cfg(windows)]
pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool {
let (
authorized,
peer_pid,
peer_session_id,
server_session_id,
peer_is_system,
peer_is_elevated,
) = stream.server_authorization_status();
let (authorized, peer_pid, peer_session_id, server_session_id, peer_is_system) =
stream.server_authorization_status();
if !authorized {
log_rejected_windows_ipc_connection(
postfix,
@ -673,7 +664,6 @@ pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix
peer_session_id,
server_session_id,
peer_is_system,
peer_is_elevated,
);
return false;
}
@ -786,14 +776,7 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
fn server_authorization_status(
&self,
) -> (
bool,
Option<u32>,
Option<u32>,
Option<u32>,
Option<bool>,
Option<bool>,
) {
) -> (bool, Option<u32>, Option<u32>, Option<u32>, Option<bool>) {
let peer_pid = self.peer_pid();
let server_session_id = crate::platform::windows::get_current_process_session_id();
let peer_session_id =
@ -803,34 +786,20 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
let peer_is_system = peer_is_system_result
.as_ref()
.and_then(|r| r.as_ref().ok().copied());
let session_authorized = is_allowed_windows_session_scoped_peer(
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)
{
if server_session_id.is_none() && !peer_is_system.unwrap_or(false) {
// 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!(
"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_session_id
);
}
// Main IPC trusts same-session peers, LocalSystem, and elevated administrators.
// Service-scoped IPC channels keep their own stricter authorization paths.
let authorized = session_authorized || peer_is_elevated.unwrap_or(false);
let authorized = is_allowed_windows_session_scoped_peer(
peer_is_system.unwrap_or(false),
peer_session_id,
server_session_id,
);
if !authorized {
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) {
log::debug!(
@ -839,13 +808,6 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
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,
@ -853,7 +815,6 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
peer_session_id,
server_session_id,
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", "كلمة المرور مخفية"),
("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].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"),
("password-hidden-tip", "Permanent password is set (hidden)."),
("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();
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].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)."),
("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."),
("Enable privacy mode", "Privacymodus inschakelen"),
("allow-remote-toolbar-docking-any-edge", ""),
].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."),
("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View file

@ -744,6 +744,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].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-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"),
("Continue with {}", "Continuar com {}"),
("Display Name", "Nome de Exibição"),
("password-hidden-tip", "A senha permanente está definida como (oculta)."),
("preset-password-in-use-tip", "A senha predefinida está sendo usada."),
("Enable privacy mode", "Habilitar modo de privacidade"),
("allow-remote-toolbar-docking-any-edge", ""),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
].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."),
("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"),
("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."),
("Filter by intersection", "Filtrează prin intersecție"),
("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."),
("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
use std::{
collections::HashMap,
net::SocketAddr,
sync::{
atomic::{AtomicBool, Ordering},
@ -21,8 +22,13 @@ use hbb_common::{
rendezvous_proto::*,
sleep,
socket_client::{self, connect_tcp, is_ipv4, new_direct_udp_for, new_udp_for},
tokio::{self, select, sync::Mutex, time::interval},
tokio::{
self, select,
sync::{mpsc, Mutex},
time::interval,
},
udp::FramedSocket,
webrtc::WebRTCStream,
AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr,
};
@ -32,11 +38,13 @@ use crate::{
};
type Message = RendezvousMessage;
type RendezvousSender = mpsc::UnboundedSender<Message>;
lazy_static::lazy_static! {
static ref SOLVING_PK_MISMATCH: Mutex<String> = Default::default();
static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
static ref LAST_RELAY_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
static ref WEBRTC_ICE_TXS: Mutex<HashMap<String, mpsc::UnboundedSender<String>>> = Default::default();
}
static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false);
@ -72,6 +80,7 @@ pub struct RendezvousMediator {
host: String,
host_prefix: String,
keep_alive: i32,
rz_sender: RendezvousSender,
}
impl RendezvousMediator {
@ -182,11 +191,13 @@ impl RendezvousMediator {
let host = check_port(&host, RENDEZVOUS_PORT);
log::info!("start udp: {host}");
let (mut socket, mut addr) = new_udp_for(&host, CONNECT_TIMEOUT).await?;
let (rz_sender, mut rz_out_rx) = mpsc::unbounded_channel::<Message>();
let mut rz = Self {
addr: addr.clone(),
host: host.clone(),
host_prefix: Self::get_host_prefix(&host),
keep_alive: crate::DEFAULT_KEEP_ALIVE,
rz_sender,
};
let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT));
@ -246,6 +257,9 @@ impl RendezvousMediator {
},
}
},
Some(msg_out) = rz_out_rx.recv() => {
Sink::Framed(&mut socket, &addr).send(&msg_out).await?;
},
_ = timer.tick() => {
if SHOULD_EXIT.load(Ordering::SeqCst) {
break;
@ -367,6 +381,17 @@ impl RendezvousMediator {
allow_err!(rz.handle_intranet(fla, server).await);
});
}
Some(rendezvous_message::Union::IceCandidate(ice)) => {
let tx = WEBRTC_ICE_TXS.lock().await.get(&ice.session_key).cloned();
if let Some(tx) = tx {
let _ = tx.send(ice.candidate);
} else {
log::debug!(
"dropping ICE candidate for unknown WebRTC session key {}",
ice.session_key
);
}
}
Some(rendezvous_message::Union::ConfigureUpdate(cu)) => {
let v0 = Config::get_rendezvous_servers();
Config::set_option(
@ -389,11 +414,13 @@ impl RendezvousMediator {
let mut conn = connect_tcp(host.clone(), CONNECT_TIMEOUT).await?;
let key = crate::get_key(true).await;
crate::secure_tcp(&mut conn, &key).await?;
let (rz_sender, mut rz_out_rx) = mpsc::unbounded_channel::<Message>();
let mut rz = Self {
addr: conn.local_addr().into_target_addr()?,
host: host.clone(),
host_prefix: Self::get_host_prefix(&host),
keep_alive: crate::DEFAULT_KEEP_ALIVE,
rz_sender,
};
let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT));
let mut last_register_sent: Option<Instant> = None;
@ -421,6 +448,9 @@ impl RendezvousMediator {
let msg = Message::parse_from_bytes(&bytes)?;
rz.handle_resp(msg.union, Sink::Stream(&mut conn), &server, &mut update_latency).await?
}
Some(msg_out) = rz_out_rx.recv() => {
Sink::Stream(&mut conn).send(&msg_out).await?;
}
_ = timer.tick() => {
if SHOULD_EXIT.load(Ordering::SeqCst) {
break;
@ -472,6 +502,7 @@ impl RendezvousMediator {
rr.secure,
false,
Default::default(),
String::new(),
rr.control_permissions.clone().into_option(),
)
.await
@ -486,6 +517,7 @@ impl RendezvousMediator {
secure: bool,
initiate: bool,
socket_addr_v6: bytes::Bytes,
webrtc_sdp_answer: String,
control_permissions: Option<ControlPermissions>,
) -> ResultType<()> {
let peer_addr = AddrMangle::decode(&socket_addr);
@ -504,6 +536,7 @@ impl RendezvousMediator {
socket_addr: socket_addr.into(),
version: crate::VERSION.to_owned(),
socket_addr_v6,
webrtc_sdp_answer,
..Default::default()
};
if initiate {
@ -571,6 +604,7 @@ impl RendezvousMediator {
true,
true,
socket_addr_v6,
String::new(),
fla.control_permissions.into_option(),
)
.await
@ -613,6 +647,81 @@ impl RendezvousMediator {
Ok(())
}
async fn spawn_webrtc_answerer(
&self,
ph: &PunchHole,
force_relay: bool,
server: ServerPtr,
peer_addr: SocketAddr,
control_permissions: Option<ControlPermissions>,
) -> ResultType<String> {
let mut stream =
WebRTCStream::new(&ph.webrtc_sdp_offer, force_relay, CONNECT_TIMEOUT).await?;
let answer = stream.get_local_endpoint().await?;
let session_key = stream.session_key().to_owned();
let return_route = ph.socket_addr.clone();
let (remote_ice_tx, mut remote_ice_rx) = mpsc::unbounded_channel::<String>();
WEBRTC_ICE_TXS
.lock()
.await
.insert(session_key.clone(), remote_ice_tx);
let stream_for_remote_ice = stream.clone();
tokio::spawn(async move {
while let Some(candidate) = remote_ice_rx.recv().await {
if let Err(err) = stream_for_remote_ice.add_remote_ice_candidate(&candidate).await
{
log::warn!("failed to add remote WebRTC ICE candidate: {}", err);
}
}
});
if let Some(mut local_ice_rx) = stream.take_local_ice_rx() {
let sender = self.rz_sender.clone();
let socket_addr = return_route.clone();
let session_key_for_ice = session_key.clone();
tokio::spawn(async move {
while let Some(candidate) = local_ice_rx.recv().await {
let mut msg = Message::new();
msg.set_ice_candidate(IceCandidate {
socket_addr: socket_addr.clone(),
session_key: session_key_for_ice.clone(),
candidate,
..Default::default()
});
let _ = sender.send(msg);
}
});
}
let session_key_for_cleanup = session_key.clone();
tokio::spawn(async move {
let result = stream.wait_connected(CONNECT_TIMEOUT).await;
WEBRTC_ICE_TXS
.lock()
.await
.remove(&session_key_for_cleanup);
if let Err(err) = result {
log::warn!("webrtc wait_connected failed: {}", err);
return;
}
if let Err(err) = crate::server::create_tcp_connection(
server,
Stream::WebRTC(stream),
peer_addr,
true,
control_permissions,
)
.await
{
log::warn!("failed to create WebRTC server connection: {}", err);
}
});
Ok(answer)
}
async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> {
let mut peer_addr = AddrMangle::decode(&ph.socket_addr);
let last = *LAST_MSG.lock().await;
@ -624,7 +733,23 @@ impl RendezvousMediator {
let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6);
let relay = use_ws() || Config::is_proxy() || ph.force_relay;
let mut socket_addr_v6 = Default::default();
let control_permissions = ph.control_permissions.into_option();
let control_permissions = ph.control_permissions.clone().into_option();
let webrtc_sdp_answer = if !ph.webrtc_sdp_offer.is_empty() {
self.spawn_webrtc_answerer(
&ph,
relay,
server.clone(),
peer_addr,
control_permissions.clone(),
)
.await
.unwrap_or_else(|err| {
log::warn!("failed to create WebRTC answer: {}", err);
String::new()
})
} else {
String::new()
};
if peer_addr_v6.port() > 0 && !relay {
socket_addr_v6 = start_ipv6(
peer_addr_v6,
@ -651,6 +776,7 @@ impl RendezvousMediator {
true,
true,
socket_addr_v6.clone(),
webrtc_sdp_answer.clone(),
control_permissions,
)
.await;
@ -664,6 +790,7 @@ impl RendezvousMediator {
nat_type: nat_type.into(),
version: crate::VERSION.to_owned(),
socket_addr_v6,
webrtc_sdp_answer,
..Default::default()
};
if ph.udp_port > 0 {