diff --git a/docs/cli_commands.md b/docs/cli_commands.md index b5932e8e5..4fe70d78f 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -481,10 +481,10 @@ send text.flood checking ridge link - Region hierarchy edits are not persistent until `region save` is run. After `region def west pnw wa w-wa sea`, run `region save` before enabling the alert if the hierarchy must survive a reboot. - A region must have a usable transport key. Public named regions derive one automatically; a private region without an available key is rejected. - The first alert is suppressed until the repeater has been up for at least 30 minutes. After that, the repeater checks every 30 minutes and sends low-battery warnings to the `#repeaters` channel in the selected region. -- Once an alert is successfully queued, another battery alert is suppressed for at least 12 hours. Battery recovery or toggling alerts off and back on does not bypass that cooldown during the same boot. +- Once an alert finishes transmitting, another battery alert is suppressed for at least 12 hours. A queue rejection, stale-queue drop, or radio send failure does not start the cooldown. Battery recovery or toggling alerts off and back on does not bypass a completed alert's cooldown during the same boot. - With `region def west pnw wa w-wa sea`, `set battery.alert on` selects `sea`; `set battery.alert on w-wa` overrides that default. - `get battery.alert.region` returns the selected scope, for example `> sea`. -- The battery check never requests a wake earlier than its 30-minute deadline. If the normal loop is already awake when that deadline has elapsed, the check is effectively free of an additional wake. Time in light/event sleep counts toward the startup delay, and a queued alert keeps the repeater awake until the packet is handled. +- The battery check never requests a wake earlier than its 30-minute deadline. If the normal loop is already awake when that deadline has elapsed, the check is effectively free of an additional wake. Time in light/event sleep counts toward the startup delay, and a pending alert keeps the repeater awake until the packet is handled. --- diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md index 8009e48ad..55e8b4b3f 100644 --- a/docs/halo_keymind_settings.md +++ b/docs/halo_keymind_settings.md @@ -105,7 +105,8 @@ checks every 30 minutes and sends a flood text warning to `#repeaters` when voltage is above `1 V` and the estimated battery percent is below `battery.alert.low`. -Warnings and critical alerts both use a `12`-hour resend cooldown. +Warnings and critical alerts both use a `12`-hour resend cooldown, beginning +only after the radio reports that the alert transmission completed. Defaults: diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 27e97b5bd..9980ebdec 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -689,39 +689,39 @@ bool MyMesh::allowPacketForward(const mesh::Packet* packet) { return _prefs.client_repeat != 0; } -void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) { +bool MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) { if (scope.isNull()) { - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + return sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); } else { uint16_t codes[2]; codes[0] = scope.calcTransportCode(pkt); codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? - sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1); + return sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1); } } -void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { +bool MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { // TODO: dynamic send_scope, depending on recipient and current 'home' Region if (send_unscoped) { - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped + return sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app explicitly requested un-scoped } else { TransportKey default_scope; memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key)); auto scope = send_scope.isNull() ? &default_scope : &send_scope; - sendFloodScoped(*scope, pkt, delay_millis); + return sendFloodScoped(*scope, pkt, delay_millis); } } -void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { +bool MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { // TODO: have per-channel send_scope if (send_unscoped) { - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped + return sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app explicitly requested un-scoped } else { TransportKey default_scope; memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key)); auto scope = send_scope.isNull() ? &default_scope : &send_scope; - sendFloodScoped(*scope, pkt, delay_millis); + return sendFloodScoped(*scope, pkt, delay_millis); } } @@ -1375,9 +1375,8 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { if (replacement_entry != NULL) { - // The newest successfully-composed submission wins. Keep the older - // sequence intact if packet allocation failed. sendMessage() already - // stopped only its matching retries before registering the new send. + // The newest successfully-queued submission wins. Keep the older + // entry intact if composition, validation, or queueing failed. clearExpectedAck(*replacement_entry, false); } if (expected_ack) { @@ -1708,6 +1707,9 @@ void MyMesh::handleCmdFrame(size_t len) { bool applied = radio_driver.setParams(new_freq, new_bw, sf, cr); #endif if (!applied) { + // Persisted settings remain authoritative after a rejected change. + saved_radio_apply_pending = true; + radio_apply_retry_at = 0; writeErrFrame(ERR_CODE_BAD_STATE); return; } @@ -2085,7 +2087,7 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_BAD_STATE); } - } else if (cmd_frame[0] == CMD_SEND_TRACE_PATH && len > 10 && len - 10 < MAX_PACKET_PAYLOAD-5) { + } else if (cmd_frame[0] == CMD_SEND_TRACE_PATH && len > 10 && len - 10 <= MAX_PACKET_PAYLOAD - 9) { uint8_t path_len = len - 10; uint8_t flags = cmd_frame[9]; uint8_t path_sz = flags & 0x03; // NEW v1.11+ @@ -2097,16 +2099,19 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(&auth, &cmd_frame[5], 4); auto pkt = createTrace(tag, auth, flags); if (pkt) { - sendDirect(pkt, &cmd_frame[10], path_len); - - uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2); + // Compute before handing ownership to sendDirect(), which releases the + // packet itself if validation or queueing fails. + uint32_t t = _radio->getEstAirtimeFor(9 + path_len + 2); uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len >> path_sz); - - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); + if (sendDirect(pkt, &cmd_frame[10], path_len)) { + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } } else { writeErrFrame(ERR_CODE_TABLE_FULL); } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index fb325ab22..73bf8bfc8 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -114,9 +114,9 @@ protected: bool filterRecvFloodPacket(mesh::Packet* packet) override; bool allowPacketForward(const mesh::Packet* packet) override; - void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis); - void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override; - void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override; + bool sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis); + bool sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override; + bool sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override; void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; bool isAutoAddEnabled() const override; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 08fb54f9b..8c5117b59 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1069,17 +1069,8 @@ uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { return base_wait_millis; } - // Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now. - float kbps = (((float)active_sf) * active_bw * ((float)active_cr)) / ((float)(1UL << active_sf)); - if (kbps <= 0.0f) { - return base_wait_millis; - } - - // Wait roughly long enough for our TX, the next hop's receive/forward window, and its echo back. - uint32_t bits = ((uint32_t)packet->getRawLength()) * 8; - float length_factor = (float)getDirectRetryPacketAirtimeFactor(packet); - uint32_t scaled_wait_millis = (uint32_t)((((float)bits) * length_factor) / kbps); - return base_wait_millis + scaled_wait_millis; + // Use the driver's LoRa airtime calculation for the echo window. + return base_wait_millis + getDirectRetryPacketAirtimeDelay(packet); } static uint8_t decodeDirectRetryTraceHashSize(uint8_t flags, uint8_t route_bytes) { @@ -2327,6 +2318,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc next_battery_alert_check = 0; next_recent_repeater_sweep = 0; last_battery_alert_sent = 0; + pending_battery_alert_packet = NULL; battery_alert_sent = false; dirty_contacts_expiry = 0; active_bw = 0.0f; @@ -2638,7 +2630,9 @@ bool MyMesh::resolveBatteryAlertScope(TransportKey& scope) { return region != NULL && getBatteryAlertScopeForRegion(*region, scope); } -bool MyMesh::sendRepeatersFloodText(const char* text, const TransportKey* scope) { +bool MyMesh::sendRepeatersFloodText(const char* text, const TransportKey* scope, + mesh::Packet** queued_packet) { + if (queued_packet != NULL) *queued_packet = NULL; if (text == NULL || *text == 0) return false; mesh::GroupChannel channel; @@ -2683,7 +2677,27 @@ bool MyMesh::sendRepeatersFloodText(const char* text, const TransportKey* scope) } const TransportKey& send_scope = scope == NULL ? default_scope : *scope; - return sendFloodScoped(send_scope, pkt, 0, _prefs.path_hash_mode + 1); + if (!sendFloodScoped(send_scope, pkt, 0, _prefs.path_hash_mode + 1)) { + return false; + } + if (queued_packet != NULL) *queued_packet = pkt; + return true; +} + +void MyMesh::onSendComplete(mesh::Packet* packet) { + mesh::Mesh::onSendComplete(packet); + if (packet == pending_battery_alert_packet) { + pending_battery_alert_packet = NULL; + battery_alert_sent = true; + last_battery_alert_sent = uptime_millis + (uint32_t)(millis() - last_millis); + } +} + +void MyMesh::onSendFail(mesh::Packet* packet) { + mesh::Mesh::onSendFail(packet); + if (packet == pending_battery_alert_packet) { + pending_battery_alert_packet = NULL; + } } void MyMesh::checkBatteryAlert() { @@ -2695,25 +2709,33 @@ void MyMesh::checkBatteryAlert() { return; } + const uint64_t current_uptime_millis = + uptime_millis + (uint32_t)(millis() - last_millis); + // Ignore startup voltage sag and give solar/charger hardware time to settle. // uptime_millis is 64-bit and includes time spent in the platform's light or // event sleep, so this guard remains reliable across millis() wraparound. // Arm the remaining startup delay once so subsequent loops use the cheaper // 32-bit deadline check above and powersaving can include it as a wake limit. - if (uptime_millis < LOW_BATTERY_STARTUP_DELAY) { + if (current_uptime_millis < LOW_BATTERY_STARTUP_DELAY) { next_battery_alert_check = futureMillis( - (unsigned long)(LOW_BATTERY_STARTUP_DELAY - uptime_millis)); + (unsigned long)(LOW_BATTERY_STARTUP_DELAY - current_uptime_millis)); return; } next_battery_alert_check = futureMillis(LOW_BATTERY_CHECK_INTERVAL); - // A successful queue operation starts one fixed cooldown. Do this before - // region resolution and ADC sampling so repeat checks during the cooldown - // stay as cheap as possible. Battery recovery and alert toggles must not - // bypass the cooldown within this boot. + // Only a completed over-the-air transmission starts the fixed cooldown. + // A queued packet remains tracked until Dispatcher reports success/failure. + if (pending_battery_alert_packet != NULL) { + return; + } + + // Do this before region resolution and ADC sampling so repeat checks during + // the cooldown stay cheap. Battery recovery and alert toggles must not bypass + // the cooldown within this boot. if (battery_alert_sent) { - if (uptime_millis - last_battery_alert_sent < LOW_BATTERY_ALERT_INTERVAL) { + if (current_uptime_millis - last_battery_alert_sent < LOW_BATTERY_ALERT_INTERVAL) { return; } battery_alert_sent = false; @@ -2738,9 +2760,9 @@ void MyMesh::checkBatteryAlert() { ? "CRITICAL BATTERY" : "LOW BATTERY"; snprintf(text, sizeof(text), "%s %u%% (%u mV)", severity, (uint32_t)batt_pct, (uint32_t)batt_mv); - if (sendRepeatersFloodText(text, &alert_scope)) { - battery_alert_sent = true; - last_battery_alert_sent = uptime_millis; + mesh::Packet* queued_packet = NULL; + if (sendRepeatersFloodText(text, &alert_scope, &queued_packet)) { + pending_battery_alert_packet = queued_packet; } } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index c83c1a503..1077fa50d 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -138,6 +138,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { unsigned long next_battery_alert_check; unsigned long next_recent_repeater_sweep; uint64_t last_battery_alert_sent; + mesh::Packet* pending_battery_alert_packet; bool battery_alert_sent; bool _logging; NodePrefs _prefs; @@ -263,7 +264,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); - bool sendRepeatersFloodText(const char* text, const TransportKey* scope = nullptr); + bool sendRepeatersFloodText(const char* text, const TransportKey* scope = nullptr, + mesh::Packet** queued_packet = nullptr); const RegionEntry* findNarrowestBatteryAlertRegion(bool& ambiguous); bool getBatteryAlertScopeForRegion(const RegionEntry& region, TransportKey& scope); bool resolveBatteryAlertScope(TransportKey& scope); @@ -374,6 +376,8 @@ protected: #endif mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override; + void onSendComplete(mesh::Packet* packet) override; + void onSendFail(mesh::Packet* packet) override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index a7c9f5a8f..1a82ffd2d 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -254,6 +254,11 @@ void Dispatcher::releaseDroppedOutbound() { } bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) { + if (pkt == NULL || raw == NULL || len < 2 || len > MAX_TRANS_UNIT) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): packet length invalid, len=%d", getLogDateTime(), len); + return false; + } + int i = 0; pkt->tx_cr = 0; @@ -264,12 +269,20 @@ bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) { } if (pkt->hasTransportCodes()) { + if (i + (int)sizeof(pkt->transport_codes) + 1 > len) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): incomplete transport header, len=%d", getLogDateTime(), len); + return false; + } memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2; memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2; } else { pkt->transport_codes[0] = pkt->transport_codes[1] = 0; } + if (i >= len) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): missing path header, len=%d", getLogDateTime(), len); + return false; + } pkt->path_len = raw[i++]; uint8_t path_mode = pkt->path_len >> 6; // upper 2 bits (legacy firmware: 00) if (path_mode == 3) { // Reserved for future @@ -278,7 +291,7 @@ bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) { } uint8_t path_byte_len = (pkt->path_len & 63) * pkt->getPathHashSize(); - if (path_byte_len > MAX_PATH_SIZE || i + path_byte_len > len) { + if (path_byte_len > MAX_PATH_SIZE || path_byte_len > len - i) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", getLogDateTime(), len); return false; } @@ -336,8 +349,9 @@ void Dispatcher::checkRecv() { Serial.print(" hash="); mesh::Utils::printHex(Serial, packet_hash, MAX_HASH_SIZE); - if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ - || pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + if (pkt->payload_len >= 2 + && (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ + || pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG)) { Serial.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); } else { Serial.printf("\n"); @@ -498,8 +512,9 @@ void Dispatcher::checkSend() { Serial.print(getLogDateTime()); Serial.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", len, outbound->getPayloadType(), outbound->isRouteDirect() ? "D" : "F", outbound->payload_len); - if (outbound->getPayloadType() == PAYLOAD_TYPE_PATH || outbound->getPayloadType() == PAYLOAD_TYPE_REQ - || outbound->getPayloadType() == PAYLOAD_TYPE_RESPONSE || outbound->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + if (outbound->payload_len >= 2 + && (outbound->getPayloadType() == PAYLOAD_TYPE_PATH || outbound->getPayloadType() == PAYLOAD_TYPE_REQ + || outbound->getPayloadType() == PAYLOAD_TYPE_RESPONSE || outbound->getPayloadType() == PAYLOAD_TYPE_TXT_MSG)) { Serial.printf(" [%02X -> %02X]\n", (uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]); } else { Serial.printf("\n"); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index deb3b1252..f72f1642a 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -30,6 +30,12 @@ static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; static const uint8_t FLOOD_RETRY_MAX_PATH_DEFAULT = 1; +static bool hasValidEncryptedPayloadLength(uint16_t payload_len, uint16_t clear_prefix_len) { + const uint16_t overhead = clear_prefix_len + CIPHER_MAC_SIZE; + if (payload_len < overhead + CIPHER_BLOCK_SIZE) return false; + return ((payload_len - overhead) % CIPHER_BLOCK_SIZE) == 0; +} + static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { uint8_t code = flags & 0x03; uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation @@ -443,6 +449,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { + if (pkt->payload_len < 9) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete TRACE packet", getLogDateTime()); + return ACTION_RELEASE; + } if (pkt->path_len < MAX_PATH_SIZE) { uint8_t i = 0; uint32_t trace_tag; @@ -473,7 +483,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { return ACTION_RELEASE; } - if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) { + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL + && pkt->payload_len >= 1 && (pkt->payload[0] & 0x80) != 0) { if (pkt->getPathHashCount() == 0) { onControlDataRecv(pkt); } @@ -484,11 +495,13 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) { // check for 'early received' ACK if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - int i = 0; - uint32_t ack_crc; - memcpy(&ack_crc, &pkt->payload[i], 4); i += 4; - if (i <= pkt->payload_len) { + if (pkt->payload_len >= sizeof(uint32_t)) { + uint32_t ack_crc; + memcpy(&ack_crc, pkt->payload, sizeof(ack_crc)); onAckRecv(pkt, ack_crc); + } else { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete early ACK packet", getLogDateTime()); + return ACTION_RELEASE; } } @@ -514,7 +527,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { _tables->markSeen(pkt); bool final_hop_retry = pkt->getPathHashCount() == 1 && pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG - && pkt->payload_len >= 2 + CIPHER_MAC_SIZE; + && hasValidEncryptedPayloadLength(pkt->payload_len, 2); removePathPrefix(pkt, 1); uint32_t d = getDirectRetransmitDelay(pkt); @@ -534,12 +547,11 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { switch (pkt->getPayloadType()) { case PAYLOAD_TYPE_ACK: { - int i = 0; - uint32_t ack_crc; - memcpy(&ack_crc, &pkt->payload[i], 4); i += 4; - if (i > pkt->payload_len) { + if (pkt->payload_len < sizeof(uint32_t)) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete ACK packet", getLogDateTime()); } else if (!_tables->wasSeen(pkt)) { + uint32_t ack_crc; + memcpy(&ack_crc, pkt->payload, sizeof(ack_crc)); _tables->markSeen(pkt); onAckRecv(pkt, ack_crc); action = routeRecvPacket(pkt); @@ -550,14 +562,17 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { case PAYLOAD_TYPE_REQ: case PAYLOAD_TYPE_RESPONSE: case PAYLOAD_TYPE_TXT_MSG: { + if (!hasValidEncryptedPayloadLength(pkt->payload_len, 2)) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); + break; + } + int i = 0; uint8_t dest_hash = pkt->payload[i++]; uint8_t src_hash = pkt->payload[i++]; uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data - if (i + CIPHER_MAC_SIZE >= pkt->payload_len) { - MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); - } else if (!_tables->wasSeen(pkt)) { + if (!_tables->wasSeen(pkt)) { _tables->markSeen(pkt); // NOTE: this is a 'first packet wins' impl. When receiving from multiple paths, the first to arrive wins. // For flood mode, the path may not be the 'best' in terms of hops. @@ -585,7 +600,12 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } uint8_t hash_size = (path_len >> 6) + 1; uint8_t hash_count = path_len & 63; - uint8_t* path = &data[k]; k += hash_size*hash_count; + uint16_t path_bytes = (uint16_t)hash_size * hash_count; + if ((uint16_t)k + path_bytes + 1 > (uint16_t)len) { + MESH_DEBUG_PRINTLN("%s PAYLOAD_TYPE_PATH, incomplete path data", getLogDateTime()); + break; + } + uint8_t* path = &data[k]; k += path_bytes; uint8_t extra_type = data[k++] & 0x0F; // upper 4 bits reserved for future use uint8_t* extra = &data[k]; uint8_t extra_len = len - k; // remainder of packet (may be padded with zeroes!) @@ -614,14 +634,17 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { break; } case PAYLOAD_TYPE_ANON_REQ: { + if (!hasValidEncryptedPayloadLength(pkt->payload_len, 1 + PUB_KEY_SIZE)) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete anonymous request", getLogDateTime()); + break; + } + int i = 0; uint8_t dest_hash = pkt->payload[i++]; uint8_t* sender_pub_key = &pkt->payload[i]; i += PUB_KEY_SIZE; uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data - if (i + 2 >= pkt->payload_len) { - MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); - } else if (!_tables->wasSeen(pkt)) { + if (!_tables->wasSeen(pkt)) { _tables->markSeen(pkt); if (self_id.isHashMatch(&dest_hash)) { Identity sender(sender_pub_key); @@ -643,13 +666,16 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } case PAYLOAD_TYPE_GRP_DATA: case PAYLOAD_TYPE_GRP_TXT: { + if (!hasValidEncryptedPayloadLength(pkt->payload_len, 1)) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete group packet", getLogDateTime()); + break; + } + int i = 0; uint8_t channel_hash = pkt->payload[i++]; uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data - if (i + 2 >= pkt->payload_len) { - MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); - } else if (!_tables->wasSeen(pkt)) { + if (!_tables->wasSeen(pkt)) { _tables->markSeen(pkt); // scan channels DB, for all matching hashes of 'channel_hash' (max 4 matches supported ATM) GroupChannel channels[4]; @@ -669,6 +695,12 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { break; } case PAYLOAD_TYPE_ADVERT: { + const size_t min_advert_len = PUB_KEY_SIZE + sizeof(uint32_t) + SIGNATURE_SIZE; + if (pkt->payload_len < min_advert_len) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete advertisement packet", getLogDateTime()); + break; + } + int i = 0; Identity id; memcpy(id.pub_key, &pkt->payload[i], PUB_KEY_SIZE); i += PUB_KEY_SIZE; @@ -677,9 +709,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { memcpy(×tamp, &pkt->payload[i], 4); i += 4; const uint8_t* signature = &pkt->payload[i]; i += SIGNATURE_SIZE; - if (i > pkt->payload_len) { - MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete advertisement packet", getLogDateTime()); - } else if (self_id.matches(id.pub_key)) { + if (self_id.matches(id.pub_key)) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): receiving SELF advert packet", getLogDateTime()); } else if (!_tables->wasSeen(pkt)) { _tables->markSeen(pkt); @@ -815,6 +845,10 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) { } DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { + if (pkt == NULL || pkt->payload_len < 1) { + return ACTION_RELEASE; + } + uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent uint8_t type = pkt->payload[0] & 0x0F; @@ -1242,14 +1276,14 @@ bool Mesh::canDecodeDirectPayloadForSelf(const Packet* packet) { case PAYLOAD_TYPE_REQ: case PAYLOAD_TYPE_RESPONSE: case PAYLOAD_TYPE_TXT_MSG: { - if (packet->payload_len < 2) { + if (!hasValidEncryptedPayloadLength(packet->payload_len, 2)) { return false; } int i = 0; uint8_t dest_hash = packet->payload[i++]; uint8_t src_hash = packet->payload[i++]; - if (i + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) { + if (!self_id.isHashMatch(&dest_hash)) { return false; } @@ -1267,9 +1301,13 @@ bool Mesh::canDecodeDirectPayloadForSelf(const Packet* packet) { } case PAYLOAD_TYPE_ANON_REQ: { + if (!hasValidEncryptedPayloadLength(packet->payload_len, 1 + PUB_KEY_SIZE)) { + return false; + } + int i = 0; uint8_t dest_hash = packet->payload[i++]; - if (i + PUB_KEY_SIZE + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) { + if (!self_id.isHashMatch(&dest_hash)) { return false; } @@ -1506,6 +1544,32 @@ bool Mesh::cancelActiveRetries(const uint8_t retry_key[MAX_HASH_SIZE]) { return cancelled; } +void Mesh::replaceActiveRetries(const Packet* replacement_packet, + const uint8_t retry_key[MAX_HASH_SIZE]) { + cancelActiveRetries(retry_key); + if (replacement_packet == NULL) return; + + if (replacement_packet->isRouteDirect()) { + uint8_t priority; + if (replacement_packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + priority = getTraceDirectPriority(replacement_packet); + } else { + priority = replacement_packet->getPayloadType() == PAYLOAD_TYPE_PATH ? 1 : 0; + } + maybeScheduleDirectRetry(replacement_packet, priority); + } else if (replacement_packet->isRouteFlood()) { + uint8_t priority; + if (replacement_packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + priority = 2; + } else if (replacement_packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { + priority = 3; + } else { + priority = 1; + } + maybeScheduleFloodRetry(replacement_packet, priority); + } +} + bool Mesh::hasActiveRetries(const uint8_t retry_key[MAX_HASH_SIZE]) const { if (retry_key == NULL || (_active_direct_retry_count == 0 && _active_flood_retry_count == 0)) { return false; @@ -2084,20 +2148,37 @@ bool Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m return sendPacket(packet, pri, delay_millis); } -void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis) { +bool Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis) { + if (packet == NULL) return false; + packet->header &= ~PH_ROUTE_MASK; packet->header |= ROUTE_TYPE_DIRECT; uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { // TRACE packets are different + if ((path_len > 0 && path == NULL) + || packet->payload_len > sizeof(packet->payload) + || path_len > sizeof(packet->payload) - packet->payload_len) { + MESH_DEBUG_PRINTLN("%s Mesh::sendDirect(): TRACE path is too long", getLogDateTime()); + releasePacket(packet); + return false; + } // for TRACE packets, path is appended to end of PAYLOAD. (path is used for SNR's) - memcpy(&packet->payload[packet->payload_len], path, path_len); // NOTE: path_len here can be > 64, and NOT in the new scheme + if (path_len > 0) { + memcpy(&packet->payload[packet->payload_len], path, path_len); // path_len can be > 64 (TRACE raw route bytes) + } packet->payload_len += path_len; packet->path_len = 0; pri = getTraceDirectPriority(packet); } else { - packet->path_len = Packet::copyPath(packet->path, path, path_len); + uint8_t path_bytes = (path_len & 63) * ((path_len >> 6) + 1); + if (!Packet::isValidPathLen(path_len) || (path_bytes > 0 && path == NULL)) { + MESH_DEBUG_PRINTLN("%s Mesh::sendDirect(): invalid path_len=%u", getLogDateTime(), (uint32_t)path_len); + releasePacket(packet); + return false; + } + packet->path_len = path_bytes > 0 ? Packet::copyPath(packet->path, path, path_len) : path_len; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { pri = 1; // slightly less priority } else { @@ -2106,7 +2187,7 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin } _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us maybeScheduleDirectRetry(packet, pri); - sendPacket(packet, pri, delay_millis); + return sendPacket(packet, pri, delay_millis); } void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) { diff --git a/src/Mesh.h b/src/Mesh.h index 5af6d4e6b..f17e07ed4 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -204,6 +204,13 @@ protected: */ bool cancelActiveRetries(const uint8_t retry_key[MAX_HASH_SIZE]); + /** + * \brief After a replacement packet is queued, retire the prior retry + * sequence and ensure the queued packet owns the retry slot. + */ + void replaceActiveRetries(const Packet* replacement_packet, + const uint8_t retry_key[MAX_HASH_SIZE]); + // Cancel every queued/future retry of one route type. A packet already on // air is allowed to finish, but no later retry is armed from it. void cancelAllDirectRetries(); @@ -457,7 +464,7 @@ public: /** * \brief send a locally-generated Packet with Direct routing */ - void sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis=0); + bool sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis=0); /** * \brief send a locally-generated Packet to just neighbor nodes (zero hops) diff --git a/src/Packet.cpp b/src/Packet.cpp index 3aab6349c..37d2633c6 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -64,22 +64,29 @@ uint8_t Packet::writeTo(uint8_t dest[]) const { } bool Packet::readFrom(const uint8_t src[], uint8_t len) { + if (src == NULL || len < 2) return false; // header + path_len + uint8_t i = 0; tx_cr = 0; header = src[i++]; + if (getPayloadVer() > PAYLOAD_VER_1) return false; + if (hasTransportCodes()) { + if ((uint16_t)i + sizeof(transport_codes) + 1 > len) return false; memcpy(&transport_codes[0], &src[i], 2); i += 2; memcpy(&transport_codes[1], &src[i], 2); i += 2; } else { transport_codes[0] = transport_codes[1] = 0; } + + if (i >= len) return false; path_len = src[i++]; if (!isValidPathLen(path_len)) return false; // bad encoding uint8_t bl = getPathByteLen(); + if ((uint16_t)i + bl > len) return false; memcpy(path, &src[i], bl); i += bl; - if (i >= len) return false; // bad encoding payload_len = len - i; if (payload_len > sizeof(payload)) return false; // bad encoding memcpy(payload, &src[i], payload_len); //i += payload_len; diff --git a/src/Utils.cpp b/src/Utils.cpp index 5a829b23c..4ed2e26eb 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -28,6 +28,11 @@ void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int fra } int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { + if (shared_secret == NULL || dest == NULL || src == NULL || src_len <= 0 + || (src_len % CIPHER_BLOCK_SIZE) != 0) { + return 0; + } + AES128 aes; uint8_t* dp = dest; const uint8_t* sp = src; @@ -72,17 +77,20 @@ int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uin } int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { - if (src_len <= CIPHER_MAC_SIZE) return 0; // invalid src bytes + if (shared_secret == NULL || dest == NULL || src == NULL || src_len <= CIPHER_MAC_SIZE) return 0; + + const int enc_len = src_len - CIPHER_MAC_SIZE; + if ((enc_len % CIPHER_BLOCK_SIZE) != 0) return 0; // reject partial AES blocks before hashing/decrypting uint8_t hmac[CIPHER_MAC_SIZE]; { SHA256 sha; sha.resetHMAC(shared_secret, PUB_KEY_SIZE); - sha.update(src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); + sha.update(src + CIPHER_MAC_SIZE, enc_len); sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac, CIPHER_MAC_SIZE); } if (memcmp(hmac, src, CIPHER_MAC_SIZE) == 0) { - return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); + return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, enc_len); } return 0; // invalid HMAC } diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 0aaf6a64f..2bdfabb77 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -9,11 +9,11 @@ #define TXT_ACK_DELAY 200 #endif -void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { - sendFlood(pkt, delay_millis); +bool BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { + return sendFlood(pkt, delay_millis); } -void BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { - sendFlood(pkt, delay_millis); +bool BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { + return sendFlood(pkt, delay_millis); } mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) { @@ -447,26 +447,35 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, if (packet_hash != NULL) { pkt->calculatePacketHash(packet_hash); } - if (replace_retry_key != NULL) { - // Composition succeeded, so the new submission now owns this semantic DM. - // Cancel before sendDirect()/sendFloodScoped() registers its retry: when - // timestamp and attempt are unchanged, both encrypted packets have the - // same retry key and registering first would suppress the new sequence. - cancelActiveRetries(replace_retry_key); + if (recipient.out_path_len != OUT_PATH_UNKNOWN + && !mesh::Packet::isValidPathLen(recipient.out_path_len)) { + releasePacket(pkt); + expected_ack = 0; + est_timeout = 0; + return MSG_SEND_FAILED; } - uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); int rc; + bool sent; if (recipient.out_path_len == OUT_PATH_UNKNOWN) { - sendFloodScoped(recipient, pkt); - txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); + sent = sendFloodScoped(recipient, pkt); + est_timeout = calcFloodTimeoutMillisFor(t); rc = MSG_SEND_SENT_FLOOD; } else { - sendDirect(pkt, recipient.out_path, recipient.out_path_len); - txt_send_timeout = futureMillis(est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len)); + sent = sendDirect(pkt, recipient.out_path, recipient.out_path_len); + est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len); rc = MSG_SEND_SENT_DIRECT; } + if (!sent) { + expected_ack = 0; + est_timeout = 0; + return MSG_SEND_FAILED; + } + if (replace_retry_key != NULL) { + replaceActiveRetries(pkt, replace_retry_key); + } + txt_send_timeout = futureMillis(est_timeout); return rc; } diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index deacd744c..8ba572b1d 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -125,8 +125,8 @@ protected: virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0; virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len); - virtual void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0); - virtual void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0); + virtual bool sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0); + virtual bool sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0); // storage concepts, for sub-classes to override/implement virtual int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; } // not implemented diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 0ca6f39db..8e85cf5f0 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -48,6 +48,7 @@ private: uint32_t _direct_dups, _flood_dups; RecentRepeaterInfo* _recent_repeaters; int _max_recent_repeaters; + int _recent_repeater_count; bool hasSeenHash(const uint8_t* hash) const { const uint8_t* sp = _hashes; @@ -198,7 +199,8 @@ public: SimpleMeshTables(RecentRepeaterInfo* recent_repeaters = NULL, int max_recent_repeaters = 0) : _recent_repeaters(recent_repeaters), _max_recent_repeaters(recent_repeaters != NULL && max_recent_repeaters > 0 - ? max_recent_repeaters : 0) { + ? max_recent_repeaters : 0), + _recent_repeater_count(0) { memset(_hashes, 0, sizeof(_hashes)); _next_idx = 0; memset(_ack_hashes, 0, sizeof(_ack_hashes)); @@ -329,7 +331,6 @@ public: prefix_len = MAX_ROUTE_HASH_BYTES; } - int empty_idx = -1; int oldest_idx = 0; #if ARDUINO const uint32_t now = millis(); @@ -337,15 +338,12 @@ public: bool have_oldest = false; #endif - // Find a match, the first empty slot, and the oldest occupied slot in one - // pass. Keep exact prefixes distinct so a 1-byte path prefix does not - // collapse independent 2/3-byte repeaters that share the same first byte. - for (int i = 0; i < _max_recent_repeaters; i++) { + // Occupied entries are kept packed at the front of the externally supplied + // array. Normal traffic therefore scans only learned repeaters, not the + // full 512/2048-slot capacity. Keep exact prefixes distinct so a 1-byte + // path prefix does not collapse 2/3-byte repeaters sharing its first byte. + for (int i = 0; i < _recent_repeater_count; i++) { RecentRepeaterInfo& existing = _recent_repeaters[i]; - if (existing.prefix_len == 0) { - if (empty_idx < 0) empty_idx = i; - continue; - } if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) { #if ARDUINO uint32_t age = (uint32_t)(now - existing.last_heard_millis); @@ -368,7 +366,12 @@ public: // Non-Arduino tests have no monotonic clock, so a full table retains the // historical deterministic fallback of evicting slot zero. - int slot_idx = empty_idx >= 0 ? empty_idx : oldest_idx; + int slot_idx; + if (_recent_repeater_count < _max_recent_repeaters) { + slot_idx = _recent_repeater_count++; + } else { + slot_idx = oldest_idx; + } RecentRepeaterInfo& slot = _recent_repeaters[slot_idx]; memset(slot.prefix, 0, sizeof(slot.prefix)); @@ -403,16 +406,7 @@ public: return false; } int getRecentRepeaterCount() const { - if (_max_recent_repeaters == 0) { - return 0; - } - int count = 0; - for (int i = 0; i < _max_recent_repeaters; i++) { - if (_recent_repeaters[i].prefix_len > 0) { - count++; - } - } - return count; + return _recent_repeater_count; } const RecentRepeaterInfo* getRecentRepeaterBySortedIdx(int idx_wanted) const { if (_max_recent_repeaters == 0) { @@ -427,11 +421,8 @@ public: for (int rank = 0; rank <= idx_wanted; rank++) { const RecentRepeaterInfo* best = NULL; int best_idx = -1; - for (int i = 0; i < _max_recent_repeaters; i++) { + for (int i = 0; i < _recent_repeater_count; i++) { const RecentRepeaterInfo* info = &_recent_repeaters[i]; - if (info->prefix_len == 0) { - continue; - } if (last != NULL && !recentRepeaterComesBefore(*last, last_idx, *info, i)) { continue; } @@ -460,11 +451,8 @@ public: // Prefer exact matches. If none exists, fall back to the longest overlapping // prefix, using highest SNR to break ties. const RecentRepeaterInfo* best = NULL; - for (int i = 0; i < _max_recent_repeaters; i++) { + for (int i = 0; i < _recent_repeater_count; i++) { const RecentRepeaterInfo* info = &_recent_repeaters[i]; - if (info->prefix_len == 0) { - continue; - } if (info->prefix_len == hash_len && memcmp(info->prefix, hash, hash_len) == 0) { return info; } @@ -481,6 +469,7 @@ public: if (_max_recent_repeaters > 0) { memset(_recent_repeaters, 0, _max_recent_repeaters * sizeof(RecentRepeaterInfo)); } + _recent_repeater_count = 0; } int expireRecentRepeaters(uint32_t now_millis, uint32_t max_age_millis) { if (_max_recent_repeaters == 0) { @@ -488,12 +477,17 @@ public: } int expired = 0; - for (int i = 0; i < _max_recent_repeaters; i++) { + for (int i = 0; i < _recent_repeater_count; ) { RecentRepeaterInfo& info = _recent_repeaters[i]; - if (info.prefix_len > 0 - && (uint32_t)(now_millis - info.last_heard_millis) > max_age_millis) { - memset(&info, 0, sizeof(info)); + if ((uint32_t)(now_millis - info.last_heard_millis) > max_age_millis) { + _recent_repeater_count--; + if (i != _recent_repeater_count) { + info = _recent_repeaters[_recent_repeater_count]; + } + memset(&_recent_repeaters[_recent_repeater_count], 0, sizeof(RecentRepeaterInfo)); expired++; + } else { + i++; } } return expired; diff --git a/src/helpers/bridges/ESPNowBridge.cpp b/src/helpers/bridges/ESPNowBridge.cpp index 47526a215..b292666f3 100644 --- a/src/helpers/bridges/ESPNowBridge.cpp +++ b/src/helpers/bridges/ESPNowBridge.cpp @@ -197,7 +197,6 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) { } if (!_seen_packets.wasSeen(packet)) { - _seen_packets.markSeen(packet); // Check the serialized size before writing into the ESP-NOW-sized buffer. const int expectedMeshPacketLen = packet->getRawLength(); if (expectedMeshPacketLen < 0 || (size_t)expectedMeshPacketLen > MAX_PAYLOAD_SIZE) { @@ -240,6 +239,7 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) { esp_err_t result = esp_now_send(broadcastAddress, buffer, totalPacketSize); if (result == ESP_OK) { + _seen_packets.markSeen(packet); BRIDGE_DEBUG_PRINTLN("TX, len=%d\n", meshPacketLen); } else { BRIDGE_DEBUG_PRINTLN("TX FAILED!\n"); diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 1de8bf76c..4abd5d00c 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -99,6 +99,12 @@ bool RadioLibWrapper::setParams(float freq, float bw, uint8_t sf, uint8_t cr, uint8_t resume_rx = beginReconfigure(); if (resume_rx > 1) return false; + const bool had_previous_params = _params_valid; + const float previous_freq = _cur_freq; + const float previous_bw = _cur_bw; + const uint8_t previous_sf = _cur_sf; + const uint8_t previous_cr = _cur_cr; + bool success = applyParams(freq, bw, sf, cr); if (success) { cacheParams(freq, bw, sf, cr); @@ -107,6 +113,27 @@ bool RadioLibWrapper::setParams(float freq, float bw, uint8_t sf, uint8_t cr, _rx_ps_rx_us = rx_ps_timings[0]; _rx_ps_sleep_us = rx_ps_timings[1]; } + } else { + bool restored = had_previous_params + && applyParams(previous_freq, previous_bw, previous_sf, previous_cr); + + if (!restored && radioDeepInit()) { + _rx_ps_armed = false; + state = STATE_IDLE; + _radio->setPacketReceivedAction(setFlag); + if (had_previous_params) { + restored = applyParams(previous_freq, previous_bw, previous_sf, previous_cr); + } else { + _preamble_sf = getSpreadingFactor(); + restored = _radio->setPreambleLength(preambleLengthForSF(_preamble_sf)) == RADIOLIB_ERR_NONE; + } + if (_dbm_valid) _radio->setOutputPower(_cur_dbm); + if (_rx_boosted_gain_valid) applyRxBoostedGainMode(_cur_rx_boosted_gain); + } + + if (!restored) { + MESH_DEBUG_PRINTLN("RadioLibWrapper: failed to restore radio parameters after apply failure"); + } } endReconfigure(resume_rx); diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 90cb7495a..9630e9cc1 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -97,6 +97,7 @@ public: _rx_ps_rx_us(RX_PS_FALLBACK_RX_US), _rx_ps_sleep_us(RX_PS_FALLBACK_SLEEP_US), _wd_last_busy(false), _wd_stage(0), _wd_strikes(0), _startrx_fails(0), _wd_last_transition(0), _wd_stuck_thresh(0), _wd_observe_until(0), _wd_observe_ms(0), + _cur_freq(0), _cur_bw(0), _cur_sf(0), _cur_cr(0), _cur_dbm(0), _cur_rx_boosted_gain(false), _params_valid(false), _dbm_valid(false), _rx_boosted_gain_valid(false), _nf_calib_active(false), _nf_last_calib(0), _nf_calib_deadline(0), _nf_sample_from(0), _nf_next_sample_at(0) diff --git a/test/test_mesh_tables/test_simple_mesh_tables.cpp b/test/test_mesh_tables/test_simple_mesh_tables.cpp index 09d5e5ecd..2cb5a4053 100644 --- a/test/test_mesh_tables/test_simple_mesh_tables.cpp +++ b/test/test_mesh_tables/test_simple_mesh_tables.cpp @@ -269,6 +269,33 @@ TEST(SimpleMeshTables, FullRecentRepeaterTableStillEvictsDeterministically) { EXPECT_EQ(2, t.getRecentRepeaterCount()); } +TEST(SimpleMeshTables, ExpirationKeepsOccupiedEntriesPacked) { + SimpleMeshTables::RecentRepeaterInfo storage[4]; + SimpleMeshTables t(storage, 4); + const uint8_t first[] = {0x10}; + const uint8_t expired[] = {0x20}; + const uint8_t third[] = {0x30}; + const uint8_t added[] = {0x40}; + + ASSERT_TRUE(t.setRecentRepeater(first, 1, 4)); + ASSERT_TRUE(t.setRecentRepeater(expired, 1, 8)); + ASSERT_TRUE(t.setRecentRepeater(third, 1, 12)); + storage[0].last_heard_millis = 100; + storage[1].last_heard_millis = 0; + storage[2].last_heard_millis = 100; + + EXPECT_EQ(1, t.expireRecentRepeaters(101, 50)); + EXPECT_EQ(2, t.getRecentRepeaterCount()); + EXPECT_NE(nullptr, t.findRecentRepeaterByHash(first, 1)); + EXPECT_EQ(nullptr, t.findRecentRepeaterByHash(expired, 1)); + EXPECT_NE(nullptr, t.findRecentRepeaterByHash(third, 1)); + + ASSERT_TRUE(t.setRecentRepeater(added, 1, 16)); + EXPECT_EQ(3, t.getRecentRepeaterCount()); + EXPECT_NE(nullptr, t.findRecentRepeaterByHash(added, 1)); + EXPECT_EQ(0, storage[3].prefix_len); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/test/test_packet_manager/test_rx_reserve_packet_manager.cpp b/test/test_packet_manager/test_rx_reserve_packet_manager.cpp index 094f9a362..8595ada08 100644 --- a/test/test_packet_manager/test_rx_reserve_packet_manager.cpp +++ b/test/test_packet_manager/test_rx_reserve_packet_manager.cpp @@ -44,8 +44,73 @@ public: return getNextQueueWakeDelay(delay_millis); } bool queuedWorkDue() const { return hasQueuedWorkDue(); } + bool parse(mesh::Packet* packet, const uint8_t* raw, int len) { + return tryParsePacket(packet, raw, len); + } }; +TEST(Packet, ReadFromRejectsTruncatedHeadersAndPaths) { + mesh::Packet packet; + const uint8_t one_byte[] = {ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT)}; + EXPECT_FALSE(packet.readFrom(one_byte, sizeof(one_byte))); + + const uint8_t short_transport[] = { + ROUTE_TYPE_TRANSPORT_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0 + }; + EXPECT_FALSE(packet.readFrom(short_transport, sizeof(short_transport))); + + const uint8_t missing_path_byte[] = { + ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 1 + }; + EXPECT_FALSE(packet.readFrom(missing_path_byte, sizeof(missing_path_byte))); +} + +TEST(Packet, ReadFromAcceptsACompletePacketWithEmptyPayload) { + mesh::Packet packet; + const uint8_t raw[] = { + ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0 + }; + ASSERT_TRUE(packet.readFrom(raw, sizeof(raw))); + EXPECT_EQ(0U, packet.path_len); + EXPECT_EQ(0U, packet.payload_len); +} + +TEST(Dispatcher, ParserRejectsTruncatedHeadersAndPaths) { + RxReservePacketManager manager(4, 1); + TestClock clock; + TestRadio radio; + TestDispatcher dispatcher(radio, clock, manager); + mesh::Packet packet; + + const uint8_t one_byte[] = {ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT)}; + EXPECT_FALSE(dispatcher.parse(&packet, one_byte, sizeof(one_byte))); + + const uint8_t short_transport[] = { + ROUTE_TYPE_TRANSPORT_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0 + }; + EXPECT_FALSE(dispatcher.parse(&packet, short_transport, sizeof(short_transport))); + + const uint8_t missing_path_byte[] = { + ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 1 + }; + EXPECT_FALSE(dispatcher.parse(&packet, missing_path_byte, sizeof(missing_path_byte))); +} + +TEST(Dispatcher, ParserAcceptsACompletePacketWithEmptyPayload) { + RxReservePacketManager manager(4, 1); + TestClock clock; + TestRadio radio; + TestDispatcher dispatcher(radio, clock, manager); + mesh::Packet packet; + const uint8_t raw[] = { + ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT), 0 + }; + + ASSERT_TRUE(dispatcher.parse(&packet, raw, sizeof(raw))); + EXPECT_EQ(0U, packet.path_len); + EXPECT_EQ(0U, packet.payload_len); +} + TEST(StaticPoolPacketManager, ReportsEarliestQueueTimesWithoutDequeuing) { StaticPoolPacketManager manager(8); mesh::Packet* later = manager.allocNew(); diff --git a/test/test_utils/test_tohex.cpp b/test/test_utils/test_tohex.cpp index 51b2986e8..34dfc76b3 100644 --- a/test/test_utils/test_tohex.cpp +++ b/test/test_utils/test_tohex.cpp @@ -1,5 +1,6 @@ #include #include "Utils.h" +#include using namespace mesh; @@ -73,6 +74,40 @@ TEST(UtilsFromHex, RejectsWrongLength) { EXPECT_FALSE(Utils::fromHex(output, sizeof(output), "001122")); } +TEST(UtilsCrypto, RejectsNonBlockAlignedCiphertext) { + uint8_t key[PUB_KEY_SIZE] = {}; + uint8_t plaintext[MAX_PACKET_PAYLOAD] = {}; + uint8_t ciphertext[15] = {}; + + EXPECT_EQ(0, Utils::decrypt(key, plaintext, ciphertext, sizeof(ciphertext))); + + uint8_t framed[CIPHER_MAC_SIZE + sizeof(ciphertext)] = {}; + for (size_t i = 0; i < sizeof(ciphertext); i++) { + framed[CIPHER_MAC_SIZE + i] = (uint8_t)(i + 1); + } + SHA256 sha; + sha.resetHMAC(key, sizeof(key)); + sha.update(framed + CIPHER_MAC_SIZE, sizeof(ciphertext)); + sha.finalizeHMAC(key, sizeof(key), framed, CIPHER_MAC_SIZE); + + EXPECT_EQ(0, Utils::MACThenDecrypt(key, plaintext, framed, sizeof(framed))); +} + +TEST(UtilsCrypto, AcceptsBlockAlignedCiphertextWithValidMac) { + uint8_t key[PUB_KEY_SIZE] = {}; + uint8_t plaintext[MAX_PACKET_PAYLOAD] = {}; + uint8_t framed[CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE] = {}; + for (size_t i = 0; i < CIPHER_BLOCK_SIZE; i++) { + framed[CIPHER_MAC_SIZE + i] = (uint8_t)(i + 1); + } + SHA256 sha; + sha.resetHMAC(key, sizeof(key)); + sha.update(framed + CIPHER_MAC_SIZE, CIPHER_BLOCK_SIZE); + sha.finalizeHMAC(key, sizeof(key), framed, CIPHER_MAC_SIZE); + + EXPECT_EQ(CIPHER_BLOCK_SIZE, Utils::MACThenDecrypt(key, plaintext, framed, sizeof(framed))); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();