mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-10 15:43:36 -04:00
* Report the ack proof verdict to the client * Test that a proven ack reaches the phone as VALID * Say why the verdict clear and the pending gate sit where they do
338 lines
17 KiB
C++
338 lines
17 KiB
C++
#include "ReliableRouter.h"
|
|
#include "AckProof.h"
|
|
#include "Default.h"
|
|
#include "MeshTypes.h"
|
|
#include "NodeDB.h"
|
|
#include "UptimeClock.h"
|
|
#include "configuration.h"
|
|
#include "memGet.h"
|
|
#include "mesh-pb-constants.h"
|
|
#include "modules/NodeInfoModule.h"
|
|
#include "modules/RoutingModule.h"
|
|
|
|
// ReliableRouter::ReliableRouter() {}
|
|
|
|
/**
|
|
* If the message is want_ack, then add it to a list of packets to retransmit.
|
|
* If we run out of retransmissions, send a nak packet towards the original client to indicate failure.
|
|
*/
|
|
ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
|
|
{
|
|
if (isBlockedEventCoordinatePacket(p)) {
|
|
LOG_DEBUG("Suppress reliable coordinate send on event (everyone) channel");
|
|
packetPool.release(p);
|
|
return meshtastic_Routing_Error_NOT_AUTHORIZED;
|
|
}
|
|
|
|
const GlobalPacketId key(p);
|
|
const bool retransmitting = p->want_ack;
|
|
|
|
if (p->want_ack) {
|
|
DEBUG_HEAP_BEFORE;
|
|
auto copy = packetPool.allocCopy(*p);
|
|
DEBUG_HEAP_AFTER("ReliableRouter::send", copy);
|
|
|
|
if (copy) {
|
|
const uint8_t totalAttempts = isBroadcast(p->to) ? NUM_RELIABLE_RETX : NUM_RELIABLE_UNICAST_ATTEMPTS;
|
|
startRetransmission(copy, totalAttempts);
|
|
}
|
|
}
|
|
|
|
/* If we have pending retransmissions, add the airtime of this packet to it, because during that time we cannot receive an
|
|
(implicit) ACK. Otherwise, we might retransmit too early.
|
|
*/
|
|
for (auto i = pending.begin(); i != pending.end(); i++) {
|
|
if (i->first.id != p->id) {
|
|
i->second.nextTxMsec += iface->getPacketTime(p);
|
|
}
|
|
}
|
|
|
|
ErrorCode result = isBroadcast(p->to) ? FloodingRouter::send(p) : NextHopRouter::send(p);
|
|
// Duty-cycle rejections may clear before the scheduled retry.
|
|
if (retransmitting && result != ERRNO_OK && result != meshtastic_Routing_Error_DUTY_CYCLE_LIMIT)
|
|
stopRetransmission(key);
|
|
|
|
return result;
|
|
}
|
|
|
|
void ReliableRouter::perhapsGenerateImplicitAckForOwnOverheard(const meshtastic_MeshPacket *p)
|
|
{
|
|
// Note: do not use getFrom() here, because we want to ignore messages sent from phone
|
|
if (p->from != getNodeNum())
|
|
return;
|
|
|
|
printPacket("Rx someone rebroadcasting for us", p);
|
|
|
|
// We are seeing someone rebroadcast one of our transmissions. If this is the first time we saw
|
|
// this, cancel any retransmissions we have queued up and generate an internal ack for the
|
|
// original sending process. Header-only (from/id), so it works even for a packet we cannot
|
|
// decrypt - notably a PKI DM we originated, which is opaque to us when overheard.
|
|
|
|
// This "optimization", does save lots of airtime. For DMs, you also get a real ACK back
|
|
// from the intended recipient.
|
|
auto key = GlobalPacketId(getFrom(p), p->id);
|
|
auto old = findPendingPacket(key);
|
|
if (old) {
|
|
LOG_DEBUG("Generate implicit ack");
|
|
// NOTE: we do NOT check p->wantAck here because p is the INCOMING rebroadcast and that packet is not expected to be
|
|
// marked as wantAck
|
|
// Pass the overheard rebroadcast as the relay source so the ack carries the relaying node's id
|
|
// and the RSSI/SNR we heard it at.
|
|
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel, 0, false, p);
|
|
|
|
// Only stop retransmissions if the rebroadcast came via LoRa
|
|
if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) {
|
|
stopRetransmission(key);
|
|
}
|
|
} else {
|
|
LOG_DEBUG("Didn't find pending packet");
|
|
}
|
|
}
|
|
|
|
bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
|
{
|
|
perhapsGenerateImplicitAckForOwnOverheard(p);
|
|
|
|
/* At this point we have already deleted the pending retransmission if this packet was an (implicit) ACK to it.
|
|
Now for all other pending retransmissions, we have to add the airtime of this received packet to the retransmission timer,
|
|
because while receiving this packet, we could not have received an (implicit) ACK for it.
|
|
If we don't add this, we will likely retransmit too early.
|
|
*/
|
|
for (auto i = pending.begin(); i != pending.end(); i++) {
|
|
i->second.nextTxMsec += iface->getPacketTime(p, true);
|
|
}
|
|
|
|
return isBroadcast(p->to) ? FloodingRouter::shouldFilterReceived(p) : NextHopRouter::shouldFilterReceived(p);
|
|
}
|
|
|
|
/**
|
|
* If we receive a want_ack packet (do not check for wasSeenRecently), send back an ack (this might generate multiple ack sends in
|
|
* case the our first ack gets lost)
|
|
*
|
|
* If we receive an ack packet (do check wasSeenRecently), clear out any retransmissions and
|
|
* forward the ack to the application layer.
|
|
*
|
|
* If we receive a nak packet (do check wasSeenRecently), clear out any retransmissions
|
|
* and forward the nak to the application layer.
|
|
*
|
|
* Otherwise, let superclass handle it.
|
|
*/
|
|
void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c)
|
|
{
|
|
lastAckProof = {};
|
|
if (isToUs(p)) { // ignore ack/nak/want_ack packets that are not address to us (we only handle 0 hop reliability)
|
|
if (!MeshModule::currentReply) {
|
|
if (p->want_ack) {
|
|
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
|
/* A response may be set to want_ack for retransmissions, but we don't need to ACK a response if it received
|
|
an implicit ACK already. If we received it directly or via NextHopRouter, only ACK with a hop limit of 0 to
|
|
make sure the other side stops retransmitting. */
|
|
|
|
if (shouldSuccessAckWithWantAck(p)) {
|
|
// If this packet should always be ACKed reliably with want_ack back to the original sender, make sure we
|
|
// do that unconditionally.
|
|
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel,
|
|
routingModule->getHopLimitForResponse(*p), true);
|
|
} else if (!p->decoded.request_id && !p->decoded.reply_id) {
|
|
// If it's not an ACK or a reply, send an ACK.
|
|
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel,
|
|
routingModule->getHopLimitForResponse(*p));
|
|
} else if ((getHopsAway(*p) == 0) || p->next_hop != NO_NEXT_HOP_PREFERENCE) {
|
|
// If we received the packet directly from the original sender, send a 0-hop ACK since the original sender
|
|
// won't overhear any implicit ACKs. If we received the packet via NextHopRouter, also send a 0-hop ACK to
|
|
// stop the immediate relayer's retransmissions.
|
|
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
|
}
|
|
} else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 &&
|
|
(nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->public_key.size == 0)) {
|
|
LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY");
|
|
sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(),
|
|
routingModule->getHopLimitForResponse(*p));
|
|
} else {
|
|
// Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded
|
|
sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(),
|
|
routingModule->getHopLimitForResponse(*p));
|
|
}
|
|
} else if (p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum()) && p->hop_limit > 0) {
|
|
// No wantAck, but we need to ACK with hop limit of 0 if we were the next hop to stop their retransmissions
|
|
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
|
}
|
|
} else {
|
|
LOG_DEBUG("Another module replied to this message, no need for 2nd ack");
|
|
}
|
|
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && c &&
|
|
c->error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) {
|
|
if (owner.public_key.size == 32) {
|
|
LOG_INFO("PKI decrypt failure, send a NodeInfo");
|
|
nodeInfoModule->sendOurNodeInfo(p->from, false, p->channel, true);
|
|
}
|
|
}
|
|
// We consider an ack to be either a !routing packet with a request ID or a routing packet with !error
|
|
PacketId ackId = ((c && c->error_reason == meshtastic_Routing_Error_NONE) || !c) ? p->decoded.request_id : 0;
|
|
|
|
// A nak is a routing packt that has an error code
|
|
PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0;
|
|
|
|
// We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records
|
|
if ((ackId || nakId) && ackProofPermitsAction(p, ackId ? ackId : nakId, ackId != 0) &&
|
|
// Implicit ACKs from MQTT should not stop retransmissions
|
|
!(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) {
|
|
LOG_DEBUG("Received a %s for 0x%08x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId);
|
|
if (ackId) {
|
|
stopRetransmission(p->to, ackId);
|
|
// M3: an end-to-end ACK proves the directed route to the ACK's sender currently works,
|
|
// so clear its failure count and refresh freshness (keeps a good route pinned).
|
|
if (!isBroadcast(getFrom(p)))
|
|
noteRouteSuccess(getFrom(p), Time::stampMillis());
|
|
} else {
|
|
stopRetransmission(p->to, nakId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// handle the packet as normal
|
|
isBroadcast(p->to) ? FloodingRouter::sniffReceived(p, c) : NextHopRouter::sniffReceived(p, c);
|
|
}
|
|
|
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
|
static meshtastic_MeshPacket_AckProofStatus toAckProofStatus(AckProofResult result)
|
|
{
|
|
switch (result) {
|
|
case AckProofResult::VALID:
|
|
return meshtastic_MeshPacket_AckProofStatus_ACK_PROOF_VALID;
|
|
case AckProofResult::INVALID:
|
|
return meshtastic_MeshPacket_AckProofStatus_ACK_PROOF_INVALID;
|
|
case AckProofResult::NO_KEY:
|
|
return meshtastic_MeshPacket_AckProofStatus_ACK_PROOF_NO_KEY;
|
|
case AckProofResult::ABSENT:
|
|
break;
|
|
}
|
|
return meshtastic_MeshPacket_AckProofStatus_ACK_PROOF_ABSENT;
|
|
}
|
|
#endif
|
|
|
|
meshtastic_MeshPacket_AckProofStatus ReliableRouter::ackProofStatusFor(const meshtastic_MeshPacket &p) const
|
|
{
|
|
if (lastAckProof.from == getFrom(&p) && lastAckProof.id == p.id)
|
|
return lastAckProof.status;
|
|
return meshtastic_MeshPacket_AckProofStatus_ACK_PROOF_ABSENT;
|
|
}
|
|
|
|
bool ReliableRouter::ackProofPermitsAction(const meshtastic_MeshPacket *p, PacketId originalId, bool isAck)
|
|
{
|
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
|
// Cheap gate FIRST: verification costs one X25519 per call (setDHPublicKey runs Curve25519::dh2
|
|
// and nothing caches the result), and an attacker with the channel key picks when we pay it. A
|
|
// packet id is visible in the cleartext header, so without this gate a forged ack for any id at
|
|
// all forces a DH. With it, only ids we genuinely have outstanding can, and only while pending.
|
|
//
|
|
// The cost of that gate: a relay's rebroadcast of our own DM already cleared this entry via
|
|
// perhapsGenerateImplicitAckForOwnOverheard, so on most multi-hop DMs the peer's genuine proof
|
|
// arrives after the entry is gone and reads ABSENT. Do not "fix" that by dropping the gate - it
|
|
// is the DoS guard. Checking the proof before the echo clears the entry, or keeping a short-lived
|
|
// record of completed sends to verify against, is the shape of a real fix.
|
|
PendingPacket *orig = findPendingPacket(p->to, originalId);
|
|
if (!orig)
|
|
return true;
|
|
const bool expected = orig->packet && orig->packet->pki_encrypted;
|
|
|
|
// Only the node we ADDRESSED can prove receipt. The MAC only proves its author holds a pairwise
|
|
// key with us, and every keyed peer holds one - so verifying against the key of whoever the ack
|
|
// claims to be from would accept a proof minted by any of them. C, whose key we hold, can read
|
|
// our packet id out of the cleartext header and mint a VALID-looking receipt for a packet that
|
|
// went to B. Bind to orig->packet->to instead.
|
|
//
|
|
// Bailing out here rather than verifying against the recipient key and reporting INVALID is
|
|
// deliberate on both counts: it skips the X25519 an attacker would otherwise choose when we
|
|
// pay, and a third-party ack is "not a receipt" rather than "a forged receipt" - naks from
|
|
// intermediates (NO_CHANNEL, PKI_UNKNOWN_PUBKEY, MAX_RETRANSMIT) legitimately arrive from a
|
|
// node that is not the destination, and must not be logged as proof mismatches.
|
|
//
|
|
// A broadcast original has no single recipient to bind to, so there is nothing to check, and
|
|
// any recipient may answer it. That case permits unconditionally - holding it to the sender
|
|
// check under enforcement would stop every reliable broadcast from ever being acked.
|
|
if (!orig->packet || isBroadcast(orig->packet->to))
|
|
return true;
|
|
|
|
if (getFrom(p) != orig->packet->to) {
|
|
LOG_DEBUG("Ack for 0x%08x is not from the node we addressed, not a receipt", originalId);
|
|
// Advisory today, so both arms are true and this reads as dead code. It is not: under
|
|
// enforcement a bare `return true` here would be a hole, since the sender field is
|
|
// unauthenticated and an attacker would simply address the ack from anyone other than the
|
|
// node we sent to, skipping the proof requirement entirely. Only success acks are held to
|
|
// it; a nak from an intermediate is legitimate and keeps today's behavior either way.
|
|
//
|
|
// Closing this does NOT make enforcement sound on its own - ABSENT still permits, so an
|
|
// attacker spoofing `from` and sending no proof gets through anyway, and ABSENT cannot be
|
|
// made to block for the reasons recorded at ACK_PROOF_ENFORCE. The point is narrower: a
|
|
// flag named "enforce" should not have a branch that silently ignores it.
|
|
//
|
|
// Written as an if rather than `isAck ? !ACK_PROOF_ENFORCE : true` because with the flag
|
|
// off both arms of that ternary are `true`, which cppcheck reports as duplicateValueTernary.
|
|
if (isAck && ACK_PROOF_ENFORCE)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// Safe to key off getFrom(p) below only because it is now known equal to orig->packet->to.
|
|
const AckProofResult verdict = ackProofVerify(p, originalId);
|
|
lastAckProof = {getFrom(p), p->id, toAckProofStatus(verdict)};
|
|
switch (verdict) {
|
|
case AckProofResult::VALID:
|
|
LOG_DEBUG("ACK proof OK for 0x%08x", originalId);
|
|
return true;
|
|
case AckProofResult::INVALID:
|
|
// Somebody produced an ack for our packet without holding the shared secret.
|
|
LOG_WARN("ACK proof MISMATCH for 0x%08x from 0x%08x%s", originalId, getFrom(p),
|
|
ACK_PROOF_ENFORCE ? ", ignoring ack" : " (advisory)");
|
|
return !ACK_PROOF_ENFORCE;
|
|
case AckProofResult::NO_KEY:
|
|
LOG_DEBUG("ACK proof present for 0x%08x but no authoritative key for 0x%08x", originalId, getFrom(p));
|
|
return true;
|
|
case AckProofResult::ABSENT:
|
|
// What every current firmware sends, and what every non-PKI peer will always send, so this
|
|
// can only ever be reported - never enforced. See ACK_PROOF_ENFORCE in AckProof.h.
|
|
if (expected)
|
|
LOG_INFO("Unproven ack for PKI packet 0x%08x from 0x%08x", originalId, getFrom(p));
|
|
return true;
|
|
}
|
|
#else
|
|
(void)p;
|
|
(void)originalId;
|
|
(void)isAck;
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* If we ACK this packet, should we set want_ack=true on the ACK for reliable delivery of the ACK packet?
|
|
*/
|
|
bool ReliableRouter::shouldSuccessAckWithWantAck(const meshtastic_MeshPacket *p)
|
|
{
|
|
// Don't ACK-with-want-ACK outgoing packets
|
|
if (isFromUs(p))
|
|
return false;
|
|
|
|
// Only ACK-with-want-ACK if the original packet asked for want_ack
|
|
if (!p->want_ack)
|
|
return false;
|
|
|
|
// Only ACK-with-want-ACK packets to us (not broadcast)
|
|
if (!isToUs(p))
|
|
return false;
|
|
|
|
// Special case for text message DMs:
|
|
bool isTextMessage =
|
|
(p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) &&
|
|
IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP);
|
|
|
|
if (isTextMessage) {
|
|
// If it's a non-broadcast text message, and the original asked for want_ack,
|
|
// let's send an ACK that is itself want_ack to improve reliability of confirming delivery back to the sender.
|
|
// This should include all DMs regardless of whether or not reply_id is set.
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|