Files
MeshCore/src/helpers/FloodRuleExtensions.h
T

419 lines
18 KiB
C++

#pragma once
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <Packet.h>
#include "FloodRuleTargets.h"
#include "TxtDataHelpers.h"
#include "FloodTextPattern.h"
#include "FloodGroupPlainText.h"
namespace mesh { class Utils; }
namespace FloodRuleExtensions {
enum Route : uint8_t { LegacyRoute, FloodRoute, DirectRoute, AnyRoute };
enum PathPosition : uint8_t { Prefix, Anywhere, Suffix, Exact };
// Zero initialization retains every FPF7 default. Keep the old path's first
// nine bytes in its established field; only a fourth 3-byte ID needs a tail.
struct Fields {
uint16_t min_payload;
uint16_t max_payload;
uint8_t route;
uint8_t probability;
uint8_t path_position;
uint8_t disabled;
uint8_t payload_set;
uint8_t path_tail[3];
char sender[64];
char text[64];
};
static_assert(sizeof(Fields) <= 140, "Update the extended filter RAM budget");
inline bool hasFields(const Fields& fields) {
return fields.min_payload || fields.max_payload || fields.route
|| fields.probability || fields.path_position || fields.disabled || fields.payload_set
|| fields.path_tail[0] || fields.path_tail[1] || fields.path_tail[2]
|| fields.sender[0] || fields.text[0];
}
inline bool sameFields(const Fields& left, const Fields& right) {
return left.min_payload == right.min_payload && left.max_payload == right.max_payload
&& left.route == right.route && left.probability == right.probability
&& left.path_position == right.path_position && left.disabled == right.disabled
&& left.payload_set == right.payload_set
&& !memcmp(left.path_tail, right.path_tail, sizeof(left.path_tail))
&& !strcmp(left.sender, right.sender) && !strcmp(left.text, right.text);
}
template<typename Entry>
inline bool tableUsesExtensions(const Entry* entries, unsigned count) {
for (unsigned i = 0; i < count; ++i)
if (entries[i].active && (hasFields(entries[i].extra) || entries[i].path_hops > 3)) return true;
return false;
}
inline bool contentRequested(const Fields& fields) {
return fields.sender[0] || fields.text[0];
}
inline bool validFields(const Fields& fields) {
return fields.route <= AnyRoute && fields.probability <= 100
&& fields.path_position <= Exact && fields.disabled <= 1 && fields.payload_set <= 1
&& fields.min_payload <= MAX_PACKET_PAYLOAD
&& fields.max_payload <= MAX_PACKET_PAYLOAD
&& (!fields.payload_set || fields.min_payload <= fields.max_payload)
&& memchr(fields.sender, 0, sizeof(fields.sender))
&& memchr(fields.text, 0, sizeof(fields.text))
&& mesh::flood::textPatternValid(fields.sender, strlen(fields.sender))
&& mesh::flood::textPatternValid(fields.text, strlen(fields.text));
}
template<typename Entry>
inline bool validRuleFields(const Entry& entry) {
if (!validFields(entry.extra)) return false;
const bool path_valid = entry.path_hops == 0
? entry.path_hash_size <= 3 || entry.path_hash_size == 0x82
: entry.path_hash_size >= 1 && entry.path_hash_size <= 3
&& entry.path_hops <= 4 && !entry.match_blacklisted_path;
const bool content_valid = !contentRequested(entry.extra)
|| ((entry.channel_key_len == 16 || entry.channel_key_len == 32)
&& (entry.payload_type == 0xFF || entry.payload_type == PAYLOAD_TYPE_GRP_TXT));
const bool flood_actions = FloodRuleTargets::scopeName(entry)[0] || FloodRuleTargets::regionName(entry)[0]
|| entry.retry_on_match || entry.scope_uses_slow_timing;
return path_valid && content_valid && (!flood_actions
|| entry.extra.route == LegacyRoute || entry.extra.route == FloodRoute);
}
// Strip grouping quotes, preserving regex escapes and all quoted whitespace.
// Token pointers borrow this buffer; malformed input never reaches a setter.
inline int tokenize(char* buffer, char* tokens[], unsigned capacity) {
char* read = buffer;
char* write = buffer;
unsigned count = 0;
while (*read) {
while (*read == ' ' || *read == '\t') ++read;
if (!*read) break;
if (count == capacity) return -1;
tokens[count++] = write;
char quote = 0;
while (*read) {
char c = *read++;
if (c == '\\' && *read) {
if (quote && (*read == quote || *read == '\\')) *write++ = *read++;
else { *write++ = c; *write++ = *read++; }
} else if (quote && c == quote) {
quote = 0;
} else if (!quote && (c == '"' || c == '\'')) {
quote = c;
} else if (!quote && (c == ' ' || c == '\t')) {
break;
} else {
*write++ = c;
}
}
if (quote) return -1;
*write++ = 0;
}
return int(count);
}
inline bool unsignedValue(const char* text, uint16_t maximum, uint16_t& value) {
if (!text || !*text) return false;
uint32_t parsed = 0;
do {
if (*text < '0' || *text > '9') return false;
parsed = parsed * 10U + uint8_t(*text++ - '0');
if (parsed > maximum) return false;
} while (*text);
value = uint16_t(parsed);
return true;
}
inline bool enabledValue(const char* value, uint8_t& disabled) {
if (!strcmp(value, "on") || !strcmp(value, "1")) disabled = 0;
else if (!strcmp(value, "off") || !strcmp(value, "0")) disabled = 1;
else return false;
return true;
}
// Return 0 for an existing-parser token, 1 when handled, -1 on invalid input.
inline int parseToken(const char* token, Fields& fields, uint8_t& seen, char* reply) {
const char* value = strchr(token, '=');
if (!value) return 0;
const size_t length = size_t(value++ - token);
unsigned bit;
if ((length == 6 && !strncmp(token, "sender", 6)) || (length == 1 && *token == 'n')) bit = 0;
else if ((length == 4 && !strncmp(token, "text", 4)) || (length == 1 && *token == 'x')) bit = 1;
else if ((length == 3 && !strncmp(token, "len", 3)) || (length == 1 && *token == 'l')) bit = 2;
else if ((length == 5 && !strncmp(token, "route", 5)) || (length == 2 && !strncmp(token, "rt", 2))) bit = 3;
else if ((length == 4 && !strncmp(token, "prob", 4)) || (length == 2 && !strncmp(token, "pr", 2))) bit = 4;
else if ((length == 7 && !strncmp(token, "enabled", 7)) || (length == 1 && *token == 'e')) bit = 5;
else return 0;
if (seen & (1U << bit)) {
strcpy(reply, "Err - duplicate rule parameter");
return -1;
}
seen |= uint8_t(1U << bit);
bool valid = true;
if (bit <= 1) {
valid = *value && strlen(value) < sizeof(fields.sender)
&& mesh::flood::textPatternValid(value, strlen(value));
if (valid) strcpy(bit == 0 ? fields.sender : fields.text, value);
else strcpy(reply, "Err - invalid pattern (maximum 63 bytes)");
} else if (bit == 2) {
char range[20];
valid = strlen(value) < sizeof(range);
if (valid) {
strcpy(range, value);
char* separator = strchr(range, '-');
char* plus = strchr(range, '+');
if (separator) *separator++ = 0;
if (plus) *plus = 0;
valid = unsignedValue(range, MAX_PACKET_PAYLOAD, fields.min_payload);
if (valid && separator) valid = !plus
&& unsignedValue(separator, MAX_PACKET_PAYLOAD, fields.max_payload)
&& fields.min_payload <= fields.max_payload;
else if (valid) {
valid = !plus || plus[1] == 0;
fields.max_payload = plus ? MAX_PACKET_PAYLOAD : fields.min_payload;
}
}
if (valid) fields.payload_set = 1;
if (!valid) strcpy(reply, "Err - len is N, N+, or N-M payload bytes");
} else if (bit == 3) {
if (!strcmp(value, "flood") || !strcmp(value, "f")) fields.route = FloodRoute;
else if (!strcmp(value, "direct") || !strcmp(value, "d")) fields.route = DirectRoute;
else if (!strcmp(value, "any") || !strcmp(value, "a")) fields.route = AnyRoute;
else { valid = false; strcpy(reply, "Err - route is flood, direct, or any"); }
} else if (bit == 4) {
uint16_t probability;
valid = unsignedValue(value, 100, probability) && probability != 0;
if (valid) fields.probability = probability == 100 ? 0 : uint8_t(probability);
else strcpy(reply, "Err - prob must be 1-100");
} else {
valid = enabledValue(value, fields.disabled);
if (!valid) strcpy(reply, "Err - enabled is on or off");
}
return valid ? 1 : -1;
}
inline bool routeMatches(const Fields& fields, const mesh::Packet* packet, uint8_t context) {
if (!packet) return false;
if (fields.route == LegacyRoute) return context != 0 || packet->isRouteFlood();
return fields.route == AnyRoute || (fields.route == FloodRoute ? packet->isRouteFlood() : packet->isRouteDirect());
}
inline bool matchesEnvelope(const Fields& fields, const mesh::Packet* packet, uint8_t context) {
return packet && !fields.disabled && routeMatches(fields, packet, context)
&& (!fields.payload_set || (packet->payload_len >= fields.min_payload
&& packet->payload_len <= fields.max_payload));
}
inline bool matchesProbability(const Fields& fields, uint32_t roll) {
return fields.probability == 0 || fields.probability == 100
|| roll % 100U < fields.probability;
}
// A received packet can be evaluated during both timing and forwarding.
// Use one boot-randomized decision per payload/rule/admission phase, stable
// across path growth and scope rewrites. Repeated evaluation cannot reroll it.
template <typename PacketType>
inline uint32_t probabilityRoll(const PacketType* packet, uint32_t seed,
unsigned slot, uint8_t context) {
uint32_t hash = 2166136261UL;
const auto add = [&hash](uint8_t byte) { hash = (hash ^ byte) * 16777619UL; };
for (unsigned i = 0; i < 4; ++i) add(uint8_t(seed >> (8 * i)));
add(uint8_t(slot)); add(context);
if (packet) {
add(packet->getPayloadType());
add(uint8_t(packet->payload_len)); add(uint8_t(packet->payload_len >> 8));
const unsigned length = packet->payload_len <= MAX_PACKET_PAYLOAD
? packet->payload_len : MAX_PACKET_PAYLOAD;
for (unsigned i = 0; i < length; ++i) add(packet->payload[i]);
}
hash ^= hash >> 16;
hash *= 0x85ebca6bUL;
hash ^= hash >> 13;
hash *= 0xc2b2ae35UL;
return hash ^ (hash >> 16);
}
inline int hexValue(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
inline bool parsePath(const char* value, uint8_t& width, uint8_t& hops,
uint8_t first[9], Fields& fields, bool prefix_legacy) {
memset(first, 0, 9);
memset(fields.path_tail, 0, sizeof(fields.path_tail));
width = hops = 0;
fields.path_position = prefix_legacy ? Prefix : Anywhere;
if (!strcmp(value, "*")) { fields.path_position = Prefix; return true; }
bool start = *value == '^';
if (start) ++value;
const size_t size = strlen(value);
bool end = size && value[size - 1] == '$';
const char* finish = value + size - (end ? 1 : 0);
if (start || end) fields.path_position = start ? (end ? Exact : Prefix) : Suffix;
uint8_t bytes[12] = {};
while (value < finish) {
const char* next = value;
while (next < finish && *next != '>' && *next != ',') ++next;
const size_t digits = size_t(next - value);
if ((digits != 2 && digits != 4 && digits != 6) || hops >= 4
|| (width && digits != size_t(width) * 2)) return false;
width = uint8_t(digits / 2);
for (unsigned i = 0; i < width; ++i) {
const int high = hexValue(value[i * 2]), low = hexValue(value[i * 2 + 1]);
if (high < 0 || low < 0) return false;
bytes[hops * width + i] = uint8_t((high << 4) | low);
}
++hops;
if (next == finish) break;
value = next + 1;
if (value == finish) return false;
}
if (!hops) return false;
memcpy(first, bytes, 9);
memcpy(fields.path_tail, bytes + 9, 3);
return true;
}
inline bool pathMatches(const Fields& fields, const mesh::Packet* packet,
uint8_t width, uint8_t hops, const uint8_t first[9]) {
if (!hops) return true; // Existing hash-width-only condition is checked separately.
if (!packet || !packet->isRouteFlood() || packet->getPayloadType() == 9
|| width < 1 || width > 3 || width != packet->getPathHashSize()
|| hops > 4 || packet->getPathHashCount() < hops
|| unsigned(packet->getPathHashCount()) * width > MAX_PATH_SIZE) return false;
const unsigned count = packet->getPathHashCount();
if (fields.path_position == Exact && count != hops) return false;
uint8_t wanted[12];
memcpy(wanted, first, 9);
memcpy(wanted + 9, fields.path_tail, 3);
unsigned begin = fields.path_position == Suffix ? count - hops : 0;
unsigned last = fields.path_position == Anywhere ? count - hops : begin;
for (unsigned at = begin; at <= last; ++at)
if (!memcmp(packet->path + at * width, wanted, hops * width)) return true;
return false;
}
template <typename PacketType, typename Crypto = mesh::Utils>
inline bool matchesContent(const Fields& fields, const PacketType* packet,
const uint8_t* secret, uint8_t key_len,
uint32_t* work = nullptr, bool conservative = false,
bool* budget_exceeded = nullptr) {
if (budget_exceeded) *budget_exceeded = false;
if (!contentRequested(fields)) return true;
if (!packet || !secret || (key_len != 16 && key_len != 32)
|| packet->getPayloadType() != PAYLOAD_TYPE_GRP_TXT
|| packet->payload_len <= PATH_HASH_SIZE + CIPHER_MAC_SIZE) return false;
uint8_t plain[MAX_PACKET_PAYLOAD];
int size = Crypto::MACThenDecrypt(secret, plain,
packet->payload + PATH_HASH_SIZE, packet->payload_len - PATH_HASH_SIZE);
mesh::FloodGroupPlainText::View view;
if (size <= 0 || !mesh::FloodGroupPlainText::parse(
plain, size_t(size), TXT_TYPE_PLAIN, view)) return false;
using mesh::flood::PatternMatchResult;
if (fields.sender[0]) {
if (!view.has_sender) return false;
const auto result = mesh::flood::matchTextPattern(fields.sender, strlen(fields.sender),
view.sender.data, view.sender.size, false, work);
if (result == PatternMatchResult::BudgetExceeded) {
if (budget_exceeded) *budget_exceeded = true;
return conservative;
}
if (result != PatternMatchResult::Match) return false;
}
if (!fields.text[0]) return true;
const auto result = mesh::flood::matchTextPattern(fields.text, strlen(fields.text),
view.text.data, view.text.size, false, work);
if (result == PatternMatchResult::BudgetExceeded && budget_exceeded)
*budget_exceeded = true;
return result == PatternMatchResult::Match
|| (result == PatternMatchResult::BudgetExceeded && conservative);
}
template <typename File>
bool readFields(File& file, Fields& fields) {
memset(&fields, 0, sizeof(fields));
auto read = [&file](void* out, size_t size) {
return file.read(static_cast<uint8_t*>(out), size) == size;
};
uint8_t sender_size = 0, text_size = 0;
return read(&fields.min_payload, 2) && read(&fields.max_payload, 2)
&& read(&fields.route, 1) && read(&fields.probability, 1)
&& read(&fields.path_position, 1) && read(&fields.disabled, 1)
&& read(&fields.payload_set, 1)
&& read(fields.path_tail, 3)
&& read(&sender_size, 1) && sender_size < sizeof(fields.sender)
&& read(fields.sender, sender_size)
&& read(&text_size, 1) && text_size < sizeof(fields.text)
&& read(fields.text, text_size) && validFields(fields);
}
template <typename Writer>
bool writeFields(Writer& write, const Fields& fields) {
uint8_t sender_size = uint8_t(strlen(fields.sender));
uint8_t text_size = uint8_t(strlen(fields.text));
return write(&fields.min_payload, 2) && write(&fields.max_payload, 2)
&& write(&fields.route, 1) && write(&fields.probability, 1)
&& write(&fields.path_position, 1) && write(&fields.disabled, 1)
&& write(&fields.payload_set, 1)
&& write(fields.path_tail, 3)
&& write(&sender_size, 1) && write(fields.sender, sender_size)
&& write(&text_size, 1) && write(fields.text, text_size);
}
template <typename Writer>
void appendQuoted(Writer& writer, const char* key, const char* value) {
writer.append(" %s=\"", key);
for (const char* p = value; *p; ++p) {
if (*p == '"' || *p == '\\') writer.append("\\");
writer.append("%c", *p);
}
writer.append("\"");
}
template <typename Writer>
void appendFields(Writer& writer, const Fields& fields, bool compact) {
if (fields.sender[0]) appendQuoted(writer, compact ? "n" : "sender", fields.sender);
if (fields.text[0]) appendQuoted(writer, compact ? "x" : "text", fields.text);
if (fields.payload_set) {
const char* key = compact ? "l" : "len";
if (fields.max_payload == fields.min_payload) writer.append(" %s=%u", key, unsigned(fields.min_payload));
else if (fields.max_payload == MAX_PACKET_PAYLOAD) writer.append(" %s=%u+", key, unsigned(fields.min_payload));
else writer.append(" %s=%u-%u", key, unsigned(fields.min_payload), unsigned(fields.max_payload));
}
if (fields.route) writer.append(" %s=%s", compact ? "rt" : "route",
fields.route == FloodRoute ? (compact ? "f" : "flood")
: fields.route == DirectRoute ? (compact ? "d" : "direct") : (compact ? "a" : "any"));
if (fields.probability)
writer.append(" %s=%u", compact ? "pr" : "prob", unsigned(fields.probability));
if (fields.disabled) writer.append(" %s=off", compact ? "e" : "enabled");
}
template <typename Writer>
void appendPath(Writer& writer, const Fields& fields, uint8_t width,
uint8_t hops, const uint8_t first[9], bool compact) {
if (!hops) return;
const bool legacy = fields.path_position == Prefix && hops <= 3;
writer.append(" %s=", legacy ? (compact ? "p" : "prefix") : (compact ? "pa" : "path"));
if (!legacy && (fields.path_position == Prefix || fields.path_position == Exact)) writer.append("^");
uint8_t path[12];
memcpy(path, first, 9); memcpy(path + 9, fields.path_tail, 3);
for (unsigned i = 0; i < hops; ++i) {
if (i) writer.append(legacy ? "," : ">");
for (unsigned j = 0; j < width; ++j) writer.append("%02X", unsigned(path[i * width + j]));
}
if (fields.path_position == Suffix || fields.path_position == Exact) writer.append("$");
}
} // namespace FloodRuleExtensions