From 82766cdc87ab5ac2aab07a41cf955b687de8af1a Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Wed, 29 Apr 2026 13:21:42 -0500 Subject: [PATCH] fix(proto): tolerate undecodable peer datagrams instead of disconnecting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DTLS recv loops in src/listen.rs and src/connect.rs each read one full datagram per call. A failed `try_into::()` means the datagram's leading EventType byte didn't match any known variant — a misalignment is impossible because DTLS is message-framed, not stream-framed. Previously, src/listen.rs would `break` out of the loop on parse failure (tearing down the connection) and src/connect.rs would silently swallow the error with no log. Both are wrong as forward-compat behavior: any future protocol addition (e.g. a new event variant) would force every existing peer to disconnect rather than gracefully ignoring the unknown event. Skip-and-continue on both sides, with a debug-level log so the behavior is observable. Pre-requisite for any future ProtoEvent variant to land without forcing a coordinated upgrade across every peer in a deployment. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/connect.rs | 23 +++++++++++++++-------- src/listen.rs | 12 ++++++++++-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/connect.rs b/src/connect.rs index cc067f7..af17461 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -255,16 +255,23 @@ async fn receive_loop( ) { let mut buf = [0u8; MAX_EVENT_SIZE]; while conn.recv(&mut buf).await.is_ok() { - if let Ok(event) = buf.try_into() { - log::trace!("{addr} <==<==<== {event}"); - match event { - ProtoEvent::Pong(b) => { - client_manager.set_active_addr(handle, Some(addr)); - client_manager.set_alive(handle, b); - ping_response.borrow_mut().insert(addr); + match buf.try_into() { + Ok(event) => { + log::trace!("{addr} <==<==<== {event}"); + match event { + ProtoEvent::Pong(b) => { + client_manager.set_active_addr(handle, Some(addr)); + client_manager.set_alive(handle, b); + ping_response.borrow_mut().insert(addr); + } + event => tx.send((handle, event)).expect("channel closed"), } - event => tx.send((handle, event)).expect("channel closed"), } + // Skip undecodable datagrams without dropping the + // connection. Each DTLS recv is one framed message, so + // skipping is safe and keeps us forward-compatible with + // peers that send event types we don't yet know about. + Err(e) => log::debug!("ignoring undecodable event from {addr}: {e}"), } } log::warn!("recv error"); diff --git a/src/listen.rs b/src/listen.rs index bd50858..e0a9475 100644 --- a/src/listen.rs +++ b/src/listen.rs @@ -259,8 +259,16 @@ async fn read_loop( .send(ListenEvent::Msg { event, addr }) .expect("channel closed"), Err(e) => { - log::warn!("error receiving event: {e}"); - break; + // Skip the malformed/unknown datagram and keep + // listening. Each DTLS recv returns one full + // datagram, so a parse error here can't desync a + // stream; the next call gets a fresh, framed + // message. This makes the protocol forward- + // compatible: a peer running a newer Lan Mouse + // version can introduce additional event types + // and old peers will simply ignore them rather + // than dropping the connection. + log::debug!("ignoring undecodable event from {addr}: {e}"); } } }