From 0264c9e27526e5e2cd3b5636bbfaaed9b601cdcc Mon Sep 17 00:00:00 2001 From: fr34aky <162515565+fr34aky@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:49:43 +0000 Subject: [PATCH] fix(node): clear the connected socket when a peer's address rotates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `handle_encrypted_frame` updated the peer's current address and discarded the flag saying it had changed, so a peer that roamed kept its per-peer `connect()`-ed UDP socket pinned to the old 5-tuple and went on sending where it used to be. The decrypt-worker completion path already captures that flag and clears the socket for exactly this reason; this path did not. Pre-existing, and not something this branch touched — but the first-sight rule added here now rests on the invariant it breaks. That rule compares the socket's pinned source against a probe to the peer's *current* address, so a socket left behind after a roam makes those two disagree for as long as it survives, and the peer reports a move on first sight that nothing local caused. Fixed rather than documented as an assumption, because the stale socket is a defect on its own terms: it is aimed at an address the peer has left. --- CHANGELOG.md | 6 ++++++ src/node/dataplane/encrypted.rs | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31667d9f..36c26c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 would have floored `node.heartbeat_interval_secs` at two seconds, so a configured value below that would silently not have been honoured. +- A peer that rotates its address no longer keeps sending from a socket + aimed where it used to be. The authenticated-frame path updated the + peer's address and discarded the flag saying it had changed, so the + per-peer `connect()`-ed UDP socket stayed pinned to the old 5-tuple; + the sibling path already cleared it. + #### Data plane - A peer that stops reading can no longer stall the node. TCP, Tor, Nym and diff --git a/src/node/dataplane/encrypted.rs b/src/node/dataplane/encrypted.rs index e06880ab..72aff90b 100644 --- a/src/node/dataplane/encrypted.rs +++ b/src/node/dataplane/encrypted.rs @@ -264,6 +264,7 @@ impl Node { let ce_flag = header.flags & FLAG_CE != 0; let sp_flag = header.flags & FLAG_SP != 0; + let mut address_changed = false; if let Some(peer) = self.peers.get_mut(&node_addr) { if let Some(mmp) = peer.mmp_mut() { mmp.receiver.record_recv( @@ -275,12 +276,28 @@ impl Node { ); let _spin_rtt = mmp.spin_bit.rx_observe(sp_flag, header.counter, now_ms); } - peer.set_current_addr(packet.transport_id, packet.remote_addr.clone()); + address_changed = + peer.set_current_addr(packet.transport_id, packet.remote_addr.clone()); peer.link_stats_mut() .record_recv(packet.data.len(), packet.timestamp_ms); peer.touch(packet.timestamp_ms); } + // Address rotation invalidates the per-peer connect()-ed UDP socket, + // which is still pinned to the old 5-tuple. The decrypt-worker + // completion path already does this; this one discarded the flag, so a + // peer that roamed kept sending from a socket aimed where it used to + // be. `netmon`'s first-sight rule now rests on this too: it compares + // that socket's pinned source against a probe to the peer's *current* + // address, and a socket left behind makes those two disagree for as + // long as it survives. + #[cfg(any(target_os = "linux", target_os = "macos"))] + if address_changed { + self.clear_connected_udp_for_peer(&node_addr); + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + let _ = address_changed; + // Dispatch to link message handler self.dispatch_link_message(&node_addr, link_message, ce_flag) .await;