mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-07-01 12:54:52 +03:00
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:
committed by
Ferdinand Schober
parent
82766cdc87
commit
72c86c0d83
@@ -282,6 +282,12 @@ impl ClientManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_peer_commit(&self, handle: ClientHandle, commit: Option<[u8; 8]>) {
|
||||
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
|
||||
s.peer_commit = commit;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn active_addr(&self, handle: ClientHandle) -> Option<SocketAddr> {
|
||||
self.clients
|
||||
.borrow()
|
||||
|
||||
@@ -28,6 +28,18 @@ use shadow_rs::shadow;
|
||||
|
||||
shadow!(build);
|
||||
|
||||
/// Local build's 8-byte ASCII short commit hash, suitable for use
|
||||
/// in [`lan_mouse_proto::ProtoEvent::Hello`]. Pads with `'?'` if
|
||||
/// shadow_rs returns an unexpected length so the field is always
|
||||
/// well-formed on the wire.
|
||||
pub fn local_commit() -> [u8; 8] {
|
||||
let bytes = build::SHORT_COMMIT.as_bytes();
|
||||
let mut out = [b'?'; 8];
|
||||
let n = bytes.len().min(8);
|
||||
out[..n].copy_from_slice(&bytes[..n]);
|
||||
out
|
||||
}
|
||||
|
||||
const CONFIG_FILE_NAME: &str = "config.toml";
|
||||
const CERT_FILE_NAME: &str = "lan-mouse.pem";
|
||||
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::config::local_commit;
|
||||
use crate::listen::{LanMouseListener, ListenEvent, ListenerCreationError};
|
||||
use futures::StreamExt;
|
||||
use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError};
|
||||
@@ -150,6 +151,16 @@ impl ListenTask {
|
||||
}
|
||||
ProtoEvent::Input(event) => self.emulation_proxy.consume(event, addr),
|
||||
ProtoEvent::Ping => self.listener.reply(addr, ProtoEvent::Pong(self.emulation_proxy.emulation_active.get())).await,
|
||||
// Mirror the peer's version handshake. The
|
||||
// outgoing connect side initiated this with
|
||||
// its own Hello; we echo ours back so the
|
||||
// peer's connect-side receive_loop can
|
||||
// populate `peer_commit`. We don't store
|
||||
// the peer's commit on the listen side —
|
||||
// the user-visible state lives on outgoing
|
||||
// connections, where the same peer is also
|
||||
// configured as a `[[clients]]` entry.
|
||||
ProtoEvent::Hello { .. } => self.listener.reply(addr, ProtoEvent::Hello { commit: local_commit() }).await,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ fn run() -> Result<(), LanMouseError> {
|
||||
#[cfg(feature = "gtk")]
|
||||
{
|
||||
let mut service = start_service()?;
|
||||
let res = lan_mouse_gtk::run();
|
||||
let res = lan_mouse_gtk::run(config::local_commit());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// on unix we give the service a chance to terminate gracefully
|
||||
|
||||
Reference in New Issue
Block a user