mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 13:22:22 +00:00
Support atomic two-packet signed fleet commands
Automatically split longer signed envelopes into two bounded group-data packets, reserve both before queueing, and roll back exact packet copies and retries on admission failure. Collect fragments in either order with a fixed deadline and verify the complete command before durable replay reservation and dispatch. Raise whole-fleet protocol capacity to 230 bytes, preserve target-specific budgets and radio acknowledgement barriers, and reject rule-token and USB rescue-line truncation. Document existing app input limits and add real-signature, sanitizer, queue failure, timeout, replay, and compact-board regressions.
This commit is contained in:
1 parent
d74a0f8b44
commit
fdad79eaf7
18 files changed
+1649
-143
No files matched your search
@@ -1691,6 +1691,64 @@ bool MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pk
|
||||
}
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_FLEET_CONTROL
|
||||
void MyMesh::onRadioProfileCopyQueued(mesh::Packet* packet, const mesh::Packet* original, uint8_t priority) {
|
||||
mesh::Mesh::onRadioProfileCopyQueued(packet, original, priority);
|
||||
if (original && original == _fleet_admission_original) _fleet_admission_copy = packet;
|
||||
}
|
||||
|
||||
bool MyMesh::sendFleetCommandData(mesh::GroupChannel& channel, const uint8_t* envelope, size_t length) {
|
||||
if (!envelope || !length || length > mesh::FleetCommand::MaxEnvelopeLength) return false;
|
||||
if (length <= mesh::FleetCommand::MaxPayloadLength) {
|
||||
return sendGroupData(channel, nullptr, OUT_PATH_UNKNOWN,
|
||||
mesh::FleetCommand::DataType, envelope, (int)length);
|
||||
}
|
||||
mesh::Packet* reserved[2] = {};
|
||||
uint8_t group[3 + mesh::FleetCommand::MaxPayloadLength];
|
||||
group[0] = (uint8_t)mesh::FleetCommand::DataType;
|
||||
group[1] = (uint8_t)(mesh::FleetCommand::DataType >> 8);
|
||||
// Reserve both encrypted packets before handing either to the dispatcher.
|
||||
for (uint8_t part = 0; part < 2; ++part) {
|
||||
const size_t bytes = mesh::FleetCommand::fragment(envelope, length, part,
|
||||
group + 3, sizeof(group) - 3);
|
||||
if (bytes) {
|
||||
group[2] = (uint8_t)bytes;
|
||||
reserved[part] = createGroupDatagram(PAYLOAD_TYPE_GRP_DATA, channel, group, 3 + bytes);
|
||||
}
|
||||
if (!reserved[part]) {
|
||||
if (reserved[0]) releasePacket(reserved[0]);
|
||||
memset(group, 0, sizeof(group));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
memset(group, 0, sizeof(group));
|
||||
_fleet_admission_original = reserved[0];
|
||||
_fleet_admission_copy = nullptr;
|
||||
const bool first_queued = sendFloodScoped(channel, reserved[0]);
|
||||
mesh::Packet* first_copy = _fleet_admission_copy;
|
||||
_fleet_admission_original = nullptr;
|
||||
_fleet_admission_copy = nullptr;
|
||||
if (!first_queued) {
|
||||
// sendFloodScoped/sendPacket owns and releases the failed first packet.
|
||||
releasePacket(reserved[1]);
|
||||
return false;
|
||||
}
|
||||
if (sendFloodScoped(channel, reserved[1])) return true;
|
||||
// A failed second send has already been released. No dispatcher loop runs
|
||||
// inside admission, so the first packet and its optional copy are still
|
||||
// queued and can be retired by exact pointer without touching other work.
|
||||
for (int i = _mgr->getOutboundTotal() - 1; i >= 0; --i) {
|
||||
const mesh::Packet* queued = _mgr->getOutboundByIdx(i);
|
||||
if (queued != reserved[0] && queued != first_copy) continue;
|
||||
mesh::Packet* cancelled = _mgr->removeOutboundByIdx(i);
|
||||
cancelOutboundRadioRetry(cancelled);
|
||||
onSendFail(cancelled); // Clear its pending flood retry metadata too.
|
||||
releasePacket(cancelled);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const char *text) {
|
||||
#if MESH_ENABLE_ONE_KEY_DM
|
||||
@@ -5264,7 +5322,10 @@ void MyMesh::resetUsbHostSessionInput() {
|
||||
// CLI rescue deliberately remains active across reconnects, but no bytes
|
||||
// typed by the old host may complete a command for the new one. This is
|
||||
// especially important for the rescue shell's erase/rm commands.
|
||||
if (_cli_rescue) memset(cli_command, 0, sizeof(cli_command));
|
||||
if (_cli_rescue) {
|
||||
memset(cli_command, 0, sizeof(cli_command));
|
||||
_cli_line_overflow = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::handleCmdFrame(size_t len) {
|
||||
@@ -9520,6 +9581,7 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
void MyMesh::enterCLIRescue() {
|
||||
_cli_rescue = true;
|
||||
cli_command[0] = 0;
|
||||
_cli_line_overflow = false;
|
||||
mesh::usbTerminalPort().println("========= CLI Rescue =========");
|
||||
}
|
||||
|
||||
@@ -9784,16 +9846,15 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
|
||||
snprintf(reply, reply_capacity, "Error: wait for the next clock second before fleet send");
|
||||
return true;
|
||||
}
|
||||
uint8_t payload[mesh::FleetCommand::MaxPayloadLength];
|
||||
uint8_t payload[mesh::FleetCommand::MaxEnvelopeLength];
|
||||
const size_t length = mesh::FleetCommand::encode(
|
||||
self_id, channel.channel.secret, sequence,
|
||||
sequence + mesh::FleetCommand::MaxLifetime, targets, text,
|
||||
payload, sizeof(payload));
|
||||
if (!length) {
|
||||
snprintf(reply, reply_capacity,
|
||||
"Error: fleet targets and command exceed one packet; use fewer targets");
|
||||
} else if (!sendGroupData(channel.channel, nullptr, OUT_PATH_UNKNOWN,
|
||||
mesh::FleetCommand::DataType, payload, (int)length)) {
|
||||
"Error: fleet targets and command exceed two packets; use fewer targets");
|
||||
} else if (!sendFleetCommandData(channel.channel, payload, length)) {
|
||||
snprintf(reply, reply_capacity, "Error: could not queue fleet command");
|
||||
} else {
|
||||
last_fleet_sequence = sequence;
|
||||
@@ -10076,30 +10137,42 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
|
||||
void MyMesh::checkCLIRescueCmd() {
|
||||
Stream& output = mesh::usbTerminalPort();
|
||||
Stream& input = mesh::usbCompanionPort();
|
||||
int len = strlen(cli_command);
|
||||
// `cli_command` must stay NUL-terminated within its bounds. If it ever isn't,
|
||||
// strlen() above can return >= sizeof(cli_command) and the loop below would
|
||||
// then index past the buffer, so clamp defensively.
|
||||
if (len >= (int)sizeof(cli_command)) {
|
||||
size_t len = strnlen(cli_command, sizeof(cli_command));
|
||||
if (len == sizeof(cli_command)) {
|
||||
cli_command[0] = 0;
|
||||
len = 0;
|
||||
_cli_line_overflow = true;
|
||||
}
|
||||
while (input.available() && len < sizeof(cli_command)-1) {
|
||||
unsigned budget = sizeof(cli_command);
|
||||
while (budget-- && input.available()) {
|
||||
char c = input.read();
|
||||
if (c != '\n') {
|
||||
cli_command[len++] = c;
|
||||
cli_command[len] = 0;
|
||||
}
|
||||
output.print(c); // echo
|
||||
}
|
||||
if (len == sizeof(cli_command)-1) { // buffer full: treat as a completed line
|
||||
cli_command[sizeof(cli_command)-2] = '\r'; // place end-of-line marker inside the buffer
|
||||
cli_command[sizeof(cli_command)-1] = 0; // keep the buffer NUL-terminated
|
||||
if (c == '\r' || c == '\n') {
|
||||
if (_cli_line_overflow) {
|
||||
_cli_line_overflow = false;
|
||||
#if defined(STM32_PLATFORM)
|
||||
output.println(" Error: unknown command");
|
||||
#else
|
||||
output.print("ERR: too long\r\n");
|
||||
#endif
|
||||
} else if (len) goto complete_line;
|
||||
return;
|
||||
}
|
||||
if (_cli_line_overflow) continue;
|
||||
if (len == sizeof(cli_command)-1) {
|
||||
_cli_line_overflow = true;
|
||||
cli_command[0] = 0;
|
||||
len = 0;
|
||||
continue;
|
||||
}
|
||||
cli_command[len++] = c;
|
||||
cli_command[len] = 0;
|
||||
}
|
||||
|
||||
if (len > 0 && cli_command[len - 1] == '\r') { // received complete line
|
||||
cli_command[len - 1] = 0; // replace newline with C string null terminator
|
||||
return;
|
||||
|
||||
complete_line:
|
||||
{
|
||||
reply_buf[0] = 0;
|
||||
#if COMPANION_FEATURE_READER
|
||||
if (mesh::handleReaderCommand(cli_command, output)) {
|
||||
|
||||
@@ -355,6 +355,10 @@ protected:
|
||||
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;
|
||||
#if MESH_ENABLE_FLEET_CONTROL
|
||||
bool sendFleetCommandData(mesh::GroupChannel& channel, const uint8_t* envelope, size_t length);
|
||||
void onRadioProfileCopyQueued(mesh::Packet* packet, const mesh::Packet* original, uint8_t priority) override;
|
||||
#endif
|
||||
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
|
||||
#if MESH_PACKET_LOGGING && !MESH_PACKET_LOGGING_COMPACT
|
||||
@@ -466,6 +470,11 @@ public:
|
||||
bool canRecoverUsbLogging() const;
|
||||
|
||||
private:
|
||||
#if MESH_ENABLE_FLEET_CONTROL
|
||||
// Exact ownership of the optional radio copy during two-part admission.
|
||||
mesh::Packet* _fleet_admission_original = nullptr;
|
||||
mesh::Packet* _fleet_admission_copy = nullptr;
|
||||
#endif
|
||||
// Only snapshot the changed value: CompanionNodePrefs owns self-referencing
|
||||
// runtime adapters and must not be copied as a transaction snapshot.
|
||||
template <typename T> bool savePreference(T& preference, T value) {
|
||||
@@ -723,6 +732,7 @@ private:
|
||||
#endif
|
||||
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
|
||||
char cli_command[80];
|
||||
bool _cli_line_overflow = false;
|
||||
char reply_buf[166];
|
||||
uint8_t app_target_ver;
|
||||
uint8_t *sign_data;
|
||||
|
||||
@@ -7956,12 +7956,12 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
}
|
||||
}
|
||||
cursor = skipFloodFilterSpaces(cursor);
|
||||
if (strlen(cursor) >= 192) {
|
||||
char params[256];
|
||||
if (strlen(cursor) >= sizeof(params)) {
|
||||
strcpy(reply, "Err - rule parameters too long");
|
||||
return;
|
||||
}
|
||||
|
||||
char params[192];
|
||||
strcpy(params, cursor);
|
||||
char* tokens[18];
|
||||
int token_count = 0;
|
||||
@@ -8244,9 +8244,12 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
}
|
||||
char rate_text[24];
|
||||
const bool compact = floodFilterAsciiStartsWith(tokens[i], "q=");
|
||||
StrHelper::strncpy(rate_text,
|
||||
tokens[i] + (compact ? 2 : strlen("rate=")),
|
||||
sizeof(rate_text));
|
||||
const char* value = tokens[i] + (compact ? 2 : strlen("rate="));
|
||||
if (strlen(value) >= sizeof(rate_text)) {
|
||||
strcpy(reply, "Err - rate value too long");
|
||||
return;
|
||||
}
|
||||
StrHelper::strncpy(rate_text, value, sizeof(rate_text));
|
||||
char* slash = strchr(rate_text, '/');
|
||||
if ((!compact && slash == NULL)
|
||||
|| (slash != NULL && !(strcmp(slash, "/min") == 0
|
||||
@@ -8574,12 +8577,16 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
}
|
||||
}
|
||||
cursor = skipFloodFilterSpaces(cursor);
|
||||
if (strlen(cursor) >= 140) {
|
||||
#if MESH_ENABLE_FLEET_CONTROL
|
||||
char params[256];
|
||||
#else
|
||||
char params[140];
|
||||
#endif
|
||||
if (strlen(cursor) >= sizeof(params)) {
|
||||
strcpy(reply, "Err - filter parameters too long");
|
||||
return;
|
||||
}
|
||||
|
||||
char params[140];
|
||||
strcpy(params, cursor);
|
||||
char* tokens[7];
|
||||
int token_count = 0;
|
||||
@@ -10175,6 +10182,10 @@ void MyMesh::setFloodGroupModeration(const char* args, char* reply) {
|
||||
return;
|
||||
}
|
||||
char rate_text[24];
|
||||
if (strlen(option + 5) >= sizeof(rate_text)) {
|
||||
strcpy(reply, "Err - rate value too long");
|
||||
return;
|
||||
}
|
||||
StrHelper::strncpy(rate_text, option + 5, sizeof(rate_text));
|
||||
char* slash = strchr(rate_text, '/');
|
||||
if (slash == NULL || !(strcmp(slash, "/min") == 0 || strcmp(slash, "/m") == 0)) {
|
||||
|
||||
@@ -1275,12 +1275,12 @@ void FloodRuleEngine::set(const char* args, char* reply,
|
||||
}
|
||||
}
|
||||
cursor = skipSpaces(cursor);
|
||||
if (strlen(cursor) >= 192) {
|
||||
char params[256];
|
||||
if (strlen(cursor) >= sizeof(params)) {
|
||||
copyString(reply, "Err - rule parameters too long", 160);
|
||||
return;
|
||||
}
|
||||
|
||||
char params[192];
|
||||
copyString(params, cursor, sizeof(params));
|
||||
char* tokens[18];
|
||||
int token_count = 0;
|
||||
@@ -1564,9 +1564,12 @@ void FloodRuleEngine::set(const char* args, char* reply,
|
||||
}
|
||||
bool compact = asciiStartsWith(tokens[i], "q=");
|
||||
char rate_text[24];
|
||||
copyString(rate_text,
|
||||
tokens[i] + (compact ? 2 : strlen("rate=")),
|
||||
sizeof(rate_text));
|
||||
const char* value = tokens[i] + (compact ? 2 : strlen("rate="));
|
||||
if (strlen(value) >= sizeof(rate_text)) {
|
||||
copyString(reply, "Err - rate value too long", 160);
|
||||
return;
|
||||
}
|
||||
copyString(rate_text, value, sizeof(rate_text));
|
||||
char* slash = strchr(rate_text, '/');
|
||||
if ((!compact && slash == NULL)
|
||||
|| (slash != NULL && !(strcmp(slash, "/min") == 0
|
||||
|
||||
Reference in new issue
Block a user