Third review round. Two of these are regressions from the previous one.
- The RelayResponse race predicate was `is_direct_transport(result.2)`,
which answers true for the label "WebRTC" - but WebRTC is only a
direct path when ICE nominated a non-TURN pair. A TURN-relayed WebRTC
result therefore committed instantly and cancelled the IPv6 attempt
racing beside it, which is the same inversion the previous fix removed
in the other direction. (That fix was also argued from a wrong premise:
the site does carry an IPv6 future, pushed ~50 lines earlier than the
relay one.) Each future now resolves whether its path is direct and
the predicate reads that bool, matching the outer race, and the
downstream recomputation goes away.
- policy_relay still folded in Config::is_proxy(), and that is what gets
persisted into the peer's config as force-always-relay - so one
session through a proxy pinned the peer to relay forever and disabled
WebRTC for it, exactly the latch the previous round fixed for
WebSocket. Split out peer_relay: the saved option or an explicit
request for THIS peer, and the only part written back.
- The controlled side buffered remote ICE candidates in an unbounded
channel while the controller caps the same buffer at 64, and draining
one costs a JSON parse plus the ICE agent's lock. Whoever can reach a
session's route could grow it without limit inside the long-lived
service process. Bounded, with the overflow logged through the
existing throttle.
- That route was also removed by key alone when an answerer finished, so
a punch retry that built a fresh answerer under the same fingerprint
had its live sender deleted by the previous one's cleanup - after
which it received no candidates at all. Evict only our own sender, the
way the session cache already guards the analogous case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
WebSocket support folds into force_relay because a ws tunnel kills
classic TCP/UDP punching — but that conflated transport necessity with
relay policy, and the WebRTC decisions keyed off the merged flag: a ws
client built no offerer at all without TURN, and only a Relay-only-ICE
one with it. ws deployments could never reach a direct WebRTC
connection, which is exactly the path they are supposed to live on.
Split the flag. LoginConfigHandler now tracks policy_relay (the
force-always-relay option, an explicit relay request — /r ids and
retry-via-relay included — and proxy) separately; force_relay stays
policy_relay || use_ws() and keeps governing the classic paths, so
non-ws behavior is unchanged everywhere:
- the offerer's existence and ICE policy follow policy_relay: under
pure ws the offer gathers every candidate type and may go direct;
under relay-by-policy it stays Relay-only ICE, TURN-gated, exactly
as before;
- the RelayResponse race applies the prefer-P2P window under ws (a
direct ICE path is worth delaying an already-ready relay for) while
policy relay keeps first-success semantics;
- the request carries webrtc_all_ice (hbb_common 64b54ab) so the
controlled side knows the offer is full-ICE: it answers with full ICE
and no TURN requirement, while offers without the bit keep today's
relay-only answer path on every version-skew combination.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
* fix(clipboard): clean up stale Linux FUSE mounts
Recover Linux file clipboard FUSE mount points before remounting and stop treating a cached
context as valid when the mount has already gone away.
This fixes the desktop file manager copy failure that shows dialogs such as
"Error while copying a" and "There was an error copying the file into xxx".
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(clipboard): fuse, reduce dups
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: clear Linux file clipboard before unmounting FUSE
Ensure Linux client teardown clears RustDesk file clipboard URLs while
the FUSE context is still available. Also prefer fusermount before
umount to avoid noisy unprivileged teardown attempts.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(clipboard): return and log errors
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(client): serialize X11 keyboard grab and debounce focus feedback
When two RustDesk sessions run fullscreen on separate monitors on
Linux/X11, keyboard input gets stuck on the wrong session or stops
working entirely. This happens because each Flutter isolate calls
change_grab_status concurrently, racing on KEYBOARD_HOOKED and the
rdev grab channel.
Additionally, XGrabKeyboard causes a focus-change feedback loop:
grab shifts focus away from the Flutter window, triggering PointerExit,
which releases the grab, restoring focus, triggering PointerEnter,
which re-grabs -- cycling at ~10 Hz and blocking keyboard input.
Fix by:
- Serializing grab transitions with a mutex and tracking the owning
session (by lc.session_id), so a stale Wait from session A cannot
clobber session B's freshly acquired grab.
- Debouncing Wait events (300 ms) from the same session that just
acquired the grab, breaking the X11 focus feedback loop.
- Refreshing the debounce timer on idempotent Run calls (enterView
while already owner), keeping the grab stable during normal use.
Signed-off-by: Sergiusz Michalik <github@latens.me>
* fix(client): add deferred release and dedup for debounced Wait
When a Wait is debounced (within 300ms of grab acquisition), schedule
a deferred release thread that re-checks after the debounce window.
If no new Run refreshed the grab, the deferred thread releases it,
ensuring a genuine leave within the debounce window is not lost.
Add a deferred_pending flag to GrabOwnerState to prevent spawning
redundant threads during the X11 focus feedback loop.
Signed-off-by: Sergiusz Michalik <github@latens.me>
* fix(client): use window-scoped ID and fix deferred-release re-arming
Address PR review feedback:
- Use per-window UUID instead of connection-scoped lc.session_id so two
windows viewing the same peer get distinct grab owners
- Reset deferred_pending on both idempotent Run refresh and owner
handoff, so a subsequent Wait can always spawn a fresh timer
- Replace manual Default impl with derive
* fix(client): recover from poisoned mutex instead of panicking
* docs: clarify cross-platform rationale for GrabOwnerState
* fix(client): only clear deferred_pending when timer snapshot matches
* fix(client): use full u128 window ID, downgrade grab logs to debug
- Widen GrabOwnerState.owner to u128 to avoid theoretical collision
from truncating a 128-bit UUID to 64 bits
- Downgrade all grab transition log::info! to log::debug! to reduce
log noise during routine window switches
- Clear deferred_pending on post-debounce release path to maintain
the "deferred_pending => timer in flight" invariant
* fix(client): gate GRAB_DEBOUNCE_MS with cfg(target_os = "linux")
* fix(grab): release grabbed keys without clobbering new owner state
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(keyboard): Simple refactor
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: Sergiusz Michalik <github@latens.me>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
* feat: Add relative mouse mode
- Add "Relative Mouse Mode" toggle in desktop toolbar and bind to InputModel
- Implement relative mouse movement path: Flutter pointer deltas -> `type: move_relative` -> new `MOUSE_TYPE_MOVE_RELATIVE` in Rust
- In server input service, simulate relative movement via Enigo and keep latest cursor position in sync
- Track pointer-lock center in Flutter (local widget + screen coordinates) and re-center OS cursor after each relative move
- Update pointer-lock center on window move/resize/restore/maximize and when remote display geometry changes
- Hide local cursor when relative mouse mode is active (both Flutter cursor and OS cursor), restore on leave/disable
- On Windows, clip OS cursor to the window rect while in relative mode and release clip when leaving/turning off
- Implement platform helpers: `get_cursor_pos`, `set_cursor_pos`, `show_cursor`, `clip_cursor` (no-op clip/hide on Linux for now)
- Add keyboard shortcut Ctrl+Alt+Shift+M to toggle relative mode (enabled by default, works on all platforms)
- Remove `enable-relative-mouse-shortcut` config option - shortcut is now always available when keyboard permission is granted
- Handle window blur/focus/minimize events to properly release/restore cursor constraints
- Add MOUSE_TYPE_MASK constant and unit tests for mouse event constants
Note: Relative mouse mode state is NOT persisted to config (session-only).
Note: On Linux, show_cursor and clip_cursor are no-ops; cursor hiding is handled by Flutter side.
Signed-off-by: fufesou <linlong1266@gmail.com>
* feat(mouse): relative mouse mode, exit hint
Signed-off-by: fufesou <linlong1266@gmail.com>
* refact(relative mouse): shortcut
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
* option `allow-d3d-render`, default false
Add this option because it fails on some machines
Signed-off-by: 21pages <sunboeasy@gmail.com>
* only add nokhwa to windows and linux dependencies
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
1. Fix auto record outgoing sessions ignore record permission
2. Stop record if record permission changed
3. Update hwcodec
4. Make video thread finish faster when connection closed
Signed-off-by: 21pages <sunboeasy@gmail.com>
* seperate video decoding thread for each display
1. Separate Video Decoding Thread for Each Display
2. Fix Decode Errors When Clearing the Queue
Previously, on-flight frames after clearing the queue could not be decoded successfully. This issue can be resolved by setting a discard_queue flag when sending a refresh message. The flag will be reset upon receiving a keyframe.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* update video format along with fps to flutter
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Fix keyframe interval when auto record outgoing sessions
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Add option auto record outgoing session
* In the same connection, all displays and all windows share the same
recording state.
todo:
Android check external storage permission
Known issue:
* Sciter old issue, stop the process directly without stop record, the record file can't play.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Calculate fps without distinguish displays, use one fps control
because the controlled side control fps of all displays with one FPS
variable.
* Because all displays decode frame in one thread, when there are N
displays, the video frames received in one second is `fps * N`, so the
calculated decode fps should be divided by N. Because the actual
display count is not obvious in rust, when no data frame is received for 5 seconds, the display is considered inactive, and only the active display is used as the dividend.
Signed-off-by: 21pages <sunboeasy@gmail.com>