On Linux, keeping the host awake during an incoming session asks keepawake for
three things at once: the display through org.freedesktop.ScreenSaver on the
session bus, and idle plus sleep through logind on the system bus. keepawake
takes the ScreenSaver one FIRST and abandons the whole request if it fails, and
WakeLock::new discarded the error with .ok(). So on any session where that name
is missing, RustDesk silently holds NOTHING - not the display inhibit it could
not take, and not the logind inhibits it never got to. On a host whose logind
IdleAction is not the default, that means the machine can suspend in the middle
of an active remote session, with any capture backend.
The name is missing on a GNOME login screen. Measured on a GNOME/Wayland GDM
greeter: org.freedesktop.ScreenSaver answers "was not provided by any .service
files" and cannot be activated, while org.gnome.SessionManager is on the same
bus and its idle inhibit works there. Same machine, same state: with it held the
output was still lit at 129.9 s of idle, without it the compositor disabled the
output after 30.3 s. Disabled, not blanked - an idle compositor releases the
CRTC, so there is no scanout left for anything to read.
So on the failure path, take both halves separately instead of neither:
- ask keepawake again without the display part, which restores the logind
idle/sleep inhibits that have nothing to do with the missing session name;
- and get the display half from whichever session interface this desktop has,
trying org.gnome.SessionManager and then org.freedesktop.PowerManagement.
Only the failure path changes: a session where the ScreenSaver inhibit works is
untouched. Where no session interface answers, the log now names every one that
was tried and the error each returned, which is the whole diagnostic for a
desktop nobody here can test on.
Verified on a GNOME/Wayland greeter with a live client: the inhibit is taken
86 ms before anything else happens on the connection, and appears to
gnome-session as "RustDesk: incoming session (idle)". The PowerManagement entry
is NOT verified - it is the interface KDE and XFCE implement, it costs one extra
failed call where it is absent, and the log is what will tell us whether it is
the right one.
* fix: retry audit posts and add per-record nonce
A single post_request attempt meant any transient failure (timeout,
DNS, connection reset) silently dropped the audit record. Retry up to
3 times with backoff and log at error level when a record is finally
dropped.
Retries (and the existing TCP-proxy fallback) can deliver the same
record twice; attach a per-record nonce so the api server can dedup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: fail audit posts on http error status
post_request discards the status code, so a 5xx from a reverse proxy
(e.g. nginx answering 502 while hbbs restarts) or any 4xx rejection was
treated as success and the audit record silently dropped without a log
line. Add post_request_with_status (same semantics and TCP-proxy
fallback as post_request, status preserved; existing callers untouched)
and use it for audit posts: 2xx succeeds, transport errors and 5xx
retry, 4xx fails immediately since retrying a deterministic rejection
cannot help.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: report audit posts rejected with 200 error body
hbbs maps handler failures (e.g. a database write error) to HTTP 200
with an {"error": ...} body (WebError::ServerError), so the client
treated them as success and the audit record was silently dropped.
Detect the error body and fail visibly. No retry: the server already
consumed the nonce, and persistence failures are the server's job to
solve; the client's job is to make the loss visible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: give audit retries a delay long enough to outlive a restart
The backoff was 1s then 2s, so all three attempts landed within about three
seconds. That does not cover the case the retry exists for: a reverse proxy
answering 502 while the api server restarts fails fast, so every attempt hits
the same outage and the record is dropped anyway.
Use 10s and 30s instead. The window is bounded on the other side - the api
server dedups by nonce for five minutes, and a retry arriving after that
expired would be stored twice - so the worst case is now about three minutes,
leaving room under that limit.
Derive the attempt count from the delay table so the two cannot drift apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: retry audit posts the server answered with an error body
hbbs reports handler failures as 200 with an {"error": ...} body, and this
treated them as final on the grounds that the server had already consumed the
record's nonce. That is no longer how the server behaves: it releases the nonce
when the write fails, and answers a post whose earlier attempt is still being
written with an error as well. Both are exactly the cases where trying again is
what gets the record stored, so giving up after the first attempt drops audit
records the retry was added to save.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: bound audit retries by elapsed time, and retry 408 and 429
The comment claimed the retry window fit inside the server's five-minute nonce
memory with room to spare, and that was wrong: one attempt is up to 84s, not
12s, because post_request_ retries the TLS handshake up to four times at 12s
each before the 36s TCP-proxy fallback. Three of those plus the delays is 292s
against a 300s window, and a suspend between attempts stretches the wall clock
without any bound at all, so counting attempts cannot bound this. Stop by
elapsed time instead: no new attempt starts past 120s, which leaves the last
one room to finish well inside the server's window.
Also retry 408 and 429. Both are transient - the request timed out upstream, or
a proxy is shedding load - but the 5xx test dropped the record after the first
attempt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: only an empty 2xx body counts as a stored audit
The success check was inverted: any 2xx body that failed to parse as an
{"error": ...} object was reported as stored. A proxy interposing a 2xx
maintenance page, or a malformed error value, therefore ended the retry loop
with success and silently dropped the record - the exact loss the retry was
added to prevent.
The audit handlers' success contract is an empty body, so treat exactly that as
success. A nonempty body with a valid error message stays a retryable server
error; any other nonempty body is now a retryable "unexpected response body"
instead of an accepted store. Both old and new hbbs answer success with an
empty body, and no caller reads the returned text, so nothing depends on the
previous acceptance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: do not start an audit retry past the deadline
The deadline was only checked after an attempt returned, so an attempt could
still begin up to one backoff delay past it - starting as late as ~150s and
landing at ~234s, while the comment claimed no attempt starts past 120s.
Re-check after the delay so the stated bound actually holds: the last attempt
now starts before 120s and lands by ~204s, inside the server's five-minute
nonce window with margin restored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: drop a retry rationale the server no longer backs
The comment claimed hbbs answers a post whose earlier attempt is still being
written with an error, so that retrying it is what stores the record. That
stopped being true: hbbs now answers a concurrent duplicate as already stored
rather than as retryable, having dropped the in-flight rejection along with the
claim state machine it needed.
Nothing in the handling changes - a 2xx carrying an {"error": ...} body is
still retried, and that is still right, because the server releases the
record's nonce when its write fails. Only the half of the rationale the server
no longer backs is gone, since this comment is where the contract between the
two repos is written down.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(android): close outgoing sessions when the task is swiped away
Swiping RustDesk away from recents destroys the UI but does not
necessarily end the process: when MainService is running (screen share
enabled, or started at boot) the process survives, and with it the
native io_loop of any active outgoing session.
That orphaned io_loop keeps echoing TestDelay (client.rs handle_test_delay
runs entirely on the network thread, no UI involved), which keeps
refreshing last_recv_time on the controlled side. Its 30s inactivity
timeout in server/connection.rs therefore never fires, so the remote
session stays established with no UI left to close it, and the peer
cannot be reconnected to.
Close client sessions from Service.onTaskRemoved, which fires only on
explicit task removal -- not on Home or backgrounding, so ordinary
backgrounding is unaffected. The service itself keeps running, so
incoming connections and the device staying reachable are unchanged.
This complements 152c5c71b, which covered the route-pop path via
dispose(); dispose() does not run when the task is removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(android): also close sessions on activity destroy
Review follow-up. onTaskRemoved only reaches MainService, but the
accessibility InputService keeps the process alive on its own: a user
with input control enabled and screen sharing off has a surviving
process after a swipe while MainService is not running, so the callback
never fires and the session still outlives its UI. onTaskRemoved cannot
cover that -- InputService is bound by the system, not started, so the
callback is not delivered there.
Close from MainActivity.onDestroy() as well, which runs while the
process is still alive regardless of which service keeps it up. Guarded
on isFinishing so a destroy for recreation (configuration change, "don't
keep activities") does not tear down a live session. Both paths are
idempotent.
Also drop the now-wrong "on task removed" wording from the Rust log,
which has two distinct callers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(android): release held keys before draining the session map
close_all_sessions drained SESSIONS first, then called
release_remote_keys. The release path sends through get_cur_session(),
which resolves against SESSIONS, so every generated key-up was dropped
after take_remote_keys() had already cleared TO_RELEASE: a key held as
the task is removed stays down on the controlled side until its own
timeout, with the state lost locally.
Release first, while a session is still registered. It is a no-op when
no key is held, so the previous is_empty() guard is not needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* swtich_code for hbbs to bypass ACL
* improve register_switch_grant: skip public server, log at error level
Also document why registration is fire-and-forget with no retry: the
peer connects within seconds, so a late retry would land after its
punch request was already rejected; a failed switch is recovered by
the user triggering it again, which registers a fresh grant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* add timestamp
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(switch-sides): handle grant registration clock skew
- retry registration once with the server-provided timestamp
- require an explicit accepted response from hbbs
- report malformed or incomplete responses
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(switch-sides): register grants with code verifiers
- send a derived verifier instead of the raw switch code
- use detached signatures for grant registration
- add verifier and signed-message tests
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Update nl.rs
Updates plus a small improvement to the Dutch language file
* Update nl.rs
Now including fixes for coderabbit reportings
* Update nl.rs
Three more fixes re. greptile
* Update nl.rs
typo 'loskoppelenn' fixed as well
quit_gui() ends the process on Windows (std::process::exit) and macOS
(NSApp terminate), but on Linux it calls gtk_main_quit(), which has no
effect in the Flutter connection manager: flutter/linux/main.cc runs
g_application_run() (GtkApplication), so gtk_main() is never called and
the assertion inside gtk_main_quit() just fails.
quit_cm() is the only caller that relies on quit_gui() to end the
process. The main window path in ipc.rs calls std::process::exit(-1)
right after it, and the two remaining call sites are in the Sciter UI,
which is not compiled for flutter builds. So a connection manager
reaching quit_cm() on Linux kept running while no longer serving the
`_cm` ipc endpoint, which also stops the server from reusing it, so the
next connection spawns one more.
NOTE: this is a fallback, not an explanation for the stale processes of
#15698: a client merely disconnecting does not reach quit_cm(), the
Flutter side closes the window instead.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* docs(agents): require minimally invasive, additive-first patches
Codify the review feedback from the tray ghost-icon fix: fixes should
add self-contained code around existing lines instead of restructuring
them, keep platform-specific logic in src/platform/ with fn-local
imports, and leave only thin one-line hooks in shared files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(windows): stop duplicate tray icons from piling up (#15689)
`check_process("--tray", ..)` is used to decide whether a tray process
needs to be spawned, but it can miss one that is already running: it
cannot read the command line of an elevated process from a non-elevated
one (the installer spawns the tray elevated), and wmic, used by 32-bit
builds since #11638, is gone from newer Windows 11. `connection.rs` runs
that check once per incoming connection, so every miss added another tray
icon and they kept piling up, which is the same blind spot behind #6692.
Hold a named mutex in the session namespace as the authoritative single
instance guard, so a redundant tray process exits before creating an
icon. `ERROR_ACCESS_DENIED` also counts as "already running", since it
means the mutex belongs to a tray we may not touch.
Also remove the icon before the tray menu's "Stop service" calls
uninstall_service(): on success it ends the process with
std::process::exit, which skips the destructor that would call
Shell_NotifyIcon(NIM_DELETE), so every click left a ghost icon behind.
The icon is shown again if stopping the service failed or was cancelled.
Ghost icons from the taskkill in the install/update/service flows are
left alone here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(windows): note that update_me's pid lookup can silently find nothing
The pids are matched by command line, which comes back empty for a 32-bit
build reading 64-bit processes (hence the `wmic` fallback of #11638, and
`wmic` is no longer installed by default since Windows 11 24H2) and for a
non-elevated process reading an elevated one. `taskkill` matches by image
name and still works, but the session lists are then empty, so the restore
guard silently restores nothing and the update leaves the user without a
tray icon and main window.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(windows): record the confirmed cause of the duplicate tray icons
Process Explorer output in #15689 pinned it down: run_after_run_cmds()
spawns the tray in the caller's own context, so installing or toggling
the service from a RustDesk that was itself started elevated leaves a
high integrity tray behind, which a medium integrity main window cannot
inspect afterwards. Record where the detection fails exactly, so the next
reader doesn't have to rediscover that the executable path, not the
command line, is what comes back empty.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(recording): add visibility and service storage options
- support hide-recording-button in Flutter and Sciter
- allow a custom save directory for Windows service recordings
- sanitize peer IDs used in recording filenames
Tested:
- with hide-recording-button=Y and allow-auto-record-outgoing=Y,
outgoing sessions are recorded automatically while the recording button
remains hidden and cannot be stopped from the UI; verified on Flutter
desktop, Sciter, and Android
- windows-service-video-save-directory takes effect when the Windows client
runs as an installed service
- the Windows controlling side can save recordings for direct IP:port
connections
Signed-off-by: 21pages <sunboeasy@gmail.com>
* update hbb_common
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(recording): validate configured save directories
- trim configured recording directory paths
- reject non-absolute paths and fall back to defaults
- warn when a non-empty path is invalid
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(recording): validate configured save directories
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix: refresh wayland uinput range on compositor layout change
The uinput absolute range is computed once at session init. If the
compositor layout changes mid-session (monitor scale or position
change, or a portal virtual output appearing once capture starts),
injected coordinates are rescaled by the stale range and land offset.
Poll the live desktop bounding box from the display service loop while
subscribed (one wayland roundtrip, throttled to 1.5s, no subprocesses)
and re-apply the uinput resolution when it changes. Also read a fresh
layout when computing the initial range in check_init, since the cache
is not cleared when a session closes through the restore-token path.
This is the X component of #15601. The stale advertised origins (the Y
component) are not touched here: re-advertising DisplayInfo mid-session
trips the portal re-negotiation and can drop displays.
Signed-off-by: Cody Harris <codyharris7188@gmail.com>
* fix: bound the mouse resolution IPC wait during session init
Wrap update_mouse_resolution in the same 3s timeout the periodic
refresh uses, so a hung IPC response can't stall check_init.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: build timeout future inside runtime, split linux lazy_static
Constructing the timeout future eagerly as the block_on argument panics
with 'there is no reactor running'; move it into the async block so it is
built inside the runtime context. Also move WAYLAND_UINPUT_RECT into its
own cfg-gated lazy_static block, an attribute on a single item inside the
shared block does not compile.
* fix: confirm uinput mouse device adopted new range before caching rect
send_refresh() now waits for the mouse service to ack that it recreated the
device with the new range instead of firing and forgetting, and
update_mouse_resolution() propagates that result. The layout poller only
caches the rect after the device actually adopts the range, so a failed
refresh errors and retries on the next check. The ack read is bounded by
IPC_REQUEST_TIMEOUT, matching the keyboard get-key-state path.
* fix: propagate refresh failures instead of caching a stale range
- input_service: error when the custom-mouse downcast fails so the poller
retries instead of caching an unconfirmed refresh
- uinput: on device recreation failure, keep the current device and the
IPC connection and withhold the ack so the client retries, instead of
killing the mouse handler
* fix: remap injected wayland coords onto the live layout after a monitor moves
The range refresh corrects the uinput ABS bounds, but a single-display client
sends whole-desktop coordinates offset by the origin of the display it follows,
taken from the layout advertised at session init. When another monitor is
rescaled or moved that origin shifts, so the coordinate lands offset before it
reaches uinput and the range refresh cannot recover it.
Snapshot the per-display layout at init, poll the live layout on the existing
1.5s throttle, and when they differ remap each injected move into the followed
display's current rectangle (matched by connector name, index fallback when the
compositor reports none). No-op and lock-free while the layout is unchanged.
---------
Signed-off-by: Cody Harris <codyharris7188@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix stale primary display selection
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix stale display selection during login and switching
- resolve the primary display from the refreshed login snapshot
- defer display enumeration until authentication succeeds
- read Wayland displays and primary index from the same cache snapshot
- reject stale monitor and camera indices during display switching
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix inconsistent display snapshots during login
- return displays from the same enumeration used to select the primary
- avoid re-reading the shared display cache after updating it
- use the same converted snapshot during Wayland initialization
Signed-off-by: 21pages <sunboeasy@gmail.com>
* avoid cloning unchanged display snapshots
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix invalid display subset handling
Signed-off-by: 21pages <sunboeasy@gmail.com>
* minimize code churn in switch_display_to
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
`rustdesk --deploy --id ""` (e.g. an unset variable in a deployment
script) deploys a blank id, then wipes the local id and unconfirms the
key through the IPC config write. The Android deploy flow already guards
an empty id (#15146); apply the same guard to the CLI, and reject an
empty id at the IPC write boundary the same way the read path was fixed
in #15626.
* Fix disabled installation bypass
Prevent install.exe and --install from opening the install flow when disable-installation is set.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Refine disabled installation handling for portable clients
Document why --install must be filtered from both Rust and Flutter runner arguments for portable wrappers such as no-install.exe. Remove redundant UI-
layer installation checks because the install entry points are already gated upstream.
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
* 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>
* Support controller user attribution in audit logs
This PR supports associating audit logs with the controller user.
## Implementation:
- Add `ControlledContext { conn_audit_token }` to `PunchHole`, `RequestRelay`, and `FetchLocalAddr`.
- The server sends a controller-user identity snapshot to the controlled client through rendezvous messages.
- The controlled client sends the token back to the server when posting the `on_open` conn audit or IP whitelist alarm audit.
- This lets the server attach the controller user to audit logs.
## How the controlled client helps identify the controller user:
- Conn audit: sends the token to the server in `on_open`; the server creates the audit log and caches the user snapshot.
- File audit: sends `id` and `conn_id`; the server uses them to find the cached user snapshot.
- Alarm audit: IP whitelist sends the token directly; other alarm logs send `id` and `conn_id`, and the server uses them to find the cached user
snapshot.
## Compatibility:
- Supported only for logs created with a new server and a new controlled client.
- Does not require upgrading the controller client.
## Test
- [x] New/old clients connected to new/old servers, and conn/file/alarm audit logs worked normally.
- [x] New client connected to new server generated searchable conn/file/alarm audit logs.
- [x] Punch hole, local addr, and relay paths worked with audit logs and control role on new/old servers.
- [x] Direct IP connections produced audit logs, but do not support user audit.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* rename conn_audit_token to conn_audit_ref
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>