Commit Graph

5097 Commits

Author SHA1 Message Date
rustdesk
5e08d03b8e client: hold the clipboard until this round's login is accepted
The clipboard listener is one per process, started by the first session to
log in, and its broadcast went to every session - one whose login was still
waiting on a password, 2FA or the peer's consent included. What the user
copied meanwhile went to a machine that had not admitted them; the peer
drops it unread before authorization, but it is in that peer's hands.

The check goes on the round, not the session: a session-level one reads a
state and later a sender that a reconnect can have swapped in between, so a
broadcast that passed for the round before could still queue on the next.
Remote is the round - its queue, and is_connected set once its own PeerInfo
is in - so a Clipboard or MultiClipboards that reaches handle_msg_from_ui
before then is dropped there, on the line before it would go out. What the
login itself sends through the same queue, Auth2FA among it, goes as before.

The unauthenticated cap on the other side is how this came up: a clipboard
over 128 KiB ended such a login with "Reset by the peer". The small case had
always gone through quietly.

A test drives a round's Remote over a loopback pair before any login: a
clipboard does not reach the far end, a 2FA code does, and once the round is
connected the clipboard does too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-20 01:30:19 +08:00
rustdesk
c200cc81df server: test that the unauthenticated cap is on before the handshake reads
hbb_common covers each transport's cap; nothing covered where the connection
layer puts it. A header one byte over MAX_UNAUTHORIZED_MESSAGE, written to a
connection stalled in the identity handshake, has to end the handshake on the
header alone and release the connection's place: the test fails with the cap
moved past the handshake or dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-20 01:30:19 +08:00
rustdesk
01780948d2 server: hold an unauthenticated connection to a small message
Until a peer authorizes it sends only a public key, a login request, a test delay
and a close reason, none of them large. Nothing said so: a frame header could
declare up to whatever the transport allowed, 1 GiB on TCP and WebRTC, and a
connection holds its place for up to LOGIN_GRACE before it has to authorize. With
MAX_UNAUTHORIZED_CONNS places to fill, that is 64 GiB of header-declared payload
one peer could make us hold - or, on WebSocket, 1 GiB bought outright with a few
hundred bytes of frame headers, because tungstenite reserves a frame's declared
payload as soon as it passes max_frame_size.

The cap goes on in create_tcp_connection, before the identity handshake, so that
read is bounded too, and comes off once the login is settled. It comes off before
connect_port_forward_if_needed rather than beside the rest of authorization: a
multiplexed tunnel narrows the same knob again for its own framing and has to have
the last word.

