mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-06 08:01:03 +03:00
44d4fb59b2e5a34cceca325eeb1e53e5c04092ca
11416 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
44d4fb59b2 |
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 |
||
|
|
14dc121d23 |
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 |
||
|
|
8148795f19 |
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 |
||
|
|
a35630b499 |
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 |
||
|
|
cbec70cd6a |
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 |
||
|
|
ccf9afd069 |
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 |
||
|
|
ea7407b73b |
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>
|
||
|
|
7fe4d186e5 |
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 |
||
|
|
d60577e80d |
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 |
||
|
|
704f7495b9 |
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> |
||
|
|
3f5ce9acae |
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
|
||
|
|
b962063f03 |
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 |
||
|
|
06a7cc9239 |
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 |
||
|
|
5618e984b6 |
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 |
||
|
|
578a95289f |
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 |
||
|
|
22c4e080bb |
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> |
||
|
|
585d1fb3ca |
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 |
||
|
|
c313d6dc1c |
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 |
||
|
|
fa399e01ad |
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 |
||
|
|
324b58e04e |
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 |
||
|
|
8ad7c257cf |
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 |
||
|
|
d5cf646db3 |
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 |
||
|
|
3ca5465689 |
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 |
||
|
|
872ec56602 |
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> |
||
|
|
9b986be5a0 |
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> |
||
|
|
4dfef0e632 | fix: preserve WebRTC transport preference | ||
|
|
20d5fd8c58 |
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> |
||
|
|
f9ecc48b2e | fix: route WebRTC ICE through rendezvous paths | ||
|
|
b7f6789a8c | feat: race WebRTC as a direct transport enhancement | ||
|
|
64c2ad4d5a | feat: route WebRTC ICE on controlled side | ||
|
|
a4491c7ad1 | feat: add rendezvous WebRTC signaling fields | ||
|
|
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> |