Commit Graph

11452 Commits

Author SHA1 Message Date
rustdesk
12d652750b 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. 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-17 12:11:48 +08:00
fufesou
fd0fe592eb fix(rdev): macos, numpad flag, clear on text keys (#16253)
may fix #16227

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-17 12:04:32 +08:00
rustdesk
bedf64c8d3 bump hbb_common: cap the message an unauthenticated peer can make us hold
Carries `Stream::set_max_packet_length` to all three transports, so a connection
can hold its peer to a small message until it has authenticated: TCP had the knob,
WebRTC's reassembly ceiling becomes a per-stream bound, and WebSocket reaches
tungstenite's config through a fork of v0.26.2 that exposes `set_config`, which
upstream still does not at 0.30.0.

Nothing calls it yet, so this changes no behaviour; the hook lands separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-17 12:00:37 +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
rustdesk
4f56d6957a pin the webrtc crate to the fork that exposes max_binding_requests
The hbb_common bump before this one calls SettingEngine::set_ice_max_binding_requests(),
which the 0.13.0 release on crates.io does not have: only webrtc-util and webrtc-sctp
were patched to the fork, so the webrtc crate itself still came from the registry and
the build stopped at that call.

The three patches now point at the same fork revision, one commit past the one they
were on, which adds the setter. The webrtc crate depends on its siblings by path, so
patching it moves the rest of that workspace to the fork as well; the fork is upstream
v0.13.0 with changes to sctp and to this setter only, so those crates carry the same
code they did from the registry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-16 14:18:03 +08:00
rustdesk
f312b57909 bump hbb_common: cap the answerer's ICE binding requests at 74
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-16 13:28:32 +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
vipe3
8154c026a4 fix(android): default to entire screen for capture (#16163)
Use MediaProjectionConfig on Android 14+ to remove the app-versus-screen choice while retaining the legacy fallback.
2026-09-16 12:01:26 +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
82b6fc2f3b https://github.com/rustdesk/rustdesk/pull/16199/ 2026-09-14 14:38:55 +08:00
rustdesk
29772f6595 fix https://github.com/rustdesk/rustdesk/issues/16208 2026-09-14 14:06:03 +08:00
Kauan Kelvin
d0ee56d349 fastlane: add it-IT Android metadata (#16162)
Signed-off-by: Kauan Kelvin <kelvinkauan722@gmail.com>
2026-09-14 12:44:31 +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
21pages
34c340be4e Upgrade FFmpeg to 7.1.1 to fix HEVC WPP deadlocks (#16169)
FFmpeg 7.1 can deadlock during software HEVC decoding with WPP slice
threading, as reproduced on Linux and macOS. Version 7.1.1 includes the
upstream progress2 fix (79c47dfd25f101b6842bbec8c6ffef8d5077c3ae).

Update the overlay version and archive checksum, reset the port revision,
and document the fix. Existing FFmpeg patches and build options are
unchanged, and decoding can retain its existing thread-count policy.

Validation on macOS arm64:
- Built the overlay successfully with all 23 existing patches.
- HEVC four-thread replay: 1,000 rounds / 71,000 frames without a stall;
  resolution changes: 6,816 frames; H.264 replay: 4,100 frames.
- VideoToolbox H.264/HEVC encoding with software and hardware decoding:
  all six cases matched the FFmpeg 7.1 baseline.
- git diff --check and manifest/archive checksum validation passed.
2026-09-14 10:43:59 +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
Elyor
8b716c7046 flutter: fix End connetion typo (#16202)
Co-authored-by: Elyor1977 <elyor77q@gmail.com>
2026-09-14 10:23:33 +08:00
Marc Frank
70d45051f6 update base image in Dockerfile (#16176)
Debian Bullseye has reached end‑of‑life and is no longer supported.

Signed-off-by: Marc Frank <78813606+D-MarcFrank@users.noreply.github.com>
2026-09-14 09:36:30 +08:00
fufesou
bf1ebe5be2 Ci/native arm64 msbuild (#16170)
* ci: use native ARM64 MSBuild for MSI packaging

* ci: clarify MSBuild host and MSI target architectures
2026-09-12 14:31:16 +08:00
rustdesk
3c7c13d79d timeout of webrtc fallback to delay 2026-09-12 13:52:11 +08:00
fufesou
e54f21e10c Fix/ci (#16168)
* fix(ci): allow native x86 Rust toolchain on Windows

* docs(ci): explain the rustup 1.29.1 host check

* docs(ci): explain the unverified x64 cross-compilation alternative
2026-09-12 11:33:49 +08:00
21pages
e82dd12350 Lower QoS sample logging to trace (#16161) 2026-09-11 18:24:34 +08:00
rustdesk
d5311574be fix(flutter): show the quality monitor's Transport row on the web only
The row was added for the web client, which has no session tab to name
the transport on, but nothing gated it: a desktop session over WebRTC
showed it too, duplicating the tab tooltip's "(WebRTC)". The getter now
answers only on the web, as its own comment intended.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcJZZeJ3Nqb2MHxXuUadkb
2026-09-11 15:04:31 +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
changshenhan
91c9fccbb0 chore(deps): security bumps in Cargo.lock (RUSTSEC-2026 fixes) (#16143)
Co-authored-by: changshenhan <217217832+changshenhan@users.noreply.github.com>
2026-09-10 17:06:24 +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
rustdesk
5cfe136fb0 fix mac sign 2026-09-10 12:08:47 +08:00
Maison da Silva
14a5ed45d9 Revise full description for Android app pt-BR (#16140)
Updated documentation links and improved text clarity. pt-BR
2026-09-10 10:56:34 +08:00
Maison da Silva
435fe24a81 Fix formatting and punctuation in full_description.txt (#16138)
Fix formatting and punctuation in full_description.txt
2026-09-10 10:36:07 +08:00
Kauan Kelvin
3ffee7c1ff fastlane: add pt-BR Android metadata (#16135)
* fastlane: add pt-BR Android metadata

Signed-off-by: Kauan Kelvin <kelvinkauan722@gmail.com>

* Update fastlane/metadata/android/pt-BR/full_description.txt

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

---------

Signed-off-by: Kauan Kelvin <kelvinkauan722@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-09-10 10:33:54 +08:00
YannAntunes
97190f715b fastlane: add es-ES Android metadata (#16136)
Signed-off-by: Yann Antunes <yannantuneslopes123@gmail.com>
2026-09-10 09:58:31 +08:00
rustdesk
aa232a9dfa chore(flutter): pin our own git plugins instead of tracking their HEAD
dash_chat_2, window_manager and desktop_multi_window named only a url, so
the lock recorded `ref: HEAD` for them. That holds while the lock is used
verbatim, but any re-resolution -- an unrelated pubspec.yaml edit, a lock
conflict resolved by regenerating -- re-reads HEAD and silently moves the
plugin to whatever the fork's tip is that day. All three forks are ours and
get pushed to, which is how window_manager and desktop_multi_window drifted
across five lock bumps since July with no pubspec.yaml change to show for it.

Each ref is the resolved-ref already in the lock, and all three still match
their fork's HEAD, so nothing resolves differently today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-10 09:14: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