* fix(flutter): dispose the settings PageController and order dispose() correctly
`dispose()` began with `super.dispose()`, so the mixin chain marked the State
defunct before the WidgetsBindingObserver registration and the periodic timer
were released. The `PageController` was never disposed at all: `Get.delete`
only runs `onDelete()` for a `GetLifeCycleBase`, and a plain `ChangeNotifier`
is not one, so every open/close of the Settings tab leaked one controller with
its listener still attached.
Also guard `switch2page` on the `Rx<SettingsTabKey>` registration it actually
reads rather than only the `PageController` — now that both are really
deleted, a partial teardown would throw into the catch and silently open the
wrong tab — and re-check `mounted` after the await in the `_videoConnTimer`
tick, which `Timer::cancel` cannot stop once the body has started.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refact: finish the plugin-framework removal sweep
#15854 removed the feature but stopped short of its leftovers:
- `Uninstall`, `Enable`, `Disable`, `Options` and `Please install plugins`
were consumed only by the deleted `flutter/lib/plugin/**`; drop them from
template.rs and the 50 locale files (250 dead entries). `Update` and
`Install` stay, still used by desktop_home_page.dart.
- The server no longer sends `PrvOnFailedPlugin`, and the client no longer
offers to install plugins when privacy mode fails to turn on.
- Drop the MSI `F_Client_Plugins` / `F_Server_Plugins` localization strings;
no `.wxs` references them.
- `_DisplayMenu`'s constructor became a pure pass-through once `pluginItem`
was removed, and the cfg inside `handle_input` repeats the one on the
function itself.
- Normalize `src/lang/sl.rs` to 0644, the only executable file under src/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(client): handle legacy privacy mode plugin failures
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
* fix(linux): serve the Wayland login screen the DRM backend was built for
The login screen support in #15420 never worked on a real greeter. fufesou found
it: the session is refused, and with the refusal commented out the client gets a
failed connection instead of a screen.
One premise under all of it. `get_values_of_seat0` is
`_get_values_of_seat0(.., ignore_gdm_wayland = true)`, so a gdm/sddm Wayland
session is skipped by construction and `get_display_server` falls back to x11.
That was correct while the portal was the only backend, since the portal cannot
serve a greeter at all. The DRM path never talks to the compositor, which is
precisely why it can serve one, so the premise stops holding there and every
x11-vs-Wayland decision in the tree answers x11 at a login screen.
The central change is the memoised `IS_X11`: when it reads x11 and seat0 is a
Wayland greeter, answer Wayland. That covers fifteen routing sites at once, and
it is under `cfg(feature = "drm")`, so a build without the backend keeps the
current answer exactly. `is_x11_for_drm` is the unmemoised form for the two
retry loops that must keep asking while a boot is still naming the session, and
the memoised accessor is scoped to per-frame callers in the per-session
`--server`, which the service only spawns once it has identified the session.
Input was the last layer and lived outside all of that. `Enigo` decides
x11-vs-Wayland once in `Default::default()`, from the same seat0 lookup, and on
"x11" routes every key and mouse event to xdo; with no X server that context is
null and libxdo drops them without an error. So the uinput devices were created,
the compositor opened them, and nothing was ever written to them. `set_is_x11`
is now called where the custom devices are installed, which is only reached once
`!is_x11()` is already established. The unit test pins both directions, since a
one-directional test passes against the bug.
With no compositor reachable, the uinput desktop rect comes from the DRM display
list instead: those are the same displays being captured, so the coordinate space
matches by construction. Telling the truth about a greeter also makes four
compositor-probing paths reachable where the probe cannot answer; all four
already treat an empty output list as "nothing to do", so they skip it and 11818
"Could not find wayland compositor" warnings in one session became 1.
Tested on an sddm Plasma Wayland greeter, MacBook T2, 2880x1800: the greeter
renders, typing from the client enters characters in the password field, a click
at an absolute coordinate opens the greeter session combo, the service pre-warm
primes in 994 us instead of timing out, and the privileged service maps no EGL
during a live capture. Not proven on gdm under Wayland.
Known limitations: non-ASCII characters cannot be typed at a greeter, because
that path goes through the clipboard and the clipboard here is X11 only; and at
a multi-monitor greeter the pointer reaches the first display only, since every
DRM output reports origin (0,0) on Wayland and there is no arrangement to derive
without the compositor.
* fix(linux): a Wayland greeter the DRM backend can serve is not headless
fufesou reported the login screen still failing on Ubuntu 24.04 with gdm3, with
the client asking for OS credentials to start an X session instead of showing the
greeter. Reproduced on a real gdm greeter here.
Same premise as the rest of the branch, one more consumer. `DesktopManager::new`
reads seat0 through `get_values_of_seat0`, which skips a gdm/sddm Wayland session
by construction, so at a greeter it finds no session at all and
`get_supported_display_seat0_username` returns None from its empty-username arm.
That makes `is_headless()` true, so the service advertises headless and
`try_start_desktop` answers `LOGIN_MSG_DESKTOP_SESSION_NOT_READY`. The corrected
`IS_X11` does not reach this one: it asks who owns seat0, not which display
server is running.
So ask again, with the greeter visible, when the DRM backend can capture and
inject into it. At query time rather than in `new()`, because the DRM probe has
not necessarily settled when the desktop manager is constructed, and the answer
would latch for the process lifetime. In a normal session the latched username is
a real user and the extra read is skipped.
* chore: drop the hbb_common bump, this branch does not need it
The bump carried rustdesk/hbb_common#580, the compositor-socket fallback. Nothing
here depends on it: the greeter paths in this branch are the ones that run when
compositor data is unavailable, which is what the commit before this one states as
a known limitation. Keeping the bump would only block the greeter fix behind a
review of a separate change, and would import that change's blocking review items
into this path.
* fix(linux): let the uinput uid gate see the greeter that owns seat0
Input at a real greeter was rejected by our own authorization. Measured on Ubuntu
24.04 with gdm3: the root service logs
Rejected unauthorized connection on uinput ipc channel:
postfix=_uinput_control, peer_uid=Some(120), active_uid=None
and the greeter's `--server` gets ECONNRESET out of `setup_uinput`, so no uinput
device is ever created and neither keyboard nor mouse reaches the greeter.
uid 120 is gdm, the owner of the only active seat0 session. `active_uid` is None
because the uinput authorizer deliberately bypasses the service-loop cache and
takes a fresh seat0 lookup, and the fresh read hides a Wayland greeter by
construction. The cache-based gates do not have the problem: `Desktop::refresh`
fills it through the greeter-visible read, which is also why capture and config
sync work at a greeter while input does not.
So make the fresh read agree with the cache. It keeps the property the uinput gate
wants, a lookup that cannot be stale, and it still compares the peer against the
uid of the session that owns seat0 -- which at a greeter is the greeter.
* fix: settle the DRM probe before routing login to X11, and read seat0 fresh
Two findings from the #15792 review, both verified against the code:
- drm_login_screen_seat0_username asked the cached probe, so a client
arriving before warm_availability publishes its verdict read "no DRM"
and, with allow-linux-headless=Y, try_start_x_session could start Xorg
over a live Wayland greeter. Ask the probing form instead, and only
after the cheap seat0 read says a Wayland greeter is actually there: a
bounded definitive verdict is affordable on a login-time path.
- get_supported_display_seat0_username trusted the seat0 values cached in
DesktopManager::new(), which go stale across a logout or a fast user
switch: a stale non-greeter name skipped the greeter probe and was
returned as the supported display owner. Read seat0 fresh on every
query; every call site is connection-time, so the extra loginctl read
is cheap.
Regression-tested on a real sddm Wayland greeter: capture streams the
greeter, the RustDesk password dialog is the only prompt, and five typed
characters appeared in the greeter password field over uinput with zero
"Rejected unauthorized connection" lines in the service log.
* fix: ask the greeter compositor for the multi-monitor layout
The display arrangement and the pointer mapping were wrong at a
multi-monitor login screen, and the mechanism is measured on a two-head
virtio VM: DRM has no origins, so every display was advertised at (0,0)
(a stacked arrangement on the client), and the uinput range was taken
from the union of the DRM modes while the compositor had arranged the
outputs side by side.
Both came from the same premise, written before the hbb_common socket
fallback existed: "a login screen has no compositor to ask".
wayland_outputs_askable() skipped the wl_output augmentation at any
greeter, and update_uinput_resolution took the DRM union directly. The
premise is false now: a greeter runs a compositor, and the socket
fallback reaches it with no environment variables, measured answering
two outputs at the VM greeter while the old gate was still routing
around it.
Drop the gate and take the compositor-first path everywhere. Where the
fallback cannot answer, the output list comes back empty and both call
sites degrade to exactly the old behavior, so a build against an older
hbb_common is unchanged.
* fix: augment a single display too, and probe the desktop rect off the executor
Two follow-ups from the automated re-review of cd80c3dee, both verified:
- augment_with_wayland_geometry skipped the compositor below two DRM
displays, but on a multi-GPU host the one connector this service can
open may sit at a non-zero origin of the compositor layout, and DRM
alone reports (0,0).
- the desktop rect for uinput can now block for the socket probe
deadline, and update_uinput_resolution runs on current-thread
runtimes; move the query into spawn_blocking.
The third re-review finding, the warm-up allegedly skipping Wayland
greeters, is refuted: warm_availability probes while is_x11_for_drm()
is false, which includes a Wayland greeter, and the greeter log of the
VM run behind cd80c3dee shows the warm succeeding there.
* fix: baseline the layout from the blocking task, and augment a lone output's origin
The layout snapshot after the rect lookup still ran on the executor: a
failed compositor lookup is not cached, so the snapshot synchronously
repeated the whole socket probe there. The baseline is now computed
inside the same blocking task, from the snapshot the successful lookup
just cached, or omitted when only the raw DRM union was available,
which keeps the #15601 remap inactive exactly where origins are
unknown.
A single compositor output now hands its origin to a single connector:
the lone output can sit at a non-zero origin the DRM side cannot see.
Scale stays 1 on purpose, matching how a single display is advertised
at physical size, and more connectors than the one output stays
unaugmented, since the layout-order fallback would plant that origin on
a guess.
Also refresh the get_primary_index doc that still said augmentation
declines below two connectors.
* fix: read the DRM probe as a tri-state, and keep pre-auth seat0 checks cache-only
is_available() answered false both for a definitive no-DRM verdict and
for a probe that had simply not settled (another probe in flight, or a
failure still below the disable threshold), and the login-screen
decision turned that transient false into no-greeter: try_start_x_session
could put Xorg over a live greeter in exactly the window the probe
needed. The machinery now answers Available/Unavailable/Unsettled, and
only a definitive Unavailable routes the seat toward X11.
Connection setup also ran the whole lookup pre-auth: constructing
LinuxHeadlessHandle called is_headless() before authentication, holding
DESKTOP_MANAGER while loginctl ran and, at a greeter, while the DRM
probe waited out its handshake. An unauthenticated peer could occupy a
worker for seconds and serialize every other connection on the mutex.
is_headless() now answers from a snapshot refreshed off-thread, and the
fresh lookup became a free function called with the manager lock
released everywhere; the enforcing decisions, get_username and
try_start_x_session, still read seat0 fresh.
Also drops seat0_display_server, dead since the fresh-read change.
* fix: respect RUSTDESK_FORCED_DISPLAY_SERVER over the greeter correction
The greeter correction rewired IS_X11 and is_x11_for_drm() to Wayland
whenever seat0 looks like a Wayland greeter, including when the operator
explicitly forced the display server: get_display_server() kept honoring
the override while the DRM routing gates contradicted it, leaving
capture and input routing internally inconsistent. The correction now
only adjusts the auto-detected answer.
* fix: honest pre-auth snapshot, sticky negative verdict, and a complete forced-x11 gate
Four defects found by an adversarial review of the two previous
commits, all in their new lines:
- The empty-snapshot fallback derived headless from the manager's
boot-time seat0 read, which is blank at a Wayland greeter (the
loginctl wrapper skips greeter sessions), so the first connection of
every server process at a greeter answered headless=true, the
opposite of the comment on it. No snapshot now answers NOT headless,
the snapshot is seeded at start_xdesktop, and the boot-time cache is
gone entirely (it had no reader left).
- wait_desktop_cm_ready gated on a bool stored at construction, which
can lag one seat0 transition behind and skipped the CM-ready wait
right after a logout. It re-reads the snapshot at call time.
- A settled Unavailable was erased at NEGATIVE_TTL expiry (state to
Unknown, failure counter to zero), so a permanently helper-less box
reopened the Unsettled window every 30 seconds and the login decision
kept adopting a greeter nothing can serve. The verdict now stays
Unavailable while an off-thread re-probe re-verifies it: a failed or
empty re-probe restamps the no, and only a non-empty list flips it.
- The forced-x11 gate only covered IS_X11 and is_x11_for_drm, while
the seat0 adoption path still probed DRM and admitted greeter
sessions whose capture and input then routed to X11. Greeter
adoption now yields to an operator-forced X11, degrading to upstream
behavior: the connection is refused at the login screen.
* fix: keep the login request path off the probe entirely
try_start_desktop runs while handling a LoginRequest, before password
validation, and at a Wayland greeter its seat0 lookup reached the
probing availability form: an unauthenticated peer could park a worker
for the probe deadline. The greeter adoption now reads a cached
tri-state that never blocks; when the state is Unknown it kicks the
probe off-thread and answers Unsettled, which the login decision treats
as a possibly servable greeter until it settles. Settling lives in the
startup warm-up, that kick, and the TTL re-verifiers; the blocking form
stays for the capture-side callers, where waiting is acceptable.
* fix: run the pre-auth desktop start off the executor, guard the refresh flag, trim comments
From fufesou's #15792 re-review (no blocking issues) plus a bot pass:
- try_start_desktop now runs on spawn_blocking. It executes loginctl,
and PAM when a session must start, while handling a LoginRequest
before password validation, so a slow logind must not tie up an async
request worker; the blocking pool absorbs it.
- kick_seat0_refresh releases SEAT0_REFRESH_IN_FLIGHT through an RAII
guard, so a panic in the refresh thread cannot freeze is_headless on a
stale snapshot for the process lifetime.
- drm_can_serve_login_screen stays Available-only, and the reason is now
in the code: it is deliberately not symmetric with the seat0 adoption
gate. Adoption yields Xorg only on a definitive Unavailable; admission
accepts only on a definitive Available; both wait through an unsettled
probe. Admitting there would black-screen a client on a helper-less
box, so a review suggestion to make them agree is declined.
- Trimmed two over-long comments to the repo's three-line rule.
* fix(linux): harden DRM login-screen startup
Keep unauthenticated headless checks cache-only, bound OS-session startup to one blocking task, and surface JoinError failures.
Wire the isolated Wayland probe consumer and update hbb_common plus libdrmtap 0.5.4.
* fix(linux): headless refresh state
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(linux): keep headless startup state consistent
- gate concurrent desktop startup attempts
- route CM IPC after refreshing desktop state
- avoid blocking seat0 queries in the CM retry loop
- preserve newer seat0 snapshots during overlapping refreshes
- derive DRM geometry and primary display from one Wayland snapshot
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: rustdesk <info@rustdesk.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
* fix(client): allow switch-sides back-connection in incoming-only mode
"Switch sides" makes the controlled client run `--connect <peer>
--switch_uuid <uuid>`, which Client::_start rejected outright in
incoming-only custom clients, so the feature silently dropped the
session and never switched.
Exempt exactly that back-connection: a default-conn session carrying a
switch uuid may proceed. The uuid is then verified against the local
server process in handle_hash(); if it is missing there (forged or
expired), an incoming-only client now aborts with an error instead of
falling through to password login, so the outgoing-connection
restriction cannot be bypassed with a crafted --switch_uuid.
Fixesrustdesk/rustdesk#11200 (discussion)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(client): validate switch-back grants before connecting
- check pending peer/UUID grants before bypassing incoming-only mode
- close rejected switch-back connections and suppress retries
- keep grant consumption in handle_hash and test non-consuming checks
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(client): prevent switch-back UUID reuse
- claim pending switch-back grants before connecting
- retain claimed grants to reject duplicate requests
- bind authorization to the peer ID and UUID
- use a shared TTL for switch-back grants
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(client): defer switch UUID consumption until authentication
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(client): reject repeated hash login in incoming-only mode
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
* fix: retry audit posts and add per-record nonce
A single post_request attempt meant any transient failure (timeout,
DNS, connection reset) silently dropped the audit record. Retry up to
3 times with backoff and log at error level when a record is finally
dropped.
Retries (and the existing TCP-proxy fallback) can deliver the same
record twice; attach a per-record nonce so the api server can dedup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: fail audit posts on http error status
post_request discards the status code, so a 5xx from a reverse proxy
(e.g. nginx answering 502 while hbbs restarts) or any 4xx rejection was
treated as success and the audit record silently dropped without a log
line. Add post_request_with_status (same semantics and TCP-proxy
fallback as post_request, status preserved; existing callers untouched)
and use it for audit posts: 2xx succeeds, transport errors and 5xx
retry, 4xx fails immediately since retrying a deterministic rejection
cannot help.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: report audit posts rejected with 200 error body
hbbs maps handler failures (e.g. a database write error) to HTTP 200
with an {"error": ...} body (WebError::ServerError), so the client
treated them as success and the audit record was silently dropped.
Detect the error body and fail visibly. No retry: the server already
consumed the nonce, and persistence failures are the server's job to
solve; the client's job is to make the loss visible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: give audit retries a delay long enough to outlive a restart
The backoff was 1s then 2s, so all three attempts landed within about three
seconds. That does not cover the case the retry exists for: a reverse proxy
answering 502 while the api server restarts fails fast, so every attempt hits
the same outage and the record is dropped anyway.
Use 10s and 30s instead. The window is bounded on the other side - the api
server dedups by nonce for five minutes, and a retry arriving after that
expired would be stored twice - so the worst case is now about three minutes,
leaving room under that limit.
Derive the attempt count from the delay table so the two cannot drift apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: retry audit posts the server answered with an error body
hbbs reports handler failures as 200 with an {"error": ...} body, and this
treated them as final on the grounds that the server had already consumed the
record's nonce. That is no longer how the server behaves: it releases the nonce
when the write fails, and answers a post whose earlier attempt is still being
written with an error as well. Both are exactly the cases where trying again is
what gets the record stored, so giving up after the first attempt drops audit
records the retry was added to save.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: bound audit retries by elapsed time, and retry 408 and 429
The comment claimed the retry window fit inside the server's five-minute nonce
memory with room to spare, and that was wrong: one attempt is up to 84s, not
12s, because post_request_ retries the TLS handshake up to four times at 12s
each before the 36s TCP-proxy fallback. Three of those plus the delays is 292s
against a 300s window, and a suspend between attempts stretches the wall clock
without any bound at all, so counting attempts cannot bound this. Stop by
elapsed time instead: no new attempt starts past 120s, which leaves the last
one room to finish well inside the server's window.
Also retry 408 and 429. Both are transient - the request timed out upstream, or
a proxy is shedding load - but the 5xx test dropped the record after the first
attempt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: only an empty 2xx body counts as a stored audit
The success check was inverted: any 2xx body that failed to parse as an
{"error": ...} object was reported as stored. A proxy interposing a 2xx
maintenance page, or a malformed error value, therefore ended the retry loop
with success and silently dropped the record - the exact loss the retry was
added to prevent.
The audit handlers' success contract is an empty body, so treat exactly that as
success. A nonempty body with a valid error message stays a retryable server
error; any other nonempty body is now a retryable "unexpected response body"
instead of an accepted store. Both old and new hbbs answer success with an
empty body, and no caller reads the returned text, so nothing depends on the
previous acceptance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: do not start an audit retry past the deadline
The deadline was only checked after an attempt returned, so an attempt could
still begin up to one backoff delay past it - starting as late as ~150s and
landing at ~234s, while the comment claimed no attempt starts past 120s.
Re-check after the delay so the stated bound actually holds: the last attempt
now starts before 120s and lands by ~204s, inside the server's five-minute
nonce window with margin restored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: drop a retry rationale the server no longer backs
The comment claimed hbbs answers a post whose earlier attempt is still being
written with an error, so that retrying it is what stores the record. That
stopped being true: hbbs now answers a concurrent duplicate as already stored
rather than as retryable, having dropped the in-flight rejection along with the
claim state machine it needed.
Nothing in the handling changes - a 2xx carrying an {"error": ...} body is
still retried, and that is still right, because the server releases the
record's nonce when its write fails. Only the half of the rationale the server
no longer backs is gone, since this comment is where the contract between the
two repos is written down.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix stale primary display selection
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix stale display selection during login and switching
- resolve the primary display from the refreshed login snapshot
- defer display enumeration until authentication succeeds
- read Wayland displays and primary index from the same cache snapshot
- reject stale monitor and camera indices during display switching
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix inconsistent display snapshots during login
- return displays from the same enumeration used to select the primary
- avoid re-reading the shared display cache after updating it
- use the same converted snapshot during Wayland initialization
Signed-off-by: 21pages <sunboeasy@gmail.com>
* avoid cloning unchanged display snapshots
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix invalid display subset handling
Signed-off-by: 21pages <sunboeasy@gmail.com>
* minimize code churn in switch_display_to
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Support controller user attribution in audit logs
This PR supports associating audit logs with the controller user.
## Implementation:
- Add `ControlledContext { conn_audit_token }` to `PunchHole`, `RequestRelay`, and `FetchLocalAddr`.
- The server sends a controller-user identity snapshot to the controlled client through rendezvous messages.
- The controlled client sends the token back to the server when posting the `on_open` conn audit or IP whitelist alarm audit.
- This lets the server attach the controller user to audit logs.
## How the controlled client helps identify the controller user:
- Conn audit: sends the token to the server in `on_open`; the server creates the audit log and caches the user snapshot.
- File audit: sends `id` and `conn_id`; the server uses them to find the cached user snapshot.
- Alarm audit: IP whitelist sends the token directly; other alarm logs send `id` and `conn_id`, and the server uses them to find the cached user
snapshot.
## Compatibility:
- Supported only for logs created with a new server and a new controlled client.
- Does not require upgrading the controller client.
## Test
- [x] New/old clients connected to new/old servers, and conn/file/alarm audit logs worked normally.
- [x] New client connected to new server generated searchable conn/file/alarm audit logs.
- [x] Punch hole, local addr, and relay paths worked with audit logs and control role on new/old servers.
- [x] Direct IP connections produced audit logs, but do not support user audit.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* rename conn_audit_token to conn_audit_ref
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Fix Windows session-based logon and lock-screen detection
- scope LogonUI and locked-state checks to the current Windows session
- allow permanent password fallback for logon and lock-screen access
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Log permanent-password fallback on logon screen
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
* avatar
* refactor avatar display: unify rendering and resolve at use time
- Extract buildAvatarWidget() in common.dart to share avatar rendering
logic across desktop settings, desktop CM and mobile CM
- Add resolve_avatar_url() in Rust, exposed via FFI (SyncReturn),
to resolve relative avatar paths (e.g. "/avatar/xxx") to absolute URLs
- Store avatar as-is in local config, only resolve when displaying
(settings page) or sending (LoginRequest)
- Resolve avatar in LoginRequest before sending to remote peer
- Add error handling for network image load failures
- Guard against empty client.name[0] crash
- Show avatar in mobile settings page account tile
Signed-off-by: 21pages <sunboeasy@gmail.com>
* web: implement mainResolveAvatarUrl via js getByName
Signed-off-by: 21pages <sunboeasy@gmail.com>
* increase ipc Data enum size limit to 120 bytes
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
* feat(macos): add privacy mode support for macOS
## Summary
Add privacy mode functionality for macOS platform, allowing remote
desktop sessions to hide the screen content from local users.
## Changes
### Core Implementation (src/platform/macos.mm)
- Implement screen blackout using CGDisplayGammaTable API
- Implement input blocking using CGEventTap to intercept keyboard/mouse
- Store and restore original gamma values for proper cleanup
### Privacy Mode Integration (src/privacy_mode.rs, src/privacy_mode/macos.rs)
- Add macOS privacy mode implementation with PrivacyMode trait
- Register macOS privacy mode in PRIVACY_MODE_CREATOR
- Set DEFAULT_PRIVACY_MODE_IMPL for macOS platform
- Implement get_supported_privacy_mode_impl() for macOS
### Connection Handling (src/server/connection.rs)
- Add supported_privacy_mode_impl to platform_additions for macOS
- Enable privacy mode toggle in client UI when connecting via LAN IP
### Localization (src/lang/*.rs)
- Add "privacy_mode_impl_macos_tip" translation for en/cn/tw
## Safety & Security
- Implements Drop trait to ensure cleanup on normal exit
- macOS system automatically restores gamma table on process termination
- CGEventTap is automatically released when process terminates
- Tested with SIGKILL to verify crash recovery
## Testing
- Verified privacy mode toggle works via both ID and LAN IP connection
- Verified screen recovery after process crash (kill -9)
- Verified input restoration after process termination
* refactor: use existing 'Privacy mode' translation key
* refactor: rename gamma channel variables for better readability - rename r/g/b to red/green/blue to avoid variable shadowing confusion
* fix: add error handling for gamma table restoration with fallback to system reset
* fix: add error handling for CGEventTapCreate failure in privacy mode
* fix: only set display to black if original gamma was saved successfully
* fix: add error handling for CGSetDisplayTransferByTable when setting display to black
* fix: improve event tap callback to properly distinguish remote input from local input
* fix: missing macos.rs
* Fix: Add display validation before restoring gamma values
* Fix: Add mutex lock for thread safety in MacSetPrivacyMode
* Fix: Handle return values and add missing mouse events in macos privacy mode
* fix: only set conn_id after privacy mode is successfully turned on
* fix: reimplement privacy mode with stable display identification
Address code review concern: original gamma values stored with DisplayID
as key could become stale if display list changes between privacy mode
activations (e.g., display reconnected with different ID).
Solution:
- Use UUID instead of DisplayID as storage key (stable across reconnections)
- Clear g_originalGammas when privacy mode is turned off
- Register CGDisplayReconfigurationCallback to handle hot-plug events
- Validate display state via FindDisplayIdByUUID() before restoration
Key features:
- UUID-based display identification (stable across reconnections)
- Hot-plug support via CGDisplayReconfigurationCallback
- EventTap auto re-enable on system timeout
- Fallback to CGDisplayRestoreColorSyncSettings() for recovery
- Detailed error logging with display name/ID/UUID
* fix: ensure EventTap runs on main thread and improve gamma restore error handling
- Add SetupEventTapOnMainThread() to create EventTap on main thread using dispatch_sync, avoiding potential issues when called from background threads
- Add TeardownEventTapOnMainThread() for consistent cleanup on main thread
- Check [NSThread isMainThread] to avoid deadlock when already on main thread
- Add error tracking for gamma restoration during cleanup
- Use CGDisplayRestoreColorSyncSettings() as fallback when individual gamma restoration fails
* fix: remove invalid eventMask bits that caused undefined behavior in input blocking
* fix: address code review comments for macos privacy mode implementation
Changes to src/privacy_mode/macos.rs:
- Add check_on_conn_id() in turn_on_privacy() to prevent duplicate activation
- Add check_off_conn_id() in turn_off_privacy() to validate connection ID
- Add self.conn_id = 0 in clear() to reset connection state
Changes to src/platform/macos.mm:
- Add link comment for ENIGO_INPUT_EXTRA_VALUE referencing libs/enigo/src/macos/macos_impl.rs
- Fix NSLog format string mismatch (5 placeholders vs 4 values)
- Make ApplyBlackoutToDisplay() return bool for proper error handling
- Return false when UUID is empty since privacy mode requires ALL displays
- Add else branches with logging for:
- CGGetDisplayTransferByTable failures
- Zero gamma table capacity (not supported)
- Zero blackout capacity
- Remove unused g_uuidToDisplayId variable (was only written, never read)
* fix(macos): add early return with privacy mode exit on display hotplug failures
Why large-scale changes are needed:
The code review suggested adding early return when errors occur in
DisplayReconfigurationCallback. However, simply returning early is not
enough - when a newly connected display cannot be blacked out, we must
exit privacy mode entirely to maintain security guarantees.
The challenge is that DisplayReconfigurationCallback already holds
g_privacyModeMutex, so calling MacSetPrivacyMode(false) directly would
cause a deadlock. This necessitated:
1. Extract TurnOffPrivacyModeInternal() - a lock-free internal function
that can be safely called from within the callback
2. Refactor MacSetPrivacyMode(false) branch to use this internal function
3. Add early returns with TurnOffPrivacyModeInternal() calls at each
failure point in DisplayReconfigurationCallback
Changes in DisplayReconfigurationCallback:
- UUID empty: log + exit privacy mode + early return
- Gamma table capacity zero: log + exit privacy mode + early return
- CGGetDisplayTransferByTable fails: log + exit privacy mode + early return
- ApplyBlackoutToDisplay fails: log + exit privacy mode + early return
* fix(macos): address code review feedback and improve privacy mode stability
Code Review Fixes:
- Add detailed comments for potential deadlock scenarios in dispatch_sync
with g_privacyModeMutex (SetupEventTapOnMainThread/TeardownEventTapOnMainThread)
- Use async dispatch for privacy mode shutdown from DisplayReconfigurationCallback
to avoid unregistering callback from within itself
- Extract RestoreAllGammas() helper function to reduce code duplication
- Fix Drop implementation in macos.rs to call self.clear() for consistency
- Add comment explaining why _state parameter is ignored on macOS
- Define DISPLAY_RECONFIG_MONITOR_DURATION_MS and GAMMA_CHECK_INTERVAL_MS constants
- Add gamma restoration when UUID retrieval fails during privacy mode activation
Privacy Mode Stability Improvements (Continuous Resolution Changes):
- Implement continuous gamma value monitoring with timer polling after display
reconfiguration to handle rapid successive resolution changes
- Monitor gamma values every 200ms for 5 seconds after each resolution change
- Automatically reapply blackout if system (ColorSync) restores gamma
- Add IsDisplayBlackedOut() to detect if display gamma has been restored
- Use timestamp-based debouncing: monitoring period automatically extends
when new reconfig events occur during active monitoring
- Ensure blackout remains effective even under continuous resolution changes
where macOS may asynchronously restore gamma values multiple times
This ensures privacy mode remains stable and effective when users rapidly
change display resolution multiple times in succession.
---------
Co-authored-by: libin <libin.chat@outlook.com>
* feat: Add relative mouse mode
- Add "Relative Mouse Mode" toggle in desktop toolbar and bind to InputModel
- Implement relative mouse movement path: Flutter pointer deltas -> `type: move_relative` -> new `MOUSE_TYPE_MOVE_RELATIVE` in Rust
- In server input service, simulate relative movement via Enigo and keep latest cursor position in sync
- Track pointer-lock center in Flutter (local widget + screen coordinates) and re-center OS cursor after each relative move
- Update pointer-lock center on window move/resize/restore/maximize and when remote display geometry changes
- Hide local cursor when relative mouse mode is active (both Flutter cursor and OS cursor), restore on leave/disable
- On Windows, clip OS cursor to the window rect while in relative mode and release clip when leaving/turning off
- Implement platform helpers: `get_cursor_pos`, `set_cursor_pos`, `show_cursor`, `clip_cursor` (no-op clip/hide on Linux for now)
- Add keyboard shortcut Ctrl+Alt+Shift+M to toggle relative mode (enabled by default, works on all platforms)
- Remove `enable-relative-mouse-shortcut` config option - shortcut is now always available when keyboard permission is granted
- Handle window blur/focus/minimize events to properly release/restore cursor constraints
- Add MOUSE_TYPE_MASK constant and unit tests for mouse event constants
Note: Relative mouse mode state is NOT persisted to config (session-only).
Note: On Linux, show_cursor and clip_cursor are no-ops; cursor hiding is handled by Flutter side.
Signed-off-by: fufesou <linlong1266@gmail.com>
* feat(mouse): relative mouse mode, exit hint
Signed-off-by: fufesou <linlong1266@gmail.com>
* refact(relative mouse): shortcut
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
1. `TERM` on linux terminal.
2. `htop` command not found on macOS.
3. `vim` and `claude code cli` hung up on windows.
Signed-off-by: fufesou <linlong1266@gmail.com>
- Route Windows server-to-client file reads through CM instead of the connection layer
- Add FS IPC commands (ReadFile, CancelRead, SendConfirmForRead, ReadAllFiles) and CM data messages
(ReadJobInitResult, FileBlockFromCM, FileReadDone, FileReadError, FileDigestFromCM, AllFilesResult)
- Track pending read validations and read jobs to coordinate CM-driven file transfers and clean them up
on completion, cancellation, and errors
- Enforce a configurable file-transfer-max-files limit for ReadAllFiles and add stronger file name/path
validation on the CM side
- Improve Flutter file transfer UX and robustness:
- Use explicit percent/percentText progress fields
- Derive speed and cancel actions from the active job
- Handle job errors via FileModel.handleJobError and complete pending recursive tasks on failure
- Wrap recursive directory operations in try/catch and await sendRemoveEmptyDir when removing empty directories
Signed-off-by: fufesou <linlong1266@gmail.com>
Enhance security by implementing rate limiting on IPv6 prefixes (/64, /56, /48)
to prevent brute force attacks that exploit cheap IPv6 address generation.
* Add private get_ipv6_prefixes() to calculate network prefixes
* Implement private check_failure_ipv6_prefix() for prefix-specific limits
on IPv6 addresses
* Refactor check_failure() and update_failure() to support both IPs and prefixes
* Add ExceedIPv6PrefixAttempts to AlarmAuditType enum
Signed-off-by: Michael Bacarella <m@bacarella.com>
1. Update kcp-sys to send KCP in frames to avoid potential crashes.
2. Fix the issue when the controling side is closed, the kcp connection close is not immediately recognized by the controlled end.
* Unless the controling side receives the close reason, force the sending of the close reason to the controlled end when using KCP, and delay for 30ms to ensure the message is sent successfully.
* Move the CloseReason receiving forward, as this message needs to be received when unauthorized, especially for kcp.
Signed-off-by: 21pages <sunboeasy@gmail.com>