Commit Graph

5062 Commits

Author SHA1 Message Date
rustdesk
bef87963e5 webrtc: judge the race by the resolved path, not the label; bound the ICE queue
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
2026-09-03 17:13:15 +08:00
rustdesk
c1b50f3910 webrtc: close without an await point; do not report an unknown path as direct
- close_webrtc is no longer async (hbb_common 88f965f), so the ten call
  sites in port_forward and io_loop - all inside select! arms or futures
  the UI can abandon - can no longer be cancelled mid-teardown, which
  left the pc unclosable and its session entry stranded. Client's own
  spawn_close_webrtc went with it: the runtime-teardown guard it existed
  for now lives in close_detached, so both Drop paths share one
  implementation.

- webrtc_relayed() returns None when no candidate pair is selected or
  the pc closed under a concurrent teardown, and both call sites read
  that as "not relayed", i.e. direct. A TURN-relayed session could
  therefore be shown to the user as peer-to-peer. Claiming a direct path
  needs evidence of one, so an unknown answer now counts as relayed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
2639ede4c4 fix three ways ws + WebRTC could not work in practice
Review of #15684 and hbb_common#579. Each of these left the code reading
correct while the feature did not function.

- The RelayResponse race classified P2P with `result.2 == "IPv6"`, but
  that site's futures are only ever the relay ("Relay"/"WebSocket") and
  the WebRTC branch's own "WebRTC" — so the predicate was constantly
  false. When the relay landed first the result was still right (the
  webrtc arm's `others_fut.is_none()` fallback), but when WebRTC
  connected FIRST it was parked as if it were a relay and the relay was
  committed on arrival, discarding a live direct connection. That is the
  LAN case: the better the network, the worse the outcome. Classify by
  what the label means, via is_direct_transport, and test both orderings
  — only the relay-first one was covered.

- handle_peer_info wrote "force-always-relay=Y" into the peer's saved
  config whenever force_relay was set, which now includes the WebSocket
  transport. One ws session therefore turned the peer into a permanent
  relay-by-policy peer, and relay-by-policy means Relay-only ICE, so
  WebRTC could never go direct to it again — the flagship path worked
  exactly once. Persist policy_relay, which is the user's choice; the
  transport is a property of this client, not of the peer.

- The answerer gated on this machine's enable-webrtc option, but that is
  LocalConfig: the UI process writes it and never syncs it over IPC,
  while handle_punch_hole runs in the server process, which on Windows
  resolves LocalConfig under a different profile and reads the
  private-server default of "N". The gate refused to answer in exactly
  the self-hosted deployments the transport exists for. Drop it: the
  answerer follows the request, like the udp/ipv6 legs, and the option
  still gates the feature where it can — an offer only exists because
  some controller had it enabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
2227f161df android: define getifaddrs/freeifaddrs for the api-21 sysroot
Turning on hbb_common's "webrtc" feature pulls webrtc-util into the android
link, and its ifaces() -- reached from vnet::Net::new() on every ICE gather --
calls getifaddrs(). bionic exports getifaddrs/freeifaddrs only from API 24,
while flutter/ndk_*.sh builds against --platform 21, so every abi failed to
link on the undefined symbols.

Raising the platform to 24 would have to drag minSdkVersion 22 with it and
turn the link error into a load-time one on Android 5.1/6.0, so define the
two symbols instead, using the RTM_GETLINK + RTM_GETADDR netlink dump bionic
itself uses. The definition also shadows bionic's on API >= 24 rather than
delegating to it, so the path that ships is the path every test device runs.

Checked against synthesised netlink dumps on the host -- link/address parsing,
prefix masks, point-to-point, ipv6 scope ids, malformed and truncated messages
-- under UBSan and byte-exact guard malloc, with a deliberately unsigned
remainder as the negative control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 17:13:15 +08:00
rustdesk
654344dbd9 kcp: make the congestion-control profile opt-in, not the default
The branch had flipped KCP to nc=0 (built-in congestion window) for
every session. That is a transport-behavior change for all users made on
reasoning alone, and the reasoning does not decide it: which profile wins
depends on why packets are being lost.

nc=1 - what RustDesk has always shipped - never shrinks the send window,
so on a genuinely congested uplink it deepens the loss it is reacting to.
But nc=0's backoff is blunt: a fast retransmit halves the window while an
RTO sets cwnd = 1 outright (ikcp.c) and recovery slow-starts from one
packet, so on a link with random loss and no congestion - Wi-Fi
interference, a long-haul path - it reads loss as congestion and can
stall an interactive stream for seconds. That failure mode is also the
more visible one to a remote-desktop user.

No benchmark settles this either: a loopback A/B has no bottleneck queue,
hence no congestion to control, and would flatter nc=1 by construction.
Deciding it needs a shaped link or field data.

So keep the profile users already run and let the other one be asked for
("enable-kcp-congestion-control" = "Y"). Flipping the default later is a
one-line change once there is evidence. kcp-sys keeps its own test
covering the nc=0 path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
bea0d1d2f9 ipc/auth: replace the local throttle with the shared throttled_log!
auth.rs predated hbb_common's LogThrottle and grew its own equivalent:
same shape (last_log_at + suppressed), same 5s interval, plus a helper
and three OnceLock<Mutex<..>> statics. It also counted the other way -
excluding the event being reported - so each of the three sites carried
two near-identical log::warn! arms to avoid printing "suppressed 0".

The shared macro covers all of it: one static per call site declared by
the expansion, and the multiplicity suffix appears only when there is
one, which is what those duplicated arms were for. 102 lines out, 27 in.

Behavior difference, deliberate: a burst now reads "(x47)" - the total
including this line - instead of "(suppressed 46 similar events)". One
number, no arithmetic, and one convention across the codebase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
6c7e3c1370 kcp: client-side integration tests over real loopback sockets
kcp-sys has been through two review rounds of behavioral fixes; the
client wrapper (kcp_io pumps, connect/accept deadlines, framed-stream
adaptation, guard lifetimes) had no tests pinning what rustdesk actually
relies on. Four now do, each through real 127.0.0.1 UDP sockets and the
BytesCodec framing sessions use:

- handshake + bidirectional framed roundtrip + graceful close: the peer
  observes end-of-stream instead of hanging (guard outlives the framed
  stream so the FIN goes out);
- a writer that queues 50 frames and closes immediately loses none of
  them - the client-side pin for the close-tail-drain semantics;
- socket errors after the peer vanishes are treated as loss: writes keep
  succeeding, nothing tears down (ICMP is advisory on connected UDP);
- the connect deadline holds when nothing answers.

Mutation-checked: dropping inbound forwarding in kcp_io reddens exactly
the three tests that need the pump, and the timeout test alone stays
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
124bbfd8dc add enable-webrtc option; gate test_ipv6 under forced relay
OPTION_ENABLE_WEBRTC (hbb_common 48c2d4d) follows the udp/ipv6 punch
options end to end: default on against the public server, off against
private ones, same settings UI placement on desktop and mobile, and the
same bool2option local-option handling. Gates:

- controller: should_create_webrtc_offerer checks it first — no pc, no
  STUN/TURN gathering, no offer in the request;
- controlled: unlike the udp/ipv6 legs, which deliberately follow the
  request, answering builds a pc that gathers ICE from this host, so
  the answerer honors this machine's own switch too.

Translations for "Enable WebRTC P2P connection" added to all 50 lang
files next to the IPv6 entry (IPv6 and WebRTC are invariant terms in
the same grammatical slot in every one of them).

Also stop probing v6 reachability (test_ipv6) under any forced relay:
the v6 punch socket is never bound there, so the probe was wasted work
on every ws/proxy/relay connection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
2ba2d0a72b ws: read the all-ICE declaration from the offer envelope, drop the proto field
Companion to hbb_common 68d2729: the full-ICE declaration now lives as
an `ice_policy: "all"` key inside the webrtc:// envelope, so the request
assembly no longer sets webrtc_all_ice and the controlled side asks the
envelope (endpoint_declares_all_ice) instead of a PunchHole field. The
rendezvous server carries the offer opaquely — no forwarding to keep in
sync. Skew behavior is unchanged: an unmarked or unparseable envelope
reads as the old Relay-only semantics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
48ef3059c7 ws: decouple ICE policy from force_relay — full-ICE WebRTC over WebSocket
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
2026-09-03 17:13:15 +08:00
rustdesk
70a124696d fix: the KCP io throttle reset itself every cycle, so it never throttled
The send and recv arms shared one counter, and an ICMP error on a connected
socket is reported once and then cleared — so the steady state is an
alternation: the send succeeds and clears the counter, the next recv reports
the error and finds the counter at 1, and logs. Every error still wrote a
line, at the ~100/s the previous commit set out to stop, while the
persistent-failure and recovery branches were unreachable.

Use one LogThrottle per direction instead of a hand-rolled counter. That
removes the shared state the bug lived in, drops a third throttling mechanism
in favour of the one already added, and leaves the surrounding `if let Err`
untouched rather than reshaping it into a match.

Also fix test_udp_uat's socket-error arm, the untreated twin of the punch_udp
site: it had no backoff at all, so a persistent error re-armed recv
immediately and spun the loop at CPU speed, one warn line per iteration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
d76b98f7c0 fix: bound log volume on sites whose rate a peer or retry loop controls
Debug output goes to the log file, so a site that fires per received message
or per retry lets someone else decide how much a machine writes to disk. The
WebRTC work added the first such sites.

- KCP io loop: absorbing ICMP errors as packet loss made a broken socket write
  ~100 lines a second for the 60s until the pong timeout reaps it. Log by run
  instead: one line when a run starts, one per ~5s while it persists so a stuck
  socket stays visible, and one on recovery with the total.
- punch_udp: the recv error retries every 10ms for up to MAX_TIME, so one line
  per occurrence wrote thousands per punch. Log the first, report the count in
  the timeout message.
- ICE candidate paths (client, mediator): the peer sets the candidate rate and
  the rendezvous route carrying them needs no prior punch, so throttle to one
  line a minute each with the suppressed count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
2b9a6ef7b0 fix: evict the oldest pending ICE candidate, not the newest
Candidates arrive in gathering order — host, then srflx, then relay — so a
full buffer was discarding exactly the ones that traverse NAT while keeping
host ones that only work on a shared LAN. Evict from the front instead.

Also document why the controller's ICE bridge must not reconnect on error, in
contrast to the controlled side's per-candidate retry: its socket address is
the return route itself (mangled into PunchHole.socket_addr, echoed back in
IceCandidate.socket_addr, resolved through tcp_punch), so a reconnect would
arrive from an address no route points at, and the server drops the old entry
when the connection closes. Once it dies both directions are dead, and
abandoning WebRTC is the correct response rather than retrying.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
362966cde9 fix: don't let the preferred branch's own relay preempt a direct fallback
race_transports_prefer_webrtc committed any success from its first argument
outright, on the assumption that it is the WebRTC connect. It is not: the call
site passes a whole punch attempt, which internally falls back to request_relay
when its direct transports fail. That relay was therefore committed instantly
while the offer-less fallback's TCP punch was still in flight — inverting the
preference this function exists to enforce, since the is_p2p predicate the
caller already supplies was applied only to the `others` branch.

Apply it to both branches: a direct result from either side still commits
immediately, and a relayed result from either side is held for the window so
the other side can land something direct. Also commit a held connection when
the surviving branch errors, which the previous code only did on the first
branch's failure path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
8da465f1d1 fix: carry switch_code through WebRTC relay fallbacks after rebase
The rebase onto master (switch-code feature) added an 8th request_relay
parameter; pass the interface's switch code from both WebRTC->relay
fallback paths so a role-swap session survives the fallback. Also drop
a duplicate bindgen 0.72.1 entry the Cargo.lock merge produced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-03 17:13:15 +08:00
rustdesk
d45cb8a5f8 fix: KCP/UDP resilience to ICMP resets; optional KCP congestion control
- treat ICMP-driven UDP socket errors (WSAECONNRESET 10054 on Windows,
  ECONNREFUSED on Linux) as packet loss in punch_udp and the KCP pump
  instead of tearing the session down; KCP retransmits through them and a
  truly dead link is still reaped by the pong/app-level timeouts
- resolve STUN hostnames via tokio::net::lookup_host so DNS never blocks a
  runtime worker; fix the inverted non-IPv4 error message
- add enable-kcp-congestion-control option (default on): switch the turbo
  profile to nc=0 so brief loss on constrained links no longer spirals into
  stalls; sender-side only, no wire negotiation
- pin kcp-sys to the rustdesk-patches branch: upstream main lost the
  RustDesk patches on the EasyTier sync, and this branch also wires
  set_kcp_config_factory into connection setup, making the option effective

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 17:13:15 +08:00
rustdesk
f5c2ff7e25 feat: decouple WebRTC from UDP punch, route controlled signaling over TCP
- the WebRTC offer now rides any punch request; only an offer-less request
  may close and reuse the rendezvous socket for TCP punching
  (request_allows_tcp_punch replaces the udp_port-based invariant), with a
  separate offer-less request racing as the TCP fallback
- WebSocket mode no longer disables WebRTC — ws only tunnels the
  signaling/relay legs while ICE stays the only P2P path there; SOCKS proxy
  still disables it (ICE would bypass the proxy and leak the real IP)
- controlled side: WebRTC-only punch replies and trickled ICE candidates go
  over dedicated TCP connections to the rendezvous server instead of the UDP
  mediator channel, for ws/TCP-only hbbs deployments; drop the now-redundant
  rz_sender plumbing and the 400ms candidate re-send on that leg
- guard is_udp handling against responses to requests that advertised no
  udp_port; skip the IPv6 socket bind under force-relay
- test_udp_uat: drop the STUN port race — the punch port must come from the
  rendezvous server's TestNatResponse observing this socket's mapping, a
  STUN probe from another socket can advertise an unreachable port
- bump hbb_common (webrtc 0.13 MSRV pin rationale + upgrade checklist docs)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 17:13:15 +08:00
rustdesk
86c4ddbb1e fix: preserve WebRTC transport preference 2026-09-03 17:13:15 +08:00
rustdesk
c34f29dd30 feat: WebRTC transport racing, DTLS identity binding, and pc-leak fixes
- prefer-P2P racing (race_transports_prefer_webrtc) across punch and RelayResponse; ICE bridge with 400ms candidate resend
- controlled-side answerer and ICE routing; sign local DTLS fingerprint into SignedId, controller verifies the binding fail-closed
- fix pc leaks: close_webrtc() on insecure-decline paths (io_loop, port_forward); compute direct before disarming the offerer guard
- point hbb_common to the WebRTC data-plane commit 9f5a296

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 17:13:15 +08:00
rustdesk
49dc85b9c2 fix: route WebRTC ICE through rendezvous paths 2026-09-03 17:13:15 +08:00
rustdesk
2771979eb2 feat: race WebRTC as a direct transport enhancement 2026-09-03 17:13:15 +08:00
rustdesk
de3588313a feat: route WebRTC ICE on controlled side 2026-09-03 17:13:15 +08:00
Mariano Abad
3f93005be2 fix(drm): deliver a rotated output upright (#15886) (#15889)
* fix(drm): deliver a rotated output upright instead of sideways (#15886)

the compositor draws a rotated desktop sideways into the landscape
scanout and the physically turned monitor straightens it locally, so the
raw scanout the drm path ships reads sideways in the viewer, and nothing
rebroadcasts on rotation because the framebuffer size never changes.

the capturer now resolves the output transform once per session from the
wayland enumeration, turns accepted frames upright into its own buffer,
and sizes the session in rotated dimensions. the advertised list swaps
width and height for 90/270 outputs, which also makes a mid-session
rotation a topology change that restarts the service, and computes scale
from the post-swap width so a rotated 1:1 monitor no longer advertises
scale 16/9. a non 4-byte format on a rotated session is a hard error and
degrades through the existing health path.

the greeter path where no compositor answers keeps today's behavior:
there is no transform source there. hbb_common carries the new transform
field (submodule bump).

* fix(drm): drop the wayland snapshot when the live layout drifts (#15886)

the advertised list is augmented from the cached wayland snapshot and
nothing invalidated it mid-session, so a rotation the 1.5 s live poll
plainly saw never reached check_changed: the poll reads live, the
advertise kept serving the pre-rotation snapshot. measured before this
commit: transform applied and held, 'desktop layout changed' logged,
zero new encoders. cleared only when the poll saw an actual change, so
the probe cost stays tied to real layout events; after it, the same
stimulus rebuilds into a 1080x1920 encoder within a poll turn.

* refactor: trim comment density to the file norm

* chore: bump hbb_common to the transform field from rustdesk/hbb_common#586

pinned to the #586 commits atop the current pin rather than main tip:
main also carries an unrelated config-keys refactor the app has not
adopted yet, and both #586 commits are reachable upstream through the
merge.

* fix: advertise a lone rotated output at delivered size, one snapshot per session

review findings, both real: the 90/270 swap sat below the origin-only
cut, so a single rotated output advertised unrotated dimensions while
the capturer delivered rotated frames; and transform and origin came
from two get_displays() reads that could straddle a cache invalidation.
the swap now precedes the cut (logical-scale adoption stays multi
output), and new() resolves one snapshot for transform, origin and the
session size, with tests for both. comments trimmed to the three-line
guideline.

* fix(drm): rotate every space the rotation touches, not just the pixels

review findings on #15889, all verified against the code first.

the uinput rect's single-display branches now serve the delivered
orientation, so the pointer reaches the whole of a rotated screen (1).
DisplayRect carries the transform, making 0/180 and 90/270 flips
visible to the drift comparison (5), and the drift poll is an edge on
live-vs-previous rather than a level against the baseline, so the cache
clear fires once per real layout event instead of every 300 ms
forever (9). on the drm path the baseline promotes together with the
clear, so the remap and the client rebase never correct the same origin
delta twice (7), and the poll now runs above the login-screen return,
which was the one place with no other invalidation trigger (6).

a snapshot generation gives a rotation a rebuild path at last (3, 4):
clears bump it, the capturer records it at build, and a stale
generation asks for a rebuild without counting against display health.
the cursor bitmap and hotspot turn with the same session transform the
frames use (11). original_resolution follows the 90/270 swap (12). the
transform comes only from an identity match, never the layout-order
fallback (13), and a missing wayland snapshot at build logs the degrade
instead of silently pinning an unrotated session (10).

unrotate_bgra's body is now libyuv's ARGBRotate, which the existing
direction tests pin to the measured anchor (14). 180 stays master
behavior: i915 advertises hardware rotate-180 and wl_output cannot tell
hardware from software rotation, so undoing it blind would invert an
already-upright frame; it needs the plane rotation property on the
wire (2). the pipewire fallback guard's comment now states the rotated
reality it compares (8).

* fix(drm): one owner for the layout generation, one identity rule for rotation

adversarial pass over the previous commit, three structural findings.

the generation bump rode on the cache clear, which every video-service
start also executes, so any session init or restart tore down every
other live capturer, with no damping against a ping-pong between two
displays. the bump now has a single owner: the edge-detected layout
change in the display-service poll. cache clears are side-effect free
again, and a two-display session survives a third session's init with
zero spurious rebuilds.

the advertise side swapped dimensions for a layout-order-fallback match
while the capturer's transform refused such matches, splitting
advertised size from delivered frames into a black screen. both sides
now key off the same identity-match pass (identity_matches), so a
guessed assignment rotates nothing anywhere.

an edge observed while the drm verdict was transiently non-available
was consumed unpromoted, leaving a rotation sideways for the session;
it now stays owed until the verdict returns. an enumeration that failed
at build pinned transform 0 forever with a warn promising a retry that
did not exist; a missing snapshot now makes the first successful poll
an edge, so the degrade is bounded by the outage. the multi-display
missing-logical-size fallback serves delivered orientation, stale docs
zhou named are updated, and the resolutions list stays mode-space on
purpose: resolution changes ride xrandr, which is inert on this path.

* fix: transpose-tolerant fallback size check, log a rejected rotate geometry

whether a portal stream's caps arrive rotated on a 90/270 output is
unmeasured either way (pipewiresrc does not apply
SPA_META_VideoTransform), and this guard has already broken two readers
who reasoned from its comment - so the size half now accepts either
orientation instead of gambling a permanent offline on one. a source
stride shorter than a row logs the rejected geometry instead of
publishing a silent black frame. comments trimmed to the guideline and
the stale sole-test claim updated.

* fix(wayland): never serve a transposed PipeWire stream

The fallback accepted a stream whose dimensions were the advertised
display's transposed, but CapturerInfo keeps the stream dimensions,
nothing on the wayland side ever reconciles the client afterwards,
and the flutter renderer drops every frame whose size differs from
the advertised display - a permanently blank fallback. Accept only
the exact orientation; a transposed pair now falls into the existing
bail, the display is advertised offline, and the client recovers by
re-enumerating.

* fix(drm): keep the cursor consistent with the session transform

Two holes from the same review pass. The wire cursor id hashes only
the plane pixels and geometry, so a stream rebuilt under a new
transform resent the SAME id and the client's by-id cursor cache kept
the old orientation until the shape itself changed; fold the session
transform into the served id. And a cursor racing new()'s transform
store was processed with transform 0 and never corrected, since the
producer resends only on a shape change; hold that cursor and replay
it once the transform is in - the receive loop wakes at least every
200 ms, so the replay is prompt even on an idle wire.

* fix(wayland): the single-display carve-out must not forgive a transposed stream

The carve-out forgives a size difference (a Full Workspace stream may
report the workspace rather than the mode), but a transposed pair is
the same served-vs-advertised orientation split the previous commit
rejects, and it blanks the client the same way.

* fix(drm): a lone display with a rejected fallback is honestly offline

The transposed rejection promised 'advertised offline', but the
lone-display carve-out in mark_demoted_displays kept the display
online on the grounds that the whole-desktop fallback remains usable
- which is exactly what the rejection just refuted. The video service
then restart-looped against a stream nothing can serve, rebuilding
the portal session about once a second, while the client saw a
display list that lied.

Record the geometry rejection in the display health and let it end
the carve-out; a delivered frame or the demote-cooldown re-arm clears
it, so a recovered output comes back on its own.

* ci: retrigger, the previous run died in the actions outage (all root jobs at exactly 8m)

* fix(drm): a blind capturer owes a rebuild, and name matches reserve globally

Two of the review's findings. A capturer built during a failed wayland
enumeration recorded nothing durable: a later successful enumeration
refills the cache, wayland_snapshot_missing goes false, and the first
live poll sees no edge - the session stays sideways until an unrelated
change. The build now latches that it ran blind and the layout poll
consumes the latch into the existing owed-promotion machinery.

And the identity matcher ran per-connector, so a resolution guess for
an earlier connector could steal a later connector's exact name match
and pin its rotation on the wrong output. Names now reserve in a
global first pass; resolution pairing runs on the remainder only when
forced - one free output and one unmatched connector at that size.

* fix(drm): consume the blind-build latch even on a live-changed poll

Adversarial pass on the previous commit: the short-circuit left the
latch set on exactly the poll where live_changed fired (the common
blind-recovery ordering, since a failed enumeration is not cached and
failed_init makes the first successful poll an edge), and the stale
latch then bought a second, spurious promotion one poll later,
tearing down the freshly rebuilt capturer. The latch is now taken
unconditionally so both edge sources merge into one promotion.

* fix(wayland): hand over a layout change the poll has not seen yet

set_wayland_layout_baseline clears live, which is the edge detector's only
memory of the previous layout. ensure_inited calls it at the top of every video
service start, so a second monitor service starting between a rotation and the
next 1.5s poll recorded the rotated layout as the baseline: the poll then found
baseline == live_rects, owed no promotion, and the first capturer kept its old
transform. Under mutter's software rotation the framebuffer size does not
change and the wayland display-change check is disabled, so the stream stayed
sideways until the next layout event.

The setter now arms the promotion itself when the outgoing live differs from
the incoming baseline, which is the one choke point every caller goes through.
An empty incoming baseline is the DRM-union fallback and proves nothing.

* fix(wayland): the edge detector needs a memory a session init cannot erase

The baseline reset was also the edge detector's memory, so two session inits
straddling a rotation left nothing to compare the next poll against. Keep the
observed layout separate from the per-session input baseline; before the first
poll the outgoing baseline seeds it.

* fix(wayland): a capturer records the layout it was built on

ensure_inited() runs the wayland query before the capturer exists, and a failure
there saves an empty baseline. The capturer's own retry can succeed a moment
later and build on that layout, and because the build was not blind nothing
latched it, so a rotation before the first poll had no memory to be an edge
against and the stream stayed at the old transform.

The build now seeds the edge detector when nothing else has, and only then, so a
capturer built later cannot overwrite what the poll is keeping.

* fix(wayland): keep a capturer record that lost the race with the first poll

The constructor reads its wayland snapshot and records it in the edge
detector in two steps, and the layout poll can land between them. After a
failed session init (empty baseline) the constructor takes layout A and
publishes it, the output rotates, and the poll reads B live: nothing is
recorded yet and the snapshot is present, so it is no edge, and observe()
sets seen=B. The late note_capturer(A) then met a non-empty memory and was
dropped, so the capturer showed A while the detector held B, and B against
B never bumped the generation.

note_capturer now flags a build layout that disagrees with the poll's
memory instead of dropping it (overwriting is still wrong: on a
multi-display session that memory is what the other capturers were built
against). edge() reports the flag as an edge whatever the live layout is,
observe() consumes it right after, and a session init's baseline reset
leaves it alone. Regression test for the interleaving, with the promotion
consuming it, a baseline reset in between, and an agreeing late record as
the control.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZwSrP3DFA6ZiPKVHkU5dL

* fix(wayland): a late capturer record from a promoted generation is not a second edge

The record can also land after the poll consumed an edge but before the
bump it promotes, or after the bump with a snapshot taken before it. That
capturer is stale by generation and rebuilds on its own, but the flag it
raised survived the promotion, and the next poll spent a second promotion
on the freshly rebuilt capturers.

Tag the record with the generation the capturer read before taking its
snapshot and count it as an edge only while that generation is current;
the newest generation wins when two records land. Regression test for the
consumed-edge interleaving, with a disagreeing record at the promoted
generation and a stale record after a fresh one as controls.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZwSrP3DFA6ZiPKVHkU5dL

* chore: bump hbb_common to main tip

dc95b4f -> 05ed68f, a fast-forward: the flipped-transform warning and the
wlroots xdg-output positions (rustdesk/hbb_common#591, #592), 90-day logs,
the webrtc session cleanup deadlock fix and the hide-general-settings
option. No public API changes and no dependency changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZwSrP3DFA6ZiPKVHkU5dL

---------

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 17:10:01 +08:00
memory_clear
e4539fc304 Update cn.rs (#16041) 2026-09-03 10:08:30 +08:00
Maison da Silva
6dbd810454 Translate export-related strings to Portuguese (#16038)
Translate export-related strings to Portuguese
2026-09-03 08:51:05 +08:00
Stephan Paternotte
dfb5804dd0 Update nl.rs (#16036)
Re. export and import.
Without detailed information, reference or examples from en.rs, de.rs or fr.rs, I have simply translated the three strings verbatim
2026-09-02 21:49:37 +08:00
XLion
c312385ffd Update tw.rs (#16031)
* Update tw.rs

* Update tw.rs

* Update tw.rs
2026-09-02 14:36:49 +08:00
palmoni5
f28ac38ccf feat: optionally sync clipboard between connected sessions (#15934)
* feat(clipboard): optionally sync clipboard between connected sessions

Clipboard content received from a remote session is written to the local
clipboard with an owner marker, so the client clipboard loop deliberately
skips re-broadcasting it to avoid echo loops. As a result, text copied in
one remote window could not be pasted in another connected remote window.

Add an opt-in local option (allow-sync-clipboard-between-sessions) that
relays Clipboard/MultiClipboards messages received from one session to
all other connected sessions, excluding the source session. Per-session
clipboard permissions and view-only mode are still respected via the
existing send path, and the owner marker on the receiving peers prevents
any echo back.

Desktop (flutter) only; file clipboard is not affected.

* fix(lang): propagate sync-clipboard-between-sessions-tip to all locale files

Add the new key to template.rs and every locale file per the localization
convention, move the en.rs entry to the end of the list, and drop comments
that only restated the names next to them.

* fix(lang): add the 'Sync clipboard between sessions' label to the localization catalog

The checkbox label goes through translate(), so add it to template.rs
and every locale file so non-English locales can translate it. en.rs is
skipped since the English display text is identical to the key.

* fix(clipboard): check the source session's full clipboard permission before relaying

The relay was gated only by the incoming clipboard_allowed check
(!disable_clipboard && !view_only). Gate it with
is_text_clipboard_required() instead, which additionally respects the
source session's server_clipboard_enabled and server_keyboard_enabled
state, matching the predicate already applied to destination sessions.
A message arriving after the source permission was revoked (or from a
non-conforming peer) is no longer propagated to other sessions. The
existing local update_clipboard behavior is unchanged.

* fix(lang): translate the new clipboard sync entries in all locale files

Fill the 'Sync clipboard between sessions' label and its tooltip in
every locale file instead of leaving them blank, following each file's
existing terminology. template.rs keeps the empty master entries.
2026-09-01 10:47:26 +08:00
RustDesk
66ab0b87f6 Linux drop shell from service loop (#15979)
* perf(linux): stop the service loop from forking a shell per environment variable

The service loop re-derives the desktop every 500 ms, and every lookup on that
path forks. A healthy GNOME session spends ~104 process spawns a second, 8 full
`ps -u <uid>` scans and 2 full `ps aux` scans, to re-answer a question whose
answer has not changed. `get_env` alone is a `sh -c` pipeline of ~12 processes
per variable.

`get_envs` already reads `/proc` directly and was documented as the intended
replacement, so move the remaining `get_env` callers to it and delete it. The
xwayland probe drops from 4 pipelines (~48 processes) to one `/proc` walk, and
the pathological walk that #15952 was about drops from ~2900 processes to at
most 60 `/proc` walks. `get_cm` and `is_xwayland_running` read `/proc` instead
of forking `ps aux` and `pgrep -a`; `get_cm` also called `current_exe()` once
per line of `ps` output.

Selection semantics are preserved where they were load-bearing:

* `get_envs_of_newest` reproduces the `ps ... | tail -1` the removed pipelines
  used, so a variable the newest matching process does not have means moving on
  to the next pattern, never on to an older process that may belong to a session
  which has since logged out.
* `get_envs` keeps its own order (readdir) and its all-process ranking, so the
  existing `get_display_xauth_wayland` caller is unaffected. Only its handling
  of an exported-but-empty value changes: `DISPLAY=` no longer counts as found,
  where it used to satisfy a single-name query and return the empty value before
  a process holding a real one was examined.
* `get_envs_where` lets the caller state what a complete answer is. Ranking by
  how many of the requested names a process carries cannot know that `DISPLAY`
  is mandatory and the rest interchangeable, so it could rank a process holding
  three optional values above the one holding the pair that matters.

`is_xwayland_running` is scoped to the session's uid. The compositor starts
Xwayland as the session user, so another user's Xwayland -- a switched-away
session, a second seat -- used to route a pure-Wayland session into the Xwayland
probe, which has no display for it to find there.

Not addressed: this discovery path has never had any notion of the active
session, and filters by uid alone. Constraining candidates to the active session
is not possible for the most important one, since `xdg-desktop-portal` and its
backends run under `user@<uid>.service`, which spans sessions and carries no
`XDG_SESSION_ID`, no session cgroup and no audit sessionid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5egQpH4q4GoXJiuMoTJ5t

* fix(linux): the newest-process walk must not answer with a grep or an older PID

Three findings from review of the commit before this one.

`/proc/<pid>/environ` failing to read left the walk on to the next PID, which in
`newest_first` mode is an older process -- possibly of a session that has since
logged out -- where the `ps ... | tail -1` pipeline this replaces stopped at the
one PID it had already picked. A read that fails is a process carrying none of
the requested names, not a process to skip. The `seen` latch that was meant to
hold the newest process is deleted: `accept` is reached once per matching
process, so returning on the first is what it already did.

The regex is matched against the whole `/proc/<pid>/cmdline`, where the pipeline
had a `grep -v 'grep'`. A user running `grep Xwayland` is otherwise the newest
match for that pattern and answers with whatever environment their shell had --
an X forwarding endpoint over ssh, say. This is the one place the walk still
differs from the `get_envs` it grew out of, which never had that filter and
could take an ssh `grep` over the portal it was looking for.

`get_envs` is left exactly as it was. Its completeness test was every requested
name *present*; stating it through `accept` turned it into every name *non-empty*
and, with the empty-value change that went with it, moved which process the
existing `get_display_xauth_wayland` caller settles on. `accept` is now told the
count and asks the question the loop it replaced asked. This supersedes the
`get_envs` bullet of the previous commit message: an exported-but-empty value
counts as found again, as it always did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QgsYAUYKDei1AM5yHJsMX

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 17:51:27 +08:00
Michael Clark
d4b06a6c5c fix: android: replace all-files access with scoped storage (#15602)
* fix: android: replace all-files access with scoped storage + system picker

Remove MANAGE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE, and
WRITE_EXTERNAL_STORAGE from the Android manifest. Remove
requestLegacyExternalStorage. Replace broad external storage with
app-scoped external storage for the file-transfer workspace.

File import uses the system file_picker. File export uses Android's
SAF ACTION_CREATE_DOCUMENT with path validation that restricts
export sources to app-owned directories.

Remove the external_path dependency.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: refine file import feedback

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: use SAF for file imports

Replace file_picker imports with Android's Storage Access Framework to avoid legacy storage permissions, stale cached files, and duplicate staging of large imports. Stream selected documents into app-scoped storage with failure-safe replacement, keep exports restricted to validated app storage roots, use filesDir for the internal fallback workspace, and remove legacy permissions contributed during manifest merging.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: keep file imports in the selected directory

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: reset projection and constrain file workspace

Release capture resources when media projection is revoked externally. Keep Android local file navigation within the app-scoped workspace.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: handle scoped storage start-up regressions. Allow zero digits in POSIX filenames by rejecting NUL explicitly, and initialise the app-specific home directory before the Android service starts the native server.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: update content resolver mode to use 'wt' instead of 'w' to prevent trailing bytes from old document whilst reporting sucess

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android, enforce file workspace boundary on the server, and unblock the ui thread.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: validate rename destinations against the app workspace bound file-operation paths. report rename failures, general import failures, and unregister / reregister projection when its onStop callback fires.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: reconnect was refreshing the directory with net entry instances, while selected items retained the old instances, it was reporting a selected item, but checkbox statue used object identity, and appeared unchecked. Fixed by reconciling by path and entry type before replacing the directory snapshot, rebinding valid selections, and dropping missing ones.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: (android) add SAF folder import and multi item export - import directories using ACTION_OPEN_DOCUMENT_TREE. Export multiple files, logs, and screen recordings via export buttons, add localisation keys for new actions

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix(android): harden scoped storage file handling

- create new SAF documents instead of overwriting export sources
- reject empty peer paths except for home directory reads
- report directory backup restore and cleanup failures
- resolve log export paths from the configured app name

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

* fix(android): harden scoped-storage file operations

- snapshot directory exports before writing to the destination
- query document provider metadata off the main thread
- reject invalid remote directories without read timeouts

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

* fix(android): handle SAF directory name collisions

- reject dot-segment folder names during import
- fail imports with duplicate document display names
- only reuse matching directories during export

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

* fix(android): handle SAF folder import collisions

Reject filesystem-equivalent destination names and
avoid showing a failure when folder overwrite is skipped.

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

---------

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-08-31 16:29:51 +08:00
fufesou
0b08a83d4b fix(file-transfer): improve large directory loading (#15830)
* fix(file-transfer): improve large directory loading

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

* fix(file-transfer): avoid failing newer directory reads

Track each remote directory request by its registered completer and only remove
the task when it still matches, preventing stale failures from affecting newer
requests for the same path.

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

* fix(file-transfer): handle slow directory listings safely

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

* fix(file transfer): correlate directory responses with requests

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

* fix(file transfer): prevent automatic directory responses from matching requests

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

* fix(file-transfer): handle large remote directory listings reliably

- build file rows lazily
- register remote reads before sending requests
- handle Home paths, stale responses, errors, and timeouts
- serialize same-path reads with different hidden-file options

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

* fix(file transfer): reduce diffs

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

* fix: build

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

* fix: invalidate pending dir reads on reconnect

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

* test(file-transfer): cover remote directory read lifecycle

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-27 13:01:11 +08:00
rustdesk
e9b81e3475 typo 2026-08-27 11:47:36 +08:00
RustDesk
7220f00410 fix(linux): a Wayland session without XAUTHORITY is not incomplete (#15978)
Fixes #15952.

Hyprland runs Xwayland without exporting `XAUTHORITY`, and
`get_display_xauth_xwayland` only returns once it has both `DISPLAY` and
`XAUTHORITY`. On such a session that condition is never met, so every refresh
runs the retry loop to the end: 10 rounds x 6 process patterns x 4 variables =
240 `get_env` calls, each a `sh -c` pipeline of ~12 processes starting with a
full `ps -u <uid> -f`. That is ~2900 fork/exec per refresh, and the service loop
repeats every 500 ms. The reporter measured a full core on a low-end laptop and
~60% of a core on a 13600KF.

The Wayland side answers for such a session, so accept `DISPLAY` together with
either `XAUTHORITY` or `WAYLAND_DISPLAY` + `DBUS_SESSION_BUS_ADDRESS`. The
portal answers on the first pattern, which ends the walk there, as it already
did on desktops that do export an xauth.

The loop also assigned all four variables unconditionally per pattern, so the
patterns that do not run on a given desktop blanked out what an earlier one had
answered with -- the portal's valid `DISPLAY=:1` included. That is why the
`--server` was then started with no `WAYLAND_DISPLAY` and no
`DBUS_SESSION_BUS_ADDRESS`. Candidates are now taken from one pattern as a whole
and ranked, so a later pattern replaces an earlier answer only by being better,
and a session that can only offer a compositor and a bus still keeps them.

A compositor that starts Xwayland on demand shows the same shape from the other
side: the portal came up before Xwayland did, so its environment carries a valid
`WAYLAND_DISPLAY` and `DBUS_SESSION_BUS_ADDRESS` but no `DISPLAY`, and no pattern
here may ever produce one. That pair alone is a session the child server can be
started against -- it is exactly what `get_display_xauth_wayland` returns on --
so it outranks a bare `DISPLAY` and ends the retrying, while the rest of the
round still looks for something that completes the session.

Not specific to the drm build: the function is not feature-gated, and the commit
the report points at does not touch it.


Claude-Session: https://claude.ai/code/session_01Q5egQpH4q4GoXJiuMoTJ5t

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 11:39:59 +08:00
Jade
7c6e661fcc fix(linux): Set AppIndicator ID for tray-icon (#15981)
* set static AppIndicator ID in tray-icon init

allows DEs, eg. KDE to 'remember' the user's configuration of tray hidden/unhidden. see: https://github.com/rustdesk/rustdesk/discussions/15208

Signed-off-by: Jade <5164609+gnosticJade@users.noreply.github.com>

* Update tray.rs

---------

Signed-off-by: Jade <5164609+gnosticJade@users.noreply.github.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-08-27 09:41:24 +08:00
Mariano Abad
3f207e91f6 fix(linux): a session logout should hand the peer to the login screen (#15905)
* fix(linux): a session logout should hand the peer to the login screen

Logging out closes every window in the session, the connection manager's
included, and its close handler kicks every peer with the reason a person
gets when they disconnect one by hand. That reason is the one thing the
client never retries on, so the remote session dies on a frozen frame
instead of reconnecting to the greeter that is already there.

The close carries nothing to tell the two apart: measured on KDE, the CM
receives no signal and logind still reports the session active at that
instant, and the server is killed within a few hundred ms either way, so
neither a state check nor a grace period can decide it. What is
distinguishable is the ACTION: disconnecting a peer is not the same event
as this window going away. So the window-close path now says so, and the
server ends the session without poisoning the retry; the Disconnect
button and the app's own close control keep kicking exactly as before.
Linux only, since that is where a logout closes the window.

Verified on plasma/sddm with a client attached: a logout now reconnects
to the greeter with no dialog, while closing the manager window still
shows Closed manually by the peer.

* fix(linux): close the tunnel too, and keep the web build compiling

Three seams the first pass missed. The web bridge is hand written, not
generated, so the new call needs its stub there or flutter build web
stops compiling - and that job is disabled in CI, so it would have gone
green. try_port_forward_loop is a second consumer of the same channel
and only knew Close, so a forwarded tunnel outlived the window it was
supposed to die with. And the variant had landed inside the DRM section,
whose comment says everything below it is drm-gated.
2026-08-26 18:26:26 +08:00
fufesou
0d917c6fa1 fix: remove dup translations (#15967)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-25 18:25:49 +08:00
Abdullah Kaleem
7cc82c1575 Add Urdu language support for UI strings (#15961)
* Add Urdu language support for UI strings till 329 line

Co-authored-by: Copilot <copilot@github.com>

* Add Urdu translations for additional UI strings

* Add Urdu language support in lang.rs

* Fix Urdu translations and remove unused keys in ur.rs

---------

Co-authored-by: Copilot <copilot@github.com>
2026-08-25 09:19:37 +08:00
jhertel
f07b6e2338 Correct Danish spelling, language and translation (#15943)
* Update da.rs

Corrected spelling, language and translation mistakes.

* Update da.rs

Missed one correction.
2026-08-24 17:22:03 +08:00
fufesou
a7deef02a2 fix(msi): keep only native ProductCode uninstall entry (#15891)
* fix(msi): keep only native ProductCode uninstall entry

Move installer state outside the Uninstall registry path,
clean up legacy duplicate entries, and use the MSI ProductCode
for updates and uninstalling.

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

* fix(msi): harden update and uninstall handling

- handle legacy EXE updates without an MSI ProductCode
- propagate MsiExec uninstall failures
- validate and XML-quote custom ARP values

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

* fix(msi): validate registry state before update and uninstall

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

* fix(msi): pass WindowsInstaller state to elevated sequence

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

* fix(msi): block unsupported MSI-to-EXE upgrades

- resolve native MSI state and ProductCode safely
- suppress reboot while preserving MSI uninstall results
- publish the resolved ARP install location
- skip invalid unrelated MSI uninstall entries

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

* fix(msi): fail uninstall when ProductCode is missing

Prevent known MSI installations from falling back to
EXE cleanup when the ProductCode cannot be resolved.

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

* fix(msi): do not abort update on ARP version write failure

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-22 17:49:00 +08:00
Saverio Miroddi
cbf9440281 Prefer active X11 session display (#15933)
* Prefer active X11 session display

* Update linux.rs

* fix(linux): keep the logind display only when it is a local one

`get_display_from_session` returns the value pam_systemd was handed at session
creation, and logind never updates it afterwards. That value is not always a
usable local display: it can be qualified with this host (`myhost:0`), name an
X forwarding endpoint (`localhost:10.0`), or be a bare `:`.

Taking it unconditionally is worse than taking nothing, because a non-empty
`self.display` suppresses every fallback below it, `get_display_by_user` and the
`:0` default alike. The stripping at the end of `get_display_x11` does not save
the last two cases either: it leaves `:` as is and turns `localhost:10.0` into a
local looking `:10.0`, either of which is then exported as DISPLAY and leaves the
session unreachable, where before this PR the host got a working `:0`.

Strip this host so `myhost:0` is still accepted as `:0`, leave `localhost` in
place, and require a display number after the colon. Anything else falls through
to the existing chain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKJxvTT6NQDEcnkWBx5bLA

* docs(agents): prefer a little duplication over a restructure

The "Be minimally invasive" rules already ask for purely additive diffs, but not
in the case where the addition would otherwise reshape an existing function so
the two can share code. Repeating a few lines is the better diff there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKJxvTT6NQDEcnkWBx5bLA

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:28:12 +08:00
rustdesk
c45f7d2dd2 refactor is_public 2026-08-21 01:25:51 +08:00
RustDesk
798b73beb1 Update common.rs (#15924) 2026-08-21 00:52:31 +08:00
21pages
5679670506 fix(flutter): make Adjust Window reliable across desktop platforms (#15853)
* fix(flutter): make Adjust Window reliable across desktop platforms

  - Fix incorrect sizing on scaled displays by calculating the target from the
    rendered canvas scale and platform-specific window coordinate units.
  - Fix adjustments using the wrong monitor by querying the current remote
    window's screen, with the main window as fallback.
  - Fix stale geometry after fullscreen or maximized transitions by refreshing
    metrics before calculating and applying the target frame.
  - Fix fullscreen availability checks on Windows and macOS by predicting the
    restored window borders and caching each macOS window's pre-fullscreen work area.
  - Fix incorrect Linux work areas by handling GNOME Wayland fractional scaling
    and caching compositor/X11 work-area measurements when visibleFrame is wrong.
  - Prevent unsafe adjustments by rejecting invalid, oversized, or implausibly
    small target frames.
  - Avoid failures during window teardown by skipping adjustment when the view,
    screen, or native window frame is unavailable.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): harden Adjust Window handling

  - Use the dynamic Linux resize edge when predicting restored window bounds.
  - Treat GNOME fractional-scaling lookup failures as unknown without repeating
    the lookup for the remote window.
  - Stop adjustment safely when native window calls fail during window teardown.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): correct Linux monitor selection

Update window_size to use monitor height for vertical bounds, preventing incorrect screen selection with vertically stacked displays.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* docs(flutter): simplify Linux screen handling comments

Keep the source rationale concise and move platform measurements and investigation details out of the implementation.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): align Adjust Window resize padding

Use the shared drag-to-resize padding for Linux restored-window predictions so menu validation matches the applied frame dimensions.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): remove Adjust Window screen fallback

Return null when the current window screen is unavailable instead of using the main window's scale factor and work area.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(linux): query Mutter monitor layout mode

  Use DisplayConfig.GetCurrentState instead of inferring scaling from
  experimental features, and handle Ubuntu's UI-scaled logical mode.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): use native maximized state for Wayland cache

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): allow Adjust Window to fill work area

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): avoid racing screen info updates

Signed-off-by: 21pages <sunboeasy@gmail.com>

* refactor(flutter): remove dead Adjust Window web plumbing

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): tolerate near-unity Wayland scale factors

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): harden window screen detection

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(linux): drop deprecated GNOME session detection

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(flutter): remove GNOME monitor layout mode flutter cache

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-08-19 15:08:58 +08:00
fufesou
b0008edcb5 refact: remove linux headless (#15866)
* refact: remove linux headless

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

* fix(linux): probe DRM availability asynchronously on login

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

* revert changes in drm_capturer.rs

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

* Update submodule hbb_common

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

* docs(linux): clarify DRM availability comments

Remove stale headless and unauthenticated-request
wording, and document the Available-only login-screen gate.

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

* fix(linux): remove unreachable session cleanup branch

Remove the obsolete empty-session path and
clarify the intended use of cached DRM availability.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-18 15:02:50 +08:00
RustDesk
6a27910f34 fix(wayland): back off the polling display lookups after a failure (drm) (#15865)
* fix(wayland): back off the polling display lookups after a failure (drm)

In drm builds an enumeration that fails with no endpoint named in the
environment falls back to the socket probe, which forks a child bounded by
seconds, and the display service asks again every 300 ms -- at a greeter
with no reachable compositor that is a probe child per turn, forever. Such
a failure now stamps a shared 5 s backoff, and only the polling callers
honor it: the 300 ms displays-changed check skips its turn and the 1.5 s
live layout poll returns no answer for that turn.

Only the failure that would fork stamps. A session server is spawned with
WAYLAND_DISPLAY set, so its failed connect bails in-process before any
fork; stamping there would buy nothing and cost recovery latency, so live
sessions keep master's behavior exactly. The stamp also survives
clear_wayland_displays_cache: it describes the seat, not the cache, and
the ~1/s capturer rebuild loop clears on every teardown -- dropping the
stamp with the cache would let that loop defeat the backoff and would
turn every post-hotplug failure into a "first" one forever.

The displays-changed check weighs the backoff against what is already
published. With nothing synced yet it always populates -- an unaugmented
DRM list beats the empty broadcast the send path would otherwise emit.
With a synced layout, a suppressed turn keeps it, and a fresh first
failure keeps it too; only a failure that persists across a backoff
replaces it with the DRM stack, so a hotplug at a failing seat converges
within one backoff while a transient failure never tears down a good
layout.

One-shot callers -- session init, pipewire stream setup, capturer info --
keep probing fresh through get_displays, whose failure semantics are
unchanged: replaying a transient failure there would latch an empty answer
into session-long state. Non-drm builds compile none of this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(wayland): log DRM lookup failure once

* fix(wayland): reset lookup warning after recovery

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 09:57:06 +08:00
rustdesk
14a4b197ad translations 2026-08-17 17:22:57 +08:00
Krik JIN
8ffe3117a5 feat(flutter): add mobile canvas lock (#15877)
* feat: add mobile canvas lock

* Update flutter/lib/models/model.dart

Remove redundant canvas-lock comment

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* remove redundant logic

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Krik Jin <isjinhk@outlook.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-08-17 17:04:58 +08:00
RustDesk
5a78be03e3 feat(rdp): title the mstsc window after the peer instead of "localhost" (#15781)
* feat(rdp): title the mstsc window after the peer instead of "localhost"

The RDP tunnel launched `mstsc /v:localhost:<port>`, so with several
sessions open every window is titled "localhost" and servers cannot be
told apart.

mstsc titles the session window after the launched .rdp file's base
name, so write a temp .rdp file (containing only the tunnel address)
named after the peer alias, cached hostname, or id, and launch that
instead. Falls back to the old /v: form when no usable name remains
after filename sanitization or the file cannot be written. Credential
handling is unchanged: cmdkey targets "localhost", which is still the
host mstsc resolves credentials against.

Fixes rustdesk/rustdesk#15775 (discussion)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(rdp): set mstsc title without temporary files

  Keep launching mstsc with /v so Default.rdp settings are preserved
  and unsigned RDP file warnings and policy restrictions are avoided.

  Track the launched mstsc process and reapply the peer name when the
  window title is reset during connection or reconnection.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* docs(rdp): clarify mstsc title limitation

Signed-off-by: 21pages <sunboeasy@gmail.com>

* feat(rdp): show peer identity with hostname in mstsc title

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
2026-08-17 13:30:18 +08:00
RustDesk
7aa98d43cf Refact/plugin removal leftovers (#15864)
* fix(flutter): dispose the settings PageController and order dispose() correctly

`dispose()` began with `super.dispose()`, so the mixin chain marked the State
defunct before the WidgetsBindingObserver registration and the periodic timer
were released. The `PageController` was never disposed at all: `Get.delete`
only runs `onDelete()` for a `GetLifeCycleBase`, and a plain `ChangeNotifier`
is not one, so every open/close of the Settings tab leaked one controller with
its listener still attached.

Also guard `switch2page` on the `Rx<SettingsTabKey>` registration it actually
reads rather than only the `PageController` — now that both are really
deleted, a partial teardown would throw into the catch and silently open the
wrong tab — and re-check `mounted` after the await in the `_videoConnTimer`
tick, which `Timer::cancel` cannot stop once the body has started.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refact: finish the plugin-framework removal sweep

#15854 removed the feature but stopped short of its leftovers:

- `Uninstall`, `Enable`, `Disable`, `Options` and `Please install plugins`
  were consumed only by the deleted `flutter/lib/plugin/**`; drop them from
  template.rs and the 50 locale files (250 dead entries). `Update` and
  `Install` stay, still used by desktop_home_page.dart.
- The server no longer sends `PrvOnFailedPlugin`, and the client no longer
  offers to install plugins when privacy mode fails to turn on.
- Drop the MSI `F_Client_Plugins` / `F_Server_Plugins` localization strings;
  no `.wxs` references them.
- `_DisplayMenu`'s constructor became a pure pass-through once `pluginItem`
  was removed, and the cfg inside `handle_input` repeats the one on the
  function itself.
- Normalize `src/lang/sl.rs` to 0644, the only executable file under src/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): handle legacy privacy mode plugin failures

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-08-14 18:52:03 +08:00
fufesou
d1da05c4db refact: remove feature plugin-framework (#15854)
* refact: remove feature plugin-framework

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

* refact: remove unused translations

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

* fix: delete settings tab observable with correct type

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-14 14:31:13 +08:00