mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-07 06:44:33 -05:00
Send admin-made owner changes and admin NodeInfo requests past the routine throttle
An admin set_owner announced the new name through the routine NodeInfo path, so a rename made within 30 minutes of the last NodeInfo never went out - and the reboot set_owner schedules inherits that throttle from the persisted transmit history. Inside an edit transaction nothing was sent at all. The change is now announced on the 60 s interactive gate once it is saved, at commit for a transaction. A signed NodeInfo request from a node holding a live PKC admin session is likewise answered on the 60 s gate, past the 30 minute floor and the 12 h per-requester suppression.
This commit is contained in:
1 parent
332b5b0a2c
commit
b65fa9505c
10 files changed
+297
-6
No files matched your search
@@ -188,6 +188,12 @@ void MeshService::reloadOwner(bool shouldSave)
|
||||
}
|
||||
}
|
||||
|
||||
void MeshService::announceOwnerChange()
|
||||
{
|
||||
if (nodeInfoModule)
|
||||
nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, false, 0, true);
|
||||
}
|
||||
|
||||
// search the queue for a request id and return the matching nodenum
|
||||
NodeNum MeshService::getNodenumFromRequestId(uint32_t request_id)
|
||||
{
|
||||
|
||||
@@ -191,6 +191,9 @@ class MeshService
|
||||
/// The owner User record just got updated, update our node DB and broadcast the info into the mesh
|
||||
void reloadOwner(bool shouldSave = true);
|
||||
|
||||
/// Broadcast our NodeInfo on the interactive 60 s throttle - an admin just changed the owner
|
||||
virtual void announceOwnerChange();
|
||||
|
||||
/// Called when the user wakes up our GUI, normally sends our latest location to the mesh (if we have it), otherwise at least
|
||||
/// sends our nodeinfo
|
||||
/// returns true if we sent a position
|
||||
|
||||
@@ -190,6 +190,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
} else if (mp.pki_encrypted) {
|
||||
if (AdminKeys::isAuthorized(mp.public_key.bytes)) {
|
||||
LOG_INFO("PKC admin payload with authorized sender key");
|
||||
remoteSessionNode = mp.from;
|
||||
remoteSessionMs = Time::stampMillis();
|
||||
|
||||
// Note: PKC admin does NOT automatically authorize the
|
||||
// originating local PhoneAPI connection for content
|
||||
@@ -831,7 +833,8 @@ void AdminModule::handleSetOwner(const meshtastic_User &o)
|
||||
}
|
||||
|
||||
if (changed) { // If nothing really changed, don't broadcast on the network or write to flash
|
||||
service->reloadOwner(!hasOpenEditTransaction);
|
||||
service->reloadOwner(false);
|
||||
ownerAnnouncePending = true;
|
||||
saveChanges(SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE | (identityUpdated ? SEGMENT_CONFIG : 0) |
|
||||
(channelsSanitized ? SEGMENT_CHANNELS : 0));
|
||||
}
|
||||
@@ -1856,6 +1859,11 @@ void AdminModule::saveChanges(int saveWhat, bool shouldReboot)
|
||||
if (!hasOpenEditTransaction) {
|
||||
LOG_INFO("Save changes to disk");
|
||||
service->reloadConfig(saveWhat); // Calls saveToDisk among other things
|
||||
// The routine NodeInfo throttle would drop it, and the reboot that follows inherits that throttle.
|
||||
if (ownerAnnouncePending) {
|
||||
ownerAnnouncePending = false;
|
||||
service->announceOwnerChange();
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Delay disk save until open transaction commits");
|
||||
editTransactionActivityMs = millis(); // still in use, so not the abandoned kind we time out
|
||||
@@ -1979,8 +1987,13 @@ bool AdminModule::checkPassKey(meshtastic_AdminMessage *res)
|
||||
printBytes("Incoming session key: ", res->session_passkey.bytes, 8);
|
||||
printBytes("Expected session key: ", session_passkey, 8);
|
||||
// Key is valid for 300s from issue; sessionPasskeyValid guards an unissued session.
|
||||
return (sessionPasskeyValid && Throttle::isWithinTimespanMs(session_time, 300 * 1000UL) && res->session_passkey.size == 8 &&
|
||||
memcmp(res->session_passkey.bytes, session_passkey, 8) == 0);
|
||||
return (sessionPasskeyValid && Throttle::isWithinTimespanMs(session_time, SESSION_EXPIRE_MS) &&
|
||||
res->session_passkey.size == 8 && memcmp(res->session_passkey.bytes, session_passkey, 8) == 0);
|
||||
}
|
||||
|
||||
bool AdminModule::hasRemoteAdminSession(NodeNum node) const
|
||||
{
|
||||
return node != 0 && node == remoteSessionNode && Throttle::isWithinTimespanMs(remoteSessionMs, SESSION_EXPIRE_MS);
|
||||
}
|
||||
|
||||
bool AdminModule::messageIsResponse(const meshtastic_AdminMessage *r)
|
||||
|
||||
@@ -31,6 +31,9 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
||||
*/
|
||||
AdminModule();
|
||||
|
||||
/// True while `node` holds a remote admin session: a PKC-authorized admin message from it within the session-key lifetime
|
||||
bool hasRemoteAdminSession(NodeNum node) const;
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
|
||||
@@ -45,6 +48,7 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
||||
static constexpr uint32_t EDIT_TRANSACTION_IDLE_MS = 60 * 1000;
|
||||
uint32_t editTransactionActivityMs = 0; // millis() of the last save this transaction deferred
|
||||
int deferredEditSegments = 0; // segments that transaction has touched but not yet saved
|
||||
bool ownerAnnouncePending = false; // owner changed; announce it once the change is saved
|
||||
/// Retire an open edit transaction whose client stopped talking, persisting what it applied.
|
||||
void expireStaleEditTransaction();
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
@@ -54,6 +58,9 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
||||
uint8_t session_passkey[8] = {0};
|
||||
uint32_t session_time = 0; // millis() when the current session passkey was issued
|
||||
bool sessionPasskeyValid = false; // separate flag: millis() 0 at boot is a valid issue time
|
||||
static constexpr uint32_t SESSION_EXPIRE_MS = 300 * 1000UL;
|
||||
NodeNum remoteSessionNode = 0; // last PKC-authorized remote admin
|
||||
uint32_t remoteSessionMs = 0; // Time::stampMillis() of its last admin message
|
||||
|
||||
void saveChanges(int saveWhat, bool shouldReboot = true);
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "main.h"
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
#include "modules/AdminModule.h"
|
||||
#endif
|
||||
#include <Throttle.h>
|
||||
#include <algorithm>
|
||||
|
||||
@@ -23,6 +26,7 @@ static constexpr uint32_t NodeInfoReplySuppressSeconds = USERPREFS_NODEINFO_REPL
|
||||
bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr)
|
||||
{
|
||||
suppressReplyForCurrentRequest = false;
|
||||
adminRequestForCurrentRequest = false;
|
||||
|
||||
if (mp.from == nodeDB->getNodeNum()) {
|
||||
LOG_WARN("Ignoring packet supposed to be from our own node: 0x%08x", mp.from);
|
||||
@@ -34,11 +38,16 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
|
||||
// Suppress replies to senders we've replied to recently (12H window)
|
||||
if (mp.decoded.want_response && !isFromUs(&mp)) {
|
||||
const NodeNum sender = getFrom(&mp);
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
// Only a verified signature proves `from`, so only a signed request can claim the sender's admin session.
|
||||
adminRequestForCurrentRequest = mp.xeddsa_signed && adminModule && adminModule->hasRemoteAdminSession(sender);
|
||||
#endif
|
||||
// A local dedup window, not a wall-clock reading - uptime avoids RTC jumps and replayed
|
||||
// packets' stale rx_time perturbing it. Seconds, not millis - this is a wide window.
|
||||
const uint32_t nowSecs = Time::getUptimeSecs();
|
||||
auto it = lastNodeInfoSeen.find(sender);
|
||||
if (it != lastNodeInfoSeen.end() && (uint32_t)(nowSecs - it->second) < NodeInfoReplySuppressSeconds) {
|
||||
if (!adminRequestForCurrentRequest && it != lastNodeInfoSeen.end() &&
|
||||
(uint32_t)(nowSecs - it->second) < NodeInfoReplySuppressSeconds) {
|
||||
suppressReplyForCurrentRequest = true;
|
||||
}
|
||||
lastNodeInfoSeen[sender] = nowSecs;
|
||||
@@ -173,11 +182,12 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
|
||||
timeoutMs = hamMs;
|
||||
}
|
||||
uint32_t lastNodeInfo = transmitHistory ? transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP) : 0;
|
||||
if (!shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, timeoutMs)) {
|
||||
const bool interactive = shorterTimeout || (adminRequestForCurrentRequest && isReplyingToExternalRequest);
|
||||
if (!interactive && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, timeoutMs)) {
|
||||
LOG_DEBUG("Skip send NodeInfo since we sent it <%us ago", timeoutMs / 1000);
|
||||
ignoreRequest = true; // Mark it as ignored for MeshModule
|
||||
return NULL;
|
||||
} else if (shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, 60 * 1000)) {
|
||||
} else if (interactive && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, 60 * 1000)) {
|
||||
// For interactive/urgent requests (e.g., user-triggered or implicit requests), use a shorter 60s timeout
|
||||
LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago");
|
||||
ignoreRequest = true;
|
||||
|
||||
@@ -61,6 +61,8 @@ class NodeInfoModule : public ProtobufModule<meshtastic_User>, private concurren
|
||||
/// Set across sendOurNodeInfo()'s own allocReply(), so the transmit stamp waits for an accepted send.
|
||||
bool deferHistoryStamp = false;
|
||||
bool suppressReplyForCurrentRequest = false;
|
||||
/// The request came from a node holding a remote admin session, so it is answered on the interactive path.
|
||||
bool adminRequestForCurrentRequest = false;
|
||||
/// Sender -> uptime seconds (Time::getUptimeSecs()) at our last reply. Seconds, not millis:
|
||||
/// the suppression window is hours wide. See handleReceivedProtobuf().
|
||||
std::map<NodeNum, uint32_t> lastNodeInfoSeen;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The one class AdminModule.h befriends (test seam): exposes the protected/private handlers to the
|
||||
// native suites and defers persistence so setters exercise in-RAM logic without disk/reboot effects.
|
||||
#include "MeshTypes.h" // packetPool
|
||||
#include "UptimeClock.h"
|
||||
#include "modules/AdminModule.h"
|
||||
|
||||
class AdminModuleTestShim : public AdminModule
|
||||
@@ -30,6 +31,12 @@ class AdminModuleTestShim : public AdminModule
|
||||
int savedSegments() const { return lastSaveWhatForTest; }
|
||||
|
||||
bool editTransactionOpen() const { return hasOpenEditTransaction; }
|
||||
// Stand in for a PKC-authorized admin message from `node`, without the key setup that path needs.
|
||||
void openRemoteSession(NodeNum node)
|
||||
{
|
||||
remoteSessionNode = node;
|
||||
remoteSessionMs = Time::stampMillis();
|
||||
}
|
||||
// Backdate past the idle window so a test sees an abandoned transaction without waiting it out.
|
||||
void ageEditTransaction() { editTransactionActivityMs = millis() - EDIT_TRANSACTION_IDLE_MS - 1; }
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ extern uint32_t hash(const char *str);
|
||||
// set of messages produced by a sequence of admin messages. This shadows test/support/MockMeshService.h's
|
||||
// release-only stub because these tests need to inspect the captured message text, not just avoid leaks.
|
||||
static std::vector<std::string> capturedWarnings;
|
||||
static int ownerAnnouncements;
|
||||
|
||||
class MockMeshService : public MeshService
|
||||
{
|
||||
@@ -52,6 +53,7 @@ class MockMeshService : public MeshService
|
||||
capturedWarnings.push_back(n->message);
|
||||
releaseClientNotificationToPool(n);
|
||||
}
|
||||
void announceOwnerChange() override { ownerAnnouncements++; }
|
||||
};
|
||||
|
||||
static MockMeshService *mockMeshService;
|
||||
@@ -2287,6 +2289,86 @@ static void test_warn_license_transaction_coalescedToSingleMessage()
|
||||
TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Owner change announcement
|
||||
// -----------------------------------------------------------------------
|
||||
//
|
||||
// An admin set_owner must put the new name on the air through MeshService::announceOwnerChange(),
|
||||
// which sends on NodeInfo's 60 s interactive throttle. The routine path it replaced
|
||||
// (reloadOwner(true) -> sendOurNodeInfo()) sat behind the 30 min floor, and the reboot set_owner
|
||||
// schedules inherits that floor from the persisted transmit history, so a rename made within the
|
||||
// window never reached the mesh - not even the admin node that made it. Inside an edit transaction
|
||||
// the announcement waits for the save at commit; the old code skipped it there entirely.
|
||||
|
||||
extern uint32_t rebootAtMsec;
|
||||
|
||||
static void sendSetOwnerName(const char *longName)
|
||||
{
|
||||
meshtastic_AdminMessage m = meshtastic_AdminMessage_init_zero;
|
||||
m.which_payload_variant = meshtastic_AdminMessage_set_owner_tag;
|
||||
strncpy(m.set_owner.long_name, longName, sizeof(m.set_owner.long_name) - 1);
|
||||
sendAdmin(m);
|
||||
testAdmin->drainReply();
|
||||
}
|
||||
|
||||
static void primeOwnerAnnounceTest()
|
||||
{
|
||||
owner = meshtastic_User_init_zero;
|
||||
strncpy(owner.long_name, "Old Name", sizeof(owner.long_name) - 1);
|
||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||
nodeInfoModule = reinterpret_cast<NodeInfoModule *>(1); // reloadOwner(false) only checks presence
|
||||
}
|
||||
|
||||
static void test_setOwner_announcesOnSave()
|
||||
{
|
||||
primeOwnerAnnounceTest();
|
||||
const uint32_t savedRebootAt = rebootAtMsec;
|
||||
|
||||
sendSetOwnerName("New Name");
|
||||
rebootAtMsec = savedRebootAt; // set_owner schedules a reboot; don't leak it into later tests
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("New Name", owner.long_name);
|
||||
TEST_ASSERT_EQUAL_INT(1, ownerAnnouncements);
|
||||
}
|
||||
|
||||
static void test_setOwner_unchangedNameDoesNotAnnounce()
|
||||
{
|
||||
primeOwnerAnnounceTest();
|
||||
|
||||
sendSetOwnerName("Old Name");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, ownerAnnouncements);
|
||||
}
|
||||
|
||||
static void test_setOwner_inTransaction_announcesOnceAtCommit()
|
||||
{
|
||||
primeOwnerAnnounceTest();
|
||||
sendBeginEdit();
|
||||
|
||||
sendSetOwnerName("New Name");
|
||||
sendSetOwnerName("Newer Name");
|
||||
TEST_ASSERT_EQUAL_INT(0, ownerAnnouncements);
|
||||
|
||||
sendCommitEdit();
|
||||
TEST_ASSERT_EQUAL_STRING("Newer Name", owner.long_name);
|
||||
TEST_ASSERT_EQUAL_INT(1, ownerAnnouncements);
|
||||
}
|
||||
|
||||
// An abandoned transaction still commits what it applied, so the owner change still goes out.
|
||||
static void test_setOwner_abandonedTransaction_announcesOnRetire()
|
||||
{
|
||||
primeOwnerAnnounceTest();
|
||||
sendBeginEdit();
|
||||
sendSetOwnerName("New Name");
|
||||
TEST_ASSERT_EQUAL_INT(0, ownerAnnouncements);
|
||||
|
||||
testAdmin->ageEditTransaction();
|
||||
sendGetDeviceMetadata();
|
||||
|
||||
TEST_ASSERT_FALSE(testAdmin->editTransactionOpen());
|
||||
TEST_ASSERT_EQUAL_INT(1, ownerAnnouncements);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Node-DB admin metadata: favorite / ignore / mute
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -2514,6 +2596,7 @@ void setUp(void)
|
||||
service = mockMeshService;
|
||||
testAdmin = new AdminModuleTestShim();
|
||||
capturedWarnings.clear();
|
||||
ownerAnnouncements = 0;
|
||||
// Every test gets its own NodeDB and its own copy of the globals the admin handlers write.
|
||||
replaceAdminRadioGlobals();
|
||||
}
|
||||
@@ -2661,6 +2744,10 @@ void setup()
|
||||
RUN_TEST(test_editTransaction_abandoned_isRetiredOnNextAdminMessage);
|
||||
RUN_TEST(test_editTransaction_abandoned_laterWriteIsNoLongerDeferred);
|
||||
RUN_TEST(test_editTransaction_active_isNotRetired);
|
||||
RUN_TEST(test_setOwner_announcesOnSave);
|
||||
RUN_TEST(test_setOwner_unchangedNameDoesNotAnnounce);
|
||||
RUN_TEST(test_setOwner_inTransaction_announcesOnceAtCommit);
|
||||
RUN_TEST(test_setOwner_abandonedTransaction_announcesOnRetire);
|
||||
RUN_TEST(test_warn_license_noTransaction_emittedImmediately);
|
||||
RUN_TEST(test_warn_license_transaction_coalescedToSingleMessage);
|
||||
|
||||
|
||||
@@ -235,6 +235,41 @@ void test_session_gate_accepts_key_from_a_get_response(void)
|
||||
TEST_ASSERT_FALSE(admin->checkPassKey(&bad));
|
||||
}
|
||||
|
||||
// hasRemoteAdminSession() lets NodeInfoModule answer a NodeInfo request past its routine throttles,
|
||||
// so only a PKC-authorized admin message may open it, and only for the node that sent it.
|
||||
static meshtastic_MeshPacket makeRemoteMetadataRequest(meshtastic_AdminMessage &out)
|
||||
{
|
||||
meshtastic_MeshPacket mp = makeRemoteSetOwner("unused", nullptr, 0, out);
|
||||
out = meshtastic_AdminMessage_init_zero;
|
||||
out.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_request_tag;
|
||||
out.get_device_metadata_request = true;
|
||||
return mp;
|
||||
}
|
||||
|
||||
void test_pkc_admin_message_opens_session_for_its_sender_only(void)
|
||||
{
|
||||
TEST_ASSERT_FALSE(admin->hasRemoteAdminSession(ADMIN_NODE));
|
||||
|
||||
meshtastic_AdminMessage m;
|
||||
meshtastic_MeshPacket mp = makeRemoteMetadataRequest(m);
|
||||
admin->handleReceivedProtobuf(mp, &m);
|
||||
admin->drainReply();
|
||||
|
||||
TEST_ASSERT_TRUE(admin->hasRemoteAdminSession(ADMIN_NODE));
|
||||
TEST_ASSERT_FALSE(admin->hasRemoteAdminSession(STRANGER_NODE));
|
||||
}
|
||||
|
||||
void test_unauthorized_pkc_key_opens_no_session(void)
|
||||
{
|
||||
meshtastic_AdminMessage m;
|
||||
meshtastic_MeshPacket mp = makeRemoteMetadataRequest(m);
|
||||
memset(mp.public_key.bytes, 0x77, 32); // not an admin key
|
||||
admin->handleReceivedProtobuf(mp, &m);
|
||||
admin->drainReply();
|
||||
|
||||
TEST_ASSERT_FALSE(admin->hasRemoteAdminSession(ADMIN_NODE));
|
||||
}
|
||||
|
||||
// Decode the SecurityConfig out of the get_config response a handler queued in myReply.
|
||||
static bool decodeSecurityFromReply(meshtastic_MeshPacket *reply, meshtastic_Config_SecurityConfig &out)
|
||||
{
|
||||
@@ -663,6 +698,8 @@ void setup()
|
||||
RUN_TEST(test_remote_setter_without_session_is_rejected);
|
||||
RUN_TEST(test_expected_session_key_is_zero_before_any_get);
|
||||
RUN_TEST(test_session_gate_accepts_key_from_a_get_response);
|
||||
RUN_TEST(test_pkc_admin_message_opens_session_for_its_sender_only);
|
||||
RUN_TEST(test_unauthorized_pkc_key_opens_no_session);
|
||||
RUN_TEST(test_remote_security_config_omits_private_key);
|
||||
RUN_TEST(test_local_security_config_keeps_private_key);
|
||||
RUN_TEST(test_local_security_config_applies_packet_signature_policy);
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
// The window probes step an injected clock (Time::setTestMillis) rather than sleeping;
|
||||
// TransmitHistory, which is where allocReply() reads "last sent" from, reads the same clock.
|
||||
//
|
||||
// 3. A NodeInfo request from a node that holds a remote admin session (AdminModule::
|
||||
// hasRemoteAdminSession()) is answered on the interactive 60 second gate, past both the 30 minute
|
||||
// floor and the 12 hour per-requester reply suppression - an admin who just renamed or re-roled a
|
||||
// node has to be able to see the result. The bypass needs a verified XEdDSA signature, because
|
||||
// only that proves the request's `from`; an unsigned request naming the admin node gets the
|
||||
// routine treatment. Regressions guarded: dropping the signature check (anyone can spoof the
|
||||
// admin's node number), and a bypass that outlives the session or skips the 60 second gate.
|
||||
//
|
||||
// Also covered here: a relayed NodeInfo payload leaves the module byte-for-byte as received, so the
|
||||
// sender's XEdDSA signature still verifies downstream, and coerceNodeInfoUserId() - which applies the
|
||||
// user.id fix to local copies instead - rewrites only a mismatched id.
|
||||
@@ -52,6 +60,7 @@
|
||||
#include "mesh/TransmitHistory.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include "support/AdminModuleTestShim.h"
|
||||
#include "support/MockMeshService.h"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
@@ -64,6 +73,19 @@ class NodeInfoModuleTestShim : public NodeInfoModule
|
||||
public:
|
||||
using MeshModule::alterReceived;
|
||||
using NodeInfoModule::runOnce;
|
||||
|
||||
// The receive-then-reply sequence MeshModule::callModules() runs for a want_response request.
|
||||
// Returns the reply, or nullptr when the module declined to answer.
|
||||
meshtastic_MeshPacket *replyTo(const meshtastic_MeshPacket &req)
|
||||
{
|
||||
meshtastic_User user = meshtastic_User_init_zero;
|
||||
pb_decode_from_bytes(req.decoded.payload.bytes, req.decoded.payload.size, &meshtastic_User_msg, &user);
|
||||
currentRequest = &req;
|
||||
handleReceivedProtobuf(req, &user);
|
||||
meshtastic_MeshPacket *reply = allocReply();
|
||||
currentRequest = nullptr;
|
||||
return reply;
|
||||
}
|
||||
};
|
||||
|
||||
namespace
|
||||
@@ -403,6 +425,99 @@ static void test_coerceUserId_rewritesOnlyAMismatchedId(void)
|
||||
TEST_ASSERT_EQUAL_STRING("remote", user.long_name);
|
||||
}
|
||||
|
||||
constexpr NodeNum kAdminNode = 0x0B0B0B0B;
|
||||
constexpr NodeNum kOtherNode = 0x0D0D0D0D;
|
||||
|
||||
// A unicast NodeInfo request to us, as the router hands it over: xeddsa_signed is the router's
|
||||
// verdict on the signature, never a field the sender controls.
|
||||
static meshtastic_MeshPacket makeNodeInfoRequest(NodeNum from, bool signedBySender)
|
||||
{
|
||||
meshtastic_MeshPacket p = makeNodeInfoPacket(from, "");
|
||||
p.to = nodeDB->getNodeNum();
|
||||
p.decoded.want_response = true;
|
||||
p.xeddsa_signed = signedBySender;
|
||||
return p;
|
||||
}
|
||||
|
||||
// True when the module answered `req`. Releases the reply so the pool does not leak.
|
||||
static bool answers(const meshtastic_MeshPacket &req)
|
||||
{
|
||||
meshtastic_MeshPacket *reply = mod->replyTo(req);
|
||||
if (!reply)
|
||||
return false;
|
||||
packetPool.release(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A fresh TransmitHistory: no "last sent" stamp, so only the per-requester suppression can refuse.
|
||||
static void clearTransmitHistory()
|
||||
{
|
||||
delete transmitHistory;
|
||||
transmitHistory = nullptr;
|
||||
transmitHistory = TransmitHistory::getInstance();
|
||||
}
|
||||
|
||||
static AdminModuleTestShim *openAdminSession(NodeNum node)
|
||||
{
|
||||
AdminModuleTestShim *admin = new AdminModuleTestShim();
|
||||
admin->openRemoteSession(node);
|
||||
adminModule = admin;
|
||||
return admin;
|
||||
}
|
||||
|
||||
static void closeAdminSession(AdminModuleTestShim *admin)
|
||||
{
|
||||
adminModule = nullptr;
|
||||
delete admin;
|
||||
}
|
||||
|
||||
static void test_adminRequest_answeredInsideTheFloor(void)
|
||||
{
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||
Time::advanceTestMillis(5 * 60 * 1000);
|
||||
AdminModuleTestShim *admin = openAdminSession(kAdminNode); // after the jump: the session lasts 300 s
|
||||
|
||||
TEST_ASSERT_FALSE_MESSAGE(answers(makeNodeInfoRequest(kOtherNode, true)), "a non-admin request is inside the floor");
|
||||
TEST_ASSERT_FALSE_MESSAGE(answers(makeNodeInfoRequest(kAdminNode, false)),
|
||||
"an unsigned request cannot prove it came from the admin node");
|
||||
TEST_ASSERT_TRUE_MESSAGE(answers(makeNodeInfoRequest(kAdminNode, true)), "the admin's signed request is answered");
|
||||
closeAdminSession(admin);
|
||||
}
|
||||
|
||||
static void test_adminRequest_bypassesPerRequesterSuppression(void)
|
||||
{
|
||||
AdminModuleTestShim *admin = openAdminSession(kAdminNode);
|
||||
TEST_ASSERT_TRUE(answers(makeNodeInfoRequest(kOtherNode, true)));
|
||||
clearTransmitHistory();
|
||||
TEST_ASSERT_TRUE(answers(makeNodeInfoRequest(kAdminNode, true)));
|
||||
|
||||
clearTransmitHistory(); // take the floor out, leaving the 12 h suppression as the only gate
|
||||
Time::advanceTestMillis(2 * 60 * 1000);
|
||||
TEST_ASSERT_FALSE_MESSAGE(answers(makeNodeInfoRequest(kOtherNode, true)), "a repeat requester is suppressed for 12 h");
|
||||
TEST_ASSERT_TRUE_MESSAGE(answers(makeNodeInfoRequest(kAdminNode, true)), "the admin is not");
|
||||
closeAdminSession(admin);
|
||||
}
|
||||
|
||||
static void test_adminRequest_keepsTheSixtySecondGate(void)
|
||||
{
|
||||
AdminModuleTestShim *admin = openAdminSession(kAdminNode);
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||
Time::advanceTestMillis(30 * 1000);
|
||||
|
||||
TEST_ASSERT_FALSE(answers(makeNodeInfoRequest(kAdminNode, true)));
|
||||
closeAdminSession(admin);
|
||||
}
|
||||
|
||||
static void test_adminRequest_endsWithTheSession(void)
|
||||
{
|
||||
AdminModuleTestShim *admin = openAdminSession(kAdminNode);
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
||||
Time::advanceTestMillis(5 * 60 * 1000 + 1000); // past the 300 s session, still inside the floor
|
||||
|
||||
TEST_ASSERT_FALSE(answers(makeNodeInfoRequest(kAdminNode, true)));
|
||||
closeAdminSession(admin);
|
||||
}
|
||||
|
||||
NI_TEST_ENTRY void setup()
|
||||
{
|
||||
initializeTestEnvironment();
|
||||
@@ -421,6 +536,10 @@ NI_TEST_ENTRY void setup()
|
||||
RUN_TEST(test_presetChange_isConsumedOnlyByASendThatGoesOut);
|
||||
RUN_TEST(test_relay_userPayloadUnmodified);
|
||||
RUN_TEST(test_coerceUserId_rewritesOnlyAMismatchedId);
|
||||
RUN_TEST(test_adminRequest_answeredInsideTheFloor);
|
||||
RUN_TEST(test_adminRequest_bypassesPerRequesterSuppression);
|
||||
RUN_TEST(test_adminRequest_keepsTheSixtySecondGate);
|
||||
RUN_TEST(test_adminRequest_endsWithTheSession);
|
||||
exit(UNITY_END());
|
||||
}
|
||||
NI_TEST_ENTRY void loop() {}
|
||||
Reference in new issue
Block a user