mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-29 08:46:32 +00:00
8102f728f146faaec0cddaba35a4ae6599993f66
11494
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8102f728f1 |
perf(keyboard): shortcuts, parse the config once per stored value
The Flutter matcher read the local config three times and parsed its JSON three times on every key press in legacy mode, on Web and in view-only. Keep one parsed snapshot keyed by the raw stored string, so a key press costs one config read and no parsing while the config is unchanged. The existing readers are thin views of the same snapshot. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D |
||
|
|
63654263ed |
fix(keyboard): shortcuts, drop the Toggle Input Source action
The action switches the key capture backend between key down and key up. Fired-key ownership lives in whichever matcher saw the press, the Flutter dispatcher or the Rust set, and there is no handoff between them: after the switch the repeats and the release of the held key reach the other backend, which forwards them to the remote and, while the chord is held, matches the action again and toggles the source back. Remove the shortcut instead of building a handoff for one action. The toolbar radio menu is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D |
||
|
|
001bdbcdd9 |
fix(keyboard): shortcuts, keep a fired key owned across session close
Close tab is itself a shortcut, so the session that consumed the key press is gone before the key is released. Both matchers dropped the key with the session: the Flutter dispatcher removed its own entries in clear(), and the Rust set was keyed by session and cleared in session_close. The release, and the repeats while the key stayed held, then reached the next session's remote as ordinary input. Ownership of a fired key is physical, so it is now one set shared by every session on each side, and closing a session no longer touches it: clear() only drops the pending action and modifier replay, and the Rust session state only tracks released modifiers. A key whose release is missed heals itself on its next press. Entering view-only with the Flutter input source still clears the Rust set, since that source stops routing key events to it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D |
||
|
|
4a60d221c9 | fix(shortcuts): preserve input state and refresh action availability | ||
|
|
c69b2a60e6 |
fix(keyboard): shortcuts, keep fired keys per session
FIRED_KEYS was process-global while dispatch is per session. Closing or entering any session cleared the fired keys of all of them, so a key still held in another session leaked its release to that remote, and a key fired in one session made the same key in another session look like a repeat. Key the fired set by session: try_dispatch resolves the session first and only looks at that session's keys, and session_close clears only the closing session. Entering a session view no longer clears anything: it is a pointer enter, and a fired key may still be physically held across it. A key whose release is missed otherwise heals itself: its next press is consumed as a repeat and that release removes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D |
||
|
|
63fc0a3b20 |
fix(keyboard): shortcuts, track every fired key until its release
FIRED_KEY held a single key, so overlapping shortcut keys broke it: with the chord held, P then C overwrote P, P's release reached the remote, and a P repeat could fire the screenshot again. A repeat after the modifiers were released was also treated as a new press and leaked to the remote. Keep a set of fired keys instead. Once a press fires, every event of that key belongs to RustDesk until its release: repeats are consumed without dispatching, whatever the modifiers do, and the release removes the key. Entries whose release never arrives are dropped when a session view is entered or a session is closed, not by guessing from the modifiers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D |
||
|
|
228bc7e586 |
fix(keyboard): shortcuts, keep an empty binding list on re-enable
setEnabled seeded the defaults whenever the binding list was empty, so a user who cleared every binding got all defaults back after disable and enable. Seed only when the config has never held a bindings list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D |
||
|
|
c133948a91 |
fix(keyboard): shortcuts, record the physical key
The recording dialog and the Web Flutter matcher named keys by LogicalKeyboardKey, while the native matcher sees the physical key through its USB HID usage and the Web JS matcher through KeyboardEvent.code. On AZERTY, QWERTZ, Dvorak and similar layouts a recorded binding then did not fire, or fired from another key. Name keys by their physical position everywhere: physicalKeyName maps the USB HID usage to the stored name. A new fixture pins every name to its USB HID usage, checked on the Dart side and against rdev::usb_hid_key_from_code on the Rust side, and a test covers AZERTY and QWERTZ events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D |
||
|
|
e6704be093 |
fix(keyboard): shortcuts, keep local modifiers and release keys on the right session
When a chord fired, the shortcut path reused release_remote_keys(), which replays synthetic releases through process_event(). That cleared the local MODIFIERS_STATE while the user still held the modifiers, so the next key of a held chord no longer matched and leaked to the remote, and it sent the releases to the globally current session instead of the one that typed the chord. The shortcut path now releases the remote keys itself: it sends through the caller's session, and restores the modifiers it released on the remote as still held locally. The press that fired is remembered until its release, so its auto repeats and its release are consumed instead of reaching the remote. Also state the contract for unavailable actions: a bound chord always belongs to RustDesk and is a no-op when the session has no handler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D |
||
|
|
4387e1eb3d |
langs: add shortcut keys to az, gl, ur
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D |
||
|
|
7899f2fb16 |
fix(keyboard): iPad, icon
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
1e82f7bb44 |
feat(keyboard): shortcuts, debug web
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
cd9d6abd87 |
feat(keyboard): shortcuts, release keys before shortcut callback
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
a4be67e9be |
feat(keyboard): shortcuts, color of "Reset to defaults"
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
f6ed6f7cd8 |
fix(keyboard): shortcuts, harden config and callback lifecycle
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
26b9f37eb0 | langs | ||
|
|
e872f6acaa |
feat(shortcuts): user-configurable keyboard shortcuts for session actions
Adds a keyboard shortcut feature (Rust matcher + Dart UI + cross-language
parity tests) that lets users bind combinations like Ctrl+Alt+Shift+P to
session actions. Bindings are stored in LocalConfig under
`keyboard-shortcuts`; the matcher gates dispatch on `enabled` and
`pass_through` flags so flipping the master switch off is a hard stop.
Wire-up summary:
- src/keyboard/shortcuts.rs: matcher, default bindings, parity test against
flutter/test/fixtures/default_keyboard_shortcuts.json
- src/keyboard.rs: shortcut intercept in process_event{,_with_session},
feature-gated to `flutter`; runs before key swapping so users bind to
physical keys
- src/flutter_ffi.rs: main_reload_keyboard_shortcuts +
main_get_default_keyboard_shortcuts; reload_from_config seeded in main_init
- flutter/lib/common/widgets/keyboard_shortcuts/: shared config page body,
recording dialog, shortcut display formatter, action group registry
- flutter/lib/desktop/pages/desktop_keyboard_shortcuts_page.dart and
flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart: platform
shells around the shared body
- flutter/lib/models/shortcut_model.dart: per-session ShortcutModel +
registerSessionShortcutActions for actions with no toolbar TToggleMenu /
TRadioMenu (fullscreen, switch display/tab, close tab, voice call, etc.)
- flutter/lib/common/widgets/toolbar.dart: optional `actionId` field on
TToggleMenu / TRadioMenu, plus per-helper auto-register pass that wires
tagged entries' existing onChanged into the ShortcutModel
- flutter/test/keyboard_shortcuts_test.dart + fixtures: cross-language
parity (default bindings, supported key vocabulary)
Design principles applied during review:
1. Additions are fine; modifications to original logic must be deliberate.
Tagging an existing TToggleMenu entry with `actionId:` is an addition.
Rewriting its onChanged to satisfy a new contract is a modification —
and was reverted for every case where the original click behavior was
working. Four closures were touched and then reverted (mobile View
Mode, Privacy mode multi-impl, Relative mouse mode, Reverse mouse
wheel); their shortcuts are wired via standalone closures in
shortcut_model.dart instead.
2. Toolbar auto-register is reserved for entries whose onChanged is
inherently self-flipping — typically `sessionToggleOption(name)` where
the named option is flipped in place and the input bool is unused. The
register pass passes `!menu.value` from registration time, which is
harmless under self-flipping but wrong for closures that consume the
input bool directly. Tagging a non-self-flipping entry forces a closure
rewrite; choose non-toolbar registration in that case.
3. When shortcuts are disabled, toolbar behavior must be bit-for-bit
unchanged. The matcher's `enabled`-gate already guarantees no
dispatch; the auto-register pass is left unconditional (its only effect
is HashMap operations on a separate ShortcutModel) so mid-session
enable works without a reconnect. The trade-off is intentional and
documented at the top of toolbarControls.
4. Comments stay terse. Rationale lives in one place — the doc comment of
the helper or registration site, not duplicated at every call site.
5. Where an existing helper needs a new optional behavior (e.g.
`_OptionCheckBox` gaining a tooltip slot), the new branch must reduce
to byte-identical output for existing callers (`trailing == null`
case → original `Expanded(Text)` layout). Verified.
6. Action IDs and labels stay consistent. Renamed `reset_cursor` →
`reset_canvas` so the action ID matches its user-facing label
("Reset canvas") and capability flag.
Out-of-scope but included:
- AGENTS.md: documents flutter_rust_bridge no-codegen workflow and the
Web target's hand-written TS client, since both are load-bearing for
any new FFI work.
- remote_toolbar.dart: i18n fix for the per-monitor tooltip ("All
monitors" / "Monitor #N"), unrelated to shortcuts but kept here.
|
||
|
|
0efa6151ef |
fix web break introduced in 38f130071 fix(linux): enable mouse side buttons in remote sessions (#14848)
|
||
|
|
a8ae0d6d66 |
Review and complete pt_PT.rs translation (European Portuguese) (#16311)
* Update pt_PT.rs * Update pt_PT.rs * Update src/lang/pt_PT.rs fiz misspelling of "Apenas" Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Fix config_screen: replace trailing slash with period Per CodeRabbit's suggestion — the slash was rendered directly in the permission prompt text, not as a line continuation, so it read as a stray character instead of proper punctuation. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix terminal-clipboard-write-tip: translate full 3-sentence source text Per Greptile's review — the previous translation for this key only covered a short summary (matching an equally incomplete ptbr.rs entry), not the full English source defined in en.rs, which explains the permission's scope (persists across all connections until disabled in Settings) and that manual copy/paste is unaffected. Translated from the actual en.rs source text as required by AGENTS.md's localization guidelines. --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
2e333f64a4 |
Update sc.rs (#16314)
* Update sc.rs * Update sc.rs |
||
|
|
e424b57fca |
Kx v1 (#16326)
* key exchange version 1 on the rendezvous and peer handshakes Bump hbb_common for key exchange version 1, one stream key per direction, and negotiate it on both handshakes. Rendezvous: `secure_tcp` reads the version the server advertises in `KeyExchange.version`, answers with the highest both speak, splits the key when that is at least 1, and arms the check of the server's echo on its first encrypted message. Peer: the controlled side advertises `KX_VERSION_LATEST` inside the signed `IdPk`, the controller answers in `PublicKey.kx_version` and both split the key when the pick is at least 1. A pick above what was offered is refused as a bug or tampering. `decode_id_pk_dtls` returns the advertised version along with the fingerprint. An absent field on either side is version 0, so any old peer or server keeps today's stream byte for byte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * bump hbb_common: keep the advertisement check armed until an echo arrives Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * bump hbb_common: drop box_pk_of until something calls it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * key exchange: say 0 on WebRTC, where no stream key applies A WebRTC stream is encrypted by DTLS and `set_key_split` collapses to `set_key` on it, which sets nothing but `peer_verified`. Both sides still put 1 on the wire, the controlled peer in its signed identity and the controller in its pick, and agreed on a version neither applied. That held only because both fell into the same branch: the day one side splits keys on WebRTC and the other does not, they would agree on 1, derive different keys, and have no version mismatch to point at. The controlled peer now advertises 0 on WebRTC and refuses a pick above what it advertised rather than above the newest it speaks anywhere; the controller picks 0 there. What is on the wire is what runs. Also bumps hbb_common for the echo contract: the server tags every message after the exchange, not the first alone, so dropping one frame at a known position does not rid an attacker of the downgrade check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * key exchange: verify the server's signed version before trusting it bump hbb_common d9f519e: KeyExchange.signed_version A server that signs its version sets bit 255 of the ephemeral key inside the signed keys[0]. A client that sees that bit, or the field itself, verifies sign("rdkx-ver" || key || version LE) under the server's signing key before picking a version and refuses the exchange otherwise, so a version lowered in the clear is caught at the handshake rather than one frame pair later by the echo. A legacy server sends canonical keys and no field and takes the unchanged path; the echo still checks its version. Tests: version 1 keys match the server both ways; an advertisement lowered in transit is refused at the first echo; legacy, signed-1 and signed-3 servers each exchange application data; nine tamperings of the signed version are refused before the client replies. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: comments say what the fields and functions are bump hbb_common e3452ac: the same on the shared side. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: drop the advertisement echo bump hbb_common 5194159: RendezvousMessage.kx_advertised and the check on decrypted frames are gone. signed_version settles the version inside the exchange, so the client arms nothing after it; the stub in the tests stops tagging its frames and the test of a lowered advertisement goes, the signed-version cases covering that refusal before any reply. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: verify KxParams, the server's signed structure bump hbb_common e74a188: KeyExchange.signed_params and KxParams. The client parses the signed bytes as KxParams and compares its fields to the key it verified and the version it was shown, rather than matching a fixed byte string, so a field added to KxParams later parses past an older client. The tamper cases now include a payload signed as the old byte string. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: require the KxParams signing prefix before parsing bump hbb_common 26582e1: KX_PARAMS_DOMAIN. Without it a signed IdPk, which the server signs with the same key, parsed as a KxParams whose pk was the id and whose version was 0, so an id registered with the bytes of a marked ephemeral key could stand in for the params at version 0. Two tamper cases pin this: params signed without the prefix, and a signed IdPk in their place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: one call sets the negotiated key bump hbb_common cbc8772: Stream::set_negotiated_key. The rendezvous client, the controller and the controlled side each branched on the picked version to choose between split and single keys. They now hand the transcript to Stream, which makes that choice once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * Update hbb_common: refuse unknown key exchange versions, pin wire vectors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * client: prove the peer handshake picks its version and encrypts under it The rendezvous exchange had tests against a stub server; the peer handshake had none, so a controller that fell back to version 0 would still connect, still report secured, and pass every test. These run Client::secure_connection against a stub controlled peer and check the pick it sees and the application data both ways: new peers pick 1, a peer without versions gets 0, and a pick lowered in transit leaves the two sides unable to read each other. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * Update hbb_common: pin the subkeys for an advertisement above the pick Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * Update hbb_common to main, with #614 merged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: name the picked version, say why the marker bit is safe Review follow-ups with no change in behaviour: the rendezvous pick is called picked, as in the peer handshake and KxTranscript; the marker comment says X25519 ignores the bit and the bytes stay as signed; the controlled side's refusal names the version it offered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b30f781fd6 | fix: restrict VRAM stall detection to AMD SDK (#16319) | ||
|
|
68a35e65f2 |
fix(unix): ignore SIGPIPE in native Flutter entry (#16322)
* fix(linux): ignore SIGPIPE in Flutter runner Initialize SIGPIPE before entering the Rust core so writes to closed IPC peers return EPIPE instead of terminating the server. Add a native-entry regression test for the failure reported in #16297. * test(linux): remove SIGPIPE runner test * fix(flutter): initialize SIGPIPE in the Rust native entry Share SIGPIPE initialization between the Linux and macOS native runners before core startup. Remove the Linux C++ initialization. * fix: share SIGPIPE initialization with Sciter startup Initialize SIGPIPE before global_init in core_main, shared by Flutter and Sciter on Linux and macOS. * fix(flutter): keep SIGPIPE setup in the native FFI entry Sciter already receives SIGPIPE initialization from the Rust runtime. Scope the missing startup initialization to native Flutter runners on Linux and macOS. |
||
|
|
7c21b4569c |
test(drm): bound the excused shapes at their worst angle, not only on average (#16320)
Both of the review follow-ups on rustdesk#16242. One comment still said alias was 0.11 px worse than master. That was the number before the directional rule; it is 0.51. And the exemption for help and alias was bounded only on the average over the angles master handles. The worst single angle is the larger number and the one that would be felt - help is 8.00 px under master at 270 and 21.54 here - so leaving it unbounded let the exemption widen quietly. Both directions are bounded now, at the measured values: alias 0.51 mean and 8.94 at 90, help 4.73 mean and 13.54 at 270. Tests only. |
||
|
|
c6a358afbe |
Update lang.rs - Splits ambiguous pt into pt-pt and pt-br (#16155)
* Update lang.rs - Splits ambiguous pt into pt-pt and pt-br Splits the ambiguous "Português" entry into two explicit options: Português (Portugal) [pt-pt] and Português (Brasil) [pt-br]. The pt_PT.rs translation file already existed in the repo (fully translated, key-complete) but was never wired into lang.rs. Kept the old "pt" and "br" locale codes mapped to ptbr::T for backward compatibility with existing user configs. * Preserve pt-PT/pt-BR region in automatic locale resolution Preserve pt-PT/pt-BR region in automatic locale resolution * Fix pt-pt detection: check region suffix instead of redundant "pt" substring Fix pt-pt detection: check region suffix instead of redundant "pt" substring * Normalize legacy pt/br saved lang option to pt-br Normalize legacy pt/br saved lang option to pt-br * Normalize legacy pt/br lang option on Web client * Add regression tests for pt-pt/pt-br locale resolution * Follow CLDR inheritance for Portuguese regional locales (pt-AO, pt-MZ, etc.) |
||
|
|
458ffe44c6 |
Update Ukrainian README (#16309)
* Start Ukrainian README terminology update Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> * Synchronize Ukrainian README with current documentation Update content, terminology, build instructions, badges, and project structure to match the current English README. Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> * Polish Ukrainian README dependency wording Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> * Clarify Docker build argument placement Address review feedback by explaining where optional build arguments should be appended to the docker run command. Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> --------- Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> |
||
|
|
2e7255f083 |
fix: suppress heartbeat HTTP retry and TCP proxy logs (#16273)
* fix: suppress heartbeat HTTP retry and TCP proxy logs Signed-off-by: 21pages <sunboeasy@gmail.com> * refactor: move API log interval to common module --------- Signed-off-by: 21pages <sunboeasy@gmail.com> |
||
|
|
12f61b1fda |
Fix/drm rotated cursor (#16242)
* fix(drm): turn the cursor by the output's real angle, and guess its hotspot upright Two things were wrong with a rotated output's cursor, and the maintainer's physical test found both. The bare-metal hotspot is a guess - the top-left of the sprite's opaque box, since HOTSPOT_X/Y only exist on para-virtualised drivers - and it was guessed on the sprite as scanned out, then mapped as if it were a point on it. A turned arrow's tip is never in that corner: at 90 the guess is off by the arrow's width along x, small enough to pass as fine, and at 270 by its height along y, which is what was noticed. Measured on an i915 box with a 64x64 Adwaita arrow, hotspot against tip: +11,0 at 90 and 0,+18 at 270 before, 0,0 at every angle after. A guessed hotspot is now guessed on the upright sprite, in the consumer, and the wire says whether the driver measured it so a real one still maps as a point. And 180 was clamped to 0 for the whole session because a hardware-rotated primary plane scans out upright. That holds for the frame and not for the cursor: the compositor pre-rotates the sprite in software either way, so it arrived upside down. The frame keeps the clamp; the cursor gets the real angle. (cherry picked from commit 6a73100b4783fbf9c8f43e0dc7651b7568db9ea9) * test(drm): drive the cursor path, and drop the field nothing reads The two tests called the helpers, so nothing pinned the production decision: restore master's `t == 90 || t == 270` in deliver_drm_cursor and both still pass while the 180 sprite goes back to upside down. The new test drives deliver_drm_cursor and reads back what it published, and it fails under exactly that mutation. The other way to reintroduce the bug was to point the receive thread at `Shared::transform` again. That field had no readers left once both gates moved to `cursor_transform` - it was stored and never loaded - so it is gone, and the doc above TRANSFORM_PENDING now names the field that actually holds a racing cursor back. Also four comments that said more than the code does: the sprite is treated as pre-rotated at every angle because wl_output cannot say otherwise, not because the compositor provably always does it; the old flow was off at every angle, not just at one of 90/270; the 180 symptom had its own cause, the clamp, rather than all of it coming out of the hotspot guess; and the test arrow's opaque box is its left three columns, not the whole bitmap. * docs(drm): say what the 180 measurement actually showed The loop asserts the old mapping was wrong at 180 too, and that is true of the mapping, but master never ran it there: the clamp sent 180 down the untouched branch, so guess and sprite turned together and the hotspot landed on the tip. The bench measured exactly that - (0,0) at 180, with only the sprite upside down. The comment said the old flow was wrong at every angle without that distinction, which is the kind of gap between a claim and a measurement worth closing before someone else finds it. * fix(drm): centre the guessed hotspot of an I-beam lying either way The bounding-box guess only centred TALL shapes, so a HORIZONTAL I-beam was handed the top-left corner of its box. Adwaita's `vertical-text` is exactly that shape: at 24 px its opaque box is 20x9 and the theme's own hotspot is (12, 11), its centre. The corner guess lands 11.2 px away; the centre lands 1.4 px away. It matters more since this branch turns the sprite upright before guessing. Such a cursor used to reach a quarter-turned output as a TALL bitmap, where the tall-only rule centred it by accident; guessing on the upright bitmap - correct in itself, and what fixes arrows at every angle - is what exposed the asymmetry. Found by fufesou at pixel level on rustdesk#16242, and reproduced here against the installed theme before changing anything: same declared hotspot, same bounding box, same two errors. Then checked across ALL 35 shapes of that theme at 24 px: making the aspect test symmetric moves exactly one of them, `vertical-text`, from 11.2 px to 1.4 px, and leaves the other 34 byte-identical. The old flow is not the fix. It was right for this one shape and wrong for arrows at every angle, which is what `a_guessed_hotspot_is_guessed_on_the_upright_sprite` pins; the fix belongs in the guess, not in the order of operations. The regression test that missed this used only `arrow()`, as fufesou pointed out. Three things the first version of the replacement got wrong, all of them in what it CLAIMED rather than in the fix: - Its expected point was the centre of the BITMAP, which equals the centre of the opaque box only because the fixtures filled their bitmaps symmetrically. Both I-beam fixtures are now off-centre in their buffers, the way a sprite sits in a 64x64 hardware cursor, and the test asserts up front that the two centres differ so the fixture cannot quietly stop discriminating. - It called the arrow's box "elongated by less than the factor of two", when 3x6 IS the factor of two and the arrow keeps its tip only because the comparison is strict. That makes the arrow the control for one side of the threshold, and it is now described as such. - It said the case was driven "through the whole delivery path" while chaining the helpers itself. It now calls `deliver_drm_cursor` and reads the published hotspot back out of DRM_CURSOR, at 0, 90, 180 and 270, feeding it what the producer really sends for a guessed hotspot: the producer's own guess, made on the sprite as scanned out. And the threshold was pinned from one side only. A third fixture carries the real 2.22:1 aspect of the shape that was wrong, so a stricter rule cannot pass on 6:1 bars while silently un-centring the actual cursor. Mutations, each restored byte-for-byte afterwards: back to tall-only, the bitmap centre instead of the box centre, the threshold loosened to one and tightened to three. All four fail the test. * build(drm): point the pin at the synced mirror and drop the helper fallback The mirror is synced, so the pin moves from 5da68a3a (libdrmtap 0.5.4) to 49b204f (0.5.6), which is what rustdesk-org/libdrmtap now carries. -Dhelper=disabled, as the maintainer asked on #16242. rustdesk never uses the privileged helper: every drmtap_open lives in src/ipc/drm.rs, i.e. the root service, which already holds CAP_SYS_ADMIN, and the unprivileged side opens a render node instead. What the library carries without the option is a fallback that walks six hardcoded paths, two of them under /usr/local, and execs the first one that passes access(X_OK) -- no check of its owner, no check of its mode -- from inside the ROOT process. The option compiles that path out. Asserted on the artifact as well as passed as a flag, for the same reason the EGL check is: a flag cannot notice a stale build-pkg or an object substituted by hand, and meson accepts an unknown -D silently on versions predating the option. Measured on the produced .so: socketpair 4 -> 0, the helper search paths 3 -> 0, and nm -D shows no fork, execl or socketpair import. The check fails as it should when pointed at a .so built with the helper. * fix(drm): use the library's hotspot provenance, and keep legacy IPC meaning Answers the two blockers in the maintainer's review of |
||
|
|
6a0eb9a1fd |
fix(terminal): preserve trackpad scrolling in alternate screen
Prevent the inner terminal scrollable from accepting user scrolling in the alternate buffer, so xterm's existing outer handler receives trackpad gestures after main-screen history. Keep wheel encoding and drag reporting unchanged. Cover trackpad forwarding, subsequent drag coordinates, arrow fallback, and return to local scrollback. |
||
|
|
763d4eeb05 |
bump hbb_common: tungstenite 0.29 forks, read buffer grown as bytes arrive
hbb_common 7dc56d6..d9895ff, one commit: tokio-tungstenite and tungstenite up from 0.26 to rustdesk-org's rustdesk-patches-0.29 branches. The tungstenite fork grows the read buffer a read_buffer_size chunk at a time as bytes arrive, where upstream reserved the whole length a frame header declared. It is pinned here at the workspace root, the one place cargo reads [patch], so hbb_common's copy and tokio-tungstenite's resolve to the same one. The lockfile moves only those two crates and drops utf-8, which 0.29 no longer pulls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
a5d4ef97e8 |
fix(ws): reject text frames after encryption is enabled (#16295)
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
dd9b21cf86 |
server: hold an unauthenticated connection to a small message (#16254)
* server: hold an unauthenticated connection to a small message Until a peer authorizes it sends only a public key, a login request, a test delay and a close reason, none of them large. Nothing said so: a frame header could declare up to whatever the transport allowed, 1 GiB on TCP and WebRTC, and a connection holds its place for up to LOGIN_GRACE before it has to authorize. With MAX_UNAUTHORIZED_CONNS places to fill, that is 64 GiB of header-declared payload one peer could make us hold - or, on WebSocket, 1 GiB bought outright with a few hundred bytes of frame headers, because tungstenite reserves a frame's declared payload as soon as it passes max_frame_size. The cap goes on in create_tcp_connection, before the identity handshake, so that read is bounded too, and comes off once the login is settled. It comes off before connect_port_forward_if_needed rather than beside the rest of authorization: a multiplexed tunnel narrows the same knob again for its own framing and has to have the last word. 128 KiB is several times the largest login request anyone sends - a long hostname, an os_login, an avatar URL, a file-transfer path - and is also the read buffer tungstenite allocates per WebSocket connection whatever we do, so on that transport the bound costs nothing beyond a floor already paid. A server hands that avatar out as a URL; only a custom client that inlines an image into the avatar option instead can reach the bound at all. Together with MAX_UNAUTHORIZED_CONNS it holds every unauthorized connection to 8 MiB. Redis answered this same shape in CVE-2021-32675 with 16 KiB, tighter because a per-message bound is the only one it has; here the connection count is the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * server: test that the unauthenticated cap is on before the handshake reads hbb_common covers each transport's cap; nothing covered where the connection layer puts it. A header one byte over MAX_UNAUTHORIZED_MESSAGE, written to a connection stalled in the identity handshake, has to end the handshake on the header alone and release the connection's place: the test fails with the cap moved past the handshake or dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * client: hold the clipboard until this round's login is accepted The clipboard listener is one per process, started by the first session to log in, and its broadcast went to every session - one whose login was still waiting on a password, 2FA or the peer's consent included. What the user copied meanwhile went to a machine that had not admitted them; the peer drops it unread before authorization, but it is in that peer's hands. The check goes on the round, not the session: a session-level one reads a state and later a sender that a reconnect can have swapped in between, so a broadcast that passed for the round before could still queue on the next. Remote is the round - its queue, and is_connected set once its own PeerInfo is in - so a Clipboard or MultiClipboards that reaches handle_msg_from_ui before then is dropped there, on the line before it would go out. What the login itself sends through the same queue, Auth2FA among it, goes as before. The unauthenticated cap on the other side is how this came up: a clipboard over 128 KiB ended such a login with "Reset by the peer". The small case had always gone through quietly. A test drives a round's Remote over a loopback pair before any login: a clipboard does not reach the far end, a 2FA code does, and once the round is connected the clipboard does too. 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> |
||
|
|
97811acbdd | Update Dutch translation (#16284) | ||
|
|
7a367f7cc6 |
bump webrtc fork: one IPv6 host candidate per interface and prefix (#16263)
* bump webrtc fork: one IPv6 host candidate per interface and prefix ICE gathered every IPv6 address on every interface. Beside the temporary address that privacy extensions rotate, a prefix usually carries a stable one, often derived from the MAC, that the OS never picks as a source: a host candidate for it hands the peer an identifier that outlives every rotation and that nothing else this machine sends out ever shows. RFC 8445 §5.1.1.1 has the trackable addresses of an interface and prefix left out once a privacy one is gathered. There is no portable way to tell the two apart, so the fork's `local_interfaces` stands in for that rule with a best-effort approximation: of an interface's addresses in one prefix, it asks the OS which one it sends from - a UDP `connect` inside the group's own prefix, nothing sent, nothing outside this machine's own prefixes involved - and if the answer is one of them, keeps that one alone. If the answer is none of them, the whole group is kept, as it was: the probe is bound to no interface, so where Ethernet and Wi-Fi share a LAN the route picks one of them and the answer for the other is an address it does not hold, and the enumeration order would be no better a guess - on macOS its first address is the stable one. Every other interface and prefix keeps its address, a VPN's unique-local one among them. Two static addresses in one prefix keep one, the recorded price of the stand-in; the interface a shared prefix's route bypasses keeps both of its addresses, a gap the stand-in leaves open rather than a regression. The Windows enumeration, which named every adapter "" with no mask, now carries the adapter's name and on-link prefix, without which Ethernet and Wi-Fi on one LAN would have been a single group. That reaches IPv4 too: its addresses carry the adapter's name and mask where they were "" at /32, so the candidates gathered are the same but `interface_filter` sees the real names. hbb_common is untouched: its fe80::/10 filter still applies to what the fork keeps. Two more fork commits ride along, found by the same review. SCTP never reported a DATA chunk received again: `handle_data` asks `can_push` before `push`, and `push` was where a duplicate was noted, so the no-cwnd sender's reordering window, which widens on reported duplicates, never heard of one from another of these endpoints; and the SACK, its gap blocks and now its duplicates unbounded, could outgrow the MTU the DATA chunks keep to under a thousand chunks in flight with holes among them. Duplicates are now listed and SACKed at once (RFC 9260 §6.2), and the SACK reports the lowest gap blocks that fit (§6.7). And the Windows adapter struct, split into nested parts, read `Ipv6IfIndex` eight bytes late on x64 - only into a scope id that `Interface::convert` drops, so nothing gathered wrongly, but the field an interface-bound probe would need; it is flat now, with a compile-time check written for Rust 1.75, the version this crate builds with - `offset_of!` would have wanted 1.77. rustdesk-org/webrtc 80d5a20..49c89bd8, six commits: the heuristic, the fail-open it grew in review, its comments brought in line with that, the SCTP duplicates and SACK bound, the flat Windows adapter struct, and its check made to build on 1.75. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * pin hbb_common to main 7a5ad52 0eb1759..7a5ad52, six commits: the message cap's tests on WebSocket and WebRTC with the fragmented WebSocket message bounded too (e999dce, f0f1548); hyper_util's debug logs out of the default filters (#608); and `new_direct_udp_for_unverified`, the controller's UDP socket on the resolver's preferred address for the rendezvous server, with a second for the lookup and without the TCP connection `test_target` opens and drops to prove it - while `new_direct_udp_for`, `new_udp_for` and `rebind_udp_for`, which the controlled side's punch reply and its registration live on, keep that proof (f688d41, 0bc8336, 7a5ad52). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * ipv6 punch: ask the kernel for the route without resolving a name first `test_ipv6` is awaited on the connection path by both sides of a punch, and before it looked for a public IPv6 address in the background it resolved the STUN hosts' names inline - racing the four, so that one resolver that hangs would not decide. It could still: `select_ok` returns the first success or the last failure, so with no resolver answering the probe waits for the slowest lookup to give up, as long as the system resolver takes, once a minute, on the first connection of that minute. The name was never needed. `connect` on a UDP socket sends nothing; it has the kernel pick a route and a source address for the destination, and any global address serves, so the probe now names one - the one libwebrtc's QueryDefaultLocalAddress asks for - and touches no network at all: a bind, a connect, a local_addr. A machine without an IPv6 route learns so from the connect's error, at once, as before. Two smaller things beside it. The minute's gate read the timestamp under one lock and set it under another, so two connections arriving together both found it over and both probed; it is one critical section now. And the background STUN probe, bounded so far by the STUN client's own ten seconds and the resolver's, has a deadline of its own, five seconds: a probe that outlived the minute could write an earlier network's address over a later probe's. Test: the route probe completes within a second, an address found or not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * controller: the NAT test takes the resolver's address for the server `new_direct_udp_for` learned which address to send the UDP NAT test to by opening a TCP connection to the rendezvous server and dropping it - a handshake, one round trip, on every connection the controller starts, right before `_start_inner` opens the connection it keeps to the same host. The NAT test now takes the resolver's preferred address, `new_direct_udp_for_unverified`, which spends no round trip proving it and gives its lookup a second: an address the server does not answer on costs this connection its UDP punch, which the TCP punch and the relay cover, as they do whenever the test finds no port. The controlled side's punch reply keeps `new_direct_udp_for` and its proof - a reply sent to such an address is a device online and unreachable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
ea04f04f9d |
add hide-elevate-button-in-accept-window (#16271)
A portable client handed to standard users offers them "Accept and Elevate", which they have no credentials to complete. The builtin option takes that button out of the accept window and leaves elevation to the controlling side's "Request Elevation" during the session. https://github.com/rustdesk/rustdesk-server-pro/discussions/1016 Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ef00ed78a7 |
Update missing Ukrainian UI translations (#16272)
* Update Ukrainian translations Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> * Complete Ukrainian voice call translation Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> --------- Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> |
||
|
|
5278fcab68 |
fix(cursor): correct native cursor sizing and validate received images (#16213)
* fix(flutter): shrink the unzoomed remote cursor by DPR on macOS and Linux With "Zoom cursor" off in Adaptive or Custom view, the remote cursor bitmap was registered at scale 1.0. NSCursor and GdkCursor treat the bitmap size as logical pixels, so on a HiDPI controller the cursor was drawn DPR times larger than in Original view (which already passes 1/DPR) and than on Windows (whose cursor path is in physical pixels). A HiDPI remote such as KDE Wayland sends a 48-64 px bitmap, which then showed up 3-4x too big on a Retina Mac. Scale the bitmap by 1/DPR in that case, and scale the Flutter-painted cursor used while the peer moves the mouse the same way so its size does not jump. The new branch is an identity at DPR 1 and the Windows paths are untouched. Fixes https://github.com/rustdesk/rustdesk/discussions/15363 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8HSHTEHo27mDcJXVyVfSP * fix(flutter): check the cursor height against the min cursor size `_checkUpdateScale` computed the scaled height from `width`, so the min-size clamp never looked at the height. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8HSHTEHo27mDcJXVyVfSP * fix(flutter): keep the painted cursor hotspot in place when zoom cursor is off `CursorPaint` subtracted the hotspot in remote pixels and then scaled it by the canvas scale, but drew the image at scale 1.0, so the hotspot landed hotx * (1 - scale) logical pixels away from the remote cursor position. Cursors with a centered hotspot (I-beam, crosshair) were off by up to half their size in Adaptive view. Subtract the hotspot after scaling the position instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8HSHTEHo27mDcJXVyVfSP * fix(flutter): read the live DPR and clamp the painted cursor like the native one CanvasModel caches devicePixelRatio and only refreshes it when the view style changes, so after the window moves to a monitor with a different DPR the unzoomed cursor kept the previous monitor's scale. Read it from MediaQuery instead, which also rebuilds the cursor when it changes. The native path clamps the scaled bitmap to kMinCursorSize; apply the same clamp to the painted cursor so a small cursor does not change size when the peer moves the mouse. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8HSHTEHo27mDcJXVyVfSP * build(cursor): declare existing Zstd dependency for bounded decoding * fix(cursor): validate and bound received cursor images * fix(cursor): preserve thin cursor sizes when scaling * fix(cursor): limit view changes to native cursor sizing * fix(cursor): apply long-edge minimum to Web cursor sizing * fix: preserve Linux cursor alpha and match Windows Custom scale * fix(cursor): keep scaled buffers and raster dimensions in sync * fix(cursor): preserve Windows peer alpha when resizing * fix(cursor): preserve mixed alpha during downsampling * comments Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): match desktop cursor size to peer display density * refactor(cursor): clarify desktop and web scale branches * fix(cursor): correct adaptive display scaling and fixed cursor size * fix(cursor): apply all-display adaptive density on every desktop * fix(cursor): normalize unzoomed all-display cursor density * fix(cursor): synchronize original view on DPR changes * fix(cursor): match original zoom to the active renderer * fix: align Wayland software scrolling with input coordinates * fix: preserve scroll offsets when switching scrolling modes * fix: correct cursor size and pointer mapping with custom scale Apply the hovered Linux display density to Custom cursor zoom in All Displays. Refresh scroll fractions after layout so changing the Custom percentage uses current scrollbar extents and detached controllers. Validated with macOS and Windows component tests, formatting, and static analysis. * fix(linux): pad tall native cursors to prevent clipping * fix(cursor): preserve macOS point size in unzoomed views * fix(linux): pad rectangular cursors to square canvases * fix(cursor): divide dpr on Linux -> macOS Signed-off-by: fufesou <linlong1266@gmail.com> * fix: cursor size test Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): cursor size of controlled side macOS Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): limit received cursor allocations * test(cursor): retain reverse raster transition coverage * fix(cursor): bound compressed cursor input * fix(cursor): restrict the scaled size of the cursor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): align hotspots with resized raster dimensions Calculate each hotspot axis from the actual raster-to-source ratio. Update existing boundary tests and run cursor tests in Flutter CI. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): align Sciter hotspots with raster dimensions Calculate native and overlay hotspots from the final raster size. Preserve input coordinate scaling and cursor refresh order. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): use `1.0` instead of `1.0/dpr` on Linux -> macOS, Zoom off, Scale adaptive The mouse cursor currently appears somewhat large. However, this is difficult to adjust because the cursor size is fixed while the window size varies; its relative size depends on the specific desktop environment. We can modify it if users actually provide feedback. Ideally, we should check the "Zoom cursor" option. Further adjustments may also be needed later based on cursor density. Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: rustdesk <info@rustdesk.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
53ec874c39 | Translate terminal clipboard tips to Chinese (#16266) | ||
|
|
04974591b8 |
Refine Portuguese translations for user options (#16264)
Updated Portuguese translations for clarity and conciseness. |
||
|
|
4da167517e |
Turkish: translate the three empty entries (#16261)
terminal-clipboard-write-tip, "Allow terminal apps to copy to clipboard" and the voice-call hint were empty, so Turkish users saw the English text. |
||
|
|
d4ac2c07b7 |
temporary password: rotate when a peer is let in, not when it leaves (#16080)
The one-time password was regenerated after the connection loop exited, so a remote desktop session kept it valid for hours and a port-forward tunnel for as long as its mapping lived. It now rotates the moment a connection becomes authorized. Reconnects and windows opened from a live session are unaffected: they log in on the password the session remembers, for 30 seconds past its last activity. Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0b3a1ddd80 |
rendezvous: cap unauthenticated punches in flight (#16256)
A PunchHole needs no authentication: anyone who knows this id can ask hbbs to have us open a punch, and each one hbbs sends is spawned without a limit. Every punch pays per request. punch_udp_hole binds a socket of its own and start_ipv6 another; the TCP punch opens a listener on the ephemeral port of its own connection to hbbs, with a punch of its own in flight beside it - `reuse` sets the socket flags, it shares no fd - and the LAN listen a FetchLocalAddr opens is that listener again. All of them then wait up to CONNECT_TIMEOUT for the peer, so a stream of requests holds as many sockets as it likes for as long as it keeps sending. Each takes a place in one of two pools of 32 - UDP over v4 and v6 in one, the TCP punch and the LAN listen in the other - and gives it back the moment the peer's session is up - the KCP accept, the TCP stream in hand - or when the wait ends without one: a place stands for a socket waiting for its peer and nothing past it, and from the session on the connection layer's own limits apply, the same handoff the WebRTC answerer's slot makes at its open data channel. At the limit an arrival is declined rather than an older punch cut short, since the places turn over on their own, within CONNECT_TIMEOUT and the few seconds the punch phases add to it, and cutting one short would drop a socket that may be a moment from carrying a session. Two pools rather than one, so that neither transport pays for the other's crowd: a connection costs a place in each, the controller's preferred request punching UDP and its TCP fallback request punching TCP, and the transports that lose the race hold theirs for the whole wait, so one pool of 32 would be about ten connections setting up at once and a crowd of UDP punches would decline a TCP punch that had nothing to do with it. A place is taken only for a punch that will be made: whether the v4 legs relay is decided first, since the relay branch runs the whole session and a place held across it would let ordinary relay traffic use the pool up. A declined LAN listen falls to the relay, as one that fails for any other reason does. Declined is the listen alone, never the reply. One PunchHoleSent carries the WebRTC answer, the v6 address and the relay server along with the v4 punch, and the controller sends one request for all of them and retries it three times before it fails outright, so a reply withheld for want of a v4 place would lose the transports that needed none. The reply goes out as before, on a socket that then goes at once, resends and all - a declined request is not worth a socket kept for its reply's sake, and the controller re-asks on its own - and what the controller loses is its v4 attempt at a mapping that no longer answers; its others go on. A request that carries a v6 address costs two places, and the v4 one is taken first: the v6 punch starts before the v4 one does, and taken in that order the last place would go to the transport the peer may have no route for and leave the one it can count on with none. A declined v6 punch leaves the v4 path to carry the connection, as it already does wherever this machine has no public IPv6 address. Each kind logs at most one line a minute, carrying the number of requests it stands for, since the peer decides how often it asks. Tests: the places are the bound and come back on drop; a full UDP pool leaves the TCP places alone; at the last place it is the v6 punch that goes without; a declined UDP punch still sends its PunchHoleSent, answer and v6 address intact, to a loopback stand-in for hbbs; a listen whose peer never probes gives its place back. Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
01e3917390 |
fix(ci): restore global configurations (#16257)
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
fd0fe592eb |
fix(rdev): macos, numpad flag, clear on text keys (#16253)
may fix #16227 Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
bedf64c8d3 |
bump hbb_common: cap the message an unauthenticated peer can make us hold
Carries `Stream::set_max_packet_length` to all three transports, so a connection can hold its peer to a small message until it has authenticated: TCP had the knob, WebRTC's reassembly ceiling becomes a per-stream bound, and WebSocket reaches tungstenite's config through a fork of v0.26.2 that exposes `set_config`, which upstream still does not at 0.30.0. Nothing calls it yet, so this changes no behaviour; the hook lands separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
4df7404a6c |
webrtc: cap concurrent answerer setups, pin the DTLS fingerprint binding with a test (#16225)
A WebRTC offer reaches the controlled side before any password or accept prompt, and answering one builds a peer connection that binds a socket per interface and runs ICE for up to CONNECT_TIMEOUT. A forged TCP punch reuses the mediator's local port for one connect; a forged offer costs all of that, and nothing bounded how many could be in flight at once. SESSIONS dedups by offer fingerprint, which only stops replays of one offer. spawn_webrtc_answerer now takes one of 16 slots before building the peer connection. The wait for the data channel is bounded by CONNECT_TIMEOUT, and what the slot stands for is the peer connection an unauthenticated offer had this machine build, ICE, DTLS and SCTP: on an open channel it is given back at once, and on a failed one it goes with the pc into the detached teardown and comes back when that has finished. pc.close() has no timeout of its own, so a slot freed where the task gives up would let a teardown that never finished pile pcs up unbounded with the count reading zero; held, a stuck teardown costs WebRTC capacity and the offers past the cap degrade to punch and relay. Every failure before the pc exists releases the slot through the guard's drop. From the open channel on the connection is one like any other, and the connection layer bounds unauthenticated connections in number and in time for every transport alike (#16237), a peer that stalls in the identity handshake or after it included. So this guard stays inside the WebRTC path, sized above what legitimate controllers reach at once in the seconds ICE takes. Past the cap the offer is declined with an empty answer, the reply the controller already gets from a peer without WebRTC, so it carries on over punch and relay. Declines log through the throttled-log macro. At the cap a re-sent PunchHole for a live session also gets an empty answer rather than the cached one, since the slot is taken before the cache is consulted; only reachable at the cap, where degrading is the point. The other change is regression coverage for the signed DTLS fingerprint binding, which is unchanged. The controller's defence against a rendezvous or relay that swaps SDP fingerprints is the fingerprint the controlled side signs into IdPk and the comparison in secure_connection, and neither had a test. The comparison moves into dtls_fingerprint_bound so it can have one, along with decode_id_pk_dtls: the fingerprint round-trips under the signature, another key or an edited payload yields nothing, empty never binds, and decode_id_pk still sees the same id and pk. Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f4808b71d4 |
webrtc: encrypt the signalling legs to the rendezvous server, or send no WebRTC signalling (#16224)
Three TCP connections carry WebRTC signalling to hbbs in the clear: the controller's punch connection, which carries the offer up and the answer and both sides' ICE candidates through it, and on the controlled side the short-lived connection that returns the answer and the one that trickles its candidates. Candidates are every interface address of both machines, and the controller is the side most often on a network it does not trust. `secure_tcp` is fail-open by design: a server that answers the first message with anything but a key exchange, or with nothing, leaves the stream in the clear and the call returns Ok, which the paths from before such servers rely on. That is not a channel WebRTC signalling may go out on. So the four legs use `secure_tcp_required`: Ok only once the server's key exchange has encrypted the stream, an error otherwise. WebSocket is treated as `secure_tcp` treats it, as a transport encrypted already. On the controller an error drops the offer, closes its peer connection through the guard and reconnects, then punches without WebRTC on the fresh socket, with the legacy condition applied to it as before; the failed exchange may have consumed a message on the old one. On the controlled side an error abandons that WebRTC attempt: the answer is not sent, or the candidates are not, and the controller falls back to its other transports. A relay response carrying an answer, which the symmetric-NAT and forced-relay branches send on a connection of their own, keeps the relay and loses only the answer: the response goes without it, on a fresh socket. Degrade to no WebRTC, never to WebRTC signalling in the clear. `secure_tcp` itself is unchanged; the exchange moves into `key_exchange`, which reports whether it happened. A punch without an offer is unchanged: the legacy secure condition takes this socket straight to the punch as before, and every other punch waits for the UDP NAT test as before. The exchange does not replace that wait, it spends part of the same budget, which now runs from before it: what is left is waited out, and a probe that has already answered is taken at once. Tests run a loopback stand-in for hbbs: a server that answers with another message, or closes, is refused where `secure_tcp` would carry on in the clear; a completed exchange is accepted and the stub decodes the reply with its ephemeral key. Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0ac2e7fb52 |
Avoid Xwayland scans on X11 sessions (#16232)
Desktop refresh runs repeatedly for active sessions, spawning 'psgrep' procs and producing significant CPU load. Guard the Xwayland process scan with the session protocol so X11 sessions skip work that cannot contribute session information. This preserves the existing Wayland discovery path while leaving X11 refresh behavior on its established display and xauth values. |
||
|
|
092a961b62 |
server: bound unauthenticated connections in number and in time (#16237)
A connection that never logs in costs whatever its transport costs, for as long as it keeps itself alive: the only limit was the 30s idle timeout, which any message resets. Nothing bounded how many such connections one machine holds, on any transport. The shape sshd_config answers with LoginGraceTime and MaxStartups. Every connection is admitted among the unauthorized ones before its identity handshake, in create_tcp_connection, and holds that place until it authorizes or ends: the count of live places is the bound, not a ledger beside the connections: the resource bound. One address may hold sixteen, a quarter of the room; a further connection from it is refused before the handshake. That share is a fairness cap against the cheapest flood, one host with one address, not a security boundary: any pool of addresses passes it, and the global limit is what holds. With 64 held in all, a further arrival is refused too, and the oldest connection is told to go, unless one is on its way out already: the handshake is raced against that eviction and ends at once, and the session loop has it as a branch of its select, so the place opens as soon as the connection has actually gone and not on a timer tick. The newcomer is not let in on a place still occupied; the controller retries on its own with backoff, and by then the place is free. At most one connection is ever on its way out, so a burst of refused arrivals clears no more room than a single one, and the retry that takes the freed place counts against its address's share: one address turns out at most as many connections as it may hold. One deadline, from the moment the connection starts, a branch of the session loop's select rather than a check on the TestDelay tick: a connection not authorized after 180s is closed, however alive it keeps itself, a wrong password, a pending 2FA, an accept prompt or an admin-terminal credential prompt left unanswered. The controller reconnects on its own and the prompt comes back. It closes with the Timeout reason the idle path uses, and that path still ends a connection that says nothing for 30s. There is no shorter deadline for the first login request: an admin-terminal controller shows its credential prompt before sending one, and a peer that wanted to dodge such a deadline would only have to send a login request, so it would bound nothing. The peer address is normalized with try_into_v4 before admission, the same form Connection::start keys the whitelist on, so an IPv4 peer and its IPv4-mapped IPv6 form are one address and not two shares. The WebRTC answerer's slot keeps bounding peer connection setup up to the open data channel; from there this covers it like every other transport. Tests cover the registry and the live bound: an address over its share is refused while others are admitted; at the limit the newcomer is refused, the oldest is told to go, nobody else is while it is on its way out, and its place frees only when it has; an address at the limit turns out no more connections than its share and is then refused without evicting anyone; and with the limit held by 64 connections stalled in the handshake, one more arrival is refused while the oldest handshake ends at once and only then is there a place again. Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
4f56d6957a |
pin the webrtc crate to the fork that exposes max_binding_requests
The hbb_common bump before this one calls SettingEngine::set_ice_max_binding_requests(), which the 0.13.0 release on crates.io does not have: only webrtc-util and webrtc-sctp were patched to the fork, so the webrtc crate itself still came from the registry and the build stopped at that call. The three patches now point at the same fork revision, one commit past the one they were on, which adds the setter. The webrtc crate depends on its siblings by path, so patching it moves the rest of that workspace to the fork as well; the fork is upstream v0.13.0 with changes to sctp and to this setter only, so those crates carry the same code they did from the registry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |