mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 23:51:04 +03:00
master
11387 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
942810d432 |
bump hbb_common: main after the webrtc merge
rustdesk/hbb_common 3d6fb2c, the merge of PR #579; the tree is the one 470612b already pointed at, now reachable from main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns |
||
|
|
ae6af2de43 |
Webrtc (#15684)
* feat: add rendezvous WebRTC signaling fields * feat: route WebRTC ICE on controlled side * feat: race WebRTC as a direct transport enhancement * fix: route WebRTC ICE through rendezvous paths * feat: WebRTC transport racing, DTLS identity binding, and pc-leak fixes - prefer-P2P racing (race_transports_prefer_webrtc) across punch and RelayResponse; ICE bridge with 400ms candidate resend - controlled-side answerer and ICE routing; sign local DTLS fingerprint into SignedId, controller verifies the binding fail-closed - fix pc leaks: close_webrtc() on insecure-decline paths (io_loop, port_forward); compute direct before disarming the offerer guard - point hbb_common to the WebRTC data-plane commit 9f5a296 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: preserve WebRTC transport preference * feat: decouple WebRTC from UDP punch, route controlled signaling over TCP - the WebRTC offer now rides any punch request; only an offer-less request may close and reuse the rendezvous socket for TCP punching (request_allows_tcp_punch replaces the udp_port-based invariant), with a separate offer-less request racing as the TCP fallback - WebSocket mode no longer disables WebRTC — ws only tunnels the signaling/relay legs while ICE stays the only P2P path there; SOCKS proxy still disables it (ICE would bypass the proxy and leak the real IP) - controlled side: WebRTC-only punch replies and trickled ICE candidates go over dedicated TCP connections to the rendezvous server instead of the UDP mediator channel, for ws/TCP-only hbbs deployments; drop the now-redundant rz_sender plumbing and the 400ms candidate re-send on that leg - guard is_udp handling against responses to requests that advertised no udp_port; skip the IPv6 socket bind under force-relay - test_udp_uat: drop the STUN port race — the punch port must come from the rendezvous server's TestNatResponse observing this socket's mapping, a STUN probe from another socket can advertise an unreachable port - bump hbb_common (webrtc 0.13 MSRV pin rationale + upgrade checklist docs) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: KCP/UDP resilience to ICMP resets; optional KCP congestion control - treat ICMP-driven UDP socket errors (WSAECONNRESET 10054 on Windows, ECONNREFUSED on Linux) as packet loss in punch_udp and the KCP pump instead of tearing the session down; KCP retransmits through them and a truly dead link is still reaped by the pong/app-level timeouts - resolve STUN hostnames via tokio::net::lookup_host so DNS never blocks a runtime worker; fix the inverted non-IPv4 error message - add enable-kcp-congestion-control option (default on): switch the turbo profile to nc=0 so brief loss on constrained links no longer spirals into stalls; sender-side only, no wire negotiation - pin kcp-sys to the rustdesk-patches branch: upstream main lost the RustDesk patches on the EasyTier sync, and this branch also wires set_kcp_config_factory into connection setup, making the option effective Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: carry switch_code through WebRTC relay fallbacks after rebase The rebase onto master (switch-code feature) added an 8th request_relay parameter; pass the interface's switch code from both WebRTC->relay fallback paths so a role-swap session survives the fallback. Also drop a duplicate bindgen 0.72.1 entry the Cargo.lock merge produced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * fix: don't let the preferred branch's own relay preempt a direct fallback race_transports_prefer_webrtc committed any success from its first argument outright, on the assumption that it is the WebRTC connect. It is not: the call site passes a whole punch attempt, which internally falls back to request_relay when its direct transports fail. That relay was therefore committed instantly while the offer-less fallback's TCP punch was still in flight — inverting the preference this function exists to enforce, since the is_p2p predicate the caller already supplies was applied only to the `others` branch. Apply it to both branches: a direct result from either side still commits immediately, and a relayed result from either side is held for the window so the other side can land something direct. Also commit a held connection when the surviving branch errors, which the previous code only did on the first branch's failure path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * fix: evict the oldest pending ICE candidate, not the newest Candidates arrive in gathering order — host, then srflx, then relay — so a full buffer was discarding exactly the ones that traverse NAT while keeping host ones that only work on a shared LAN. Evict from the front instead. Also document why the controller's ICE bridge must not reconnect on error, in contrast to the controlled side's per-candidate retry: its socket address is the return route itself (mangled into PunchHole.socket_addr, echoed back in IceCandidate.socket_addr, resolved through tcp_punch), so a reconnect would arrive from an address no route points at, and the server drops the old entry when the connection closes. Once it dies both directions are dead, and abandoning WebRTC is the correct response rather than retrying. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * fix: bound log volume on sites whose rate a peer or retry loop controls Debug output goes to the log file, so a site that fires per received message or per retry lets someone else decide how much a machine writes to disk. The WebRTC work added the first such sites. - KCP io loop: absorbing ICMP errors as packet loss made a broken socket write ~100 lines a second for the 60s until the pong timeout reaps it. Log by run instead: one line when a run starts, one per ~5s while it persists so a stuck socket stays visible, and one on recovery with the total. - punch_udp: the recv error retries every 10ms for up to MAX_TIME, so one line per occurrence wrote thousands per punch. Log the first, report the count in the timeout message. - ICE candidate paths (client, mediator): the peer sets the candidate rate and the rendezvous route carrying them needs no prior punch, so throttle to one line a minute each with the suppressed count. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * fix: the KCP io throttle reset itself every cycle, so it never throttled The send and recv arms shared one counter, and an ICMP error on a connected socket is reported once and then cleared — so the steady state is an alternation: the send succeeds and clears the counter, the next recv reports the error and finds the counter at 1, and logs. Every error still wrote a line, at the ~100/s the previous commit set out to stop, while the persistent-failure and recovery branches were unreachable. Use one LogThrottle per direction instead of a hand-rolled counter. That removes the shared state the bug lived in, drops a third throttling mechanism in favour of the one already added, and leaves the surrounding `if let Err` untouched rather than reshaping it into a match. Also fix test_udp_uat's socket-error arm, the untreated twin of the punch_udp site: it had no backoff at all, so a persistent error re-armed recv immediately and spun the loop at CPU speed, one warn line per iteration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * bump kcp-sys: 14 review fixes on rustdesk-patches (6e44b93 -> fa51c15) Picks up the handshake-recovery work plus the review round on top of it: ABBA deadlock between the endpoint's two DashMaps, graceful-close tail truncation, mid-stream hole on ikcp_send failure, FIN retransmission for lost-FIN half-open hangs, SYN-ACK budget burned on dropped packets, spurious ConnectTimeout after a completed handshake, accept-backlog overflow stranding conns, aliasing UB in the output callback, and the log-facade/throttling cleanup (per-packet sites no longer reach the debug-level file logger, peer-rate warns throttled). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * ws: decouple ICE policy from force_relay — full-ICE WebRTC over WebSocket WebSocket support folds into force_relay because a ws tunnel kills classic TCP/UDP punching — but that conflated transport necessity with relay policy, and the WebRTC decisions keyed off the merged flag: a ws client built no offerer at all without TURN, and only a Relay-only-ICE one with it. ws deployments could never reach a direct WebRTC connection, which is exactly the path they are supposed to live on. Split the flag. LoginConfigHandler now tracks policy_relay (the force-always-relay option, an explicit relay request — /r ids and retry-via-relay included — and proxy) separately; force_relay stays policy_relay || use_ws() and keeps governing the classic paths, so non-ws behavior is unchanged everywhere: - the offerer's existence and ICE policy follow policy_relay: under pure ws the offer gathers every candidate type and may go direct; under relay-by-policy it stays Relay-only ICE, TURN-gated, exactly as before; - the RelayResponse race applies the prefer-P2P window under ws (a direct ICE path is worth delaying an already-ready relay for) while policy relay keeps first-success semantics; - the request carries webrtc_all_ice (hbb_common 64b54ab) so the controlled side knows the offer is full-ICE: it answers with full ICE and no TURN requirement, while offers without the bit keep today's relay-only answer path on every version-skew combination. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * bump kcp-sys: 7 review fixes on rustdesk-patches (fa51c15 -> 023a006) Reverts the connect/accept/add_conn changes that regressed concurrent connects (the state_map guard held across add_conn is load-bearing), states the single-conn contract on KcpEndpoint so shared-endpoint behaviour stops consuming review effort, pins the two invariants that keep truncated input from aborting under panic='abort', and fixes three findings from external review: sendwnd() echoing raw config instead of KCP's effective window (a non-positive factory value stalled sending forever), the passive closer's lost final FIN delaying EOF by up to ~20s, and the doubled window overflowing for extreme factory values. Lock-only change: cargo update -p kcp-sys also re-picked libloading's windows-targets between two versions already present in the lock; that was reverted to keep this commit to the one line it is about. cargo metadata --locked passes on the result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ws: read the all-ICE declaration from the offer envelope, drop the proto field Companion to hbb_common 68d2729: the full-ICE declaration now lives as an `ice_policy: "all"` key inside the webrtc:// envelope, so the request assembly no longer sets webrtc_all_ice and the controlled side asks the envelope (endpoint_declares_all_ice) instead of a PunchHole field. The rendezvous server carries the offer opaquely — no forwarding to keep in sync. Skew behavior is unchanged: an unmarked or unparseable envelope reads as the old Relay-only semantics. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * add enable-webrtc option; gate test_ipv6 under forced relay OPTION_ENABLE_WEBRTC (hbb_common 48c2d4d) follows the udp/ipv6 punch options end to end: default on against the public server, off against private ones, same settings UI placement on desktop and mobile, and the same bool2option local-option handling. Gates: - controller: should_create_webrtc_offerer checks it first — no pc, no STUN/TURN gathering, no offer in the request; - controlled: unlike the udp/ipv6 legs, which deliberately follow the request, answering builds a pc that gathers ICE from this host, so the answerer honors this machine's own switch too. Translations for "Enable WebRTC P2P connection" added to all 50 lang files next to the IPv6 entry (IPv6 and WebRTC are invariant terms in the same grammatical slot in every one of them). Also stop probing v6 reachability (test_ipv6) under any forced relay: the v6 punch socket is never bound there, so the probe was wasted work on every ws/proxy/relay connection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * kcp: client-side integration tests over real loopback sockets kcp-sys has been through two review rounds of behavioral fixes; the client wrapper (kcp_io pumps, connect/accept deadlines, framed-stream adaptation, guard lifetimes) had no tests pinning what rustdesk actually relies on. Four now do, each through real 127.0.0.1 UDP sockets and the BytesCodec framing sessions use: - handshake + bidirectional framed roundtrip + graceful close: the peer observes end-of-stream instead of hanging (guard outlives the framed stream so the FIN goes out); - a writer that queues 50 frames and closes immediately loses none of them - the client-side pin for the close-tail-drain semantics; - socket errors after the peer vanishes are treated as loss: writes keep succeeding, nothing tears down (ICMP is advisory on connected UDP); - the connect deadline holds when nothing answers. Mutation-checked: dropping inbound forwarding in kcp_io reddens exactly the three tests that need the pump, and the timeout test alone stays green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * ipc/auth: replace the local throttle with the shared throttled_log! auth.rs predated hbb_common's LogThrottle and grew its own equivalent: same shape (last_log_at + suppressed), same 5s interval, plus a helper and three OnceLock<Mutex<..>> statics. It also counted the other way - excluding the event being reported - so each of the three sites carried two near-identical log::warn! arms to avoid printing "suppressed 0". The shared macro covers all of it: one static per call site declared by the expansion, and the multiplicity suffix appears only when there is one, which is what those duplicated arms were for. 102 lines out, 27 in. Behavior difference, deliberate: a burst now reads "(x47)" - the total including this line - instead of "(suppressed 46 similar events)". One number, no arithmetic, and one convention across the codebase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * kcp: make the congestion-control profile opt-in, not the default The branch had flipped KCP to nc=0 (built-in congestion window) for every session. That is a transport-behavior change for all users made on reasoning alone, and the reasoning does not decide it: which profile wins depends on why packets are being lost. nc=1 - what RustDesk has always shipped - never shrinks the send window, so on a genuinely congested uplink it deepens the loss it is reacting to. But nc=0's backoff is blunt: a fast retransmit halves the window while an RTO sets cwnd = 1 outright (ikcp.c) and recovery slow-starts from one packet, so on a link with random loss and no congestion - Wi-Fi interference, a long-haul path - it reads loss as congestion and can stall an interactive stream for seconds. That failure mode is also the more visible one to a remote-desktop user. No benchmark settles this either: a loopback A/B has no bottleneck queue, hence no congestion to control, and would flatter nc=1 by construction. Deciding it needs a shaped link or field data. So keep the profile users already run and let the other one be asked for ("enable-kcp-congestion-control" = "Y"). Flipping the default later is a one-line change once there is evidence. kcp-sys keeps its own test covering the nc=0 path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * android: define getifaddrs/freeifaddrs for the api-21 sysroot Turning on hbb_common's "webrtc" feature pulls webrtc-util into the android link, and its ifaces() -- reached from vnet::Net::new() on every ICE gather -- calls getifaddrs(). bionic exports getifaddrs/freeifaddrs only from API 24, while flutter/ndk_*.sh builds against --platform 21, so every abi failed to link on the undefined symbols. Raising the platform to 24 would have to drag minSdkVersion 22 with it and turn the link error into a load-time one on Android 5.1/6.0, so define the two symbols instead, using the RTM_GETLINK + RTM_GETADDR netlink dump bionic itself uses. The definition also shadows bionic's on API >= 24 rather than delegating to it, so the path that ships is the path every test device runs. Checked against synthesised netlink dumps on the host -- link/address parsing, prefix masks, point-to-point, ipv6 scope ids, malformed and truncated messages -- under UBSan and byte-exact guard malloc, with a deliberately unsigned remainder as the negative control. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix three ways ws + WebRTC could not work in practice Review of #15684 and hbb_common#579. Each of these left the code reading correct while the feature did not function. - The RelayResponse race classified P2P with `result.2 == "IPv6"`, but that site's futures are only ever the relay ("Relay"/"WebSocket") and the WebRTC branch's own "WebRTC" — so the predicate was constantly false. When the relay landed first the result was still right (the webrtc arm's `others_fut.is_none()` fallback), but when WebRTC connected FIRST it was parked as if it were a relay and the relay was committed on arrival, discarding a live direct connection. That is the LAN case: the better the network, the worse the outcome. Classify by what the label means, via is_direct_transport, and test both orderings — only the relay-first one was covered. - handle_peer_info wrote "force-always-relay=Y" into the peer's saved config whenever force_relay was set, which now includes the WebSocket transport. One ws session therefore turned the peer into a permanent relay-by-policy peer, and relay-by-policy means Relay-only ICE, so WebRTC could never go direct to it again — the flagship path worked exactly once. Persist policy_relay, which is the user's choice; the transport is a property of this client, not of the peer. - The answerer gated on this machine's enable-webrtc option, but that is LocalConfig: the UI process writes it and never syncs it over IPC, while handle_punch_hole runs in the server process, which on Windows resolves LocalConfig under a different profile and reads the private-server default of "N". The gate refused to answer in exactly the self-hosted deployments the transport exists for. Drop it: the answerer follows the request, like the udp/ipv6 legs, and the option still gates the feature where it can — an offer only exists because some controller had it enabled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * webrtc: close without an await point; do not report an unknown path as direct - close_webrtc is no longer async (hbb_common 88f965f), so the ten call sites in port_forward and io_loop - all inside select! arms or futures the UI can abandon - can no longer be cancelled mid-teardown, which left the pc unclosable and its session entry stranded. Client's own spawn_close_webrtc went with it: the runtime-teardown guard it existed for now lives in close_detached, so both Drop paths share one implementation. - webrtc_relayed() returns None when no candidate pair is selected or the pc closed under a concurrent teardown, and both call sites read that as "not relayed", i.e. direct. A TURN-relayed session could therefore be shown to the user as peer-to-peer. Claiming a direct path needs evidence of one, so an unknown answer now counts as relayed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * scrap/benchmark: give the Duration divisor an explicit u32 The webrtc feature pulls time 0.3 into scrap's graph (hbb_common -> webrtc -> webrtc-dtls -> der-parser -> asn1-rs), and that crate carries an `impl Div<time::Duration> for std::time::Duration`. Orphan rules allow it because the RHS is its own type, and trait impls are visible across the whole dependency graph without a use, so std::time::Duration now has two Div candidates. `yuv_count as _` casts to a plain inference variable, which both candidates fit, so it stops resolving: error[E0282]: type annotations needed --> libs/scrap/examples/benchmark.rs:146:33 Only two of the four sites are reported - rustc emits one E0282 per function body - so all four are annotated. The already-explicit `as u32` at the hwcodec site and `start.elapsed() / cnt` are unaffected, the latter because an integer literal's variable can only unify with an integral type and rules the time impl out on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * webrtc: judge the race by the resolved path, not the label; bound the ICE queue Third review round. Two of these are regressions from the previous one. - The RelayResponse race predicate was `is_direct_transport(result.2)`, which answers true for the label "WebRTC" - but WebRTC is only a direct path when ICE nominated a non-TURN pair. A TURN-relayed WebRTC result therefore committed instantly and cancelled the IPv6 attempt racing beside it, which is the same inversion the previous fix removed in the other direction. (That fix was also argued from a wrong premise: the site does carry an IPv6 future, pushed ~50 lines earlier than the relay one.) Each future now resolves whether its path is direct and the predicate reads that bool, matching the outer race, and the downstream recomputation goes away. - policy_relay still folded in Config::is_proxy(), and that is what gets persisted into the peer's config as force-always-relay - so one session through a proxy pinned the peer to relay forever and disabled WebRTC for it, exactly the latch the previous round fixed for WebSocket. Split out peer_relay: the saved option or an explicit request for THIS peer, and the only part written back. - The controlled side buffered remote ICE candidates in an unbounded channel while the controller caps the same buffer at 64, and draining one costs a JSON parse plus the ICE agent's lock. Whoever can reach a session's route could grow it without limit inside the long-lived service process. Bounded, with the overflow logged through the existing throttle. - That route was also removed by key alone when an answerer finished, so a punch retry that built a fresh answerer under the same fingerprint had its live sender deleted by the previous one's cleanup - after which it received no candidates at all. Evict only our own sender, the way the session cache already guards the analogous case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * webrtc: trim the comments to AGENTS.md length; drop is_direct_transport 386 added comment lines down to 287 across client, mediator, kcp_stream and common. Same rule as hbb_common 3d64e43: out go past-bug narration, rejected alternatives, measurements and restatements of the code; the non-derivable why stays. is_direct_transport goes with them. Judging the race by a transport label was replaced by the resolved direct flag, leaving it used only by its own test — and, having been inserted between the doc comment and race_transports_prefer_webrtc, it had also taken that function's contract with it. Removing it reattaches the doc where it belongs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * webrtc: fix race edge cases that discard or mislabel a direct connection Three correctness fixes in the transport race, plus three convention cleanups. - race_transports_prefer_webrtc committed a relayed result while a direct attempt was still in flight: the others arm returned on webrtc_fut.is_none() even with an unfinished direct future, and the WebRTC-error arm returned a held relay without checking others_fut. A relay is now committed only when nothing direct can still arrive (or the window expires); a parked relay is also preferred over composing an error when both sides fail. Three regression tests, mutation-checked. - connect()'s plain select_ok let a TURN-relayed WebRTC win as "first success", dropping still-racing UDP/IPv6 direct attempts and reporting the relayed pair as direct. It now runs through the same prefer-P2P race with each attempt carrying whether its path is direct, and the WebRTC future resolves is_relayed() so a TURN win is held behind direct attempts, not committed as one. - The RelayResponse path kept direct == true when a WebRTC win's DTLS handshake failed and it fell back to relay, so the relay was reported P2P. Clear the flag with the transport switch. - Trim the OffererGuard doc to the three-line max; move the new enable-webrtc localization key to the end of every lang list; the KCP option constant moved to hbb_common config::keys (0f663aa). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ * bump hbb_common: WebRTC peer connections own their I/O runtime Closing the controlling window left the controlled side waiting out ICE decay — ~25-30s in the peer's log, its disconnected/failed ladder running to completion — where TCP delivers a FIN at once. The session end closed the pc by spawning onto io_loop's own `#[tokio::main(flavor = "current_thread")]` runtime, which is dropped the moment io_loop returns, and nothing after that call yields: the task was never polled even once, so no DTLS close_notify ever left. Every attempt to fix that on the caller's side failed the same way, because the mismatch was never about where the close ran: a pc's UDP sockets register with the reactor, and its ICE/DTLS/SCTP pumps spawn on the runtime, that is current while it is built — so a pc created by a session outlives the only runtime that can drive its I/O, and a close driven anywhere else completes without reaching the wire. The bump homes them where they can outlive any caller: WebRTCStream builds on a process-lifetime runtime and every detached close runs there as its own never-cancelled task. io_loop keeps its plain close_webrtc() calls and only documents why nothing here may spawn or await the teardown on the dying session runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HV43uh1ztv6Wm5qi3Y1ne * fix: give the UDP NAT test a real window when the TCP clock is faked The punch request carries udp_port only if the rendezvous server's TestNatResponse has arrived, and the wait for it was bounded by rtt / 2 — half the TCP connect time, on the assumption that TCP and UDP round trips are comparable and the test, started earlier, has already answered. A transparent TCP proxy breaks that assumption: a TUN-mode VPN on the host, or a redirect-mode proxy on the LAN gateway serving every device behind it, completes the handshake locally in ~3ms while the real UDP round trip is hundreds of ms. Log-confirmed against 5.161.65.208: ping 341ms, TCP connect 3.7ms, connect to a dead port there "succeeds" just as fast. The window collapsed to ~1.5ms, udp_port stayed 0 on every attempt, and UDP punch was never even requested — although UDP itself passes such gateways untouched. So use the TCP clock only when it is believable: below a plausible WAN round trip it says nothing about the UDP path, and a flat ceiling applies instead. The loop still exits the moment the port arrives, so a genuinely nearby server pays nothing and only a UDP-dead network waits out the ceiling — on the udp-carrying round alone, while the parallel pure-TCP round is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HV43uh1ztv6Wm5qi3Y1ne * feat: make the TCP punch a user option, with TCP as the backstop TCP punching was the one direct transport without a switch, while UDP, IPv6 and WebRTC each had one. Add "Enable TCP hole punching" above the UDP toggle on both desktop and mobile, default on — including on self-hosted servers, since unlike the other three (whose default-off there guards against an hbbs that cannot forward their fields) TCP punching has always been supported by every server. Turning all four off would leave no way to punch at all, so TCP runs regardless in that case. That backstop keys off the switches alone: a transport that is enabled but fails to materialize — no public v6 address, no NAT port, a failed offerer — is already covered by the relay fallback for a round that ends up with no usable direct transport. With the TCP punch off, the fallback request is skipped too: it exists only to carry that punch, and would otherwise reach connect() with nothing to try and merely open a second relay. Known cost, unchanged behavior for the peer: the request carries no field for this choice, so a peer that receives one with no udp_port and no offer still punches a TCP hole and listens for a connection the controller will not make. Representing the transport choice on the wire needs a proto field and the server forwarding it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HV43uh1ztv6Wm5qi3Y1ne * bump hbb_common: name the punch by every transport it carries `get_local_endpoint_trickle` became `local_endpoint() -> &str`, which cannot fail, so both call sites lose an unreachable error arm — the mediator's closed a pc against a failure that no longer exists. `punch_type` named one transport, and picked it off `allow_tcp_punch`. A round carries several at once — a NAT port and a v6 address and an offer — and since the TCP punch became a switch it can carry none, so one name had to misreport both: the logs of the round that broke WebRTC read "#1 UDP punch attempt" while the request also carried the v6 address and the offer that was actually failing, and a round with nothing to punch with was labelled "WebRTC". List them instead — "UDP+IPv6+WebRTC" — and call the empty round "Relay", which is what it can still end as and what `typ` prints for it. The offer is moved into the request rather than cloned into it; that was its last use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UzcMTdYTEv2QbMHcTSUy3 * bump hbb_common: drop link-local IPv6 from ICE gathering Also pin webrtc-util to a fork of 0.11.0 carrying a Windows IPv6 enumeration fix. `ifaces` reads the adapter list's on-wire IPv6 bytes as host-order `[u16; 8]`, so on a little-endian host every group comes out byte-swapped and unbindable: a peer's real 240e:369:9606:4600:f52a:7a8d:2530:4de0 is enumerated as e24:6903:696:46:2af5:8d7a:3025:e04d, ::1 as ::100 and fe80:: as 80fe::. Each fails to bind with WSAEADDRNOTAVAIL, so ICE gathers no IPv6 host candidate at all on Windows - where a globally routable address is the one NAT-free path a CGNAT'd peer has. Never reported upstream; the unix twin of the same bug was fixed in webrtc-rs#475 (2023). Fork: rustdesk-org/webrtc, branch rustdesk-patches, tag webrtc-util-0.11.0-win-ipv6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UzcMTdYTEv2QbMHcTSUy3 * bump hbb_common: name the family a WebRTC session runs over `stream_type` reaches the UI as the transport that won the race, and every other transport already carries the family in that label - the v6 punch reports `IPv6`. WebRTC does not: one label covers both families, and it is the one path whose real remote address can differ from the rendezvous-observed one the session is identified by. Refine it at the hand-off to the UI rather than at the source: five sites in client.rs compare `typ == "WebRTC"`, so widening the label there would silently move control flow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UzcMTdYTEv2QbMHcTSUy3 * bump hbb_common: one STUN list, and drop the dead IPv4 half `test_ipv6` kept its own hand-written copy of the STUN servers. It now reads `WebRTCStream::stun_servers()`, so an operator who points OPTION_ICE_SERVERS at their own server gets it on both paths instead of one. `test_bind_ipv6` sends nothing - `connect` only makes the kernel pick a route and a source address - so the whole cost is DNS. It races the lookups rather than betting this host's IPv6 support on whether the first entry happens to publish a AAAA where the user resolves from; google's does not, from a Chinese resolver, and it was the entry being bet on. `stun_ipv4_test`, `STUNS_V4` and `test_nat_ipv4` have had no callers since the punch stopped taking its port from a second socket, and go. `get_kcp_cc_enabled` reads the renamed option through `option2bool`, like every other one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UzcMTdYTEv2QbMHcTSUy3 * webrtc: take dcsctp's retransmission timings and IPv6-safe MTU webrtc-sctp ships RFC 4960's RTO.Initial/RTO.Min (3000/1000), TCP's values for arbitrary public paths. On this workload they set the recovery time outright: a request/response exchange keeps one chunk in flight, so no later SACK ever raises miss_indicator to the 3 that arms fast retransmit, and the T3 floor is the only way back. A single loss during a handshake or a first keyframe therefore costs whole seconds. The fork now carries dcsctp's numbers instead - the SCTP implementation Google wrote to replace usrsctp for Chrome's WebRTC data channels, the same realtime workload: rto_initial 500, rto_min 400, a 220ms floor under the RTT variance, and mtu 1191. INITIAL_MTU 1228 plus DTLS/UDP/IPv6 overhead is 1313, past the 1280 minimum, so every full-size chunk fragmented on an IPv6 path. Both patch entries move to the new branch, which also carries the Windows IPv6 byte-swap fix, so one rev matches the whole webrtc 0.13 stack. * udp: make the punch prove itself, and keep the listener answering punch_udp sent a zero-length datagram and called the hole open on whatever arrived next. The rendezvous NAT test's own leftover replies satisfy that immediately - connect() does not flush the receive queue - so the retry loop never ran and success meant nothing. The dead socket then cost KCP its full timeout to rediscover, which is how a failed punch came to take 18 seconds. Probes now carry a magic and a 64-bit transaction id, and both ends answer each other's probes, so returning is a fact: a reply echoing our own id is the one thing that proves the pair carries traffic both ways. With failure now distinguishable from 'not yet', the window drops from 20s to 3s. Two asymmetries fall out of that: Only the connector stops on its own acknowledgement, because only it has something to send next. An acknowledgement proves our probe came back, not that the peer's probe was answered - and after punch_udp returns nothing answers probes any more, since KCP's io loop drops anything shorter than its header. A listener that stopped there would go mute while a peer whose own probe or answer was lost - the normal state of a hole still opening - kept probing an endpoint that works, until it timed out. So the listener stops on the peer's first real packet instead, and hands that packet to KcpStream::accept as its init_packet: its arrival proves the pair as well as an acknowledgement would, and KCP never retransmits its SYN. * webrtc: correct the RTT variance floor to dcsctp's scaling The earlier commit took dcsctp's min_rtt_variance = 220 as a raw floor under rttvar. dcsctp divides the option by kHeuristicVarianceAdjustment = 8.0 first, a historical accident it kept because downstream users had measured good values with it, so the intended floor is 27.5ms of variance contributing 110ms to RTO. Flooring at 220 contributed 880ms instead, which on a 50ms path left RTO within 7% of the 1000ms default this change exists to escape. The fork also now records why T1/T2 share T3's RTO manager here, unlike dcsctp's separate control timers: RTO_INITIAL is the T3 value for the first DATA chunk, since no RTT sample exists before the first SACK. * webrtc: skip the controller's ICE re-send instead of queueing it twice The controller sends every candidate twice, because the server's hop to a peer registered over UDP can lose one. The ICE agent that dedups repeats sits downstream of the answerer's queue, so the answerer paid for both copies: a slot, a JSON parse, and the ICE agent's lock, once per repeat. Remember a digest of what was queued and skip the repeat. Recorded only once queued, so a candidate a full queue refused stays repairable by the re-send. The queue's depth is unchanged. A real peer gathers well under it - four STUN servers, link-local IPv6 filtered, one component - and the drain empties it as candidates trickle in, so what this removes is the redundant work, not an overflow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * tcp: repeat the punch across the controller's dial window The single punch leaves before hbbs has told the controller where to dial, so it is never in flight at the same time as the controller's SYN: it opens our NAT, meets nothing, and a gateway that answers it with RST takes the mapping down with it, leaving the listener waiting on a hole that no longer exists. Punch again while the controller may still be dialing, and race those punches against the accept. That is two ways in where there was one: the mapping is rebuilt if a RST took it, and once the controller sits in SYN_SENT one of the punches meets its SYN and completes as a simultaneous open - which a punch sent before the controller had been told anything never could. The crossing reaches the punch rather than the listener because the two sockets share the address but only the punch matches the four-tuple, which the tests now pin down. There is no instant to aim at, and no window either. `Client::connect` sizes the controller's dial only after our PunchHoleSent, from its own rendezvous time and the direct failures it has recorded for us: CONNECT_TIMEOUT between two known-asymmetric NATs that never failed, punch_time_used times three or six otherwise, floored at a second - so a peer that failed once dials for a second or two from then on, and none of that reaches this side. The repeats therefore cover our own ceiling instead, CONNECT_TIMEOUT, which is exactly as long as the accept has always been willing to take a connection through the hole, and back off across it: dense at the start, where every window begins and the short ones end, sparse afterwards, which is `punch_udp`'s shape for the same reason. A window past that ceiling was lost before this change too, and mostly to the controller's own kernel - Windows gives a SYN up at 21s, Linux's next re-send after 15s is at 31s; a window short of it costs a few SYNs to a port already closed. No punch is cut on a per-attempt timeout; one in flight is bounded only by the shared deadline plus PUNCH_GRACE. A punch is cancel-safe only while it is still in SYN_SENT; once the controller's SYN has crossed it the socket is half way through a handshake, and cutting it there cuts the connection the controller is opening - whose `connect` has already returned, so that attempt fails outright, there being no relay fallback after a failed TCP handshake. A timer cannot tell the two states apart, and none is needed: a gateway that answers with RST fails the connect at once and the loop punches again, while one that drops the SYN in silence leaves the socket in SYN_SENT, holding the mapping open while the kernel re-sends, which any SYN of the controller's then crosses - a second punch has nothing to add. The deadline decides whether another punch starts; one in flight runs a grace past it, enough for a crossing begun just before it to complete. The last sleep is cut at the deadline rather than run out past it, so the window ends on a punch given that grace and not on a gap of up to the backoff ceiling: the controller's window opened after ours, on the PunchHoleSent hbbs relayed, so one as long as ours is still open through our tail. Only the accept races the punch, never `accept_connection`: that one does not return until the session it goes on to run has ended, so racing it would tear a live session down. Whichever arrives first is the one connection the request produces. `meta` carries the control permissions hbbs granted for this one controller, so serving the loser as well would hand them to a second peer - and nothing about a connection tells the two apart before `create_tcp_connection` has spoken to it, least of all its address: a carrier NAT shares one between subscribers, and a NAT that pools its external addresses may dial us from a different one than hbbs saw the controller through. So the address is not checked, as `accept_connection` never checked it; the handshake says who arrived, and what holds the invariant is that there is no second serve. Those permissions are a ceiling and not a grant either way: `Connection` gates every message on `authorized`, and latches the login scope of the first request it accepts, so a peer that reached the hole still arrives with nothing. The accept loops rather than taking a single connection, so that a transient accept error does not spend the window the controller still has to arrive in. libp2p's DCUtR reaches the same place by having both peers dial at one instant agreed over the relay. Nothing we send reaches the controller directly, so we cover its dial window rather than name an instant inside it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * hbb_common: bump to the webrtc branch rebased on main Picks up upstream's session-cache eviction by pc identity (#589, adopted without its unused insert-path helper), the 90-day log retention, and the wlroots output fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * webrtc: send over SCTP without a congestion window, as KCP does The same link that streams over KCP crawls over WebRTC. webrtc-sctp runs RFC 4960's AIMD: a fast retransmit halves cwnd, a T3 drops it to one MTU, and slow start only rebuilds it while data is queued behind it. Where the loss is random rather than congestion - a lossy long-haul link - the rate settles at the Mathis ceiling MSS/(RTT*sqrt(p)) however idle the link is: about 1.3 Mbps at 70ms RTT and 1% loss, 0.6 Mbps at 5%, while 1080p wants 2-5 Mbps. KCP's turbo profile (nc=1) has no congestion window at all. The fork now carries a switch that bypasses the two places gating sends on cwnd, and hbb_common turns it on for every peer connection unless `allow-webrtc-congestion-control` is set - the same opt-in KCP has in `allow-kcp-congestion-control`, for the reason at `get_kcp_cc_enabled`. Sender-side only; a browser or an older build on the other end interoperates. Measured over a simulated link (35ms one-way, random loss both ways, 12 KB frames at 30fps, 300 frames): at 1% loss the window stretches 9.9s of video to 20.7s with a mean latency of 5.5s; without it the stream stays realtime at a mean of 113ms. At 3%: 47s and 15s against 10.2s and 290ms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * webrtc: take the fork's loss recovery for sending without a congestion window rustdesk-org/webrtc 825a0a48: without a congestion window a chunk is lost once three chunks sent after its latest transmission are acked, counted in send order so retransmitted chunks are covered too, and the fast retransmit sends every lost chunk at once, as KCP nc=1 does; before, a lost retransmission waited for T3-rtx. Also fixes the delayed SACK timer never re-arming, the switch applying to established associations, T3-rtx resending one chunk when the peer's window is full, and bounds new data to 1 MiB / 1024 chunks in flight like KCP's snd_wnd. Simulated 35ms one-way, random loss both ways, 30 fps, frames later than 200ms out of 1200: 12 KB at 5% loss 996 -> 55 (KCP 61); 40 KB at 2% loss 1183 -> 20 (KCP 39). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * bump hbb_common: decode TURN userinfo, add the webrtc_echo example Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXJJGEGdgu26wgCppvUXdZ * web: show the WebRTC toggle and transport in the web UI The web client now speaks WebRTC, but the desktop settings page hides the punch options on web and the remote page opens without the session tab that carries the transport name. Let the existing "Enable WebRTC P2P connection" checkbox through on web (the other punch options stay native-only), and add a Transport row to the quality monitor for WebRTC sessions only (with "(TURN)" when ICE relayed), on every platform. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXJJGEGdgu26wgCppvUXdZ * bump hbb_common: end the ICE forwarder at gathering complete, drop the closes Drop covers hbb_common now closes the local-candidate channel when gathering completes, so the controlled side's forwarder in spawn_webrtc_answerer ends there, and its signaling connection to hbbs with it, instead of sitting on a socket hbbs closed at 90s idle for the rest of the session. It also keeps the reassembly buffer across fragmented frames. Stream closes the WebRTC peer connection on drop (hbb_common b0b624d), so the close_webrtc() calls in port_forward and io_loop that sat immediately before a return or the end of scope did nothing Drop was not about to do, while the comments beside them still said a bare drop leaked the pc. Remove both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * bump hbb_common: quiet the webrtc-rs warnings that describe the race's normal outcome Cancelling the transport that lost the race, and trickle checking before it holds a pair, are what the design does on every session that connects - and webrtc-rs reports both at warn, 90 lines of a 386-line controlled-side log, beside connections that succeeded. agent_internal and peer_connection drop to error; agent_gather keeps warn, since an unreachable STUN server is the one upstream signal that explains a session which never connected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M54JAqUK4RynudFou89hod * port_forward: restore the `?` the close removal left as a match Dropping the explicit close_webrtc() from the parse-error arm left a match that only re-spells `?`; master just reworked this function, so the branch now leaves port_forward.rs untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * l10n: the two WebRTC keys were missing from Urdu Every other lang file on the branch carries them; ur.rs was skipped when they were added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * udp: make the punch deadline absolute, so a talking peer cannot defer it `select!` rebuilds every arm each iteration, so the relative retry sleep was restarted by each datagram that arrived before it fired. The peer sets that rate, and an old-build peer's empty datagrams match no arm and loop without even the recv-error pause, so MAX_TIME went unchecked and the retransmit was starved with it. `udp_nat_connect` awaits the punch ahead of the KCP timeout and nothing above it bounds the phase, so the punch held the direct race open and the relay fallback out of reach for as long as the peer kept sending. Absolute instants for both clocks. The new test floods empty datagrams for four times the deadline: the punch now ends at 3s where it ran the full 12s. Also note at the symmetric-NAT branch that WebRTC not following the legacy relay decision there is deliberate, so it is not later "fixed" into agreement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * bump webrtc fork: MTU-safe bundles, a reordering window, tail loss within the RTT rustdesk-org/webrtc cc6633bc, three commits on 825a0a48, all on the path that sends without a congestion window: Both bundlers counted a DATA chunk by its payload alone; with the header and padding counted, bundles of small chunks stay within the MTU, and the fragment payload rounds down to 1160 so a full chunk does too. A chunk is fast retransmitted at most five times, KCP's IKCP_FASTACK_LIMIT. A frame's chunks go out within microseconds of each other, so on a path that jitters the send-order rule resent every chunk that landed behind three of its siblings: 2.7x the payload on the wire at 10ms of jitter, and on a link without the room for that, a queue that fed on itself. A reordering window, RACK's, makes evidence count only from what was sent a quarter of an srtt after the chunk once the path is seen to reorder, widening on the duplicate TSNs the receiver reports. 5 Mbps, 1% loss, 20ms jitter: 600 of 600 frames at a 98ms mean where 290 arrived at 6.2s. A chunk lost at the tail of a burst has only T3-rtx, which ran from floors sized for a 200ms delayed ack and restarted only on the tail's predecessor's ack: 600ms and more. Every DATA chunk now carries the I bit, the floors are KCP's shape, and a fast retransmission restarts the timer. One 200-byte message per frame at 5% loss: 9 of 600 later than 200ms, from 42. Random loss without jitter is unchanged at every rate and frame size. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * bump webrtc fork: T3-rtx restarts only for the earliest chunk's fast retransmission rustdesk-org/webrtc 2b8e55bc. Sending without a congestion window, a fast retransmission of any chunk restarted T3-rtx, so a chunk past the fast retransmission cap - left to that timer - never reached it while later chunks kept being resent, which a lossy stream does every couple of frames. The timer is the earliest in-flight chunk's, and only its resend restarts it now. Nothing else changes; the benchmark is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * bump webrtc fork: T3-rtx restart on fast retransmission while shutting down too rustdesk-org/webrtc 48100bf1. The restart for the earliest chunk's fast retransmission reached only the Established branch of the write loop; the shutdown states still carry data in flight and recover it the same way, so a closing association could still resend everything on a loss its fast retransmit had already recovered. Both branches share one helper now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3fc11c0f81 |
port forward shared conn (#16062)
* hbb_common: bump to the port-forward-mux proto Also latches PortForward.multiplex into login_scope_digest, which destructures PortForward's fields exhaustively by design (a new field must be latched or deliberately ignored to compile). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: window accounting and channel frame builders Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: fix RecvWindow counter overflow on long transfers Replace cumulative accounting (granted/received) with remaining credit tracking to prevent u32 overflow after 4 GiB of data on a single channel. Wire behavior is identical, but the fix allows large file transfers without mid-stream channel closure. Add regression test for 8 GiB transfer to verify fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: credit-windowed relay halves and channel coordinator Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * server: PortForwardMux channel table and per-channel tasks Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * server: multiplexed port-forward connections stay in the protobuf loop Wire PortForwardMux into Connection: take the multiplexed path at login when the controller sets PortForward.multiplex, route PortForwardChannel frames to it from on_message, sweep the channel table's targets after open/close, and clean it up on connection close. Introduce is_port_forward() (socket-based or multiplexed) and use it at the four sites that classify the connection, so a multiplexed connection stays in the message loop, gets TestDelay keepalives, and reports features.port_forward_mux in PeerInfo. The three sites that break into the raw pipe loop or gate the keepalive still check port_forward_socket specifically, since a multiplexed connection must not take that path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cm: update a port-forward row's targets as tunnel channels come and go Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: controller tunnel with a single-writer stream loop Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: publish Muxed before spawning the tunnel loop Publishing after spawn let a loop that dies immediately reset the state first, so the later publish pinned it at Muxed with a dead handle forever. Also adds a test pinning open-before-data ordering across many concurrently opened channels, and drops an unused Clone derive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: share one multiplexed tunnel across a window's listeners Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: fix round 1 review findings Drop the mux default-false assignment now that definite-assignment proves every path that reads it has set it; the enable-port-forward-mux config commit picks up the missing attribution trailers; the default-on test pins the enable- prefix itself rather than option2bool's weaker fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: end-to-end tests over a loopback tunnel Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: fix bulk test's premature half-close, pin the half-close limitation many_channels_echo_concurrently_and_a_bulk_one_does_not_starve_them dropped its bulk write half as soon as writing finished, which shuts down the write side of the socket and, by design (see the design doc's TCP half-close non-goal; today's run_forward does the same), ends the whole channel. Keep the write half alive until the reader is done so the test measures starvation, not half-close. Add a_local_half_close_ends_the_whole_channel to pin that limitation in code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: cap send credit and other final review fixes Fix 1 (critical): clamp SendCredit to MAX_SEND_CREDIT (= CHANNEL_WINDOW) in both new() and add(), so a peer with tunnel permission can no longer advertise an unbounded window and force the controlled side's unbounded FrameSink::Direct sink to buffer unlimited target data per channel. Fix 2: rename the "starve" test to many_channels_echo_concurrently and drop its (untrue) starvation claim, since it opens every channel before the bulk transfer starts. Add a_channel_opened_during_a_bulk_transfer_ is_served_promptly, which opens the small channel while the bulk one is demonstrably mid-flight. Fix 3: only look up the tunnel permission for `open` frames in the PortForwardChannel arm of on_message, instead of once per data frame. Fix 4: two rustfmt deviations in connection.rs (matches! wrapping and a tuple literal), fixed by hand without a blanket cargo fmt run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: report a refused channel's reason as an error dialog The controlled side already answers a refused port-forward channel with opened { success: false, message }; on the multiplexed path TunnelHandle:: on_frame only logged that message at debug and closed the channel, so the user saw a closed connection with no explanation, worst on the RDP path where only the RDP client's own error remained. on_frame now returns the message the window should show, deduplicated per distinct reason (capped at MAX_REPORTED_OPEN_ERRORS) so one page load's dozen refused connections surface one dialog per reason instead of a dozen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * Use on_error for refused-channel dialog in tunnel_loop Redirect the refused-channel error through the standard on_error path instead of calling msgbox directly, for consistency with other errors in the port-forward flow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: apply the whole-branch review Correctness: - listen(): the Legacy arm is merged with the Claimed arm. On its own it ignored outcome.local_eof, so a client that hung up during login still got a target connect, an audit record and a CM row on the controlled side, and ignored outcome.mux, so a peer upgraded while a legacy window stayed open answered as a tunnel while the controller went raw. - Refusal dialogs are deduplicated per quiet spell (10 s) rather than per tunnel lifetime; the lifetime set went silent for the rest of a long-lived window after the first burst. - Android's CM listener handles UpdatePortForward; it fell into `_ => {}`. - relay_socket_to_tunnel reads into one scratch buffer per channel and sends an exact-size copy. A frame owning its 64 KiB read allocation pinned it until sent, once per byte on interactive traffic. Consistency and cleanups: - The controlled side's refusal text is the raw pipe's wording, RDP substitution included. - connection.rs: the PortForwardChannel arm is a one-line hook, the CM label is pushed from the 1 s tick alone, and the unreachable inner.tx fall-through is gone. - The Ready enum is removed; wait_ready() returns Option<Claim>. - SendCredit::add wakes with notify_one alone. - on_ui_command() replaces the two ui_receiver handlers in listen(). - TunnelHandle is no longer re-exported (unused-import warning). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: a legacy window stays legacy until it is reopened Review: the merged `Claimed | Legacy` arm gave a legacy window a hot transition to a tunnel — every accept re-negotiated, and a peer upgraded while the window stayed open was promoted underneath live connections. The product does not need a mode switch inside a window's lifetime, and the transition was extra state-machine surface for nothing: reopening the window picks up an upgraded peer. The two arms are separate again. `Claimed` negotiates once and the peer's answer fixes the window's mode. `Legacy` logs in for every accept as before, asks for no tunnel — `LoginConfigHandler::port_forward_mux` carries the request per login, so the raw pipe never has to talk to a peer that thinks it agreed to multiplex — and ignores what the peer reports. Both arms keep skipping a local socket that hung up during login. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * hbb_common: bump to main with rustdesk/hbb_common#594 merged Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * server: admit only INITIAL_WINDOW on a channel before opened The demultiplexer accepted CHANNEL_WINDOW into a pending channel's unbounded queue, four times the bound the channel task enforces once it polls. The window now starts at INITIAL_WINDOW and is widened right before `opened` advertises the rest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: a tunnel ends when its window drops the Tunnel The loop held its own handle and state sender, so once the window closed nothing was left to stop it: it kept answering TestDelay and the peer connection, CM row included, lived on until the peer went away. `Tunnel` now owns a watch sender nobody sends on; the loop's receiver errors when the last `Tunnel` drops, and the loop ends. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: one tunnel per mapping, bound to the authenticated target The login latches `PortForward.host`/`port` into the session scope and approval is shown that target, but a window-wide tunnel let any later `open` name another target with only `enable-tunnel` rechecked. A tunnel now belongs to one listener and serves the one target its login authenticated: the controlled side refuses an `open` for any other target, and a window with several targets uses one connection each, approved on its own. With one owner per tunnel the claim needs no waiters: `Establishing`, `Claim::Wait` and `wait_ready` go, and `try_claim` becomes a plain read. The CM label that followed a tunnel's targets goes with them; a row shows its mapping's target, as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: the legacy comment names the mapping, not the window Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: a window violation drops the channel on the spot Both demultiplexers only queued a `Violation` and left the entry until the channel task woke and exited, so a peer that kept sending past the window queued one more entry per frame in the meantime, bounded by nothing. The entry now goes the moment `accept` fails; later frames for that id are unknown-channel noise. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: the login's target travels with the accept, not the handler `listen()` wrote `lc.port_forward` (and, on this branch, `port_forward_mux`) into the window's shared `LoginConfigHandler` before connecting, and `create_login_msg` read them back only when the peer's `Hash` arrived. Two mappings logging in at the same time could therefore swap targets: on master that bridged a local socket to the wrong target, and with a tunnel bound to its login's target it also left the mapping refusing every later accept until it was recreated. The target is now a `PortForward` carried by the interface clone that handles one accept, passed explicitly down to `create_login_msg`; the handler no longer has a field to race on. No lock spans the login. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port_forward_mux: pin permission revocation and whole-tunnel failure in tests Both already hold; the review asked for them to be stated. `enable-tunnel` turned off mid-session refuses the next `open` while the live channel keeps relaying, and a dead tunnel ends every channel on it together, after which the next accept establishes again on the same `Tunnel`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: the legacy comment names re-adding the mapping only Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: the raw pipe runs the code it always ran The multiplexed login had replaced `connect_and_login`, so a mapping with the setting off, a peer without the feature, or a listener latched `Legacy` still went through the tunnel's state machine, the capped pre-read and the changed local-EOF rule. Feature off now means the old code: `listen()` keeps its accept arm and `connect_and_login` as they were, and the tunnel is a branch taken only when the setting is on, in `establish_tunnel` with its own `connect_and_login_mux`. The one line the raw path does differently is the target riding with the accept's interface clone instead of the shared handler. `get_port_forward_mux_enabled` had one caller and moves in here, so `common.rs` is untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: a UI login answers the challenge its own connection was given `handle_login_from_ui` hashed the typed password against `lc.hash`, the window's shared handler field, and the window's password prompt is broadcast to every listener. With two mappings both waiting on that prompt, the `Hash` that arrived last had overwritten the other's, so one of the two answered the wrong challenge and failed to log in. Master shares the same state and broadcasts the same way. The `Hash` is now a parameter of the login; `Session` keeps it beside the connection it belongs to, and the per-accept clone that `with_port_forward` makes gets a slot of its own. `lc.hash` stays for `handle_peer_info`, which only needs the salt, and that is per peer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: a mapping without its hash waits for it before answering the prompt The window's password prompt is broadcast to every mapping, and can reach one whose own connection has not received its `Hash` yet. That mapping used to answer anyway, with a digest over an empty challenge: the peer refused it and counted a failed attempt, and the empty-salt result was written into the shared `lc.password`, where the mapping that prompted had just stored the right one and the next `handle_peer_info` would persist whatever was there. The connection's challenge is now `Option<Hash>`, `None` until `handle_hash` runs, and `handle_login_from_ui` sends nothing without it. The mapping that prompted stores the salted password in the shared handler, and the waiting one logs in with that against its own challenge when its `Hash` arrives, without prompting again. Test: A answers its prompt, the same broadcast reaches B before its hash, B sends nothing, B's hash arrives and its login carries B's challenge and B's target with no dialog. It runs the real `handle_hash` for B. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: the tunnel's login is the raw pipe's, asked for by a window flag Master's fix for the shared login slots (#16069) keeps the target and the challenge in the window's `LoginConfigHandler` and serializes the mappings' logins with a turn lock, all inside `port_forward.rs`. This branch had carried a broader shape of the same fix, a `with_port_forward` on `Interface` and the target and `Hash` as parameters through the login functions, which every caller had to follow. That is gone: `Interface`, `Session`, `create_login_msg`, `send_login`, `handle_hash` and `handle_login_from_ui` are as on master. What the tunnel needs on top is one bit in the login, `multiplex`. It is a window flag beside `port_forward` in the handler, set once in `io_loop` before the window's mappings start, so an accept's claim and its login read the same value; the setting takes effect for windows opened after it changes. `connect_and_login_mux` is now master's `connect_and_login` with the tunnel's three differences and the same `hash_arrived` and `login_from_ui` calls. The raw pipe is master's, line for line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * hbb_common: bump to main with rustdesk/hbb_common#595 merged 840c8ec..f94e3fe is that one merge: the five local settings custom clients could not preset. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: the off switch gets a checkbox in Settings → General `enable-port-forward-mux` was readable only by editing the config file. It is a local setting of the controlling side, so it sits with the other outgoing ones, after "Open connection in new tab", with a tooltip saying what it does. The two new keys are translated in every language. The three that the mobile file manager added, "Export", "Export Logs" and "Import Folder", were empty everywhere but five languages; they are filled in too, and Korean's "xdp-portal-unavailable" with them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * Urdu: fill the backlog of empty and missing translations ur.rs had fallen behind: 104 keys carried an empty value and 35 keys the other languages have were absent altogether. Both are filled in, the missing ones in the order template.rs lists them. Eight entries stay empty on purpose. They are keys that only ur.rs still carries, absent from template.rs and from every other language, so their English source cannot be recovered and nothing reads them: remember_account_tip, os_account_desk_tip, another_user_login_*_tip, xorg_not_found_*_tip and no_desktop_*_tip. Twelve more dead keys keep the values they have; removing either group is a separate decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * Urdu: drop the keys template.rs no longer lists The twenty keys removed here are absent from template.rs and from every other language file; ur.rs was the only one still carrying them, eight of them with no value at all. They are leftovers of features that are gone: the plugin menu, the OS-account login prompts, the Xorg and no-desktop errors. ur.rs now holds exactly the template's key set, all of it translated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: closing the tunnel reaches channels parked on their socket A channel whose far end neither reads nor writes has both relays parked on the socket, not on the inbound queue, so `close_all` dropping the queue's sender woke neither: the socket and both tasks lived on until the far end hung up. Both sides now hold a per-tunnel teardown signal that `run_channel` selects on beside its own cancel, and `close_all` sends it after clearing the map. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: a mapping latched to the raw pipe logs in without asking for the tunnel The login copied the window's `port_forward_mux` into `multiplex`, so a mapping that had latched to the raw pipe on an old peer kept asking for the tunnel. Once that peer was upgraded it answered with a tunnel while the controller switched to raw framing, and every later connection on the mapping was dead until it was re-added. The login now carries its own `port_forward_multiplex`, filled with the target under the turn lock: the probe asks, the raw pipe does not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: a channel opened as its tunnel closes still gets the teardown `open` can straddle `close_all`: the claim passed, the frame receiver was still alive, and the channel subscribed after the signal had gone out. `watch::subscribe` marks earlier sends as seen, and the entry sits in a map that was already cleared, so nothing would ever end it. The signal is now a level: `close_all` raises it with `send_replace`, which stores even with no channel live, and `run_channel` waits for the value rather than for a change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: the connect guard counts a live tunnel as connected `connect_port_forward_if_needed` returned early only for a raw-pipe socket; called again with a tunnel up it would have built a second `PortForwardMux` and dropped every channel of the first. Not reachable today, since the logon response is sent once, but the other checks in this change already read `is_port_forward()`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * Urdu: the two terminal clipboard keys master added Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: a tunnel's TCP stream refuses packets over twice MAX_FRAME The codec takes a header declaring up to 1 GiB and hands the packet up only once it has all arrived, so the channel window bounded what the peer may send, not what this side buffers. Both sides now cap the codec at 2 * MAX_FRAME as soon as multiplexing is agreed: a data frame with its envelope and MAC fits with room to spare, and a header over the cap ends the tunnel before a byte of payload is read. TCP only; the WebSocket and WebRTC codecs carry caps of their own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: a channel id still live when the counter comes round is skipped The controller handed out `next_id` unchecked. 2^32 opens later it lands on a channel still up: the entry here was replaced, while the peer, which ignores an `open` for a live id, kept routing that id to the old socket, so the new local connection's bytes went into the old target connection. The id is now taken under the map's lock and advanced past any id in use. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
618bf37deb |
feat(terminal): add opt-in OSC 52 clipboard writes (#16072)
* feat(terminal): add opt-in OSC 52 clipboard writes * Remove dup tr Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
c1a587cfa4 |
connection page: the Connect menu offers TCP tunneling, as the peer card does (#16075)
The dropdown beside Connect listed file transfer, camera and terminal but not port forwarding, so reaching it meant having a card for the peer. `connect` already takes `isTcpTunneling`; only the menu entry and the parameter that carries it were missing. Shown on desktop only. The peer card gates the same entry on `isDesktop` because `connect` routes a tunnel through the desktop path alone; on web it would have opened a plain remote session instead. Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9a1c8da143 |
Agents regression surface (#16070)
* AGENTS.md: require a regression-surface check before a change is done The minimal-invasiveness rules say what to prefer; nothing made an agent check the final diff against them, so a feature could still route the old path through its new code while every principle was "followed". This adds the gate: audit every modified existing path, keep feature-off on the old code, report the regression surface, and treat an unnecessarily rewritten legacy path as a review finding whatever the tests say. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * AGENTS.md: a scope check before shared code is touched The minimal-invasiveness rules are principles; this adds the stop condition that makes them mechanical. A fix for one path stays in that path, and an unrelated caller needing a placeholder argument to satisfy a changed signature is the signal that it did not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
978c901f49 |
port forward: a login's target and challenge travel with its own accept (#16069)
* port forward: mappings take turns at the window's login slots A window's mappings log in concurrently, and each login is built from the shared `LoginConfigHandler`: `create_login_msg` reads `port_forward`, which `listen()` set before connecting, and `handle_login_from_ui` reads `hash`, which the last `Hash` to arrive set. Two mappings logging in at once could swap targets, bridging a local socket to the other's target, and answer each other's challenge, failing one login. The window's password prompt is broadcast to every mapping, so one whose `Hash` had not arrived answered with whatever the handler held. Each mapping now fills `port_forward` and `hash` and sends its login under a per-window turn lock, and keeps its own `Hash` beside the connection: a password typed before it arrived is left to the mapping that prompted, which stores the salted password in the shared handler for the others to log in with. The fix stays in `port_forward.rs`. `LoginConfigHandler` gains the lock and a setter for its private `hash`; `Interface`, `Session` and the login functions keep their signatures. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: the lock and the hash setter are crate-private; test the hash that arrives late on its real path Both exist only so `port_forward.rs` can reach the handler's private `hash`; neither is API. The test for a password typed before a connection's hash ended by answering the prompt again once the hash was there. What happens in the code is that the hash's arrival runs `handle_hash`, which logs in with the password the prompting mapping stored; the test now ends there, with no preset password. Answering the prompt with one's own challenge while the handler holds another's is a test of its own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * port forward: a password typed before the connection's hash answers it when it comes The previous commit dropped such a password, counting on the mapping that prompted having stored it in the shared handler by the time this connection's `Hash` arrived. The broadcast wakes both mappings at once and `select!` picks between a ready `Hash` and a ready password at random, so this one could reach `handle_hash` first, find the handler empty, and prompt again. The connection keeps the password until its `Hash` arrives and answers with it then. `login_from_ui` takes the challenge it answers; the wait is `connect_and_login`'s, in `hash_arrived`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d453a19601 |
AGENTS.md: require a regression-surface check before a change is done (#16068)
The minimal-invasiveness rules say what to prefer; nothing made an agent check the final diff against them, so a feature could still route the old path through its new code while every principle was "followed". This adds the gate: audit every modified existing path, keep feature-off on the old code, report the regression surface, and treat an unnecessarily rewritten legacy path as a review finding whatever the tests say. Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
50c4e435de |
connection: apply the non-video send timeout once the type is known (#16063)
* connection: apply the non-video send timeout once the type is known `Connection::start` set the send timeout before the login request had arrived, when `file_transfer`, `port_forward_socket` and `terminal` were all still unset, so every connection got `SEND_TIMEOUT_VIDEO` (12 s) and the `SEND_TIMEOUT_OTHER` branch never ran. A file transfer, terminal or port forward whose peer stopped draining for 12 s — a Wi-Fi roam, a VPN reconnect — was dropped. The type-specific timeout is now set in `on_message` right after the login request's union has been matched; `start` keeps the video figure for the login phase. `SEND_TIMEOUT_OTHER` also drops from 120 s to 30 s, the same horizon as the 30 s read timeout: the timeout wraps a single `send`, so it only fires when the peer makes no progress at all for that long, and beyond 30 s the read check would declare the same peer dead anyway. The raw port-forward pipe's write to its local target shares the constant and moves with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * connection: keep the raw port-forward local write at 120 s `SEND_TIMEOUT_OTHER` also bounded `forward.send` in `try_port_forward_loop`, the write to the local target, whose own idle timeout is an hour. Lowering it to 30 s made a target that stops draining for half a minute drop the whole tunnel. That write gets its own constant at the value it always had. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * connection: keep the non-video send timeout at its long-standing 120 s Lowering `SEND_TIMEOUT_OTHER` to 30 s was a policy change on top of the bug fix, argued from the 30 s read timeout, which measures something else and cannot even run while a send is blocked. The constant goes back to `SEND_TIMEOUT_VIDEO * 10`, where it has been since 2021, and the raw port-forward loop's local write shares it again. What remains is the fix alone: the type-specific timeout is chosen once the login request has said what the connection is. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d5c6d0f6b7 |
fix(custom client): msi update, preserve exe name (#16057)
Keep the configured app name casing when renaming the updated executable so legacy MSI custom actions can terminate custom client processes. Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
b6ff62c74b |
l10n: fill remaining Swedish entries (#16050)
Signed-off-by: Daniel Nylander <github@danielnylander.se> |
||
|
|
ba6de7990f |
fix(ci): ubuntu-22.04-arm, oom (#16056)
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
a59ad333fc | fix: delimit FFmpeg pkg-config option (#16055) | ||
|
|
82aa28f129 |
fix(ci): install CMake 4.3 for ARM64 vcpkg builds (#16044)
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
3f93005be2 |
fix(drm): deliver a rotated output upright (#15886) (#15889)
* fix(drm): deliver a rotated output upright instead of sideways (#15886) the compositor draws a rotated desktop sideways into the landscape scanout and the physically turned monitor straightens it locally, so the raw scanout the drm path ships reads sideways in the viewer, and nothing rebroadcasts on rotation because the framebuffer size never changes. the capturer now resolves the output transform once per session from the wayland enumeration, turns accepted frames upright into its own buffer, and sizes the session in rotated dimensions. the advertised list swaps width and height for 90/270 outputs, which also makes a mid-session rotation a topology change that restarts the service, and computes scale from the post-swap width so a rotated 1:1 monitor no longer advertises scale 16/9. a non 4-byte format on a rotated session is a hard error and degrades through the existing health path. the greeter path where no compositor answers keeps today's behavior: there is no transform source there. hbb_common carries the new transform field (submodule bump). * fix(drm): drop the wayland snapshot when the live layout drifts (#15886) the advertised list is augmented from the cached wayland snapshot and nothing invalidated it mid-session, so a rotation the 1.5 s live poll plainly saw never reached check_changed: the poll reads live, the advertise kept serving the pre-rotation snapshot. measured before this commit: transform applied and held, 'desktop layout changed' logged, zero new encoders. cleared only when the poll saw an actual change, so the probe cost stays tied to real layout events; after it, the same stimulus rebuilds into a 1080x1920 encoder within a poll turn. * refactor: trim comment density to the file norm * chore: bump hbb_common to the transform field from rustdesk/hbb_common#586 pinned to the #586 commits atop the current pin rather than main tip: main also carries an unrelated config-keys refactor the app has not adopted yet, and both #586 commits are reachable upstream through the merge. * fix: advertise a lone rotated output at delivered size, one snapshot per session review findings, both real: the 90/270 swap sat below the origin-only cut, so a single rotated output advertised unrotated dimensions while the capturer delivered rotated frames; and transform and origin came from two get_displays() reads that could straddle a cache invalidation. the swap now precedes the cut (logical-scale adoption stays multi output), and new() resolves one snapshot for transform, origin and the session size, with tests for both. comments trimmed to the three-line guideline. * fix(drm): rotate every space the rotation touches, not just the pixels review findings on #15889, all verified against the code first. the uinput rect's single-display branches now serve the delivered orientation, so the pointer reaches the whole of a rotated screen (1). DisplayRect carries the transform, making 0/180 and 90/270 flips visible to the drift comparison (5), and the drift poll is an edge on live-vs-previous rather than a level against the baseline, so the cache clear fires once per real layout event instead of every 300 ms forever (9). on the drm path the baseline promotes together with the clear, so the remap and the client rebase never correct the same origin delta twice (7), and the poll now runs above the login-screen return, which was the one place with no other invalidation trigger (6). a snapshot generation gives a rotation a rebuild path at last (3, 4): clears bump it, the capturer records it at build, and a stale generation asks for a rebuild without counting against display health. the cursor bitmap and hotspot turn with the same session transform the frames use (11). original_resolution follows the 90/270 swap (12). the transform comes only from an identity match, never the layout-order fallback (13), and a missing wayland snapshot at build logs the degrade instead of silently pinning an unrotated session (10). unrotate_bgra's body is now libyuv's ARGBRotate, which the existing direction tests pin to the measured anchor (14). 180 stays master behavior: i915 advertises hardware rotate-180 and wl_output cannot tell hardware from software rotation, so undoing it blind would invert an already-upright frame; it needs the plane rotation property on the wire (2). the pipewire fallback guard's comment now states the rotated reality it compares (8). * fix(drm): one owner for the layout generation, one identity rule for rotation adversarial pass over the previous commit, three structural findings. the generation bump rode on the cache clear, which every video-service start also executes, so any session init or restart tore down every other live capturer, with no damping against a ping-pong between two displays. the bump now has a single owner: the edge-detected layout change in the display-service poll. cache clears are side-effect free again, and a two-display session survives a third session's init with zero spurious rebuilds. the advertise side swapped dimensions for a layout-order-fallback match while the capturer's transform refused such matches, splitting advertised size from delivered frames into a black screen. both sides now key off the same identity-match pass (identity_matches), so a guessed assignment rotates nothing anywhere. an edge observed while the drm verdict was transiently non-available was consumed unpromoted, leaving a rotation sideways for the session; it now stays owed until the verdict returns. an enumeration that failed at build pinned transform 0 forever with a warn promising a retry that did not exist; a missing snapshot now makes the first successful poll an edge, so the degrade is bounded by the outage. the multi-display missing-logical-size fallback serves delivered orientation, stale docs zhou named are updated, and the resolutions list stays mode-space on purpose: resolution changes ride xrandr, which is inert on this path. * fix: transpose-tolerant fallback size check, log a rejected rotate geometry whether a portal stream's caps arrive rotated on a 90/270 output is unmeasured either way (pipewiresrc does not apply SPA_META_VideoTransform), and this guard has already broken two readers who reasoned from its comment - so the size half now accepts either orientation instead of gambling a permanent offline on one. a source stride shorter than a row logs the rejected geometry instead of publishing a silent black frame. comments trimmed to the guideline and the stale sole-test claim updated. * fix(wayland): never serve a transposed PipeWire stream The fallback accepted a stream whose dimensions were the advertised display's transposed, but CapturerInfo keeps the stream dimensions, nothing on the wayland side ever reconciles the client afterwards, and the flutter renderer drops every frame whose size differs from the advertised display - a permanently blank fallback. Accept only the exact orientation; a transposed pair now falls into the existing bail, the display is advertised offline, and the client recovers by re-enumerating. * fix(drm): keep the cursor consistent with the session transform Two holes from the same review pass. The wire cursor id hashes only the plane pixels and geometry, so a stream rebuilt under a new transform resent the SAME id and the client's by-id cursor cache kept the old orientation until the shape itself changed; fold the session transform into the served id. And a cursor racing new()'s transform store was processed with transform 0 and never corrected, since the producer resends only on a shape change; hold that cursor and replay it once the transform is in - the receive loop wakes at least every 200 ms, so the replay is prompt even on an idle wire. * fix(wayland): the single-display carve-out must not forgive a transposed stream The carve-out forgives a size difference (a Full Workspace stream may report the workspace rather than the mode), but a transposed pair is the same served-vs-advertised orientation split the previous commit rejects, and it blanks the client the same way. * fix(drm): a lone display with a rejected fallback is honestly offline The transposed rejection promised 'advertised offline', but the lone-display carve-out in mark_demoted_displays kept the display online on the grounds that the whole-desktop fallback remains usable - which is exactly what the rejection just refuted. The video service then restart-looped against a stream nothing can serve, rebuilding the portal session about once a second, while the client saw a display list that lied. Record the geometry rejection in the display health and let it end the carve-out; a delivered frame or the demote-cooldown re-arm clears it, so a recovered output comes back on its own. * ci: retrigger, the previous run died in the actions outage (all root jobs at exactly 8m) * fix(drm): a blind capturer owes a rebuild, and name matches reserve globally Two of the review's findings. A capturer built during a failed wayland enumeration recorded nothing durable: a later successful enumeration refills the cache, wayland_snapshot_missing goes false, and the first live poll sees no edge - the session stays sideways until an unrelated change. The build now latches that it ran blind and the layout poll consumes the latch into the existing owed-promotion machinery. And the identity matcher ran per-connector, so a resolution guess for an earlier connector could steal a later connector's exact name match and pin its rotation on the wrong output. Names now reserve in a global first pass; resolution pairing runs on the remainder only when forced - one free output and one unmatched connector at that size. * fix(drm): consume the blind-build latch even on a live-changed poll Adversarial pass on the previous commit: the short-circuit left the latch set on exactly the poll where live_changed fired (the common blind-recovery ordering, since a failed enumeration is not cached and failed_init makes the first successful poll an edge), and the stale latch then bought a second, spurious promotion one poll later, tearing down the freshly rebuilt capturer. The latch is now taken unconditionally so both edge sources merge into one promotion. * fix(wayland): hand over a layout change the poll has not seen yet set_wayland_layout_baseline clears live, which is the edge detector's only memory of the previous layout. ensure_inited calls it at the top of every video service start, so a second monitor service starting between a rotation and the next 1.5s poll recorded the rotated layout as the baseline: the poll then found baseline == live_rects, owed no promotion, and the first capturer kept its old transform. Under mutter's software rotation the framebuffer size does not change and the wayland display-change check is disabled, so the stream stayed sideways until the next layout event. The setter now arms the promotion itself when the outgoing live differs from the incoming baseline, which is the one choke point every caller goes through. An empty incoming baseline is the DRM-union fallback and proves nothing. * fix(wayland): the edge detector needs a memory a session init cannot erase The baseline reset was also the edge detector's memory, so two session inits straddling a rotation left nothing to compare the next poll against. Keep the observed layout separate from the per-session input baseline; before the first poll the outgoing baseline seeds it. * fix(wayland): a capturer records the layout it was built on ensure_inited() runs the wayland query before the capturer exists, and a failure there saves an empty baseline. The capturer's own retry can succeed a moment later and build on that layout, and because the build was not blind nothing latched it, so a rotation before the first poll had no memory to be an edge against and the stream stayed at the old transform. The build now seeds the edge detector when nothing else has, and only then, so a capturer built later cannot overwrite what the poll is keeping. * fix(wayland): keep a capturer record that lost the race with the first poll The constructor reads its wayland snapshot and records it in the edge detector in two steps, and the layout poll can land between them. After a failed session init (empty baseline) the constructor takes layout A and publishes it, the output rotates, and the poll reads B live: nothing is recorded yet and the snapshot is present, so it is no edge, and observe() sets seen=B. The late note_capturer(A) then met a non-empty memory and was dropped, so the capturer showed A while the detector held B, and B against B never bumped the generation. note_capturer now flags a build layout that disagrees with the poll's memory instead of dropping it (overwriting is still wrong: on a multi-display session that memory is what the other capturers were built against). edge() reports the flag as an edge whatever the live layout is, observe() consumes it right after, and a session init's baseline reset leaves it alone. Regression test for the interleaving, with the promotion consuming it, a baseline reset in between, and an agreeing late record as the control. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZwSrP3DFA6ZiPKVHkU5dL * fix(wayland): a late capturer record from a promoted generation is not a second edge The record can also land after the poll consumed an edge but before the bump it promotes, or after the bump with a snapshot taken before it. That capturer is stale by generation and rebuilds on its own, but the flag it raised survived the promotion, and the next poll spent a second promotion on the freshly rebuilt capturers. Tag the record with the generation the capturer read before taking its snapshot and count it as an edge only while that generation is current; the newest generation wins when two records land. Regression test for the consumed-edge interleaving, with a disagreeing record at the promoted generation and a stale record after a fresh one as controls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZwSrP3DFA6ZiPKVHkU5dL * chore: bump hbb_common to main tip dc95b4f -> 05ed68f, a fast-forward: the flipped-transform warning and the wlroots xdg-output positions (rustdesk/hbb_common#591, #592), 90-day logs, the webrtc session cleanup deadlock fix and the hide-general-settings option. No public API changes and no dependency changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZwSrP3DFA6ZiPKVHkU5dL --------- Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
23a147b0dc |
Filter detached DXGI outputs for Win+P single-display modes (#15814)
When Windows is set to "Show only on 1/2", DXGI still enumerates detached outputs. Preferring that unfiltered list could select a zero-size display as primary and hang clients waiting for video. |
||
|
|
e4539fc304 | Update cn.rs (#16041) | ||
|
|
6dbd810454 |
Translate export-related strings to Portuguese (#16038)
Translate export-related strings to Portuguese |
||
|
|
0fd1a0eecb |
Custom client no rebuild (#15774)
* feat(portable): load per-customer payload from a PE resource
Customizing a Windows client recompiled the packer for every customer,
because data.bin was baked in with include_bytes!. The generic payload is
identical across customers, so only the small per-customer delta needs to
vary: the branded runner exe, custom.txt and the icons.
The packer now also reads an RDPKG RCDATA resource holding a second blob in
the same format, and folds it over the compiled-in payload. A build can then
inject that resource into a prebuilt template instead of running cargo.
The executable to launch comes from the package trailer, and the extraction
directory follows its stem, which replaces the sed of APP_PREFIX. Where the
executable itself is not customized (sciter x86) it stays in the generic
payload and is only renamed, so the merge covers both shapes.
custom.txt keeps being written to disk next to the app: that is what the
client reads at startup and what the updater stages so a customization
survives an upgrade to a stock build.
Also fixes generate.py restoring os.curdir (the literal ".") instead of the
previous working directory, which left it inside the source folder.
CI: ship windows-aarch64 in the unsigned tarball, so ARM custom clients have
a template to build from.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
* ci: publish msi templates for custom client builds
Custom clients rebuild the msi through WiX for every customer, though the
package only differs by the app name, a few GUIDs and four files.
Build the msi once more per release with a __RDAPPNAME__ placeholder and ship
it unsigned in the unsigned tarball, so a customer's build can patch it rather
than run msbuild. It stays unsigned because patching would invalidate a
signature anyway.
Doing this in CI is what makes ARM custom clients possible: preprocess.py runs
the packaged exe to read its version and build date, so an arm64 msi can only
be produced on a native arm64 machine, which the runner already is and the
build agents are not. Patching runs no exe, so an x64 agent can then patch the
arm64 template.
preprocess.py rewrites res/msi in place and locates the app as <app-name>.exe
inside the dist, so the tree is reset around the second build and the dist copy
is renamed to match. Sciter x86 ships no msi and is untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
* refactor(msi): pass the app name to the printer custom actions
preprocess.py rewrote the CustomActions sources per customer so the printer
carried the app name, which meant the dll was recompiled for every custom
client and, worse, left the app name baked into a compiled binary.
Pass it through CustomActionData instead. Only the printer and its port ever
varied: the INF path and the driver name ship under their stock names and
preprocess.py already forced the driver name back to RustDesk, so a single
build of the dll now serves every custom client.
Both actions treat the name as optional and fall back to the stock name, so a
package built before this still installs and uninstalls its printer.
This also unblocks patching a prebuilt msi template, which cannot work while a
compiled dll contains the app name: replacing a string inside a PE would shift
everything after it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
* ci: use an 8.3-safe placeholder for the msi template
WiX derives a short name for any name that is not valid 8.3, and a patch
cannot rewrite a truncated placeholder, so a long placeholder would leave the
package's short names pointing at it. RDAPPNAM is eight characters like
"RustDesk" and needs no short name, keeping the template as close to the
shipped package as the mechanism allows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
* feat(msi): give a template its own cabinet for per-customer files
Rebranding recompressed the whole ~100MB payload because one cabinet held
everything. In template mode preprocess.py puts the handful of files a custom
client replaces on a second cabinet, so a patch rebuilds a few hundred KB and
leaves the payload cabinet alone. The shipped msi is built without template
mode and keeps its single cabinet.
The branding assets need conditional components. A stock build ships none of
them -- there is no icon.ico, icon.png or logo*.png, only icon.svg -- so the
template has to carry placeholders for the File rows to exist, and a customer
supplies whichever they want. Installing a placeholder unconditionally would
give a customer with no logo a placeholder image, where today a missing asset
means no logo at all: the client tries each candidate and treats the failure as
absence. So each optional asset installs only when its property says the
customer supplied one.
CI creates those placeholders and builds the template with the new mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
* ci: build the msi template with a sentinel revision
preprocess.py appends a build-time revision as the fourth version field, so a
template built without one would bake the CI clock into every customer's
package. Revision 0 marks the field as the patcher's to fill in, and makes the
template deterministic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
* fix(portable): delete files a later package no longer carries
The extraction directory is wiped only when the packer's compiled-in timestamp
changes. That used to be per customer, because generate.py ran for each build;
now the packer is compiled once per release, so every customer and every
rebuild within a release share one timestamp and nothing is ever wiped.
A customer who removes their logo and rebuilds would therefore keep showing it:
the new package simply omits logo.png, and md5 skipping only covers files that
are still present. Record the package's paths in the extraction's meta file and
delete the ones a later package drops.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
* fix(portable): build the dropped-file path from plain components
meta.toml lives in a user-writable directory and now drives deletion, but the
traversal guard tested the normalised string while the join used the raw one.
Path::join replaces the base outright when handed an absolute path, so an
edited meta.toml could point remove_file anywhere.
The path is now rebuilt from Normal components only. A colon is rejected
explicitly rather than left to the host's parser: a drive-relative "C:x" parses
as a Normal component everywhere, and only a Windows host reads "C:/..." as a
prefix, so the same input escaped when the logic was exercised off-Windows --
which is what the new test catches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
* fix(msi): pass the printer name in a format the custom action can read
[~] is MSI's escape for a NUL character, not the delimiter WcaReadStringFromCaData
splits on -- that is a literal wide char 128, which a Formatted property value
cannot carry -- and WcaGetProperty returns a null-terminated string anyway. So
the second field was unreachable: InstallPrinter always fell back to the stock
name and installed a printer and port called "RustDesk Printer" inside a
customer's branded package, while UninstallPrinter, whose data is a single field
and parsed fine, went looking for "Acme Printer" and left the real one behind
for good.
Both actions now read CustomActionData directly and split on a character that
cannot occur in a Windows path or in a validated app name. A package built
before this carries no separator and keeps the stock name, as it did.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
* fix(portable): retry failed stale branding cleanup
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(portable): reject malformed RDPKG resources
Distinguish an absent customer package from an invalid resource and
propagate package errors instead of launching the stock payload.
Signed-off-by: fufesou <linlong1266@gmail.com>
* refact: format 2 files
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(msi): match process names case-insensitively during uninstall
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(custom-client): validate portable exclusion and MSI action data
Fail when --exclude-exe does not match a file, and propagate MSI
CustomActionData read failures while preserving legacy fallback behavior.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: generate.py, exclude-exe
Signed-off-by: fufesou <linlong1266@gmail.com>
* Revert "fix: generate.py, exclude-exe"
This reverts commit
|
||
|
|
dfb5804dd0 |
Update nl.rs (#16036)
Re. export and import. Without detailed information, reference or examples from en.rs, de.rs or fr.rs, I have simply translated the three strings verbatim |
||
|
|
957dfe8c96 |
feat: add admin and control role API scripts (#16035)
- add admin role CRUD and membership management - add non-protobuf control role operations Signed-off-by: 21pages <sunboeasy@gmail.com> |
||
|
|
c312385ffd |
Update tw.rs (#16031)
* Update tw.rs * Update tw.rs * Update tw.rs |
||
|
|
f28ac38ccf |
feat: optionally sync clipboard between connected sessions (#15934)
* feat(clipboard): optionally sync clipboard between connected sessions Clipboard content received from a remote session is written to the local clipboard with an owner marker, so the client clipboard loop deliberately skips re-broadcasting it to avoid echo loops. As a result, text copied in one remote window could not be pasted in another connected remote window. Add an opt-in local option (allow-sync-clipboard-between-sessions) that relays Clipboard/MultiClipboards messages received from one session to all other connected sessions, excluding the source session. Per-session clipboard permissions and view-only mode are still respected via the existing send path, and the owner marker on the receiving peers prevents any echo back. Desktop (flutter) only; file clipboard is not affected. * fix(lang): propagate sync-clipboard-between-sessions-tip to all locale files Add the new key to template.rs and every locale file per the localization convention, move the en.rs entry to the end of the list, and drop comments that only restated the names next to them. * fix(lang): add the 'Sync clipboard between sessions' label to the localization catalog The checkbox label goes through translate(), so add it to template.rs and every locale file so non-English locales can translate it. en.rs is skipped since the English display text is identical to the key. * fix(clipboard): check the source session's full clipboard permission before relaying The relay was gated only by the incoming clipboard_allowed check (!disable_clipboard && !view_only). Gate it with is_text_clipboard_required() instead, which additionally respects the source session's server_clipboard_enabled and server_keyboard_enabled state, matching the predicate already applied to destination sessions. A message arriving after the source permission was revoked (or from a non-conforming peer) is no longer propagated to other sessions. The existing local update_clipboard behavior is unchanged. * fix(lang): translate the new clipboard sync entries in all locale files Fill the 'Sync clipboard between sessions' label and its tooltip in every locale file instead of leaving them blank, following each file's existing terminology. template.rs keeps the empty master entries. |
||
|
|
28cf1836e6 | bump to 1.5.0 | ||
|
|
1ec1b9e7e3 |
fix: android: target API 36 (#15603)
* fix: android: target API 35 Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: handle API 35 foreground service types Integrate the foreground-service and MediaProjection lifecycle changes from fufesou/rustdesk#68 while leaving storage permission handling to #15602. Co-authored-by: fufesou <linlong1266@gmail.com> Signed-off-by: fufesou <linlong1266@gmail.com> Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: bump required android sdk version to 36, per recent google requirement change. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix(android): clear microphone FGS type when capture stops Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): harden API 36 capture service lifecycle - isolate MediaProjection callbacks per session - keep foreground service types in sync with capture state - handle audio startup failures and shared frame ownership - upgrade AGP to 8.10.1 for API 36 support Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): reset capture state on FGS update failure Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): recover capture after projection failure Propagate virtual display startup failures, clean up partial video resources, and resume capture after media projection is reauthorized. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): preserve voice call during projection replacement Keep the existing capture active until a new projection is acquired, and restore the voice-call audio source when capture restarts. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): use JDK 17 in playground workflow Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): clear pending capture restart on denial Notify MainService when a recovery projection request is canceled so a later projection grant cannot restart stale capture state. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): handle audio and projection recovery failures Verify AudioRecord startup, propagate voice-call restoration failures, and clear stale capture recovery state when projection setup fails. Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: fufesou <linlong1266@gmail.com> |
||
|
|
2c84c8fb13 | change to 3.44.9 flutter for arm | ||
|
|
66ab0b87f6 |
Linux drop shell from service loop (#15979)
* perf(linux): stop the service loop from forking a shell per environment variable The service loop re-derives the desktop every 500 ms, and every lookup on that path forks. A healthy GNOME session spends ~104 process spawns a second, 8 full `ps -u <uid>` scans and 2 full `ps aux` scans, to re-answer a question whose answer has not changed. `get_env` alone is a `sh -c` pipeline of ~12 processes per variable. `get_envs` already reads `/proc` directly and was documented as the intended replacement, so move the remaining `get_env` callers to it and delete it. The xwayland probe drops from 4 pipelines (~48 processes) to one `/proc` walk, and the pathological walk that #15952 was about drops from ~2900 processes to at most 60 `/proc` walks. `get_cm` and `is_xwayland_running` read `/proc` instead of forking `ps aux` and `pgrep -a`; `get_cm` also called `current_exe()` once per line of `ps` output. Selection semantics are preserved where they were load-bearing: * `get_envs_of_newest` reproduces the `ps ... | tail -1` the removed pipelines used, so a variable the newest matching process does not have means moving on to the next pattern, never on to an older process that may belong to a session which has since logged out. * `get_envs` keeps its own order (readdir) and its all-process ranking, so the existing `get_display_xauth_wayland` caller is unaffected. Only its handling of an exported-but-empty value changes: `DISPLAY=` no longer counts as found, where it used to satisfy a single-name query and return the empty value before a process holding a real one was examined. * `get_envs_where` lets the caller state what a complete answer is. Ranking by how many of the requested names a process carries cannot know that `DISPLAY` is mandatory and the rest interchangeable, so it could rank a process holding three optional values above the one holding the pair that matters. `is_xwayland_running` is scoped to the session's uid. The compositor starts Xwayland as the session user, so another user's Xwayland -- a switched-away session, a second seat -- used to route a pure-Wayland session into the Xwayland probe, which has no display for it to find there. Not addressed: this discovery path has never had any notion of the active session, and filters by uid alone. Constraining candidates to the active session is not possible for the most important one, since `xdg-desktop-portal` and its backends run under `user@<uid>.service`, which spans sessions and carries no `XDG_SESSION_ID`, no session cgroup and no audit sessionid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5egQpH4q4GoXJiuMoTJ5t * fix(linux): the newest-process walk must not answer with a grep or an older PID Three findings from review of the commit before this one. `/proc/<pid>/environ` failing to read left the walk on to the next PID, which in `newest_first` mode is an older process -- possibly of a session that has since logged out -- where the `ps ... | tail -1` pipeline this replaces stopped at the one PID it had already picked. A read that fails is a process carrying none of the requested names, not a process to skip. The `seen` latch that was meant to hold the newest process is deleted: `accept` is reached once per matching process, so returning on the first is what it already did. The regex is matched against the whole `/proc/<pid>/cmdline`, where the pipeline had a `grep -v 'grep'`. A user running `grep Xwayland` is otherwise the newest match for that pattern and answers with whatever environment their shell had -- an X forwarding endpoint over ssh, say. This is the one place the walk still differs from the `get_envs` it grew out of, which never had that filter and could take an ssh `grep` over the portal it was looking for. `get_envs` is left exactly as it was. Its completeness test was every requested name *present*; stating it through `accept` turned it into every name *non-empty* and, with the empty-value change that went with it, moved which process the existing `get_display_xauth_wayland` caller settles on. `accept` is now told the count and asks the question the loop it replaced asked. This supersedes the `get_envs` bullet of the previous commit message: an exported-but-empty value counts as found again, as it always did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QgsYAUYKDei1AM5yHJsMX --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
169f74f8d9 |
fix(ci): check out submodules in update-webpki-roots
The root workspace lists libs/hbb_common as a member, so without the submodule cargo cannot load the workspace and `cargo update` exits 101. The job has failed on every scheduled run since it was added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gecc6fgEeSxs6VRiQmAeof |
||
|
|
d4b06a6c5c |
fix: android: replace all-files access with scoped storage (#15602)
* fix: android: replace all-files access with scoped storage + system picker Remove MANAGE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE, and WRITE_EXTERNAL_STORAGE from the Android manifest. Remove requestLegacyExternalStorage. Replace broad external storage with app-scoped external storage for the file-transfer workspace. File import uses the system file_picker. File export uses Android's SAF ACTION_CREATE_DOCUMENT with path validation that restricts export sources to app-owned directories. Remove the external_path dependency. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: refine file import feedback Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: use SAF for file imports Replace file_picker imports with Android's Storage Access Framework to avoid legacy storage permissions, stale cached files, and duplicate staging of large imports. Stream selected documents into app-scoped storage with failure-safe replacement, keep exports restricted to validated app storage roots, use filesDir for the internal fallback workspace, and remove legacy permissions contributed during manifest merging. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: keep file imports in the selected directory Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: reset projection and constrain file workspace Release capture resources when media projection is revoked externally. Keep Android local file navigation within the app-scoped workspace. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: handle scoped storage start-up regressions. Allow zero digits in POSIX filenames by rejecting NUL explicitly, and initialise the app-specific home directory before the Android service starts the native server. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: update content resolver mode to use 'wt' instead of 'w' to prevent trailing bytes from old document whilst reporting sucess Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android, enforce file workspace boundary on the server, and unblock the ui thread. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: android: validate rename destinations against the app workspace bound file-operation paths. report rename failures, general import failures, and unregister / reregister projection when its onStop callback fires. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: reconnect was refreshing the directory with net entry instances, while selected items retained the old instances, it was reporting a selected item, but checkbox statue used object identity, and appeared unchecked. Fixed by reconciling by path and entry type before replacing the directory snapshot, rebinding valid selections, and dropping missing ones. Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix: (android) add SAF folder import and multi item export - import directories using ACTION_OPEN_DOCUMENT_TREE. Export multiple files, logs, and screen recordings via export buttons, add localisation keys for new actions Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> * fix(android): harden scoped storage file handling - create new SAF documents instead of overwriting export sources - reject empty peer paths except for home directory reads - report directory backup restore and cleanup failures - resolve log export paths from the configured app name Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): harden scoped-storage file operations - snapshot directory exports before writing to the destination - query document provider metadata off the main thread - reject invalid remote directories without read timeouts Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): handle SAF directory name collisions - reject dot-segment folder names during import - fail imports with duplicate document display names - only reuse matching directories during export Signed-off-by: fufesou <linlong1266@gmail.com> * fix(android): handle SAF folder import collisions Reject filesystem-equivalent destination names and avoid showing a failure when folder overwrite is skipped. Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com> Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: fufesou <linlong1266@gmail.com> |
||
|
|
03a7fc5992 |
fix(flutter): align terminal shortcuts with platform conventions (#15970)
* fix(flutter): align terminal shortcuts with platform conventions Signed-off-by: fufesou <linlong1266@gmail.com> * fix(flutter): handle Linux terminal paste with modifier locks Detect platform-specific paste shortcuts so Ctrl+Shift+V bypasses virtual Ctrl/Alt modifiers on Linux. Add regression coverage. Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
1fe451c2e8 |
chore(flutter): bump desktop_multi_window for show recovery (#15959)
Pick up rustdesk-org/rustdesk_desktop_multi_window#37, which re-arms the existing bounded redraw timer whenever a secondary window is shown, including when its first frame was generated while hidden but not presented. This may perform one delayed child refresh on each show. It intentionally does not add a presentation-complete flag: Flutter reports frame generation rather than successful presentation, so recording success after a synthetic refresh could suppress later self-recovery without a reliable success signal. |
||
|
|
0b08a83d4b |
fix(file-transfer): improve large directory loading (#15830)
* fix(file-transfer): improve large directory loading Signed-off-by: fufesou <linlong1266@gmail.com> * fix(file-transfer): avoid failing newer directory reads Track each remote directory request by its registered completer and only remove the task when it still matches, preventing stale failures from affecting newer requests for the same path. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(file-transfer): handle slow directory listings safely Signed-off-by: fufesou <linlong1266@gmail.com> * fix(file transfer): correlate directory responses with requests Signed-off-by: fufesou <linlong1266@gmail.com> * fix(file transfer): prevent automatic directory responses from matching requests Signed-off-by: fufesou <linlong1266@gmail.com> * fix(file-transfer): handle large remote directory listings reliably - build file rows lazily - register remote reads before sending requests - handle Home paths, stale responses, errors, and timeouts - serialize same-path reads with different hidden-file options Signed-off-by: fufesou <linlong1266@gmail.com> * fix(file transfer): reduce diffs Signed-off-by: fufesou <linlong1266@gmail.com> * fix: build Signed-off-by: fufesou <linlong1266@gmail.com> * fix: invalidate pending dir reads on reconnect Signed-off-by: fufesou <linlong1266@gmail.com> * test(file-transfer): cover remote directory read lifecycle Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
e9b81e3475 | typo | ||
|
|
7220f00410 |
fix(linux): a Wayland session without XAUTHORITY is not incomplete (#15978)
Fixes #15952. Hyprland runs Xwayland without exporting `XAUTHORITY`, and `get_display_xauth_xwayland` only returns once it has both `DISPLAY` and `XAUTHORITY`. On such a session that condition is never met, so every refresh runs the retry loop to the end: 10 rounds x 6 process patterns x 4 variables = 240 `get_env` calls, each a `sh -c` pipeline of ~12 processes starting with a full `ps -u <uid> -f`. That is ~2900 fork/exec per refresh, and the service loop repeats every 500 ms. The reporter measured a full core on a low-end laptop and ~60% of a core on a 13600KF. The Wayland side answers for such a session, so accept `DISPLAY` together with either `XAUTHORITY` or `WAYLAND_DISPLAY` + `DBUS_SESSION_BUS_ADDRESS`. The portal answers on the first pattern, which ends the walk there, as it already did on desktops that do export an xauth. The loop also assigned all four variables unconditionally per pattern, so the patterns that do not run on a given desktop blanked out what an earlier one had answered with -- the portal's valid `DISPLAY=:1` included. That is why the `--server` was then started with no `WAYLAND_DISPLAY` and no `DBUS_SESSION_BUS_ADDRESS`. Candidates are now taken from one pattern as a whole and ranked, so a later pattern replaces an earlier answer only by being better, and a session that can only offer a compositor and a bus still keeps them. A compositor that starts Xwayland on demand shows the same shape from the other side: the portal came up before Xwayland did, so its environment carries a valid `WAYLAND_DISPLAY` and `DBUS_SESSION_BUS_ADDRESS` but no `DISPLAY`, and no pattern here may ever produce one. That pair alone is a session the child server can be started against -- it is exactly what `get_display_xauth_wayland` returns on -- so it outranks a bare `DISPLAY` and ends the retrying, while the rest of the round still looks for something that completes the session. Not specific to the drm build: the function is not feature-gated, and the commit the report points at does not touch it. Claude-Session: https://claude.ai/code/session_01Q5egQpH4q4GoXJiuMoTJ5t Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fd471fcf02 |
fix: show speed in desktop file transfer status (#15980)
* fix: show speed in desktop file transfer status Signed-off-by: fufesou <linlong1266@gmail.com> * fix: move file transfer speed beside progress bar Signed-off-by: fufesou <linlong1266@gmail.com> * fix: move file transfer speed into progress bar Signed-off-by: fufesou <linlong1266@gmail.com> * fix: refine file transfer speed display Signed-off-by: fufesou <linlong1266@gmail.com> * fix: adapt file transfer progress text colors Signed-off-by: fufesou <linlong1266@gmail.com> * fix: reduce file transfer speed text weight Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
7c6e661fcc |
fix(linux): Set AppIndicator ID for tray-icon (#15981)
* set static AppIndicator ID in tray-icon init allows DEs, eg. KDE to 'remember' the user's configuration of tray hidden/unhidden. see: https://github.com/rustdesk/rustdesk/discussions/15208 Signed-off-by: Jade <5164609+gnosticJade@users.noreply.github.com> * Update tray.rs --------- Signed-off-by: Jade <5164609+gnosticJade@users.noreply.github.com> Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> |
||
|
|
3f207e91f6 |
fix(linux): a session logout should hand the peer to the login screen (#15905)
* fix(linux): a session logout should hand the peer to the login screen Logging out closes every window in the session, the connection manager's included, and its close handler kicks every peer with the reason a person gets when they disconnect one by hand. That reason is the one thing the client never retries on, so the remote session dies on a frozen frame instead of reconnecting to the greeter that is already there. The close carries nothing to tell the two apart: measured on KDE, the CM receives no signal and logind still reports the session active at that instant, and the server is killed within a few hundred ms either way, so neither a state check nor a grace period can decide it. What is distinguishable is the ACTION: disconnecting a peer is not the same event as this window going away. So the window-close path now says so, and the server ends the session without poisoning the retry; the Disconnect button and the app's own close control keep kicking exactly as before. Linux only, since that is where a logout closes the window. Verified on plasma/sddm with a client attached: a logout now reconnects to the greeter with no dialog, while closing the manager window still shows Closed manually by the peer. * fix(linux): close the tunnel too, and keep the web build compiling Three seams the first pass missed. The web bridge is hand written, not generated, so the new call needs its stub there or flutter build web stops compiling - and that job is disabled in CI, so it would have gone green. try_port_forward_loop is a second consumer of the same channel and only knew Close, so a forwarded tunnel outlived the window it was supposed to die with. And the variant had landed inside the DRM section, whose comment says everything below it is drm-gated. |
||
|
|
cec4085238 |
Bump aom to v3.14.1 (#15883)
* Bump aom to v3.14.1 * Remove oboe dependency in vcpkg.json |
||
|
|
0d917c6fa1 |
fix: remove dup translations (#15967)
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
893dc27798 | docs(readme): fix broken Screenshots section anchor link (#15964) | ||
|
|
7cc82c1575 |
Add Urdu language support for UI strings (#15961)
* Add Urdu language support for UI strings till 329 line Co-authored-by: Copilot <copilot@github.com> * Add Urdu translations for additional UI strings * Add Urdu language support in lang.rs * Fix Urdu translations and remove unused keys in ur.rs --------- Co-authored-by: Copilot <copilot@github.com> |
||
|
|
f07b6e2338 |
Correct Danish spelling, language and translation (#15943)
* Update da.rs Corrected spelling, language and translation mistakes. * Update da.rs Missed one correction. |
||
|
|
a3bab27a2a | fix: Show My Cursor freezes in View Only mode when remote user mo... (#15936) | ||
|
|
7423dced37 | Update reference from AGENTS.md to @AGENTS.md | ||
|
|
a7deef02a2 |
fix(msi): keep only native ProductCode uninstall entry (#15891)
* fix(msi): keep only native ProductCode uninstall entry Move installer state outside the Uninstall registry path, clean up legacy duplicate entries, and use the MSI ProductCode for updates and uninstalling. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(msi): harden update and uninstall handling - handle legacy EXE updates without an MSI ProductCode - propagate MsiExec uninstall failures - validate and XML-quote custom ARP values Signed-off-by: fufesou <linlong1266@gmail.com> * fix(msi): validate registry state before update and uninstall Signed-off-by: fufesou <linlong1266@gmail.com> * fix(msi): pass WindowsInstaller state to elevated sequence Signed-off-by: fufesou <linlong1266@gmail.com> * fix(msi): block unsupported MSI-to-EXE upgrades - resolve native MSI state and ProductCode safely - suppress reboot while preserving MSI uninstall results - publish the resolved ARP install location - skip invalid unrelated MSI uninstall entries Signed-off-by: fufesou <linlong1266@gmail.com> * fix(msi): fail uninstall when ProductCode is missing Prevent known MSI installations from falling back to EXE cleanup when the ProductCode cannot be resolved. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(msi): do not abort update on ARP version write failure Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
d5a7f67999 | fix appimage pixbuf crash | ||
|
|
e266380ee9 |
fix(appimage): keep the XDG default data dirs on XDG_DATA_DIRS (#15938)
AppRun sets XDG_DATA_DIRS to
"$APPDIR/usr/local/share:$APPDIR/usr/share:$XDG_DATA_DIRS". When the host
leaves XDG_DATA_DIRS unset, the result contains no /usr/share, and setting
the variable at all suppresses the XDG default of /usr/local/share:/usr/share.
gdk-pixbuf 2.43+ (Arch, CachyOS, Gentoo, Fedora, openSUSE) no longer ships PNG,
JPEG or WebP as loader modules; libgdk_pixbuf links libglycin and decodes them
through it, and glycin discovers its loaders in
$XDG_DATA_DIRS/glycin-loaders/<ver>/conf.d/*.conf. With /usr/share missing,
glycin finds none and every PNG decode inside the AppImage fails with
"Unrecognized image file format".
RustDesk sends remote cursors to flutter_custom_cursor as PNG, and that plugin
returns nullptr from a std::string function when the decode fails, so the first
non-default cursor of a session aborts the process:
GdkPixbuf-CRITICAL **: gdk_pixbuf_copy: assertion 'GDK_IS_PIXBUF (pixbuf)' failed
terminate called after throwing an instance of 'std::logic_error'
what(): basic_string::_M_construct null not valid
Debian and Ubuntu compile PNG straight into libgdk_pixbuf and never reach
glycin, which is why this only affects non-Debian hosts.
Append the two XDG defaults so they are present when the host does not provide
them. They go last, so a session that sets XDG_DATA_DIRS properly keeps its own
precedence, and appending is a no-op where those paths are already listed.
Verified on CachyOS (gdk-pixbuf 2.44.7) against a stock 1.4.9 AppImage: with
only this variable changed, a full remote session runs without crashing and
renders remote cursors correctly.
Refs #4565 #5457 #7013 #9164 #10563 #11499 #12257 #14305 #14405 #15625
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
cbf9440281 |
Prefer active X11 session display (#15933)
* Prefer active X11 session display * Update linux.rs * fix(linux): keep the logind display only when it is a local one `get_display_from_session` returns the value pam_systemd was handed at session creation, and logind never updates it afterwards. That value is not always a usable local display: it can be qualified with this host (`myhost:0`), name an X forwarding endpoint (`localhost:10.0`), or be a bare `:`. Taking it unconditionally is worse than taking nothing, because a non-empty `self.display` suppresses every fallback below it, `get_display_by_user` and the `:0` default alike. The stripping at the end of `get_display_x11` does not save the last two cases either: it leaves `:` as is and turns `localhost:10.0` into a local looking `:10.0`, either of which is then exported as DISPLAY and leaves the session unreachable, where before this PR the host got a working `:0`. Strip this host so `myhost:0` is still accepted as `:0`, leave `localhost` in place, and require a display number after the colon. Anything else falls through to the existing chain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKJxvTT6NQDEcnkWBx5bLA * docs(agents): prefer a little duplication over a restructure The "Be minimally invasive" rules already ask for purely additive diffs, but not in the case where the addition would otherwise reshape an existing function so the two can share code. Repeating a few lines is the better diff there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKJxvTT6NQDEcnkWBx5bLA --------- Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6eaac17ac5 | typo | ||
|
|
92eb137178 | feat(terminal): use platform-native copy and paste shortcuts (#15931) |