Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/mesh/NextHopRouter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ bool NextHopRouter::relayOpaquePacket(const meshtastic_MeshPacket *p)
(p->next_hop != NO_NEXT_HOP_PREFERENCE && p->next_hop != nodeDB->getLastByteOfNodeNum(getNodeNum())))
return false;

// Dedup opaque relays. Opaque frames deliberately never enter PacketHistory (so unauthenticated
// traffic can't influence routing/ACK/next-hop) - but with NO dedup at all, a dense mesh re-relays
// every copy of every frame, multiplying at each hop into an unbounded broadcast storm ("let hop
// exhaustion bound it" caps depth, not count). Suppress duplicate opaque rebroadcasts with a small,
// routing-isolated seen-set. Genuine originator (re)transmissions (hop_start == hop_limit) are
// always relayed so reliable opaque unicast still propagates (mirrors FloodingRouter's isRepeated).
const bool isOriginatorTx = p->hop_start > 0 && p->hop_start == p->hop_limit;
if (opaqueWasSeenRecently(getFrom(p), p->id) && !isOriginatorTx) {
LOG_WARN("Drop duplicate opaque relay from 0x%08x id %u", getFrom(p), p->id);
return false;
}
Comment thread
jp-bennett marked this conversation as resolved.
Resolved
Comment thread
jp-bennett marked this conversation as resolved.
Resolved

Comment thread
jp-bennett marked this conversation as resolved.
Resolved
meshtastic_MeshPacket *relay = packetPool.allocCopy(*p);
if (!relay)
return false;
Expand All @@ -53,6 +65,24 @@ bool NextHopRouter::relayOpaquePacket(const meshtastic_MeshPacket *p)
return res == ERRNO_OK;
}

// Isolated dedup for opaque relays (see relayOpaquePacket). Returns true if (from,id) is already in the
// ring; otherwise records it (round-robin eviction) and returns false. A separate table from
// PacketHistory on purpose: opaque frames must never influence routing/ACK/next-hop. No timestamps -
// a stale (from,id) can't false-match a later packet because ids are effectively random.
bool NextHopRouter::opaqueWasSeenRecently(NodeNum from, PacketId id)
{
for (uint8_t i = 0; i < OPAQUE_SEEN_MAX; i++) {
if (opaqueSeen[i].sender == from && opaqueSeen[i].id == id)
return true;
}
// Not seen: record it, overwriting the oldest-written slot (FIFO). Empty slots hold id 0, which a
// real entry never has (relayOpaquePacket drops id 0), so they simply never match above.
opaqueSeen[opaqueSeenNext].sender = from;
opaqueSeen[opaqueSeenNext].id = id;
opaqueSeenNext = (uint8_t)((opaqueSeenNext + 1) % OPAQUE_SEEN_MAX);
return false;
}

PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions)
{
packet = p;
Expand Down
20 changes: 20 additions & 0 deletions src/mesh/NextHopRouter.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ class NextHopRouter : public FloodingRouter
constexpr static uint32_t ROUTE_TTL_MSEC = 30UL * 60 * 1000; // re-discover a route unconfirmed for 30 min
constexpr static uint8_t ROUTE_FAILURE_THRESHOLD = 3; // consecutive un-ACKed directed deliveries -> dead

constexpr static uint8_t OPAQUE_SEEN_MAX = 32; // opaque-relay dedup slots (see relayOpaquePacket); ~12B/slot -> ~384B
Comment thread
jp-bennett marked this conversation as resolved.
Resolved

protected:
/**
* Pending retransmissions
Expand All @@ -135,6 +137,21 @@ class NextHopRouter : public FloodingRouter
*/
RouteHealth routeHealth[ROUTE_HEALTH_MAX] = {};

/**
* Recently-seen opaque (undecryptable) frames, keyed on the outer (from,id) header. A second,
* isolated PacketHistory-style dedup: it bounds broadcast amplification of frames we can't decrypt
* WITHOUT admitting them to the real PacketHistory/NodeDB, so unauthenticated traffic can never
* influence routing / ACK / next-hop decisions. Fixed-size ring, round-robin (FIFO) eviction, no
* timestamps (a stale (from,id) can't false-match: packet ids are effectively random, and a real
* entry never has id 0 - relayOpaquePacket drops id 0 before this). RAM-only.
*/
struct OpaqueSeen {
NodeNum sender = 0;
PacketId id = 0; // 0 == empty/unused slot
};
OpaqueSeen opaqueSeen[OPAQUE_SEEN_MAX] = {};
uint8_t opaqueSeenNext = 0; // ring write cursor (round-robin eviction)

/**
* Should this incoming filter be dropped?
*
Expand All @@ -143,6 +160,9 @@ class NextHopRouter : public FloodingRouter
*/
virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override;
bool relayOpaquePacket(const meshtastic_MeshPacket *p) override;
// Dedup helper for relayOpaquePacket: true if (from,id) is already recorded; otherwise records it
// (round-robin eviction) and returns false. Pure function of the table - no clock.
bool opaqueWasSeenRecently(NodeNum from, PacketId id);

/**
* Look for packets we need to relay
Expand Down
Loading