128 KiB is several times the largest login request anyone sends - a long hostname,
an os_login, an avatar URL, a file-transfer path - and is also the read buffer
tungstenite allocates per WebSocket connection whatever we do, so on that transport
the bound costs nothing beyond a floor already paid. A server hands that avatar
out as a URL; only a custom client that inlines an image into the avatar option
instead can reach the bound at all. Together with
MAX_UNAUTHORIZED_CONNS it holds every unauthorized connection to 8 MiB. Redis
answered this same shape in CVE-2021-32675 with 16 KiB, tighter because a
per-message bound is the only one it has; here the connection count is the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-20 01:30:18 +08:00
Alex Rijckaert
97811acbdd Update Dutch translation (#16284) 2026-09-19 16:05:18 +08:00
RustDesk
7a367f7cc6 bump webrtc fork: one IPv6 host candidate per interface and prefix (#16263)
* bump webrtc fork: one IPv6 host candidate per interface and prefix

ICE gathered every IPv6 address on every interface. Beside the temporary
address that privacy extensions rotate, a prefix usually carries a stable
one, often derived from the MAC, that the OS never picks as a source: a host
candidate for it hands the peer an identifier that outlives every rotation
and that nothing else this machine sends out ever shows.

RFC 8445 §5.1.1.1 has the trackable addresses of an interface and prefix
left out once a privacy one is gathered. There is no portable way to tell
the two apart, so the fork's `local_interfaces` stands in for that rule
with a best-effort approximation: of an interface's addresses in one
prefix, it asks the OS which one it sends from - a UDP `connect` inside the
group's own prefix, nothing sent, nothing outside this machine's own
prefixes involved - and if the answer is one of them, keeps that one alone.
If the answer is none of them, the whole group is kept, as it was: the
probe is bound to no interface, so where Ethernet and Wi-Fi share a LAN the
route picks one of them and the answer for the other is an address it does
not hold, and the enumeration order would be no better a guess - on macOS
its first address is the stable one. Every other interface and prefix keeps
its address, a VPN's unique-local one among them. Two static addresses in
one prefix keep one, the recorded price of the stand-in; the interface a
shared prefix's route bypasses keeps both of its addresses, a gap the
stand-in leaves open rather than a regression.

The Windows enumeration, which named every adapter "" with no mask, now
carries the adapter's name and on-link prefix, without which Ethernet and
Wi-Fi on one LAN would have been a single group. That reaches IPv4 too:
its addresses carry the adapter's name and mask where they were "" at /32,
so the candidates gathered are the same but `interface_filter` sees the
real names. hbb_common is untouched: its fe80::/10 filter still applies to
what the fork keeps.

Two more fork commits ride along, found by the same review. SCTP never
reported a DATA chunk received again: `handle_data` asks `can_push` before
`push`, and `push` was where a duplicate was noted, so the no-cwnd sender's
reordering window, which widens on reported duplicates, never heard of one
from another of these endpoints; and the SACK, its gap blocks and now its
duplicates unbounded, could outgrow the MTU the DATA chunks keep to under a
thousand chunks in flight with holes among them. Duplicates are now listed
and SACKed at once (RFC 9260 §6.2), and the SACK reports the lowest gap
blocks that fit (§6.7). And the Windows adapter struct, split into nested
parts, read `Ipv6IfIndex` eight bytes late on x64 - only into a scope id
that `Interface::convert` drops, so nothing gathered wrongly, but the field
an interface-bound probe would need; it is flat now, with a compile-time
check written for Rust 1.75, the version this crate builds with -
`offset_of!` would have wanted 1.77.

rustdesk-org/webrtc 80d5a20..49c89bd8, six commits: the heuristic, the
fail-open it grew in review, its comments brought in line with that, the
SCTP duplicates and SACK bound, the flat Windows adapter struct, and its
check made to build on 1.75.

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

* pin hbb_common to main 7a5ad52

0eb1759..7a5ad52, six commits: the message cap's tests on WebSocket and
WebRTC with the fragmented WebSocket message bounded too (e999dce,
f0f1548); hyper_util's debug logs out of the default filters (#608); and
`new_direct_udp_for_unverified`, the controller's UDP socket on the
resolver's preferred address for the rendezvous server, with a second for
the lookup and without the TCP connection `test_target` opens and drops to
prove it - while `new_direct_udp_for`, `new_udp_for` and `rebind_udp_for`,
which the controlled side's punch reply and its registration live on, keep
that proof (f688d41, 0bc8336, 7a5ad52).

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

* ipv6 punch: ask the kernel for the route without resolving a name first

`test_ipv6` is awaited on the connection path by both sides of a punch, and
before it looked for a public IPv6 address in the background it resolved
the STUN hosts' names inline - racing the four, so that one resolver that
hangs would not decide. It could still: `select_ok` returns the first
success or the last failure, so with no resolver answering the probe waits
for the slowest lookup to give up, as long as the system resolver takes,
once a minute, on the first connection of that minute.

The name was never needed. `connect` on a UDP socket sends nothing; it has
the kernel pick a route and a source address for the destination, and any
global address serves, so the probe now names one - the one libwebrtc's
QueryDefaultLocalAddress asks for - and touches no network at all: a bind,
a connect, a local_addr. A machine without an IPv6 route learns so from the
connect's error, at once, as before.

Two smaller things beside it. The minute's gate read the timestamp under
one lock and set it under another, so two connections arriving together
both found it over and both probed; it is one critical section now. And the
background STUN probe, bounded so far by the STUN client's own ten seconds
and the resolver's, has a deadline of its own, five seconds: a probe that
outlived the minute could write an earlier network's address over a later
probe's.

Test: the route probe completes within a second, an address found or not.

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

* controller: the NAT test takes the resolver's address for the server

`new_direct_udp_for` learned which address to send the UDP NAT test to by
opening a TCP connection to the rendezvous server and dropping it - a
handshake, one round trip, on every connection the controller starts,
right before `_start_inner` opens the connection it keeps to the same
host. The NAT test now takes the resolver's preferred address,
`new_direct_udp_for_unverified`, which spends no round trip proving it and
gives its lookup a second: an address the server does not answer on costs
this connection its UDP punch, which the TCP punch and the relay cover, as
they do whenever the test finds no port. The controlled side's punch reply
keeps `new_direct_udp_for` and its proof - a reply sent to such an address
is a device online and unreachable.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 11:20:23 +08:00
RustDesk
ea04f04f9d add hide-elevate-button-in-accept-window (#16271)
A portable client handed to standard users offers them "Accept and Elevate",
which they have no credentials to complete. The builtin option takes that
button out of the accept window and leaves elevation to the controlling
side's "Request Elevation" during the session.

https://github.com/rustdesk/rustdesk-server-pro/discussions/1016


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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:54:11 +08:00
Dmytro
ef00ed78a7 Update missing Ukrainian UI translations (#16272)
* Update Ukrainian translations

Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com>

* Complete Ukrainian voice call translation

Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com>

---------

Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com>
2026-09-18 16:27:34 +08:00
fufesou
5278fcab68 fix(cursor): correct native cursor sizing and validate received images (#16213)
* fix(flutter): shrink the unzoomed remote cursor by DPR on macOS and Linux

With "Zoom cursor" off in Adaptive or Custom view, the remote cursor
bitmap was registered at scale 1.0. NSCursor and GdkCursor treat the
bitmap size as logical pixels, so on a HiDPI controller the cursor was
drawn DPR times larger than in Original view (which already passes
1/DPR) and than on Windows (whose cursor path is in physical pixels).
A HiDPI remote such as KDE Wayland sends a 48-64 px bitmap, which then
showed up 3-4x too big on a Retina Mac.

Scale the bitmap by 1/DPR in that case, and scale the Flutter-painted
cursor used while the peer moves the mouse the same way so its size
does not jump. The new branch is an identity at DPR 1 and the Windows
paths are untouched.

Fixes https://github.com/rustdesk/rustdesk/discussions/15363

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

* fix(flutter): check the cursor height against the min cursor size

`_checkUpdateScale` computed the scaled height from `width`, so the
min-size clamp never looked at the height.

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

* fix(flutter): keep the painted cursor hotspot in place when zoom cursor is off

`CursorPaint` subtracted the hotspot in remote pixels and then scaled it
by the canvas scale, but drew the image at scale 1.0, so the hotspot
landed hotx * (1 - scale) logical pixels away from the remote cursor
position. Cursors with a centered hotspot (I-beam, crosshair) were off
by up to half their size in Adaptive view. Subtract the hotspot after
scaling the position instead.

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

* fix(flutter): read the live DPR and clamp the painted cursor like the native one

CanvasModel caches devicePixelRatio and only refreshes it when the view
style changes, so after the window moves to a monitor with a different
DPR the unzoomed cursor kept the previous monitor's scale. Read it from
MediaQuery instead, which also rebuilds the cursor when it changes.

The native path clamps the scaled bitmap to kMinCursorSize; apply the
same clamp to the painted cursor so a small cursor does not change size
when the peer moves the mouse.

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

* build(cursor): declare existing Zstd dependency for bounded decoding

* fix(cursor): validate and bound received cursor images

* fix(cursor): preserve thin cursor sizes when scaling

* fix(cursor): limit view changes to native cursor sizing

* fix(cursor): apply long-edge minimum to Web cursor sizing

* fix: preserve Linux cursor alpha and match Windows Custom scale

* fix(cursor): keep scaled buffers and raster dimensions in sync

* fix(cursor): preserve Windows peer alpha when resizing

* fix(cursor): preserve mixed alpha during downsampling

* comments

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

* fix(cursor): match desktop cursor size to peer display density

* refactor(cursor): clarify desktop and web scale branches

* fix(cursor): correct adaptive display scaling and fixed cursor size

* fix(cursor): apply all-display adaptive density on every desktop

* fix(cursor): normalize unzoomed all-display cursor density

* fix(cursor): synchronize original view on DPR changes

* fix(cursor): match original zoom to the active renderer

* fix: align Wayland software scrolling with input coordinates

* fix: preserve scroll offsets when switching scrolling modes

* fix: correct cursor size and pointer mapping with custom scale

Apply the hovered Linux display density to Custom cursor zoom in All Displays. Refresh scroll fractions after layout so changing the Custom percentage uses current scrollbar extents and detached controllers.

Validated with macOS and Windows component tests, formatting, and static analysis.

* fix(linux): pad tall native cursors to prevent clipping

* fix(cursor): preserve macOS point size in unzoomed views

* fix(linux): pad rectangular cursors to square canvases

* fix(cursor): divide dpr on Linux -> macOS

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

* fix: cursor size test

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

* fix(cursor): cursor size of controlled side macOS

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

* fix(cursor): limit received cursor allocations

* test(cursor): retain reverse raster transition coverage

* fix(cursor): bound compressed cursor input

* fix(cursor): restrict the scaled size of the cursor

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

* fix(cursor): align hotspots with resized raster dimensions

Calculate each hotspot axis from the actual raster-to-source ratio.
Update existing boundary tests and run cursor tests in Flutter CI.

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

* fix(cursor): align Sciter hotspots with raster dimensions

Calculate native and overlay hotspots from the final raster size.
Preserve input coordinate scaling and cursor refresh order.

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

* fix(cursor): use `1.0` instead of `1.0/dpr` on Linux -> macOS, Zoom off, Scale adaptive

The mouse cursor currently appears somewhat large.
However, this is difficult to adjust because the cursor size
is fixed while the window size varies; its relative size
depends on the specific desktop environment.

We can modify it if users actually provide feedback.
Ideally, we should check the "Zoom cursor" option.

Further adjustments may also be needed later based on cursor density.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: rustdesk <info@rustdesk.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 12:43:07 +08:00
memory_clear
53ec874c39 Translate terminal clipboard tips to Chinese (#16266) 2026-09-18 11:45:10 +08:00
Maison da Silva
04974591b8 Refine Portuguese translations for user options (#16264)
Updated Portuguese translations for clarity and conciseness.
2026-09-18 11:05:52 +08:00
Abdullah Hüseyin Efe
4da167517e Turkish: translate the three empty entries (#16261)
terminal-clipboard-write-tip, "Allow terminal apps to copy to clipboard" and the
voice-call hint were empty, so Turkish users saw the English text.
2026-09-18 09:59:45 +08:00
RustDesk
d4ac2c07b7 temporary password: rotate when a peer is let in, not when it leaves (#16080)
The one-time password was regenerated after the connection loop exited,
so a remote desktop session kept it valid for hours and a port-forward
tunnel for as long as its mapping lived. It now rotates the moment a
connection becomes authorized. Reconnects and windows opened from a
live session are unaffected: they log in on the password the session
remembers, for 30 seconds past its last activity.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 09:15:44 +08:00
RustDesk
0b3a1ddd80 rendezvous: cap unauthenticated punches in flight (#16256)
A PunchHole needs no authentication: anyone who knows this id can ask hbbs
to have us open a punch, and each one hbbs sends is spawned without a limit.
Every punch pays per request. punch_udp_hole binds a socket of its own and
start_ipv6 another; the TCP punch opens a listener on the ephemeral port of
its own connection to hbbs, with a punch of its own in flight beside it -
`reuse` sets the socket flags, it shares no fd - and the LAN listen a
FetchLocalAddr opens is that listener again. All of them then wait up to
CONNECT_TIMEOUT for the peer, so a stream of requests holds as many sockets
as it likes for as long as it keeps sending.

Each takes a place in one of two pools of 32 - UDP over v4 and v6 in one,
the TCP punch and the LAN listen in the other - and gives it back the
moment the peer's session is up - the KCP accept, the TCP stream in hand -
or when the wait ends without one: a place stands for a socket waiting for
its peer and nothing past it, and from the session on the connection layer's
own limits apply, the same handoff the WebRTC answerer's slot makes at its
open data channel. At the limit an arrival is declined rather than an older
punch cut short, since the places turn over on their own, within
CONNECT_TIMEOUT and the few seconds the punch phases add to it, and cutting
one short would drop a socket that may be a moment from carrying a session. Two pools rather than one, so that neither
transport pays for the other's crowd: a connection costs a place in each,
the controller's preferred request punching UDP and its TCP fallback
request punching TCP, and the transports that lose the race hold theirs for
the whole wait, so one pool of 32 would be about ten connections setting up
at once and a crowd of UDP punches would decline a TCP punch that had
nothing to do with it. A place is taken only for a punch that will
be made: whether the v4 legs relay is decided first, since the relay branch
runs the whole session and a place held across it would let ordinary relay
traffic use the pool up. A declined LAN listen falls to the relay, as one
that fails for any other reason does.

Declined is the listen alone, never the reply. One PunchHoleSent carries the
WebRTC answer, the v6 address and the relay server along with the v4 punch,
and the controller sends one request for all of them and retries it three
times before it fails outright, so a reply withheld for want of a v4 place
would lose the transports that needed none. The reply goes out as before, on
a socket that then goes at once, resends and all - a declined request is not
worth a socket kept for its reply's sake, and the controller re-asks on its
own - and what the controller loses is its v4 attempt at a mapping that no
longer answers; its others go on.

A request that carries a v6 address costs two places, and the v4 one is
taken first: the v6 punch starts before the v4 one does, and taken in that
order the last place would go to the transport the peer may have no route
for and leave the one it can count on with none. A declined v6 punch leaves
the v4 path to carry the connection, as it already does wherever this
machine has no public IPv6 address. Each kind logs at most one line a
minute, carrying the number of requests it stands for, since the peer
decides how often it asks.

Tests: the places are the bound and come back on drop; a full UDP pool
leaves the TCP places alone; at the last place it
is the v6 punch that goes without; a declined UDP punch still sends its
PunchHoleSent, answer and v6 address intact, to a loopback stand-in for
hbbs; a listen whose peer never probes gives its place back.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 18:21:27 +08:00
RustDesk
4df7404a6c webrtc: cap concurrent answerer setups, pin the DTLS fingerprint binding with a test (#16225)
A WebRTC offer reaches the controlled side before any password or accept
prompt, and answering one builds a peer connection that binds a socket per
interface and runs ICE for up to CONNECT_TIMEOUT. A forged TCP punch reuses
the mediator's local port for one connect; a forged offer costs all of that,
and nothing bounded how many could be in flight at once. SESSIONS dedups by
offer fingerprint, which only stops replays of one offer.

spawn_webrtc_answerer now takes one of 16 slots before building the peer
connection. The wait for the data channel is bounded by CONNECT_TIMEOUT, and
what the slot stands for is the peer connection an unauthenticated offer had
this machine build, ICE, DTLS and SCTP: on an open channel it is given back
at once, and on a failed one it goes with the pc into the detached teardown
and comes back when that has finished. pc.close() has no timeout of its own,
so a slot freed where the task gives up would let a teardown that never
finished pile pcs up unbounded with the count reading zero; held, a stuck
teardown costs WebRTC capacity and the offers past the cap degrade to punch
and relay. Every failure before the pc exists releases the slot through the
guard's drop. From the open channel on the connection is one like any other,
and the connection layer bounds unauthenticated connections in number and in
time for every transport alike (#16237), a peer that stalls in the identity
handshake or after it included. So this guard stays inside the WebRTC path,
sized above what legitimate controllers reach at once in the seconds ICE
takes.

Past the cap the offer is declined with an empty answer, the reply the
controller already gets from a peer without WebRTC, so it carries on over
punch and relay. Declines log through the throttled-log macro. At the cap a
re-sent PunchHole for a live session also gets an empty answer rather than
the cached one, since the slot is taken before the cache is consulted; only
reachable at the cap, where degrading is the point.

The other change is regression coverage for the signed DTLS fingerprint
binding, which is unchanged. The controller's defence against a rendezvous or
relay that swaps SDP fingerprints is the fingerprint the controlled side signs
into IdPk and the comparison in secure_connection, and neither had a test.
The comparison moves into dtls_fingerprint_bound so it can have one, along
with decode_id_pk_dtls: the fingerprint round-trips under the signature,
another key or an edited payload yields nothing, empty never binds, and
decode_id_pk still sees the same id and pk.


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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:10:40 +08:00
RustDesk
f4808b71d4 webrtc: encrypt the signalling legs to the rendezvous server, or send no WebRTC signalling (#16224)
Three TCP connections carry WebRTC signalling to hbbs in the clear: the
controller's punch connection, which carries the offer up and the answer and
both sides' ICE candidates through it, and on the controlled side the
short-lived connection that returns the answer and the one that trickles its
candidates. Candidates are every interface address of both machines, and the
controller is the side most often on a network it does not trust.

`secure_tcp` is fail-open by design: a server that answers the first message
with anything but a key exchange, or with nothing, leaves the stream in the
clear and the call returns Ok, which the paths from before such servers rely
on. That is not a channel WebRTC signalling may go out on.

So the four legs use `secure_tcp_required`: Ok only once the server's key
exchange has encrypted the stream, an error otherwise. WebSocket is treated
as `secure_tcp` treats it, as a transport encrypted already. On the controller an
error drops the offer, closes its peer connection through the guard and
reconnects, then punches without WebRTC on the fresh socket, with the legacy
condition applied to it as before; the failed exchange may have consumed a
message on the old one. On the controlled side an error abandons that WebRTC
attempt: the answer is not sent, or the candidates are not, and the
controller falls back to its other transports. A relay response carrying an
answer, which the symmetric-NAT and forced-relay branches send on a
connection of their own, keeps the relay and loses only the answer: the
response goes without it, on a fresh socket. Degrade to no WebRTC, never to
WebRTC signalling in the clear. `secure_tcp` itself is unchanged; the
exchange moves into `key_exchange`, which reports whether it happened.

A punch without an offer is unchanged: the legacy secure condition takes
this socket straight to the punch as before, and every other punch waits for
the UDP NAT test as before. The exchange does not replace that wait, it
spends part of the same budget, which now runs from before it: what is left
is waited out, and a probe that has already answered is taken at once.

Tests run a loopback stand-in for hbbs: a server that answers with another
message, or closes, is refused where `secure_tcp` would carry on in the
clear; a completed exchange is accepted and the stub decodes the reply with
its ephemeral key.


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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 10:53:58 +08:00
Vorschreibung
0ac2e7fb52 Avoid Xwayland scans on X11 sessions (#16232)
Desktop refresh runs repeatedly for active sessions, spawning 'psgrep'
procs and producing significant CPU load. Guard the Xwayland process
scan with the session protocol so X11 sessions skip work that cannot
contribute session information.

This preserves the existing Wayland discovery path while leaving X11
refresh behavior on its established display and xauth values.
2026-09-16 20:24:50 +08:00
RustDesk
092a961b62 server: bound unauthenticated connections in number and in time (#16237)
A connection that never logs in costs whatever its transport costs, for as
long as it keeps itself alive: the only limit was the 30s idle timeout, which
any message resets. Nothing bounded how many such connections one machine
holds, on any transport. The shape sshd_config answers with LoginGraceTime
and MaxStartups.

Every connection is admitted among the unauthorized ones before its identity
handshake, in create_tcp_connection, and holds that place until it
authorizes or ends: the count of live places is the bound, not a ledger
beside the connections: the resource bound. One address may hold sixteen, a
quarter of the room; a further connection from it is refused before the
handshake. That share is a fairness cap against the cheapest flood, one host
with one address, not a security boundary: any pool of addresses passes it,
and the global limit is what holds. With 64 held in all, a further arrival
is refused too, and the oldest connection is told to go, unless one is on
its way out already: the handshake is raced against that eviction and ends
at once, and the session loop has it as a branch of its select, so the place
opens as soon as the connection has actually gone and not on a timer tick.
The newcomer is not let in on a place still occupied; the controller retries
on its own with backoff, and by then the place is free. At most one
connection is ever on its way out, so a burst of refused arrivals clears no
more room than a single one, and the retry that takes the freed place counts
against its address's share: one address turns out at most as many
connections as it may hold.

One deadline, from the moment the connection starts, a branch of the session
loop's select rather than a check on the TestDelay tick: a connection not
authorized after 180s is closed, however alive it keeps itself, a wrong
password, a pending 2FA, an accept prompt or an admin-terminal credential
prompt left unanswered. The controller reconnects on its own and the prompt
comes back. It closes with the Timeout reason the idle path uses, and that
path still ends a connection that says nothing for 30s. There is no shorter
deadline for the first login request: an admin-terminal controller shows
its credential prompt before sending one, and a peer that wanted to dodge
such a deadline would only have to send a login request, so it would bound
nothing.

The peer address is normalized with try_into_v4 before admission, the same
form Connection::start keys the whitelist on, so an IPv4 peer and its
IPv4-mapped IPv6 form are one address and not two shares.

The WebRTC answerer's slot keeps bounding peer connection setup up to the
open data channel; from there this covers it like every other transport.

Tests cover the registry and the live bound: an address over its share is
refused while others are admitted; at the limit the newcomer is refused, the
oldest is told to go, nobody else is while it is on its way out, and its
place frees only when it has; an address at the limit turns out no more
connections than its share and is then refused without evicting anyone; and
with the limit held by 64 connections stalled in the handshake, one more
arrival is refused while the oldest handshake ends at once and only then is
there a place again.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:47:24 +08:00
Joss Gray
f9c2f16857 fix: recover DXGI capture after access loss (#16024)
* fix: recover DXGI capture after access loss

* fix: track DXGI frame ownership

* fix: only recover DXGI access loss

* fix: stabilize DXGI recovery after mode switches

* fix: preserve DXGI recovery budget

* refactor: model DXGI frame lifecycle as enum

* docs: clarify DXGI frame cleanup order

* fix: scope DXGI frame grace to access loss recovery

* fix: preserve DXGI recovery across display changes

* chore: log DXGI recovery attempts at debug level
2026-09-16 13:18:44 +08:00
fufesou
28269af9c7 fix(audio): preserve compatible playback and recover failed Windows outputs (#16150)
* fix(audio): preserve compatible playback on replacement failure

* fix(audio): use Windows events for capture worker wakeups

* fix(audio): recover Windows playback after output device failure

* reduce diffs

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

* fix(audio): reset decoder when retaining compatible playback

* fix(audio): integrate playback retention and Windows capture wakeups

* revert(audio): remove Windows capture-event notification changes

* docs(audio): explain Windows 7 capture limitations

* docs(audio): clarify capture changes introduced by #16095

* fix(audio): preserve playback through asynchronous Windows startup

Keep a compatible active output until the replacement callback confirms startup, preserving decoder progress on promotion or rollback. Handle superseding formats and simultaneous output failures without losing recovery state. Pin the scoped CPAL WASAPI event-ownership fix and add deterministic and native regressions.

* fix(audio): retain ready output across superseding formats

* fix(audio): update CPAL teardown recovery

Pin the upstream-aligned WASAPI cleanup with shared event lifetime, self-join prevention and fallible destructor diagnostics. Preserve the existing dependency graph and audio implementation.

* Use upstream-style CPAL stream teardown

* Refact: remove low value test

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

* fix(audio): retry playback when startup confirmation times out

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

* update cpal

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-16 11:55:53 +08:00
Maison da Silva
20e25e743d Fix Portuguese translations in ptbr.rs (#16234)
Fix Portuguese translations in ptbr.rs
2026-09-16 11:00:49 +08:00
RustDesk
851d2df88c fix(audio): three 100% CPU busy loops on the _pa path (#16229)
The Linux audio service in `--server` ignored the `Err` from `next_raw()`, so
once the cm-side `_pa` peer closed, every iteration re-polled a dead socket:
tokio-util's paused `Framed` issues one 0-byte read per poll and returns ready
at once, never `Pending`. The thread never parked and burned a full core for
the life of the process. Propagate instead, so `ServiceTmpl::run`'s existing
backoff ends the inner loop and reconnects.

Two sibling loops on the same audio path have the same shape:

- `ipc::start_pa` (runs in `--cm`) ignored the `Err` from
  `psimple::Simple::read`, so a dead pulse handle spins there instead.
- `start_voice_call`'s forwarding thread polls two channels with `try_recv`
  and has no blocking primitive at all: measured 99.8% of a core for the whole
  call, against 1.0% with a 1 ms pause (audio packets arrive every 10 ms).

fix https://github.com/rustdesk/rustdesk/issues/16226

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:35:40 +08:00
solokot
be757de462 Update ru.rs (#16217) 2026-09-15 16:14:49 +08:00
fufesou
a437eb9bc6 fix(windows): recognize Hyper-V Enhanced Sessions by protocol (#16223)
Fixes #16182

Use WTSClientProtocolType to identify RDP sessions with nonstandard
names during session selection and enumeration.

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-15 15:21:19 +08:00
21pages
39d4f1854b chore(qos): remove verbose diagnostics while retaining trace logs (#16214) 2026-09-14 17:19:58 +08:00
VenusGirl❤
bd028ad350 Update Korean (#16212) 2026-09-14 16:15:55 +08:00
bovirus
b48fe17b52 Update Italian language (#16211)
* Update it.rs

* Update it.rs
2026-09-14 16:15:31 +08:00
rustdesk
29772f6595 fix https://github.com/rustdesk/rustdesk/issues/16208 2026-09-14 14:06:03 +08:00
VenusGirl❤
4515cd6463 Update Korean (#16197)
* Update Korean

* Update src/lang/ko.rs

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

* Update src/lang/ko.rs

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

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-09-14 12:43:50 +08:00
fufesou
9b5f342839 fix(audio): throttled debug logs (#16172)
* fix(audio): throttled debug logs

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

* fix(audio): preserve contention counts across throttled reports

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-14 12:26:23 +08:00
fufesou
cdcb4d4b4c Fix/android voice call (#16180)
* fix(android): handle missing audio permission in voice calls

- Avoid stop errors when voice capture was never started
- Dispatch voice call error dialogs on the main thread
- Explain how to enable Audio capture on the Screen share page

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

* fix(android): restore audio after failed voice call switches

* fix: translations

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

* fix: translatin

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

* fix(i18n): match UI label capitalization in translated messages

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-09-14 12:24:33 +08:00
bovirus
9f60bb6fc5 Update it.rs (#16171) 2026-09-14 10:43:14 +08:00
Maison da Silva
5d7e20103a Translate terminal clipboard tips to Portuguese (#16183)
* Translate terminal clipboard tips to Portuguese

* Update terminal clipboard write tip translation
2026-09-14 10:42:36 +08:00
Vojtěch Lapuník
e4b9594c5e fix(android): exclude android from fixed 64 audio buffer size (#16198) 2026-09-14 10:40:18 +08:00
rustdesk
3c7c13d79d timeout of webrtc fallback to delay 2026-09-12 13:52:11 +08:00
21pages
e82dd12350 Lower QoS sample logging to trace (#16161) 2026-09-11 18:24:34 +08:00
Maison da Silva
0843447c41 Fix Portuguese translations for error messages (#16159)
* Fix Portuguese translations for error messages

Fix Portuguese translations for error messages

* Fix translation for RustDesk desktop session message

* Update ptbr.rs
2026-09-11 11:04:17 +08:00
bovirus
67e0a1f582 Update it.rs (#16157)
* Update it.rs

* Update it.rs

* Update it.rs
2026-09-11 10:28:51 +08:00
fufesou
c4221469d8 Fix/audio stream continuity (#16095)
* fix(audio): add streaming resampler

* fix(audio): preserve stream resampling state

* fix(audio): keep playback callback nonblocking

* fix(audio): decouple capture conversion from dasp

* fix(audio): support stateful samplerate backend

* refactor(audio): isolate stream callback state

* refactor(audio): group capture output options

* fix(audio): clear stale playback state after startup failure

Reset non-Linux playback state when stream startup fails to prevent
new-format audio from using the previous stream or resampler.

Add regression tests for failed format changes and successful playback.

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

* fix(audio): honor capture resampler selection and reuse buffers

Use the selected resampling backend for fixed-frame capture.
Convert samples directly into the input queue and
reuse the PCM frame buffer.

Add tests for anti-aliasing, thread transfer, and
partial-frame draining.

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

* refact: reduce diffs

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

* test(audio): check resampler output count and passband energy

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

* fix(audio): reset incompatible Linux playback state on
  startup failure

Preserve compatible output streams when replacement
  startup fails.
Clear state when no compatible stream exists and cover
  both paths in tests.

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

* perf(audio): reuse PCM buffers in the capture pipeline

- Reuse capture framing, resampling, and channel conversion buffers
- Deliver borrowed packets and write Sinc output into reusable storage
- Add allocation and output-equivalence regression tests

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

* fix(audio): smooth buffer discard discontinuities

Signal receiver PCM discards and fade from the current playback output when the callback reaches the new timeline.

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

* fix(audio): add missing Cargo.toml

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

* perf(audio): move capture encoding off the CPAL callback

Move Opus encoding and service delivery to a dedicated worker.
Use a preallocated bounded PCM queue with explicit loss reporting.
Add tests for callback allocations and queue saturation.

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

* fix(audio): smooth capture gaps and report losses during backlog

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

* feat(audio): report capture queue high-water mark

Track peak queued PCM packets and log the approximate
queued audio duration alongside capture loss statistics.

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

* refact(audio): reduce diffs

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

* fix(audio): avoid blocking capture on encoder queue contention

Use preallocated queues with try_lock in the capture callback.
Count and drop the current packet on contention, preserving
drop-oldest behavior on overflow.

Add regressions for paused workers, buffer reuse, and sequence wrap.

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

* fix: add the missing files

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

* fix(audio): isolate zero-gate state per encoder

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

* refact: reduce diffs

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

* refact(audio): simple refactor

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

* fix(audio): avoid waiting on playback callback locks

Use one PCM try_lock attempt and preserve queued samples during contention. Replace readiness locking with per-stream atomic status and report callback errors from the receiving thread.

Cover callback progress, retained audio, recovery, and poisoned-buffer handling.

* fix(audio): restart capture after processing errors

Stop further processing until the service recreates the stream.
Document the guard as defensive recovery for an unconfirmed failure.
Group capture and resampler submodules under their parent directories.

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

* audio: report capture queue contention drops separately

- Add contention_dropped to loss reports while preserving total drop counts
- Document packet rejection on contention even when buffers are available
- Extend existing contention and saturation test assertions

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

* refact unit tests

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-10 16:00:58 +08:00
fufesou
978e2e28b9 fix(audio): restart capture when the device is unavailable (#16142)
* fix(audio): restart capture when the device is unavailable

* refact: remove low-value test

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-10 15:32:49 +08:00
fufesou
65edf214b9 fix(macos): recover system-stopped audio capture streams (#16123)
* fix(macos): recreate system-stopped audio capture streams

Pin CPAL's ScreenCaptureKit stop notifications and retain interruption
state with each capture stream. Recreate an interrupted stream through
the existing service restart path, outside the backend error callback,
and resend its audio format. Late callbacks cannot restart a replacement.

A natural -3821 stop was observed with the remote connection still open.
Its OS trigger remains unknown and it has no deterministic natural
reproducer. Controlled verification stops the real SCStream and delivers
an explicitly marked -3821 notification; this is not a natural failure.

Dependency: https://github.com/rustdesk-org/cpal/pull/5

Validation: requested macOS Rust and Flutter debug builds; three full-crate
regression tests; build check without ScreenCaptureKit; two controlled
recreations on one connection with independently recorded receiver audio.

* chore(macos): log audio capture startup and resumed samples

* Update deps, cpal

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-09 22:34:25 +08:00
RustDesk
bac8323e5d Wayland portal staged errors (#16118)
* wayland: say which step of the portal handshake failed

The XDG portal handshake is four sequential requests, and every way it can end
badly -- the user declining, the request being dismissed, a timeout, the portal
being absent or dying mid-handshake, the stream list coming back empty -- left
`request_remote_desktop` through one `bail!` carrying one string.
`map_err_scrap` then guessed a cause by looking for "dbus" or "pipewire" in
that string. Since that string always mentions "PipeWire library", a decline
and a three-minute timeout both came out as "Wayland requires higher version of
linux distro. Please try X11 desktop or change your OS." On Ubuntu 21+, where
the mapping passes the text through untouched, they came out as raw English
pointing at an unrelated GitHub issue.

The response code and the D-Bus error were in hand at the moment of failure and
were being dropped: `handle_response` collapsed all of it into one
`AtomicBool`. Record it instead, tagged with the stage that produced it, and
let the app side look the tag up. `map_err_scrap` gains one leading branch;
anything untagged -- which is everything the capture loop reports -- takes the
existing path unchanged.

What the peer is told is chosen from the tag, and only from facts the tag
actually carries:

- A decline and an interaction that ended some other way are separate outcomes
  and say so. The Request spec defines response 1 as the user cancelling, and
  guarantees nothing more about 2 than that it ended -- libportal treats 2 as a
  plain failure -- so 2 says the request ended without completing and does not
  guess who ended it or why.
- A timeout says it timed out. It does not say nobody answered: RustDesk passes
  a saved `restore_token` with `persist_mode` 2, and a restored session is
  exactly the case where the portal shows no picker at all, so there may have
  been no dialog for anyone to answer.
- Not reaching the session bus, a portal that answers but does not implement
  what was called, and a grant that fails only when the PipeWire connection is
  handed over, each get their own message. None of the three is fixed by
  restarting the portal, so none of them is told to. Each says only what its
  evidence supports: failing to open the session bus does not prove nobody is
  logged in, and `UnknownMethod` on RemoteDesktop does not prove the portal
  cannot capture a screen. Which interface was called is in the D-Bus message
  that goes to the log; the message to the peer does not claim one.
- What is left -- the portal absent, silent, or failing mid-handshake -- keeps
  the existing `xdp-portal-unavailable`, which is already translated everywhere
  and carries the one remedy that fits: `systemctl --user restart
  xdg-desktop-portal`.
- The Ubuntu-before-21 branch keeps every outcome that says something about the
  machine and yields the three that say what happened to the request.

Two more say less than they could, for the same reason. `streams_from_response`
comes back empty when the response cannot be parsed as well as when there is
nothing in it, so the message says RustDesk did not obtain a usable screen
rather than that the portal offered none. `ElementFactory::make` fails the same
way for a plugin that is absent as for one that will not load, so the message
says the component could not be loaded rather than that it is missing.

The D-Bus error name and message, the portal response code and the GStreamer
factory's own error go to the log. Only the element name also reaches the peer,
because it is the one detail that says which package to look at.

`fill_displays` needs the tag resolved at its own call site: it opens a second
portal session for cursor-based display disambiguation, and its error returns
straight up `check_init` without passing through `map_err_scrap`, so a tag
would otherwise reach the peer verbatim.

Two existing paths change, both necessarily:

- `check_init` no longer wraps `Capturer::new` in `with_context`. The peer is
  shown `format!("{}", err)` (connection.rs), which renders only the outermost
  layer, so that context was replacing the mapped code with "Failed to create
  capturer for display 0".
- The `std::process::exit(-1)` on libdbus' no-reply text is now reached only by
  the capture loop, which is what that self-heal was written for. Every D-Bus
  call in the handshake -- opening the session bus, `get_request_path`, the
  `add_match` inside `handle_response`, `create_session`, and `conn.process` in
  the wait loop -- carries a tag, so a no-reply there is reported rather than
  fatal. It is worth saying plainly what that branch did before: the portal
  proxy has a one-second timeout, so a portal slow to activate could take the
  whole service down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* wayland: lang keys for the staged portal failures

Eight keys, appended to `template.rs` and to every `src/lang/*.rs`. `it.rs`
gets empty values, as AGENTS.md requires -- it is maintained by hand by its
translator. No `en.rs` entries: each key is already its own English display
text, which is also what an older peer falls back to.

One carries a `{}`, the name of the GStreamer element that could not be created
-- the one detail that tells a user which package to look at. `lang.rs`'s
`extract_placeholder` resolves a key by replacing the first `{...}` with `{}`,
which is why the server sends the value still inside the braces and why the
scrap side strips braces out of any detail before it gets there. Everything
else technical stays in the log: a D-Bus error name or a portal response code
in a dialog is noise to the person reading it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 18:53:13 +08:00
RustDesk
f164c9a9df Dead peer recovery (#16117)
* webrtc: recover from a silent peer in about 8s instead of 30s

A controlled peer that is killed, switched away by a user switch, or rebooted
leaves no trace on a UDP transport: there is no reset to receive, so the session
sees silence, and only the 30s inactivity timeout ends it. By then the remote
machine may have finished rebooting and be reachable again, while the user has
been watching a frozen frame the whole time and is then told the peer reset the
connection.

ICE already knows sooner. It reports Disconnected about 5s after it stops
hearing from the peer, from its own task, so it stays accurate even while this
loop is busy sending. That state is transient by design - a Wi-Fi roam or a
sleep/wake recovers from it - so it is treated as suspicion, not as death: three
more seconds with the transport receiving nothing, and the session reconnects.
Receive progress cancels the suspicion, so a peer that is merely slow, or one
ICE was late to clear, is not dropped.

This only reaches the existing recovery sooner; it does not replace it. The
first reconnect goes out immediately and, if it fails, falls into the same
retry the UI already applies to any unexpected disconnect. The restart
reconnect event is reused deliberately: it is what asks for exactly that, with
no error dialog in front of it, and the UI shows "Connecting..." for it rather
than anything about restarting. Its five-minute grace stays reserved for a
restart the user actually asked for - silence is no evidence of a reboot.

The 30s timeout is unchanged and still backs every transport. TCP and WebSocket
are untouched. The controlled side is untouched: it detects a dead controller
on the same 30s, which wastes some capture but nothing a user sees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

* kcp: recover from a silent peer on the endpoint's own clock

KCP is the other transport with nothing to receive when the peer dies, and it
was the slower of the two: its endpoint reaps a connection only after 60s
without a packet, which is past the 30s inactivity timeout above it, so in
practice nothing but that timeout ever noticed.

The endpoint already tracks when each connection last heard from its peer and
now exposes it, so this reads that rather than anything derived from the session
loop - it keeps answering while that loop is busy sending. Its liveness ping now
goes out about every 2s rather than every 10s, so silence means the peer rather
than an idle link, and eight seconds of it is several missed pings.

Same threshold and the same recovery as the WebRTC half, so a user sees the same
thing on either transport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

* review: time the inactivity window off receive progress, bound the parting send

Two things the review found, both on the controlling side.

The 30s inactivity window still ran off completed messages alone, so the probe
added for the fast path did not fix what it was added for: a message larger than
the transport's fragment size yields nothing until its last fragment, and a peer
sending one steadily was still timed out mid-transfer. It is now timed off
whichever is later, a completed message or receive progress. Transports that
report no progress leave that at its starting value, so nothing else moves.

The parting close-reason send for KCP waited on send capacity with no deadline
of its own, and a queue a dead peer will never drain held the finished session's
thread until the endpoint reaped the connection a minute later. Bounded once the
peer has been declared gone. Still attempted rather than skipped: if the loss was
one-way the peer does receive it, and drops its side immediately instead of
waiting out its own timeout - which is also the one case where the note below
resolves itself.

Recorded from the same review, for the case none of this targets - a peer that
is alive behind a path that broke for five to ten seconds and then healed.
Giving up cannot deliver a close there, because the path is still down at that
moment, so the controlled side keeps the old connection until its own 30s
expires. For up to twenty of those seconds it holds two authorised connections:
its connection manager lists both, and the stale one reports a growing delay
that pins the shared frame rate low for the new one. Input is unaffected
throughout and both recover once the stale connection goes, so this trades
twenty-two seconds of a frozen, uncontrollable session for a controllable one
that looks wrong for a while. Closing the displaced connection is controlled-side
work and belongs with the rest of it, not here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

* review: reject a disconnected cached session, tidy the detector

hbb_common: `is_reusable_for` now also rejects a session ICE reports
Disconnected, so a caller is not handed one that already carries the hint; and
the receive-progress test no longer races `next()` against a sleeping sibling.

Here: the `is_some()` guard on the progress comparison was dead, since a
transport answers `None` for its whole life and `None != None` is already false.
The parting-send deadline is a `Duration` like every other constant around it
rather than bare milliseconds. And the comments are cut back to what is not
already evident from the code they sit on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

* review: keep the legacy UI's retrying error when the peer goes silent

`restarting-show` is a Flutter control event; Sciter has no case for it and
falls through to a plain dialog, which `check_if_retry` marks non-retryable
because its type is not `error`. So on that build the new detector would have
replaced a timeout that reconnects on its own after 30s with a dialog waiting
for a click at 8s - a regression for the one path this was meant to shorten.

Send it the message the timeout already sends, so its behaviour is unchanged
apart from arriving sooner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

* review: keep the 30s watchdog hard, and let Android's picker hold the reconnect

Timing the watchdog off receive progress gave away its upper bound. A fragment
bumps the counter as it arrives, ahead of the framing checks that would reject
it, so a peer sending one `FRAG_MORE` every twenty seconds and never a
`FRAG_END` refreshed the deadline forever while the reassembly buffer grew
toward `MAX_FRAME_LENGTH`, a gigabyte away. What it bought - a clipboard image
that takes longer than thirty seconds to arrive is not a dead peer - is a
pre-existing problem that predates this branch and can be fixed on its own.
Receive progress goes back to the one job it was added for, which needs no
deadline of its own: telling a transport that has gone quiet from one that is
still delivering, so ICE's disconnected hint is not acted on mid-transfer.

The Android document picker suppresses a `Connection Error` while it is open
and remembers to reconnect once it closes. The peer-gone break reconnects
under `restarting-show` with a `Connecting...` title, which matched neither
half of that test, so an eight-second stall behind an open picker - Doze and
background throttling produce them - threw a dialog up behind the picker and
lost the deferred reconnect. It is now named there by its own title rather
than by its type: an explicitly restarted remote device sends the same type
from a path this leaves alone, on every transport, and deferring that one too
would be a change to sessions this has no business touching.

The two limits are still not hard upper bounds, and the comment saying so was
wrong about why. A send is awaited inline in this loop, so one in progress
delays the tick that checks them - bounded on WebRTC by the timeout the stream
was built with, not bounded at all on KCP, whose framed stream is constructed
with none. The 30s watchdog beside it shares the loop and the same delay.

Left alone deliberately. `restarting-show` reconnects without the backoff its
`restarting` sibling uses, which can loop while each round gets far enough to
establish a session and then loses the transport within eight seconds; a
cooldown there would also delay the recovery this exists for when a peer
really does come back, and the loading it shows can be cancelled. And the KCP
limit reads an accumulated silence rather than a transient hint, so unlike the
WebRTC grace it needs no second sample to confirm - one would only move eight
seconds to nine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:56:41 +08:00
21pages
080211ff36 improve qos (#16082)
* first improve

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

* fix(qos): avoid reducing FPS for transient network jitter

  Require consecutive bad samples to confirm congestion before normal
  FPS reductions. Prefer bitrate reduction when ABR is available and
  allow an outstanding probe to complete before evaluating its effect.

  Recover FPS faster on fresh good samples while preserving severe-delay
  and timeout protection.

  Add regression coverage for jitter, bandwidth changes, and multiple viewers.

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

* test(qos): virtual clock and a closed-loop link simulation

Tests drove time by moving `Instant`s into the past, which panics on a host
that booted less than two minutes ago, and the ABR smoke had to detect ratio
changes to keep its fake clock consistent.  `VideoQoS` now reads the clock
through `now()`; tests set a virtual instant and advance it.

`tests/sim.rs` drives the controller the way `Connection` does, over a link
with variable frame sizes, wobbling capacity, heavy-tailed jitter,
retransmission stalls and link stalls, with both a bitrate-targeted
(VP8/VP9/AV1) and a fixed-rate (hardware) encoder model.  It prints one
table row per scenario; the assertions arrive with the controller changes.
The short-stall smoke sweeps the stall phase instead of three fixed values.

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

* fix(qos): keep jittery but healthy links fast, drain congestion with bitrate

A weak home Wi-Fi with plenty of capacity but frequent jitter and the odd
stall ended up at about 10 fps: single bad replies, the two second probe
timeout and a slow climb back each took their share.  The controller now
treats a transient stall and a saturated link differently.

- The probe timeout no longer slams the frame rate to 2.  Every second the
  probe stays out beyond the first halves it instead, and the late reply
  that finally closes the probe does not reduce again.
- After a reduction, good replies return halfway, then fully, to the level
  held before it.  A restored level that congests within five replies
  becomes a lower ceiling, so a real capacity drop converges instead of
  oscillating.
- VP8, VP9 and AV1 run CBR against timestamps: fewer frames only means
  bigger frames.  While the bitrate can still be reduced the frame rate
  keeps its floor, and three bad replies in a row confirm congestion and
  halve the bitrate instead of stepping it down by a fifth every three
  seconds.
- `bitrate_first` now means the bitrate can actually still drop; at the
  floor, congestion during the adjustment cooldown reduces the frame rate
  (Greptile).
- `avg_delay()` subtracts the baseline with `saturating_sub`; at exactly
  the baseline it returned the whole delay, which kept the bitrate of a
  stable high-RTT link from ever recovering.
- `HISTORY_DELAY_LEN` kept three samples, not two.
- Every ratio adjustment resets the dynamic screen counters, so a long
  congestion episode cannot make a static screen look dynamic
  (CodeRabbit).
- One `qos_trace` debug line per probe reply and per timeout, for replay
  through `tests/sim.rs`.

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

* fix(qos): adapt each viewer from its own target, not the shared minimum

`user_network_delay` started every viewer's adaptation from `self.fps`,
the minimum over all viewers.  One congested viewer therefore pulled the
others' targets down with it, and when it recovered the stream stayed low
until the others had climbed back on their own.  The per-viewer memory
added for restores made the coupling worse: a viewer recorded another
viewer's low rate as its own pre-congestion level (Greptile).

Each viewer now adapts from its own `delay.fps`, falling back to the shared
value only for its first reply; `adjust_fps` keeps aggregating the minimum.
The replay test now fails on a malformed trace value instead of dropping
it (CodeRabbit).

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

* test(qos): paired network trace, bitrate-conserving encoder, twenty seeds

The simulator drew every random quantity from one stream, so two controllers
with the same seed saw different links as soon as they produced a different
number of frames; the A/B was not paired.  The link trace (capacity wobble,
stalls, loss events) is now generated before the run from a network stream of
its own, and encoder noise and probe jitter have separate streams.

The CBR model let a scene change add three frames' worth of data every five
seconds without clawing it back, which raised the offered load of any
controller that lowered the frame rate (up to +27% at 2 fps).  The encoder now
repays the surplus over the following frames and the size spread has mean one,
so the long-term load no longer depends on the frame rate.

Every scenario runs over twenty seeds and the assertions bound the
distribution: median of the mean target, worst p10, p90 of the time below half
the limit and of the queue p95.  The bounds state what the product needs, not
what one seed produced.  New columns: produced and delivered frames per
second, delivered frame age, sustained recovery (target at the limit and queue
under 200 ms held for five seconds), cold-start minimum and time to 90% of the
limit.  The replay advances by recorded `t=` deltas when present and is
labelled as the open-loop, FPS-only diagnostic it is.

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

* fix(qos): baseline from the first reply, bitrate cuts on confirmation only

Two findings from the design re-check.

The baseline needed ten replies before it was used, so a stable 180 or
300 ms link spent its first ten seconds read as congested: the frame rate
fell to 5 and the bitrate was cut before either recovered.  The running
minimum is the baseline from the first reply on; the smoothed estimate
takes over once the window is full.

A single reply a second above the baseline, or a single timer tick with the
probe out for two seconds, cut the bitrate by a fifth.  A static screen never
earns an increase back, so repeated stalls ratcheted its ratio down and the
next dynamic episode started from there.  Bitrate cuts now need two bad
replies in a row, or a probe still outstanding at the second tick past two
seconds; the frame rate brake and the severe-reply rule are unchanged.  An
elevated but unconfirmed delay no longer restarts the ratio cooldown, so a
confirmation on the next reply is acted on at once.

`qos_trace` lines carry a millisecond timestamp for the replay test.  The
simulator asserts the intercontinental cold start: minimum target at
INIT_FPS and 90% of the limit within ten seconds.

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

* test(qos): held-out seeds and parameter sensitivity as guards against tuning

The scenario bounds now live in one function shared by the CI run over seeds
1 to 20 and by `robustness.rs`, whose two ignored tests apply the same bounds
to seeds 21 to 120 in blocks of twenty and halve or double each scenario
parameter in turn.  Anyone changing a controller constant or a bound runs
them; a bound that fails on unseen seeds was fitted to the CI seeds.

At this head every held-out block passes, with medians within a few percent
of the CI seeds, while master fails five of five blocks in every home Wi-Fi
row.  The sweep keeps the lead over master in all fourteen variants for the
frame-rate metrics and shows two limits worth knowing: at 6 Mbps of capacity
both controllers hold about 2.4 s of queue p95, and at a drop to 1.5 Mbps
both are poor because the 1 Mbps bitrate floor leaves little to drain with.

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

* test(qos): frame age bounds, wall-clock scene changes, time-indexed probe jitter

The queue metric divides the queued bytes by the nominal capacity, so during a
link stall it reports how long the backlog takes to drain afterwards, not how
long the frame at the head has already waited.  Frame age, the time a
delivered frame spent in the shared path, was computed but not bounded.  It
is now bounded per scenario, as a regression bound set from the scenario
rather than from a run: 1.5 s on the home Wi-Fi rows (isolated stalls of up
to 2.5 s are tolerated, a sustained multi-second backlog is not), 100 ms on a
clean link, 150 ms on a stable high-RTT link (frame age excludes the round
trip, so RTT earns no allowance), and the same bound as the queue on the
bandwidth-drop and mobile rows.

Two residual couplings between controller decisions and the exogenous inputs
are removed: scene changes follow the wall clock instead of the frame count,
and probe jitter is a per-second table drawn before the run, so two
controllers with the same seed meet the same content timeline and the same
jitter.  The moderate-congestion smoke profile now asserts that the frame
rate actually drops, and the held-out test builds its combined summary from
the block reports instead of simulating every seed twice.

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

* test(qos): remove the placeholder viewer from trace replay

  Clear the synthetic viewer created by smoke::session so it cannot
  cap replayed FPS at 15 when the recorded connection ID differs from 1.

  Add a regression test verifying identical FPS sequences for connection
  IDs 1 and 1652, both recovering to 30 FPS.

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

* fix(qos): cap each viewer by its own limit, judge bitrate steps per viewer

Two places still let one viewer's state leak into another's.

The per-viewer target was clamped by `highest_fps()`, the minimum of every
viewer's limit, before being stored, and a new viewer started from the shared
stream rate.  A viewer that lowered its limit dragged the others' targets down
with it, and when it left the stream stayed there until the others had climbed
back; a viewer joining a congested session started at the congested rate.
Each viewer now starts at INIT_FPS, is capped by its own limit only, and
`adjust_fps` keeps applying the shared limit to the aggregate.

`adjust_ratio` paired the maximum delay over viewers with any viewer's
confirmation, so one viewer's unconfirmed 1200 ms spike and another viewer's
two 200 ms replies produced a 20% cut, and a third mild reply a halving, when
each viewer on its own called for five percent.  Each viewer's own delay and
confirmation now decide the step it calls for, and the stream takes the most
conservative one; increases still need every viewer below the threshold.
Single-viewer behaviour is unchanged.

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

* test(qos): replay advances by the wall clock across connections

The replay kept a last timestamp per connection id, so a log with several
viewers each writing once a second advanced the virtual clock once per line
and replayed several times slower than it was recorded.  It now advances by
the delta between consecutive lines whatever their connection.  The replay is
a plain function over the log text, with the environment-driven test as its
entry point, so the time axis can be tested directly.

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

* fix(qos): first reply keeps the ratio cooldown, closing a viewer re-aggregates

A viewer's first TestDelay reply called `adjust_ratio` with no cooldown
check.  With the per-viewer steps, that scan finds the other viewers' still
confirmed evidence, so a viewer joining right after a cut halved the bitrate a
second time inside the three seconds the cut is meant to be observed for.  The
first reply is now one more trigger of the periodic adjustment and keeps its
cooldown; a fresh session still adjusts on its first reply, since its
controller was created long before.  Linux was never on this path.

`on_connection_close` removed the viewer without re-aggregating, so the
stream stayed at the departed viewer's rate until the next tick; the
remaining viewers are aggregated at once.

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

* fix(qos): the newcomer guard belongs to the viewer that joined

Re-aggregating on close applied the one-second INIT_FPS guard that the
departing viewer had set when it joined, so a viewer that connected and
dropped within a second throttled the others to 15 fps for the rest of that
second.  The guard now records which viewer set it and is cleared when that
viewer leaves; a genuinely new viewer is still capped for its first second.

The first-reply ratio adjustment's platform switch is a field instead of a
`cfg!` inside the condition, so the cooldown regression test exercises the
path on Linux CI as well.

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

* fix(qos): every newcomer carries its own start-up guard

The start-up guard had one slot, so a second viewer joining within a second
overwrote the first one's join time, and when the second viewer left the
first one's window was released early.  The join time now lives in the
viewer's own entry, `adjust_fps` caps the stream while any viewer is inside
its first second, and a departed viewer takes its guard with its entry; no
clearing logic is needed (Greptile).

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

* fix(qos): adapt delay baselines and speed up FPS recovery

  - Relearn stable baseline increases from recent fresh replies.
  - Require fresh congestion confirmation between ordinary FPS reductions.
  - Keep automatic FPS reductions above a 5 FPS floor, respecting lower caps.
  - Restore FPS after two good replies, with rollback on renewed congestion.
  - Add regression tests for baseline changes, jitter, and FPS recovery.

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

* test(qos): align FPS floor tests with the 5 FPS minimum

  - Remove the unregistered sustained tests with outdated expectations.
  - Test severe delays and timeouts independently, including lower user
    caps and recovery.
  - Move the 700 kbps scenario into active adaptation tests, checking
    the FPS floor and recovery after bandwidth returns.

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

* fix(qos): a timeout never lifts a target, and the invariants as property tests

The timeout brake floored its output at MIN_FPS + 1, so a viewer whose target
had already reached 1 fps was lifted to 2 by the next tick past two seconds:
bad evidence raising the frame rate.  Inherited from master, where the timeout
set the whole stream to MIN_FPS + 1 outright.  The brake now never exceeds the
target it found, whatever the elapsed time it is told.

The controller's six invariants become property tests over random sessions
(150 seeds, 300 steps, one to three viewers, ABR on and off): a viewer's
target is independent of other viewers; bad evidence never raises a target or
the ratio; joins and leaves only change the aggregation; a bitrate cut is
owned by a viewer's own evidence and not spent again by a newcomer; targets
stay within their caps and the stream is their aggregation; a braked probe's
late reply does not brake again.  The timeout case is also pinned exhaustively
over every target and a range of elapsed times.

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

* feat(qos): log the encode and send pipeline behind RUSTDESK_QOS_VERBOSE

The controller's target frame rate is neither the rate the encoder produced
nor the rate the send path accepted, and two facts kept that gap invisible.
libvpx drops frames on its own when the bitrate cannot carry them, so a
capture round is not a delivered frame.  The video send is inline in the
connection's message loop, so a slow write stalls capture and the delay
probe alike, and the recorded delay cannot tell the two apart.

`qos_video` reports, per second and per display, the capture rounds, the
frames that actually reached a connection, and the longest wait for the
previous frame to be picked up.  `qos_send` reports, per second and per
connection, how long `stream.send().await` blocked and how deep the video
queue is.  Both carry `t=`, so they join with `qos_trace` offline; `replay`
filters on `qos_trace` and ignores them.

The default log level is `debug`, so an unconditional line would land in
every user's log file once a second forever.  Neither line is emitted
unless `RUSTDESK_QOS_VERBOSE` is set, nothing enables it implicitly, and
without it the timing calls are skipped as well.  TestDelay, the controller
and every threshold are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PARvswNPeZ88LVT7Ew5hkp

* fix(qos): speed up FPS ramp-up on clean connections

  Double startup FPS after every two fresh low-excess-delay replies,
  up to the viewer's cap. End acceleration on queue growth, timeout,
  or FPS reduction.

  Add regression tests for startup speed, viewer caps, congestion,
  timeouts, and multiple viewers.

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:48:32 +08:00
RustDesk
01dbb76499 server: do not lock the screen for a connection a reconnect replaced (#16124)
A controlling peer whose link dies without a close reconnects, while the
connection it left behind runs on here until its own 30s inactivity timeout.
That one then ends with `on_close("Timeout", true)`, and the lock is gated
only on `lock_after_session_end` and this connection's own `keyboard` - both
set by the very controller that is at that moment working in the session its
reconnect re-established. Nothing anywhere asks whether the session is still
being controlled, so the screen locks under a peer that came back twenty-odd
seconds earlier, and the operator's desk locks itself in front of them.

The lock now also requires that no newer remote control connection of this
session is authorized.

Newer, not merely other. A connection stays in `AUTHED_CONNS` until its
`AuthedConnID` drops, which is well after `on_close` returns, so a symmetric
test would have two of one session ending together each see the other and
neither lock. Ids come from a counter, so `>` orders them: of a session's
connections the last still locks, whether they end one after another or at
once.

Remote control only, and this session only. The other kinds do not keep a
screen in use, and `send_logon_response` clears `keyboard` for a file
transfer, a terminal and a camera view, so none of those reaches the gate at
all - a port forward keeps it, and is kept out only by the client not sending
`lock_after_session_end` on one. Another peer's
session is left exactly as it is: whether its ending locks the screen while
this one is connected is a separate question, and not one a timeout on this
side should start answering.

Every close that asked to lock, not only the timeout. A connection its own
peer has already replaced should not lock the session that replaced it
however it ends, and singling out one reason would leave the same race
reachable through the others.

Two things it does not cover. A reconnect that has not authorized yet is not
in `AUTHED_CONNS`, so a timeout landing while one waits at the accept prompt
or on 2FA locks as before. And the lock is skipped, not handed on: if the
connection that replaced this one later ends through a path that asks not to
lock - a failed send, a stopped service - nothing locks. That is what those
paths already choose for a connection dying of a network error, on the
assumption that the peer will retry, and it is the same assumption this makes.


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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 15:06:24 +08:00
cui fliter
5228f91982 fix(screenshot): keep cached image when saving fails (#16120)
Signed-off-by: cuishuang <imcusg@gmail.com>
2026-09-09 07:46:19 +08:00
Jamal Ali
0f0205d336 feat: add Azerbaijani translation (#16103)
Signed-off-by: Jamal <jamalkamaladdin@gmail.com>
2026-09-08 15:20:27 +08:00
RustDesk
b50fde6910 add the base crate and repoint the moved modules at it (#16107)
* add the base crate and repoint the moved modules at it

`libs/base` (crate `base`) takes the parts of hbb_common that only this app
uses: `fs`, `platform`, `keyboard`, `message.proto`, and 145 of the 177
`config::keys` constants. hbb_common keeps what the server names, and the 32
keys it reads itself are re-exported from `base::config::keys` so call sites
still see the full set through one path.

Sources move verbatim. The only edits inside them are `crate::` prefixes that
now have to say `hbb_common::`; `keyboard.rs` and `platform/windows.rs` are
byte-identical. The crate stays on edition 2018, the edition the moved code was
written under. `log`, `lazy_static` and `anyhow` become direct dependencies so
the bare paths in that code resolve exactly as before, and its winapi features
are spelled out rather than left to feature unification.

Two call sites outside Rust and Cargo had to follow the move: the Android
protobuf source dir, which still pointed at hbb_common/protos for message.proto,
and the three AGENTS.md entries that named hbb_common for options, protos and
file transfer.

`scrap`'s `drm` feature now forwards to `base/wayland_probe`. Left pointing at
hbb_common it would still have compiled, silently dropping the Wayland
socket-probe fallback, so that forward is verified by a build with and without
the feature.

`config::keys` carries a test asserting its names stay disjoint from the ones
hbb_common kept: the glob re-export and the local constants share a namespace,
and Rust prefers the local item silently, so a name added to both sides would
otherwise let client and server disagree with no diagnostic.

Verified: macOS and Linux, debug and release, `--all-targets`; the 177 key
constants diffed name-for-name and value-for-value; the generated protobuf types
compared before and after; every `#[cfg]` gate on a moved import checked against
its original; and every file that was `rustfmt`-clean before this change still
is, compared against master file by file. Windows is checked by inspection only
-- it cannot be compiled here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* one `use` per crate, and write the rule down

`fs.rs` came out of the move with two ungated `use hbb_common::` statements,
because the original single `use crate::{...}` had to give up `message_proto`
to the new crate and the rest was left in a second block. Fold it back into one.

A scan of the whole tree for the same shape finds nothing else: every other file
with more than one top-level `use base::` or `use hbb_common::` is split by a
`#[cfg]` that does not cover the whole block, or by `pub use` next to `use`.
Those are the cases that cannot merge, so AGENTS.md now states both the rule and
the exemption.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:46:42 +08:00
Alberto Xosé Méndez Taboada
e8eead5715 Add Galician language support for UI strings (#16101) 2026-09-07 13:31:19 +08:00
cui fliter
92d787b885 fix: clipboard, preserve uncompressed special format data (#16100)
Signed-off-by: cuishuang <imcusg@gmail.com>
2026-09-07 11:12:52 +08:00
bovirus
dc04b911a1 Update it.rs (#16085)
* Update it.rs

* Update it.rs

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-09-06 11:29:17 +08:00