From e2ef3ccdb4b13991a2c32988e4b878b938309e1c Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Sat, 21 Feb 2026 10:36:00 -0600 Subject: [PATCH] Mitigate one-byte direct-hop collisions by preferring flood on ambiguous paths Direct routing currently uses one-byte hop hashes, which can collide in larger or mixed-topology meshes and cause mis-forwarding or unstable delivery when a stored path includes ambiguous hop bytes. This change adds a compatibility-safe mitigation in route selection logic for chat-style workflows: detect ambiguous direct paths using known contact prefixes and avoid direct sends when ambiguity is present. When ambiguity is detected, outbound traffic that would have used stored direct paths now falls back to scoped flood for message send APIs, ACK sending, response-path decisions, keep-alive traffic, and return-path retries. The mitigation is intentionally conservative: it does not alter packet format or routing protocol semantics, and it reduces risk of direct-route blackholing, route flapping, and repeated retry/fallback churn in collision-prone neighborhoods. A lightweight internal counter was added to track how often direct sends are avoided due to ambiguity, enabling future operational tuning and observability. --- src/helpers/BaseChatMesh.cpp | 74 ++++++++++++++++++++++++++++++------ src/helpers/BaseChatMesh.h | 4 ++ 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 6de7469d0..1b4daf2e9 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -39,7 +39,7 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl } void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) { - if (dest.out_path_len < 0) { + if (!shouldUseDirectPath(dest)) { mesh::Packet* ack = createAck(ack_hash); if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY); } else { @@ -55,6 +55,26 @@ void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) { } } +bool BaseChatMesh::isAmbiguousDirectPath(const ContactInfo& recipient) const { + if (recipient.out_path_len <= 0) return false; + for (int i = 0; i < recipient.out_path_len; i++) { + uint8_t hop = recipient.out_path[i]; + bool seen_once = false; + for (int j = 0; j < num_contacts; j++) { + if (contacts[j].id.matches(recipient.id)) continue; + if (contacts[j].id.pub_key[0] == hop) { + if (seen_once) return true; + seen_once = true; + } + } + } + return false; +} + +bool BaseChatMesh::shouldUseDirectPath(const ContactInfo& recipient) const { + return recipient.out_path_len >= 0 && !isAmbiguousDirectPath(recipient); +} + void BaseChatMesh::bootstrapRTCfromContacts() { uint32_t latest = 0; for (int i = 0; i < num_contacts; i++) { @@ -263,9 +283,12 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len); if (reply) { - if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT + if (shouldUseDirectPath(from)) { // we have an unambiguous out_path, so send DIRECT sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY); } else { + if (from.out_path_len >= 0) { + ambiguous_direct_avoids++; + } sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY); } } @@ -273,7 +296,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender } } else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) { onContactResponse(from, data, len); - if (packet->isRouteFlood() && from.out_path_len >= 0) { + if (packet->isRouteFlood() && shouldUseDirectPath(from)) { // we have direct path, but other node is still sending flood response, so maybe they didn't receive reciprocal path properly(?) handleReturnPathRetry(from, packet->path, packet->path_len); } @@ -317,7 +340,7 @@ void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit - if (packet->isRouteFlood() && from->out_path_len >= 0) { + if (packet->isRouteFlood() && shouldUseDirectPath(*from)) { // we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?) handleReturnPathRetry(*from, packet->path, packet->path_len); } @@ -327,6 +350,12 @@ void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len) { // NOTE: simplest impl is just to re-send a reciprocal return path to sender (DIRECTLY) // override this method in various firmwares, if there's a better strategy + if (!shouldUseDirectPath(contact)) { + if (contact.out_path_len >= 0) { + ambiguous_direct_avoids++; + } + return; + } mesh::Packet* rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, NULL, 0); if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay } @@ -386,7 +415,10 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); int rc; - if (recipient.out_path_len < 0) { + if (!shouldUseDirectPath(recipient)) { + if (recipient.out_path_len >= 0) { + ambiguous_direct_avoids++; + } sendFloodScoped(recipient, pkt); txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); rc = MSG_SEND_SENT_FLOOD; @@ -412,7 +444,10 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); int rc; - if (recipient.out_path_len < 0) { + if (!shouldUseDirectPath(recipient)) { + if (recipient.out_path_len >= 0) { + ambiguous_direct_avoids++; + } sendFloodScoped(recipient, pkt); txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); rc = MSG_SEND_SENT_FLOOD; @@ -500,7 +535,10 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (!shouldUseDirectPath(recipient)) { + if (recipient.out_path_len >= 0) { + ambiguous_direct_avoids++; + } sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -525,7 +563,10 @@ int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data, } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (!shouldUseDirectPath(recipient)) { + if (recipient.out_path_len >= 0) { + ambiguous_direct_avoids++; + } sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -552,7 +593,10 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_ } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (!shouldUseDirectPath(recipient)) { + if (recipient.out_path_len >= 0) { + ambiguous_direct_avoids++; + } sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -579,7 +623,10 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (!shouldUseDirectPath(recipient)) { + if (recipient.out_path_len >= 0) { + ambiguous_direct_avoids++; + } sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -683,8 +730,11 @@ void BaseChatMesh::checkConnections() { MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact not found!"); continue; } - if (contact->out_path_len < 0) { - MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact, no out_path!"); + if (!shouldUseDirectPath(*contact)) { + MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact, no direct path candidate"); + if (contact->out_path_len >= 0) { + ambiguous_direct_avoids++; + } continue; } diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index fd391b980..45f584c41 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -70,8 +70,11 @@ class BaseChatMesh : public mesh::Mesh { mesh::Packet* _pendingLoopback; uint8_t temp_buf[MAX_TRANS_UNIT]; ConnectionInfo connections[MAX_CONNECTIONS]; + uint32_t ambiguous_direct_avoids; mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack); + bool isAmbiguousDirectPath(const ContactInfo& recipient) const; + bool shouldUseDirectPath(const ContactInfo& recipient) const; void sendAckTo(const ContactInfo& dest, uint32_t ack_hash); protected: @@ -85,6 +88,7 @@ class BaseChatMesh : public mesh::Mesh { #endif txt_send_timeout = 0; _pendingLoopback = NULL; + ambiguous_direct_avoids = 0; memset(connections, 0, sizeof(connections)); }