`punch_udp_hole` resolved the rendezvous host afresh for every reply, and
since hbb_common f688d41 that is the resolver's first answer, not one a
handshake proved: with an AAAA published for a daemon that answers only on
v4, the PunchHoleSent - the WebRTC answer, the v6 address, the relay server
- went to the address the server does not listen on, and UDP loses it
without a word. The registration itself kept working over v4, so the
device looked online and could not be reached.
The mediator holds the address its registration runs on, chosen by
`start_udp` through the handshake and replaced on every rebind. The reply
goes there: no lookup, no probe, no address family the registration is
not already using. The host is resolved only where the registration's
address is not an IP, which is not the UDP mediator's case.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
`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
0eb1759..0bc8336, five 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
the per-connection UDP sockets - the controller's NAT test, the punch
reply - taking the resolver's preferred address for the rendezvous server
as `resolve_target` instead of opening and dropping a TCP connection to
learn it, a round trip per connection, while the rendezvous registration's
`new_udp_for` and `rebind_udp_for` keep the handshake-proven address they
live on (f688d41, 0bc8336).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
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
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>
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>
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>