feat(peer): transport-preference roaming cutover

When a peer is reachable over more than one transport at once, prefer the
faster one and hold it. roam_current_addr upgrades eagerly to a higher-
preference transport (BLE -> Wi-Fi Aware / UDP) but falls back to a lower
one only after the preferred link has been silent past a hysteresis
window (~2 heartbeat intervals), so a stray BLE keepalive can't drag an
active Aware session back to BLE. poll_transport_discovery additionally
stops re-probing an alternate path strictly worse than the one the peer
is already on, so BLE rediscovery no longer keeps grabbing a peer that
belongs on Aware. Equal preferences reduce to plain last-authenticated-
packet-wins roaming, so single-transport deployments are unaffected.
This commit is contained in:
Arjen
2026-07-14 13:54:05 +02:00
parent 0aed417dbd
commit 527514689f
5 changed files with 165 additions and 2 deletions
+21 -2
View File
@@ -263,6 +263,11 @@ impl Node {
let ce_flag = header.flags & FLAG_CE != 0;
let sp_flag = header.flags & FLAG_SP != 0;
// Transport-preference cutover: gate roaming so a faster transport
// (Wi-Fi Aware / UDP) isn't dragged back to a slower one (BLE) by a
// stray packet. Computed before the peer borrow (needs `self`).
let roam_pref = self.transport_preference(packet.transport_id);
let roam_hysteresis = self.roam_hysteresis_ms();
if let Some(peer) = self.peers.get_mut(&node_addr) {
if let Some(mmp) = peer.mmp_mut() {
mmp.receiver.record_recv(
@@ -274,7 +279,13 @@ impl Node {
);
let _spin_rtt = mmp.spin_bit.rx_observe(sp_flag, header.counter, now);
}
peer.set_current_addr(packet.transport_id, packet.remote_addr.clone());
peer.roam_current_addr(
packet.transport_id,
packet.remote_addr.clone(),
roam_pref,
packet.timestamp_ms,
roam_hysteresis,
);
peer.link_stats_mut()
.record_recv(packet.data.len(), packet.timestamp_ms);
peer.touch(packet.timestamp_ms);
@@ -356,10 +367,18 @@ impl Node {
return;
};
let now = Instant::now();
let roam_pref = self.transport_preference(transport_id);
let roam_hysteresis = self.roam_hysteresis_ms();
let mut address_changed = false;
if let Some(peer) = self.peers.get_mut(node_addr) {
peer.reset_decrypt_failures();
address_changed = peer.set_current_addr(transport_id, remote_addr.clone());
address_changed = peer.roam_current_addr(
transport_id,
remote_addr.clone(),
roam_pref,
packet_timestamp_ms,
roam_hysteresis,
);
peer.link_stats_mut()
.record_recv(packet_len, packet_timestamp_ms);
peer.touch(packet_timestamp_ms);
+12
View File
@@ -620,6 +620,18 @@ impl Node {
let remote_addr = peer.addr;
if self.peers.contains_key(&node_addr) {
// Don't re-probe an alternate path that is strictly worse
// than the one the peer is already on: a peer settled on a
// faster transport (Wi-Fi Aware / UDP) must not be pulled
// back to BLE by BLE rediscovery. Equal-or-better candidates
// still refresh, so BLE → Aware upgrades proceed.
if let Some(cur) = self.peers.get(&node_addr).and_then(|p| p.transport_id()) {
if self.transport_preference(cur)
> self.transport_preference(candidate_transport_id)
{
continue;
}
}
let transport_name = transport.transport_type().name;
let candidate = PeerAddress::new(transport_name, remote_addr.to_string());
if self.active_peer_candidate_is_fresh_enough_to_skip(
+23
View File
@@ -1093,6 +1093,29 @@ impl Node {
.map(|(id, _)| *id)
}
/// Link preference of a transport instance by id (higher = preferred),
/// from its transport type. See [`crate::transport::transport_link_preference`].
/// Unknown ids get 0 so any real transport outranks a stale/removed one.
pub(crate) fn transport_preference(&self, id: TransportId) -> u8 {
self.transports
.get(&id)
.map(|h| crate::transport::transport_link_preference(h.transport_type().name))
.unwrap_or(0)
}
/// How long a preferred transport must be silent before a lower-preference
/// transport may take over the peer's link (the roaming cutover hysteresis).
/// ~2 heartbeat intervals so a single missed heartbeat can't cause a
/// spurious downgrade; floored so a tiny configured interval can't flap.
pub(crate) fn roam_hysteresis_ms(&self) -> u64 {
(self
.config()
.node
.heartbeat_interval_secs
.saturating_mul(2_000))
.max(15_000)
}
/// Resolve an Ethernet peer address ("interface/mac") to a transport ID
/// and binary TransportAddr.
///