feat: peer version exchange with soft-warn UI indicator

Adds a one-shot Hello message to the lan-mouse wire protocol so each
peer can display the other end's build commit hash and warn on
version mismatch. Soft-warn only — mismatched versions never refuse
traffic.

Wire change (lan-mouse-proto)
* `ProtoEvent::Hello { commit: [u8; 8] }` carries the 8-byte ASCII
  short commit from shadow_rs's `SHORT_COMMIT`. Encoded/decoded
  alongside the existing event variants.
* `EventType::Hello` is appended to the enum so existing IDs are
  untouched. Old peers receive the event, hit `InvalidEventId`, and
  silently skip it via the forward-compat handler in
  `connect.rs::receive_loop` — the connection is unaffected.

Daemon
* Connect side sends one Hello immediately after the DTLS handshake
  authenticates and before the ping_pong loop starts. Best-effort,
  fire-and-forget — `log::debug!` on send error.
* Listen side mirrors the peer's Hello with its own (same shape as
  the existing Ping → Pong reply), so the peer's connect-side
  receive_loop populates `ClientState::peer_commit` for that
  handle.
* The disconnect path clears `peer_commit` so a stale hash isn't
  shown after the connection drops.

IPC
* `ClientState::peer_commit: Option<[u8; 8]>`. `None` means the
  peer hasn't sent Hello yet — either fresh connection or older
  build that predates the event.

GTK
* `ClientObject` exposes `peer-commit` as an `Option<String>`
  property; `peer_commit_to_string` converts the wire `[u8; 8]` to
  the displayable hex.
* `lan_mouse_gtk::run` now takes the local commit and stashes it in
  a `OnceLock` so per-row UI can compare against each peer's hash.
* `ClientRow::refresh_version_status` re-renders the collapsed
  subtitle with Pango markup whenever the property changes:
   - matched → green   "peer version: <hex> · matched"
   - mismatch → orange "peer version: <hex> · ours: <hex>"
   - unknown → orange  "peer version: unknown · ours: <hex>"
* Window invokes `refresh_version_status` from
  `update_client_state` after writing the new property, and
  `bind` calls it once on row construction so the initial
  subtitle isn't blank.

Known limitation: state-change broadcasts from the network side
(set_alive / set_active_addr / set_peer_commit) don't currently
trigger a `FrontendEvent::State` directly; the UI picks up the
latest values on the next user-driven broadcast. Same pre-existing
behavior as the alive/active_addr fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jon Kinney
2026-05-04 13:50:40 -05:00
committed by Ferdinand Schober
parent 82766cdc87
commit 72c86c0d83
12 changed files with 156 additions and 3 deletions

View File

@@ -1,4 +1,5 @@
use crate::client::ClientManager;
use crate::config::local_commit;
use lan_mouse_ipc::{ClientHandle, DEFAULT_PORT};
use lan_mouse_proto::{MAX_EVENT_SIZE, ProtoEvent};
use local_channel::mpsc::{Receiver, Sender, channel};
@@ -197,6 +198,16 @@ async fn connect_to_handle(
conns.lock().await.insert(addr, conn.clone());
connecting.lock().await.remove(&handle);
// Best-effort version handshake. Send our commit hash once
// immediately after the DTLS handshake; the listen side
// mirrors a Hello back so the receive loop can populate
// `peer_commit`. Old peers will silently skip this event
// per the forward-compat handler in [`receive_loop`].
let (buf, len) = ProtoEvent::Hello { commit: local_commit() }.into();
if let Err(e) = conn.send(&buf[..len]).await {
log::debug!("hello send to {addr} failed: {e}");
}
// poll connection for active
spawn_local(ping_pong(addr, conn.clone(), ping_response.clone()));
@@ -264,6 +275,9 @@ async fn receive_loop(
client_manager.set_alive(handle, b);
ping_response.borrow_mut().insert(addr);
}
ProtoEvent::Hello { commit } => {
client_manager.set_peer_commit(handle, Some(commit));
}
event => tx.send((handle, event)).expect("channel closed"),
}
}
@@ -287,6 +301,7 @@ async fn disconnect(
log::warn!("client ({handle}) @ {addr} connection closed");
conns.lock().await.remove(&addr);
client_manager.set_active_addr(handle, None);
client_manager.set_peer_commit(handle, None);
let active: Vec<SocketAddr> = conns.lock().await.keys().copied().collect();
log::info!("active connections: {active:?}");
}