mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 23:51:04 +03:00
* 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>