The descriptor now carries FD_FILESIZE only when the size was actually read,
which leaves both size fields at zero for an entry where it was not. The
non-stream FILECONTENTS_SIZE handler passed those fields straight back, so a
receiver asking about such an entry would be told it is empty and would write a
zero-length file without any error. The previous code returned whatever
GetFileSize() had left behind, which for a failure is INVALID_FILE_SIZE -- also
wrong, but wrong loudly.
Answer only when the descriptor has a size, and fail the request otherwise. The
receiver then fails the paste instead of completing it with an empty file, which
is the same class of outcome the old code produced for that entry.
The IStream_Stat path is unchanged; it already answers from the stream.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
Pasting files from a Windows peer got slower with the number of files and
then stopped working, while the total size made no difference: one 32 MB
file pasted at once, 45 small files took seconds, and 50 small files
totalling 8.3 MB left the shell spinning and copied nothing.
The sender never set FD_FILESIZE, although it had already read the size and
filled nFileSizeLow/nFileSizeHigh. Without that flag the receiver cannot
trust those fields, so CliprdrStream_New() asks for the size of each file
with its own FILECONTENTS_SIZE request and blocks on the reply for up to
CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS. Those streams are all built up front,
in the loop that answers the shell's request for the file group descriptor,
so the round trips run one after another inside IDataObject::GetData() and
the shell waits for every one of them before the paste can begin. The cost
is therefore per file, not per byte, which is what the reports describe.
The size is now declared, and only when it is known: GetFileSizeEx() replaces
GetFileSize(), whose INVALID_FILE_SIZE return cannot be told from a genuine
4GB-1 file without GetLastError(), and directories are skipped because the
handle opened with FILE_FLAG_BACKUP_SEMANTICS above is not a file handle.
A regular file whose size cannot be read is rejected instead of publishing an
ambiguous zero size. This is required because the Unix receiver currently
consumes the descriptor size fields regardless of FD_FILESIZE for
compatibility with older Windows senders.
Upstream FreeRDP, which this file comes from, sets FD_FILESIZE here. It has
been commented out in our copy since the file was first added in 6672087f7,
with no recorded reason; the "for compatibility" note above it was written
later, in 55005f812, about code that already looked this way. The Unix
receiver carries the other half of the same workaround in filetype.rs, where
the size is trusted whether or not the flag is set, explicitly "for
compatibility with Windows".
This is a sender-side fix: the paste gets faster once the machine the files
come from runs it, whichever version does the pasting.
Fixes#16238
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
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
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>
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>
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.
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>
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
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>
Fixes#16182
Use WTSClientProtocolType to identify RDP sessions with nonstandard
names during session selection and enumeration.
Signed-off-by: fufesou <linlong1266@gmail.com>
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.
* 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
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
* 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>
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
* 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>
* 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>