fix(node): clear the connected socket when a peer's address rotates

`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.
This commit is contained in:
fr34aky
2026-09-09 18:45:42 +00:00
committed by Johnathan Corgan
parent c0aa7ebf53
commit 0264c9e275
2 changed files with 24 additions and 1 deletions
+6
View File
@@ -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
+18 -1
View File
@@ -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;