A peer that is killed, logged out or rebooted sends no reset over UDP, so a
session only learns of it by timing out. Nothing here acts on that; these are
the two observations a caller needs to.
`rx_progress` counts arriving fragments. A message reaches the caller only once
it is whole, so a large one - a clipboard image is the case that occurs - leaves
the receive loop with nothing to observe for as long as the transfer takes, and
a peer sending steadily looks exactly like one that died. A counter, not a
timestamp: `Instant::now()` costs an order of magnitude more per read, and
callers only compare successive samples.
`webrtc_disconnected` exposes the ICE state webrtc-rs already raises ~5s after
it stops hearing from the peer. It is deliberately not a fourth
`WebRTCConnectionState`: that enum gates send and receive readiness and session
cache admissibility, and a transient state must change neither.
TCP and WebSocket answer for neither, on purpose: this change leaves their
existing close, error and timeout behaviour exactly as it was.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
hbb_common is shared with the server, but `fs`, `platform`, `keyboard`,
`message.proto` and most of `config::keys` are reached only from the client.
Keeping them here means every change to them costs a submodule round-trip.
They move to the new `base` crate in the rustdesk tree. What stays is
what the server actually names: `rendezvous.proto`, the socket/stream layer, the
`Config` core, and the 32 option keys this crate reads itself. Those 32 are
re-exported from `base::config::keys`, so callers still see one set.
`IdPk` moves from message.proto to rendezvous.proto. It is register-pk handshake
business, the server is its only other user, and the field numbers are unchanged
so the wire format is identical.
`config::patch()` resolves a home directory through the shell lookup that lived
in `platform::linux`, so `find_cmd_path`, `CMD_*` and `run_cmds_trim_newline`
stay behind in a new `sh` module. `base::platform::linux` re-exports
them, leaving every existing `platform::linux::CMD_SH` path working.
backtrace, filetime, osascript, smithay-client-toolkit and winapi go with the
code that used them, so the server no longer builds a Wayland client toolkit and
a Windows API crate it never calls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
Racing WebRTC against UDP and TCP means one transport is cancelled on every
session that connects, and cancelling it walks agent_internal's candidate list
closing sockets that are already gone - one warn per candidate, plus the
peer_connection cascade behind it (ice cancelled by caller, SCTP without DTLS,
the SRTP sessions never opened). Trickle adds pingAllCandidates on every check
tick until the first pair arrives. None of it is a fault, and all of it lands
beside a connection that succeeded, where it reads as one: 90 of 386 lines in a
controlled-side log spent saying the design worked.
agent_gather stays at warn. A STUN server it could not reach is the one thing
upstream says that explains a session which never connected, and it is a
different module from the noise, so the two separate cleanly. Errors in the
quieted modules still print, and RUST_LOG overrides the whole spec for anyone
who wants the warns back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M54JAqUK4RynudFou89hod
- send_bytes computed one deadline before acquiring the send gate and
reused it for the write, so time queued behind another clone's message
was charged to this message. A caller that only just lost the gate
race got a few milliseconds to write in and then tore the whole peer
connection down for missing them - the narrower the miss, the more
certain the teardown. The write gets its own budget; the gate wait
keeps the one it had, and still never closes (it never held the
permit).
- Stream::close_webrtc awaited WebRTCStream::close, which is exactly the
form close_detached exists to avoid: most callers sit in a select! arm
or a future the UI can abandon, and a cancelled close is unretryable
(is_closed is latched before the first await, so later attempts
early-return and the handler that evicts the session never runs). It
is now a plain fn calling close_detached - with no await point there
is nothing to cancel. close_detached is public and carries the
runtime-teardown guard the parent repo had written separately for its
Drop path, so that copy goes away.
- Log retention goes back to 31 files. Raising it to 31*8 defended the
flood case badly (a file count cannot outrun a flood; only the rate
limits at the log sites can) while silently multiplying steady-state
retention and disk for every ordinary install, on every platform.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
Continues the review pass. Eight findings, each verified against the
vendored webrtc-rs (0.13 / -data 0.11 / -ice 0.13) rather than inferred:
- a mid-message dc.write() failure returned the error and left the pc
open, so the peer kept an unterminated FRAG_MORE prefix and appended
the next message to it - undetectable, since this framing carries no
length or sequence number, and callers wrap sends in allow_err!. Close
the stream when fragments are already on the wire.
- one deadline covered the connect-wait, the gate queue and every write.
A slow ICE/DTLS completion therefore ate the budget and the write
timed out into a pc.close() an RTT from working; and the gate arm
closed the pc from a task that never held the permit, aborting a
healthy sender's fragment sequence - the exact corruption the permit
exists to prevent. Connection setup gets its own budget, and only the
arm holding the permit tears down.
- a SESSIONS hit returned a pc built for the first caller, so a replayed
offer could get an All-policy connection where the mediator had just
computed Relay-only, with is_relayed() answering from the cached
handle. It could also hand back a stream the state handler had already
closed (it closes before it evicts). Reject both; peer_verified stays
shared, being a fact about the certificate the entry is keyed by.
- the ICE-candidate sender lives in the on_ice_candidate handler and
close() clears no handler, so the receiver never closed and the
forwarder loop this API asks callers to write parked on recv() holding
a stream clone - one leaked task plus one leaked pc per connection.
The terminal-state handler now drops that closure. Regression test
included (fails, by 20s timeout, with the drop removed).
- has_turn_server() accepted the RFC 7065 spelling `turn:host:port`,
which url makes cannot-be-a-base: host_str() is None, so the server
became a hostless "turn::3478" that still passed the scheme check.
force_relay then built a Relay-only pc that could only time out. Parse
host and port out of the path (IPv6 literals included), and make the
gate require a host.
- is_relayed() read the stats report's `nominated` flag, which
webrtc-ice sets per checklist entry and never clears, so after a pair
switch several entries carry it and HashMap order picked the answer.
Read the selected pair instead - which also drops a redundant stats
round trip.
- the ICE-server parsing tests rewrote the process-global, on-disk
`ice-servers` option while sibling loopback tests were building peer
connections from it. Split parsing out of get_ice_servers so they can
test it directly; the suite now passes in parallel.
- log retention is a file count, so pairing it with the new 16 MiB size
criterion let a flooder rotate away every file predating its own
activity. Keep enough files that ~31 days survives even at full size.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
Rotating on age alone let a single day's file grow without limit, so whoever
can drive a hot log site decided how much disk this uses and no amount of
per-site throttling could bound it. Add a size criterion, which covers every
call site at once — including ones no throttle was added to.
LogThrottle: recover the guard on a poisoned lock rather than returning None.
Poisoning only means another thread panicked while holding it; the guarded
data is two counters that are still usable, and going silent for the rest of
the process is worse than a stale count. AGENTS.md permits handling lock
poisoning directly, and it forbids swallowing the error.
Keep map_or over clippy's is_none_or: that was stabilized in Rust 1.82 and CI
pins 1.75.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
Debug output is written to the log file, so a log site that fires per received
message lets whoever is sending decide how much a machine writes to disk.
Dropping the line instead would hide real faults, so collapse it: one line per
interval carrying the count of everything suppressed since the last one, with
the first occurrence after a quiet period always reported so an isolated fault
is not delayed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
- Cache machine_uid in get_uuid with retry logic for macOS
- Fallback to pk decryption when machine_uid decryption fails
- Add is_encrypted check to prevent duplicate encryption
- Add get_existing_key_pair to get key pair without generating new one
Signed-off-by: 21pages <sunboeasy@gmail.com>