mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-02 16:33:45 +00:00
1574 lines
57 KiB
C++
1574 lines
57 KiB
C++
#include "FloodRuleEngine.h"
|
|
|
|
#if defined(MESHCORE_ESP32_FULL_PROFILE)
|
|
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
|
|
#include <helpers/FloodFilterPolicy.h>
|
|
#include <helpers/RegionNameUtils.h>
|
|
|
|
namespace {
|
|
|
|
static const char RULE_FILE[] = "/flood_filter";
|
|
static const char RULE_USAGE[] =
|
|
"Err - use: set flood.rule[.n] type=<type> [hops=<range>] [...]";
|
|
static const char DUPLICATE_OPTION[] = "Err - duplicate filter option";
|
|
|
|
// Channel encryption uses a 128-bit key, while MACThenDecrypt takes a
|
|
// PUB_KEY_SIZE buffer. Keep the unused half zero-padded like GroupChannel.
|
|
static const uint8_t PUBLIC_CHANNEL_SECRET[PUB_KEY_SIZE] = {
|
|
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
|
|
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72
|
|
};
|
|
|
|
static const char* skipSpaces(const char* text) {
|
|
while (text != NULL && *text == ' ') text++;
|
|
return text == NULL ? "" : text;
|
|
}
|
|
|
|
static bool asciiEqual(const char* left, const char* right) {
|
|
if (left == NULL || right == NULL) return false;
|
|
while (*left && *right) {
|
|
char a = *left++;
|
|
char b = *right++;
|
|
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
|
|
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
|
|
if (a != b) return false;
|
|
}
|
|
return *left == 0 && *right == 0;
|
|
}
|
|
|
|
static bool asciiStartsWith(const char* text, const char* prefix) {
|
|
if (text == NULL || prefix == NULL) return false;
|
|
while (*prefix) {
|
|
char a = *text++;
|
|
char b = *prefix++;
|
|
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
|
|
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
|
|
if (a != b) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void copyString(char* dest, const char* src, size_t dest_len) {
|
|
if (dest == NULL || dest_len == 0) return;
|
|
if (src == NULL) src = "";
|
|
size_t len = strlen(src);
|
|
if (len >= dest_len) len = dest_len - 1;
|
|
memcpy(dest, src, len);
|
|
dest[len] = 0;
|
|
}
|
|
|
|
static bool parseUnsigned(const char* text, uint32_t maximum,
|
|
uint32_t& value) {
|
|
if (text == NULL || *text == 0) return false;
|
|
uint32_t parsed = 0;
|
|
for (const char* p = text; *p; p++) {
|
|
if (*p < '0' || *p > '9') return false;
|
|
uint32_t digit = (uint32_t)(*p - '0');
|
|
if (digit > maximum || parsed > (maximum - digit) / 10U) return false;
|
|
parsed = parsed * 10U + digit;
|
|
}
|
|
value = parsed;
|
|
return true;
|
|
}
|
|
|
|
static const char* payloadTypeName(uint8_t type) {
|
|
switch (type) {
|
|
case PAYLOAD_TYPE_REQ: return "req";
|
|
case PAYLOAD_TYPE_RESPONSE: return "response";
|
|
case PAYLOAD_TYPE_TXT_MSG: return "txt_msg";
|
|
case PAYLOAD_TYPE_ACK: return "ack";
|
|
case PAYLOAD_TYPE_ADVERT: return "advert";
|
|
case PAYLOAD_TYPE_GRP_TXT: return "grp_txt";
|
|
case PAYLOAD_TYPE_GRP_DATA: return "grp_data";
|
|
case PAYLOAD_TYPE_ANON_REQ: return "anon_req";
|
|
case PAYLOAD_TYPE_PATH: return "path";
|
|
case PAYLOAD_TYPE_TRACE: return "trace";
|
|
case PAYLOAD_TYPE_MULTIPART: return "multipart";
|
|
case PAYLOAD_TYPE_CONTROL: return "control";
|
|
case PAYLOAD_TYPE_OTA: return "ota";
|
|
case 0x0D: return "reserved13";
|
|
case 0x0E: return "reserved14";
|
|
case PAYLOAD_TYPE_RAW_CUSTOM: return "raw_custom";
|
|
case FloodRuleEngine::ANY_TYPE: return "any";
|
|
default: return "invalid";
|
|
}
|
|
}
|
|
|
|
static bool parsePayloadType(const char* text, uint8_t& type) {
|
|
uint32_t numeric = 0;
|
|
if (parseUnsigned(text, PH_TYPE_MASK, numeric)) {
|
|
type = (uint8_t)numeric;
|
|
return true;
|
|
}
|
|
if (text != NULL && text[0] == '0'
|
|
&& (text[1] == 'x' || text[1] == 'X') && text[2] != 0) {
|
|
uint8_t parsed = 0;
|
|
for (const char* p = text + 2; *p; p++) {
|
|
uint8_t digit;
|
|
if (*p >= '0' && *p <= '9') digit = (uint8_t)(*p - '0');
|
|
else if (*p >= 'a' && *p <= 'f') digit = (uint8_t)(*p - 'a' + 10);
|
|
else if (*p >= 'A' && *p <= 'F') digit = (uint8_t)(*p - 'A' + 10);
|
|
else return false;
|
|
if (parsed > (PH_TYPE_MASK - digit) / 16U) return false;
|
|
parsed = (uint8_t)(parsed * 16U + digit);
|
|
}
|
|
type = parsed;
|
|
return true;
|
|
}
|
|
if (asciiStartsWith(text, "payload_type_")) text += strlen("payload_type_");
|
|
if (asciiEqual(text, "any")) type = FloodRuleEngine::ANY_TYPE;
|
|
else if (asciiEqual(text, "req")) type = PAYLOAD_TYPE_REQ;
|
|
else if (asciiEqual(text, "response") || asciiEqual(text, "resp")) type = PAYLOAD_TYPE_RESPONSE;
|
|
else if (asciiEqual(text, "txt_msg") || asciiEqual(text, "txt")) type = PAYLOAD_TYPE_TXT_MSG;
|
|
else if (asciiEqual(text, "ack")) type = PAYLOAD_TYPE_ACK;
|
|
else if (asciiEqual(text, "advert")) type = PAYLOAD_TYPE_ADVERT;
|
|
else if (asciiEqual(text, "grp_txt") || asciiEqual(text, "group_text")) type = PAYLOAD_TYPE_GRP_TXT;
|
|
else if (asciiEqual(text, "grp_data") || asciiEqual(text, "group_data")) type = PAYLOAD_TYPE_GRP_DATA;
|
|
else if (asciiEqual(text, "anon_req")) type = PAYLOAD_TYPE_ANON_REQ;
|
|
else if (asciiEqual(text, "path")) type = PAYLOAD_TYPE_PATH;
|
|
else if (asciiEqual(text, "trace")) type = PAYLOAD_TYPE_TRACE;
|
|
else if (asciiEqual(text, "multipart")) type = PAYLOAD_TYPE_MULTIPART;
|
|
else if (asciiEqual(text, "control")) type = PAYLOAD_TYPE_CONTROL;
|
|
else if (asciiEqual(text, "ota")) type = PAYLOAD_TYPE_OTA;
|
|
else if (asciiEqual(text, "raw") || asciiEqual(text, "raw_custom")) type = PAYLOAD_TYPE_RAW_CUSTOM;
|
|
else return false;
|
|
return true;
|
|
}
|
|
|
|
static bool parseHopSpec(const char* text, uint8_t& min_hops,
|
|
uint8_t& max_hops) {
|
|
if (text == NULL || *text == 0) return false;
|
|
if (asciiEqual(text, "all")) {
|
|
min_hops = 0;
|
|
max_hops = FloodRuleEngine::MAX_HOPS;
|
|
return true;
|
|
}
|
|
char spec[12];
|
|
if (strlen(text) >= sizeof(spec)) return false;
|
|
copyString(spec, text, sizeof(spec));
|
|
size_t len = strlen(spec);
|
|
uint32_t parsed_min = 0;
|
|
uint32_t parsed_max = 0;
|
|
if (len > 1 && spec[len - 1] == '+') {
|
|
spec[len - 1] = 0;
|
|
if (!parseUnsigned(spec, FloodRuleEngine::MAX_HOPS, parsed_min)) return false;
|
|
min_hops = (uint8_t)parsed_min;
|
|
max_hops = FloodRuleEngine::MAX_HOPS;
|
|
return true;
|
|
}
|
|
char* dash = strchr(spec, '-');
|
|
if (dash != NULL) {
|
|
*dash++ = 0;
|
|
if (!parseUnsigned(spec, FloodRuleEngine::MAX_HOPS, parsed_min)
|
|
|| !parseUnsigned(dash, FloodRuleEngine::MAX_HOPS, parsed_max)
|
|
|| parsed_min > parsed_max) {
|
|
return false;
|
|
}
|
|
min_hops = (uint8_t)parsed_min;
|
|
max_hops = (uint8_t)parsed_max;
|
|
return true;
|
|
}
|
|
if (!parseUnsigned(spec, FloodRuleEngine::MAX_HOPS, parsed_min)) return false;
|
|
min_hops = max_hops = (uint8_t)parsed_min;
|
|
return true;
|
|
}
|
|
|
|
static void formatHopSpec(char* dest, size_t dest_len, uint8_t min_hops,
|
|
uint8_t max_hops) {
|
|
if (min_hops == 0 && max_hops == FloodRuleEngine::MAX_HOPS) {
|
|
snprintf(dest, dest_len, "all");
|
|
} else if (max_hops == FloodRuleEngine::MAX_HOPS) {
|
|
snprintf(dest, dest_len, "%u+", (unsigned int)min_hops);
|
|
} else if (min_hops == max_hops) {
|
|
snprintf(dest, dest_len, "%u", (unsigned int)min_hops);
|
|
} else {
|
|
snprintf(dest, dest_len, "%u-%u", (unsigned int)min_hops,
|
|
(unsigned int)max_hops);
|
|
}
|
|
}
|
|
|
|
static bool normalizeScopeName(const char* text, char* dest,
|
|
size_t dest_len) {
|
|
if (text == NULL || dest == NULL || dest_len < 3) return false;
|
|
if (*text == '#') text++;
|
|
if (*text == 0 || *text == '$') return false;
|
|
size_t len = 0;
|
|
while (text[len]) {
|
|
uint8_t c = (uint8_t)text[len];
|
|
if (!RegionMap::is_name_char(c) || c == '#' || c == '$') return false;
|
|
len++;
|
|
}
|
|
if (len + 2 > dest_len) return false;
|
|
dest[0] = '#';
|
|
memcpy(&dest[1], text, len + 1);
|
|
return true;
|
|
}
|
|
|
|
static bool validStoredScopeName(const char* text) {
|
|
if (text == NULL || text[0] != '#' || text[1] == 0) return false;
|
|
for (size_t i = 1; i < FloodRuleEngine::NAME_LEN; i++) {
|
|
uint8_t c = (uint8_t)text[i];
|
|
if (c == 0) return true;
|
|
if (!RegionMap::is_name_char(c) || c == '#' || c == '$') return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool validStoredRegionName(const char* text) {
|
|
if (text == NULL || text[0] == 0 || strcmp(text, "*") == 0) return false;
|
|
for (size_t i = 0; i < FloodRuleEngine::NAME_LEN; i++) {
|
|
uint8_t c = (uint8_t)text[i];
|
|
if (c == 0) return true;
|
|
if (!RegionMap::is_name_char(c)) return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void deriveScopeKey(const char* scope_name, TransportKey& scope) {
|
|
mesh::Utils::sha256(scope.key, sizeof(scope.key),
|
|
(const uint8_t*)scope_name, strlen(scope_name));
|
|
}
|
|
|
|
static bool parseChannel(const char* text, uint8_t secret[PUB_KEY_SIZE],
|
|
uint8_t& key_len, uint8_t& channel_hash,
|
|
char* name, size_t name_len) {
|
|
if (text == NULL || *text == 0) return false;
|
|
memset(secret, 0, PUB_KEY_SIZE);
|
|
if (asciiEqual(text, "public")) {
|
|
key_len = CIPHER_KEY_SIZE;
|
|
memcpy(secret, PUBLIC_CHANNEL_SECRET, sizeof(PUBLIC_CHANNEL_SECRET));
|
|
copyString(name, "public", name_len);
|
|
} else if (text[0] == '#' && text[1] != 0) {
|
|
if (strlen(text) >= name_len) return false;
|
|
key_len = CIPHER_KEY_SIZE;
|
|
mesh::Utils::sha256(secret, key_len, (const uint8_t*)text,
|
|
strlen(text));
|
|
copyString(name, text, name_len);
|
|
} else {
|
|
size_t hex_len = strlen(text);
|
|
if (hex_len != CIPHER_KEY_SIZE * 2 && hex_len != PUB_KEY_SIZE * 2) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < hex_len; i++) {
|
|
if (!mesh::Utils::isHexChar(text[i])) return false;
|
|
}
|
|
key_len = (uint8_t)(hex_len / 2);
|
|
if (!mesh::Utils::fromHex(secret, key_len, text)) return false;
|
|
uint8_t prefix[4];
|
|
mesh::Utils::sha256(prefix, sizeof(prefix), secret, key_len);
|
|
char prefix_hex[9];
|
|
mesh::Utils::toHex(prefix_hex, prefix, sizeof(prefix));
|
|
snprintf(name, name_len, "key:%s", prefix_hex);
|
|
}
|
|
mesh::Utils::sha256(&channel_hash, sizeof(channel_hash), secret, key_len);
|
|
return true;
|
|
}
|
|
|
|
static bool parsePathPrefix(const char* text, uint8_t& hash_size,
|
|
uint8_t& path_hops,
|
|
uint8_t path[FloodRuleEngine::PATH_PREFIX_BYTES_MAX]) {
|
|
memset(path, 0, FloodRuleEngine::PATH_PREFIX_BYTES_MAX);
|
|
if (text == NULL || *text == 0 || strcmp(text, "*") == 0) {
|
|
hash_size = 0;
|
|
path_hops = 0;
|
|
return text != NULL && *text != 0;
|
|
}
|
|
if (strlen(text) >= 32) return false;
|
|
char input[32];
|
|
copyString(input, text, sizeof(input));
|
|
char* token = input;
|
|
uint8_t parsed_size = 0;
|
|
uint8_t count = 0;
|
|
while (token != NULL) {
|
|
char* comma = strchr(token, ',');
|
|
if (comma != NULL) *comma = 0;
|
|
size_t hex_len = strlen(token);
|
|
if (hex_len != 2 && hex_len != 4 && hex_len != 6) return false;
|
|
uint8_t token_size = (uint8_t)(hex_len / 2);
|
|
if ((parsed_size != 0 && parsed_size != token_size)
|
|
|| count >= FloodRuleEngine::PATH_PREFIX_HOPS_MAX) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < hex_len; i++) {
|
|
if (!mesh::Utils::isHexChar(token[i])) return false;
|
|
}
|
|
parsed_size = token_size;
|
|
if (!mesh::Utils::fromHex(&path[count * parsed_size], parsed_size,
|
|
token)) {
|
|
return false;
|
|
}
|
|
count++;
|
|
token = comma == NULL ? NULL : comma + 1;
|
|
}
|
|
if (count == 0) return false;
|
|
hash_size = parsed_size;
|
|
path_hops = count;
|
|
return true;
|
|
}
|
|
|
|
static void formatPathPrefix(
|
|
char* dest, size_t dest_len, uint8_t hash_size, uint8_t path_hops,
|
|
const uint8_t path[FloodRuleEngine::PATH_PREFIX_BYTES_MAX]) {
|
|
if (hash_size == 0 || path_hops == 0) {
|
|
copyString(dest, "*", dest_len);
|
|
return;
|
|
}
|
|
size_t used = 0;
|
|
dest[0] = 0;
|
|
for (uint8_t i = 0; i < path_hops; i++) {
|
|
char hop[7];
|
|
mesh::Utils::toHex(hop, &path[i * hash_size], hash_size);
|
|
int written = snprintf(&dest[used], dest_len - used, "%s%s",
|
|
i == 0 ? "" : ",", hop);
|
|
if (written < 0 || (size_t)written >= dest_len - used) {
|
|
dest[dest_len - 1] = 0;
|
|
return;
|
|
}
|
|
used += (size_t)written;
|
|
}
|
|
}
|
|
|
|
static bool commandMatches(const char* command, const char* base) {
|
|
size_t len = strlen(base);
|
|
return strncmp(command, base, len) == 0
|
|
&& (command[len] == 0 || command[len] == '.' || command[len] == ' ');
|
|
}
|
|
|
|
static File openRead(FILESYSTEM* fs, const char* path) {
|
|
return fs->open(path);
|
|
}
|
|
|
|
static File openWrite(FILESYSTEM* fs, const char* path) {
|
|
return fs->open(path, "w", true);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
FloodRuleEngine::FloodRuleEngine() : _fs(NULL), _regions(NULL) {
|
|
memset(_entries, 0, sizeof(_entries));
|
|
}
|
|
|
|
void FloodRuleEngine::begin(FILESYSTEM* fs, RegionMap* regions) {
|
|
_fs = fs;
|
|
_regions = regions;
|
|
load();
|
|
}
|
|
|
|
void FloodRuleEngine::seedDefaults() {
|
|
memset(_entries, 0, sizeof(_entries));
|
|
Entry& entry = _entries[0];
|
|
entry.active = true;
|
|
entry.payload_type = PAYLOAD_TYPE_OTA;
|
|
entry.min_hops = 0;
|
|
entry.max_hops = MAX_HOPS;
|
|
entry.suspend_on_temp_radio = true;
|
|
entry.drop_on_match = true;
|
|
}
|
|
|
|
void FloodRuleEngine::load() {
|
|
memset(_entries, 0, sizeof(_entries));
|
|
if (_fs == NULL) {
|
|
seedDefaults();
|
|
return;
|
|
}
|
|
if (!_fs->exists(RULE_FILE)) {
|
|
seedDefaults();
|
|
save();
|
|
return;
|
|
}
|
|
|
|
File file = openRead(_fs, RULE_FILE);
|
|
if (!file) return;
|
|
|
|
Entry* loaded = _entries;
|
|
auto readExact = [&file](void* dest, size_t len) {
|
|
return file.read((uint8_t*)dest, len) == len;
|
|
};
|
|
|
|
uint8_t magic[4];
|
|
uint8_t count = 0;
|
|
bool success = readExact(magic, sizeof(magic));
|
|
bool version_6 = success && memcmp(magic, "FPF6", 4) == 0;
|
|
bool version_7 = success && memcmp(magic, "FPF7", 4) == 0;
|
|
success = (version_6 || version_7)
|
|
&& readExact(&count, sizeof(count)) && count <= RULE_SLOTS;
|
|
|
|
for (int i = 0; success && i < count; i++) {
|
|
uint8_t active = 0;
|
|
uint8_t suspend_on_temp_radio = 0;
|
|
uint8_t match_blacklisted_path = 0;
|
|
uint8_t scope_requires_region_match = 0;
|
|
uint8_t scope_uses_slow_timing = 0;
|
|
uint8_t drop_on_match = 0;
|
|
uint8_t rate_limit_enabled = 0;
|
|
uint8_t stop_on_match = 0;
|
|
|
|
success = readExact(&active, sizeof(active))
|
|
&& readExact(&loaded[i].payload_type,
|
|
sizeof(loaded[i].payload_type))
|
|
&& readExact(&loaded[i].min_hops, sizeof(loaded[i].min_hops))
|
|
&& readExact(&loaded[i].max_hops, sizeof(loaded[i].max_hops))
|
|
&& readExact(&suspend_on_temp_radio,
|
|
sizeof(suspend_on_temp_radio))
|
|
&& readExact(loaded[i].scope_name, sizeof(loaded[i].scope_name))
|
|
&& readExact(&match_blacklisted_path,
|
|
sizeof(match_blacklisted_path))
|
|
&& readExact(&scope_requires_region_match,
|
|
sizeof(scope_requires_region_match))
|
|
&& readExact(&scope_uses_slow_timing,
|
|
sizeof(scope_uses_slow_timing));
|
|
if (success && version_7) {
|
|
success = readExact(&loaded[i].incoming_scope_kind,
|
|
sizeof(loaded[i].incoming_scope_kind))
|
|
&& readExact(loaded[i].incoming_scope_name,
|
|
sizeof(loaded[i].incoming_scope_name))
|
|
&& readExact(&loaded[i].channel_key_len,
|
|
sizeof(loaded[i].channel_key_len))
|
|
&& readExact(loaded[i].channel_secret,
|
|
sizeof(loaded[i].channel_secret))
|
|
&& readExact(loaded[i].channel_name,
|
|
sizeof(loaded[i].channel_name))
|
|
&& readExact(&loaded[i].path_hash_size,
|
|
sizeof(loaded[i].path_hash_size))
|
|
&& readExact(&loaded[i].path_hops,
|
|
sizeof(loaded[i].path_hops))
|
|
&& readExact(loaded[i].path, sizeof(loaded[i].path))
|
|
&& readExact(&drop_on_match, sizeof(drop_on_match))
|
|
&& readExact(&rate_limit_enabled, sizeof(rate_limit_enabled))
|
|
&& readExact(&loaded[i].rate_per_minute,
|
|
sizeof(loaded[i].rate_per_minute))
|
|
&& readExact(loaded[i].target_region_name,
|
|
sizeof(loaded[i].target_region_name))
|
|
&& readExact(&loaded[i].priority, sizeof(loaded[i].priority))
|
|
&& readExact(&stop_on_match, sizeof(stop_on_match));
|
|
} else {
|
|
loaded[i].incoming_scope_kind = scope_requires_region_match
|
|
? FloodFilterPolicy::RULE_IN_ALLOWED
|
|
: FloodFilterPolicy::RULE_IN_ANY;
|
|
drop_on_match = loaded[i].scope_name[0] == 0 ? 1 : 0;
|
|
}
|
|
|
|
loaded[i].active = active != 0;
|
|
loaded[i].suspend_on_temp_radio = suspend_on_temp_radio != 0;
|
|
loaded[i].match_blacklisted_path = match_blacklisted_path != 0;
|
|
loaded[i].scope_uses_slow_timing = scope_uses_slow_timing != 0;
|
|
loaded[i].drop_on_match = drop_on_match != 0;
|
|
loaded[i].rate_limit_enabled = rate_limit_enabled != 0;
|
|
loaded[i].stop_on_match = stop_on_match != 0;
|
|
if (success && (active > 1 || suspend_on_temp_radio > 1
|
|
|| match_blacklisted_path > 1 || scope_requires_region_match > 1
|
|
|| scope_uses_slow_timing > 1 || drop_on_match > 1
|
|
|| rate_limit_enabled > 1 || stop_on_match > 1)) {
|
|
success = false;
|
|
}
|
|
if (!success) break;
|
|
if (!loaded[i].active) {
|
|
memset(&loaded[i], 0, sizeof(loaded[i]));
|
|
continue;
|
|
}
|
|
|
|
bool scope_name_terminated = memchr(
|
|
loaded[i].scope_name, 0, sizeof(loaded[i].scope_name)) != NULL;
|
|
bool input_name_terminated = memchr(
|
|
loaded[i].incoming_scope_name, 0,
|
|
sizeof(loaded[i].incoming_scope_name)) != NULL;
|
|
bool channel_name_terminated = memchr(
|
|
loaded[i].channel_name, 0,
|
|
sizeof(loaded[i].channel_name)) != NULL;
|
|
bool target_name_terminated = memchr(
|
|
loaded[i].target_region_name, 0,
|
|
sizeof(loaded[i].target_region_name)) != NULL;
|
|
bool direct_target = scope_name_terminated
|
|
&& loaded[i].scope_name[0] != 0;
|
|
bool region_target = target_name_terminated
|
|
&& loaded[i].target_region_name[0] != 0;
|
|
|
|
bool incoming_valid = loaded[i].incoming_scope_kind
|
|
<= FloodFilterPolicy::RULE_IN_REGION;
|
|
if (incoming_valid
|
|
&& loaded[i].incoming_scope_kind == FloodFilterPolicy::RULE_IN_SCOPE) {
|
|
incoming_valid = input_name_terminated
|
|
&& validStoredScopeName(loaded[i].incoming_scope_name);
|
|
} else if (incoming_valid
|
|
&& loaded[i].incoming_scope_kind == FloodFilterPolicy::RULE_IN_REGION) {
|
|
incoming_valid = input_name_terminated
|
|
&& validStoredRegionName(loaded[i].incoming_scope_name);
|
|
} else if (incoming_valid) {
|
|
incoming_valid = input_name_terminated
|
|
&& loaded[i].incoming_scope_name[0] == 0;
|
|
}
|
|
|
|
bool channel_valid = loaded[i].channel_key_len == 0
|
|
|| loaded[i].channel_key_len == CIPHER_KEY_SIZE
|
|
|| loaded[i].channel_key_len == PUB_KEY_SIZE;
|
|
if (channel_valid && loaded[i].channel_key_len == 0) {
|
|
channel_valid = channel_name_terminated
|
|
&& loaded[i].channel_name[0] == 0;
|
|
} else if (channel_valid) {
|
|
channel_valid = channel_name_terminated
|
|
&& loaded[i].channel_name[0] != 0;
|
|
if (channel_valid) {
|
|
mesh::Utils::sha256(&loaded[i].channel_hash,
|
|
sizeof(loaded[i].channel_hash),
|
|
loaded[i].channel_secret,
|
|
loaded[i].channel_key_len);
|
|
}
|
|
}
|
|
if (channel_valid && loaded[i].channel_key_len != 0) {
|
|
channel_valid = loaded[i].payload_type == ANY_TYPE
|
|
|| loaded[i].payload_type == PAYLOAD_TYPE_GRP_TXT
|
|
|| loaded[i].payload_type == PAYLOAD_TYPE_GRP_DATA;
|
|
}
|
|
|
|
bool path_valid = (loaded[i].path_hash_size == 0
|
|
&& loaded[i].path_hops == 0)
|
|
|| (!loaded[i].match_blacklisted_path
|
|
&& loaded[i].path_hash_size >= 1
|
|
&& loaded[i].path_hash_size <= 3
|
|
&& loaded[i].path_hops >= 1
|
|
&& loaded[i].path_hops <= PATH_PREFIX_HOPS_MAX);
|
|
bool action_valid = loaded[i].drop_on_match || direct_target
|
|
|| region_target || loaded[i].rate_limit_enabled
|
|
|| loaded[i].stop_on_match;
|
|
if (!((loaded[i].payload_type <= PH_TYPE_MASK
|
|
|| loaded[i].payload_type == ANY_TYPE)
|
|
&& loaded[i].min_hops <= loaded[i].max_hops
|
|
&& loaded[i].max_hops <= MAX_HOPS
|
|
&& scope_name_terminated
|
|
&& (!direct_target
|
|
|| validStoredScopeName(loaded[i].scope_name))
|
|
&& target_name_terminated
|
|
&& (!region_target
|
|
|| validStoredRegionName(loaded[i].target_region_name))
|
|
&& !(direct_target && region_target)
|
|
&& !(loaded[i].drop_on_match
|
|
&& (direct_target || region_target))
|
|
&& !(loaded[i].drop_on_match
|
|
&& loaded[i].rate_limit_enabled)
|
|
&& (!loaded[i].rate_limit_enabled
|
|
|| loaded[i].rate_per_minute < RATE_UNLIMITED)
|
|
&& (!loaded[i].scope_uses_slow_timing
|
|
|| direct_target || region_target)
|
|
&& incoming_valid && channel_valid && path_valid
|
|
&& action_valid)) {
|
|
success = false;
|
|
}
|
|
}
|
|
file.close();
|
|
|
|
// Invalid or truncated persistence fails open. Corrupt bytes must never
|
|
// enable a forwarding block.
|
|
if (!success) memset(_entries, 0, sizeof(_entries));
|
|
}
|
|
|
|
bool FloodRuleEngine::save() {
|
|
if (_fs == NULL) return false;
|
|
File file = openWrite(_fs, RULE_FILE);
|
|
if (!file) return false;
|
|
auto writeExact = [&file](const void* src, size_t len) {
|
|
return file.write((const uint8_t*)src, len) == len;
|
|
};
|
|
|
|
const uint8_t magic[4] = {'F', 'P', 'F', '7'};
|
|
uint8_t count = RULE_SLOTS;
|
|
bool success = writeExact(magic, sizeof(magic))
|
|
&& writeExact(&count, sizeof(count));
|
|
for (int i = 0; success && i < RULE_SLOTS; i++) {
|
|
const Entry& entry = _entries[i];
|
|
uint8_t active = entry.active ? 1 : 0;
|
|
uint8_t suspend_on_temp_radio = entry.suspend_on_temp_radio ? 1 : 0;
|
|
uint8_t match_blacklisted_path = entry.match_blacklisted_path ? 1 : 0;
|
|
uint8_t scope_requires_region_match =
|
|
entry.incoming_scope_kind == FloodFilterPolicy::RULE_IN_ALLOWED
|
|
? 1 : 0;
|
|
uint8_t scope_uses_slow_timing = entry.scope_uses_slow_timing ? 1 : 0;
|
|
uint8_t drop_on_match = entry.drop_on_match ? 1 : 0;
|
|
uint8_t rate_limit_enabled = entry.rate_limit_enabled ? 1 : 0;
|
|
uint8_t stop_on_match = entry.stop_on_match ? 1 : 0;
|
|
success = writeExact(&active, sizeof(active))
|
|
&& writeExact(&entry.payload_type, sizeof(entry.payload_type))
|
|
&& writeExact(&entry.min_hops, sizeof(entry.min_hops))
|
|
&& writeExact(&entry.max_hops, sizeof(entry.max_hops))
|
|
&& writeExact(&suspend_on_temp_radio,
|
|
sizeof(suspend_on_temp_radio))
|
|
&& writeExact(entry.scope_name, sizeof(entry.scope_name))
|
|
&& writeExact(&match_blacklisted_path,
|
|
sizeof(match_blacklisted_path))
|
|
&& writeExact(&scope_requires_region_match,
|
|
sizeof(scope_requires_region_match))
|
|
&& writeExact(&scope_uses_slow_timing,
|
|
sizeof(scope_uses_slow_timing))
|
|
&& writeExact(&entry.incoming_scope_kind,
|
|
sizeof(entry.incoming_scope_kind))
|
|
&& writeExact(entry.incoming_scope_name,
|
|
sizeof(entry.incoming_scope_name))
|
|
&& writeExact(&entry.channel_key_len, sizeof(entry.channel_key_len))
|
|
&& writeExact(entry.channel_secret, sizeof(entry.channel_secret))
|
|
&& writeExact(entry.channel_name, sizeof(entry.channel_name))
|
|
&& writeExact(&entry.path_hash_size, sizeof(entry.path_hash_size))
|
|
&& writeExact(&entry.path_hops, sizeof(entry.path_hops))
|
|
&& writeExact(entry.path, sizeof(entry.path))
|
|
&& writeExact(&drop_on_match, sizeof(drop_on_match))
|
|
&& writeExact(&rate_limit_enabled, sizeof(rate_limit_enabled))
|
|
&& writeExact(&entry.rate_per_minute,
|
|
sizeof(entry.rate_per_minute))
|
|
&& writeExact(entry.target_region_name,
|
|
sizeof(entry.target_region_name))
|
|
&& writeExact(&entry.priority, sizeof(entry.priority))
|
|
&& writeExact(&stop_on_match, sizeof(stop_on_match));
|
|
}
|
|
file.close();
|
|
return success;
|
|
}
|
|
|
|
bool FloodRuleEngine::fieldsMatch(
|
|
const Entry& entry, const mesh::Packet* packet,
|
|
bool temp_radio_active, bool incoming_is_scoped,
|
|
uint16_t incoming_transport_code, bool incoming_region_allowed,
|
|
const RegionEntry* incoming_region) const {
|
|
if (!entry.active || packet == NULL || !packet->isRouteFlood()) return false;
|
|
if (entry.suspend_on_temp_radio && temp_radio_active) return false;
|
|
// FULL room servers do not carry the repeater's separate passive blacklist.
|
|
// Legacy blacklist-qualified rows stay inert instead of widening their match.
|
|
if (entry.match_blacklisted_path) return false;
|
|
if (!FloodFilterPolicy::pathStartsWith(
|
|
packet, entry.path_hash_size, entry.path_hops, entry.path)) {
|
|
return false;
|
|
}
|
|
|
|
uint8_t type = packet->getPayloadType();
|
|
uint8_t hops = packet->getPathHashCount();
|
|
if ((entry.payload_type != ANY_TYPE && entry.payload_type != type)
|
|
|| hops < entry.min_hops || hops > entry.max_hops) {
|
|
return false;
|
|
}
|
|
|
|
if (entry.incoming_scope_kind == FloodFilterPolicy::RULE_IN_REGION) {
|
|
if (incoming_region == NULL
|
|
|| !RegionNameUtils::equivalent(
|
|
entry.incoming_scope_name, incoming_region->name)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
uint16_t wanted_transport_code = 0;
|
|
if (entry.incoming_scope_kind == FloodFilterPolicy::RULE_IN_SCOPE) {
|
|
TransportKey incoming_scope;
|
|
deriveScopeKey(entry.incoming_scope_name, incoming_scope);
|
|
wanted_transport_code = incoming_scope.calcTransportCode(packet);
|
|
}
|
|
if (!FloodFilterPolicy::ruleIncomingScopeMatches(
|
|
entry.incoming_scope_kind, incoming_is_scoped,
|
|
incoming_transport_code, incoming_region_allowed,
|
|
wanted_transport_code)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (entry.channel_key_len != 0) {
|
|
if ((type != PAYLOAD_TYPE_GRP_TXT && type != PAYLOAD_TYPE_GRP_DATA)
|
|
|| packet->payload_len
|
|
< PATH_HASH_SIZE + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE
|
|
|| ((packet->payload_len - PATH_HASH_SIZE - CIPHER_MAC_SIZE)
|
|
% CIPHER_BLOCK_SIZE) != 0
|
|
|| packet->payload[0] != entry.channel_hash) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool FloodRuleEngine::authenticateChannel(
|
|
const Entry& entry, const mesh::Packet* packet) const {
|
|
if (entry.channel_key_len == 0) return true;
|
|
uint8_t data[MAX_PACKET_PAYLOAD];
|
|
return mesh::Utils::MACThenDecrypt(
|
|
entry.channel_secret, data, &packet->payload[PATH_HASH_SIZE],
|
|
packet->payload_len - PATH_HASH_SIZE) > 0;
|
|
}
|
|
|
|
int FloodRuleEngine::nextMatch(uint32_t match_mask,
|
|
uint32_t visited_mask) const {
|
|
uint8_t priorities[RULE_SLOTS];
|
|
for (int i = 0; i < RULE_SLOTS; i++) {
|
|
priorities[i] = _entries[i].priority;
|
|
}
|
|
return FloodFilterPolicy::nextOrderedRule(
|
|
match_mask, visited_mask, priorities, RULE_SLOTS);
|
|
}
|
|
|
|
uint32_t FloodRuleEngine::applyStop(uint32_t match_mask) const {
|
|
uint8_t priorities[RULE_SLOTS];
|
|
uint8_t stop_flags[RULE_SLOTS];
|
|
for (int i = 0; i < RULE_SLOTS; i++) {
|
|
priorities[i] = _entries[i].priority;
|
|
stop_flags[i] = _entries[i].stop_on_match ? 1 : 0;
|
|
}
|
|
return FloodFilterPolicy::truncateRulesAtStop(
|
|
match_mask, priorities, stop_flags, RULE_SLOTS);
|
|
}
|
|
|
|
uint32_t FloodRuleEngine::evaluate(
|
|
const mesh::Packet* packet, bool temp_radio_active,
|
|
bool incoming_region_allowed,
|
|
const RegionEntry* incoming_region) const {
|
|
static_assert(RULE_SLOTS <= 32,
|
|
"flood rule match mask supports at most 32 slots");
|
|
if (packet == NULL || !packet->isRouteFlood()) return 0;
|
|
bool incoming_is_scoped =
|
|
packet->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD;
|
|
uint16_t incoming_transport_code = incoming_is_scoped
|
|
? packet->transport_codes[0] : 0;
|
|
bool channel_auth_checked[RULE_SLOTS] = { false };
|
|
bool channel_auth_valid[RULE_SLOTS] = { false };
|
|
uint32_t result = 0;
|
|
for (int i = 0; i < RULE_SLOTS; i++) {
|
|
const Entry& entry = _entries[i];
|
|
if (!fieldsMatch(entry, packet, temp_radio_active, incoming_is_scoped,
|
|
incoming_transport_code, incoming_region_allowed,
|
|
incoming_region)) {
|
|
continue;
|
|
}
|
|
bool authenticated = true;
|
|
if (entry.channel_key_len != 0) {
|
|
int cached = -1;
|
|
for (int j = 0; j < i; j++) {
|
|
if (channel_auth_checked[j]
|
|
&& FloodFilterPolicy::sameChannelKey(
|
|
_entries[j].channel_key_len, _entries[j].channel_secret,
|
|
entry.channel_key_len, entry.channel_secret)) {
|
|
cached = j;
|
|
break;
|
|
}
|
|
}
|
|
authenticated = cached >= 0
|
|
? channel_auth_valid[cached]
|
|
: authenticateChannel(entry, packet);
|
|
channel_auth_checked[i] = true;
|
|
channel_auth_valid[i] = authenticated;
|
|
}
|
|
if (authenticated) result |= (uint32_t)1U << i;
|
|
}
|
|
return applyStop(result);
|
|
}
|
|
|
|
bool FloodRuleEngine::applyScope(mesh::Packet* packet, uint32_t match_mask,
|
|
bool& scope_set, bool& fast_track,
|
|
bool log_change) {
|
|
scope_set = false;
|
|
fast_track = false;
|
|
if (packet == NULL || !packet->isRouteFlood()) return false;
|
|
|
|
uint32_t visited = 0;
|
|
while (true) {
|
|
int i = nextMatch(match_mask, visited);
|
|
if (i < 0) break;
|
|
visited |= (uint32_t)1U << i;
|
|
const Entry& entry = _entries[i];
|
|
if (entry.scope_name[0] == 0
|
|
&& entry.target_region_name[0] == 0) {
|
|
continue;
|
|
}
|
|
|
|
TransportKey scope;
|
|
const char* target_name = entry.scope_name;
|
|
if (entry.scope_name[0] != 0) {
|
|
deriveScopeKey(entry.scope_name, scope);
|
|
} else {
|
|
RegionEntry* region = _regions == NULL ? NULL
|
|
: _regions->findByName(entry.target_region_name);
|
|
if (region == NULL || region->isWildcard()
|
|
|| (region->flags & REGION_DENY_FLOOD) != 0
|
|
|| _regions->getTransportKeysFor(*region, &scope, 1) <= 0
|
|
|| scope.isNull()) {
|
|
continue;
|
|
}
|
|
target_name = region->name;
|
|
}
|
|
|
|
bool changed = FloodFilterPolicy::setTransportScope(
|
|
packet, scope.calcTransportCode(packet));
|
|
scope_set = true;
|
|
fast_track = FloodFilterPolicy::fastTrackScopeChange(
|
|
changed, entry.scope_uses_slow_timing);
|
|
if (changed && log_change) {
|
|
MESH_DEBUG_PRINTLN(
|
|
"flood.rule set scope slot=%d type=%d hops=%d scope=%s tx=%s",
|
|
i + 1, packet->getPayloadType(), packet->getPathHashCount(),
|
|
target_name, entry.scope_uses_slow_timing ? "slow" : "fast");
|
|
}
|
|
return changed;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FloodRuleEngine::shouldBlock(const mesh::Packet* packet,
|
|
uint32_t match_mask,
|
|
uint32_t now_millis) const {
|
|
if (packet == NULL || !packet->isRouteFlood()) return false;
|
|
uint8_t type = packet->getPayloadType();
|
|
uint8_t hops = packet->getPathHashCount();
|
|
|
|
uint32_t visited = 0;
|
|
while (true) {
|
|
int i = nextMatch(match_mask, visited);
|
|
if (i < 0) break;
|
|
visited |= (uint32_t)1U << i;
|
|
const Entry& entry = _entries[i];
|
|
bool blocked = entry.drop_on_match;
|
|
if (entry.rate_limit_enabled
|
|
&& FloodFilterPolicy::rateLimitReached(
|
|
entry.rate_window_active, now_millis,
|
|
entry.rate_window_started, entry.rate_window_count,
|
|
entry.rate_per_minute)) {
|
|
blocked = true;
|
|
}
|
|
if (blocked) {
|
|
MESH_DEBUG_PRINTLN(
|
|
"allowPacketForward: flood.rule matched slot=%d type=%d hops=%d",
|
|
i + 1, type, hops);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FloodRuleEngine::commitRates(const mesh::Packet* packet,
|
|
uint32_t match_mask,
|
|
uint32_t now_millis) {
|
|
if (packet == NULL || !packet->isRouteFlood()) return;
|
|
for (int i = 0; i < RULE_SLOTS; i++) {
|
|
Entry& entry = _entries[i];
|
|
if ((match_mask & ((uint32_t)1U << i)) == 0
|
|
|| !entry.rate_limit_enabled) {
|
|
continue;
|
|
}
|
|
if (!entry.rate_window_active
|
|
|| now_millis - entry.rate_window_started >= 60000UL) {
|
|
entry.rate_window_active = true;
|
|
entry.rate_window_started = now_millis;
|
|
entry.rate_window_count = 0;
|
|
}
|
|
if (entry.rate_window_count < 0xFFFF) entry.rate_window_count++;
|
|
}
|
|
}
|
|
|
|
void FloodRuleEngine::formatDetail(int index, char* reply,
|
|
size_t reply_len) const {
|
|
if (index < 0 || index >= RULE_SLOTS || !_entries[index].active) {
|
|
snprintf(reply, reply_len, "Err - empty filter slot");
|
|
return;
|
|
}
|
|
const Entry& entry = _entries[index];
|
|
char hops[12];
|
|
char prefix[32];
|
|
char incoming[48];
|
|
char action[72];
|
|
char rate[24];
|
|
formatHopSpec(hops, sizeof(hops), entry.min_hops, entry.max_hops);
|
|
if (entry.match_blacklisted_path) copyString(prefix, "blacklist", sizeof(prefix));
|
|
else formatPathPrefix(prefix, sizeof(prefix), entry.path_hash_size,
|
|
entry.path_hops, entry.path);
|
|
|
|
switch (entry.incoming_scope_kind) {
|
|
case FloodFilterPolicy::RULE_IN_NONE:
|
|
copyString(incoming, "none", sizeof(incoming));
|
|
break;
|
|
case FloodFilterPolicy::RULE_IN_SCOPED:
|
|
copyString(incoming, "scoped", sizeof(incoming));
|
|
break;
|
|
case FloodFilterPolicy::RULE_IN_ALLOWED:
|
|
copyString(incoming, "allowed", sizeof(incoming));
|
|
break;
|
|
case FloodFilterPolicy::RULE_IN_UNKNOWN:
|
|
copyString(incoming, "unknown", sizeof(incoming));
|
|
break;
|
|
case FloodFilterPolicy::RULE_IN_SCOPE:
|
|
snprintf(incoming, sizeof(incoming), "scope:%s",
|
|
entry.incoming_scope_name);
|
|
break;
|
|
case FloodFilterPolicy::RULE_IN_REGION:
|
|
snprintf(incoming, sizeof(incoming), "region:%s%s",
|
|
entry.incoming_scope_name,
|
|
_regions != NULL
|
|
&& _regions->findByName(entry.incoming_scope_name) != NULL
|
|
? "" : "?");
|
|
break;
|
|
default:
|
|
copyString(incoming, "any", sizeof(incoming));
|
|
break;
|
|
}
|
|
|
|
action[0] = 0;
|
|
if (entry.drop_on_match) {
|
|
copyString(action, "drop", sizeof(action));
|
|
} else if (entry.scope_name[0] != 0) {
|
|
snprintf(action, sizeof(action), "scope=%s", entry.scope_name);
|
|
} else if (entry.target_region_name[0] != 0) {
|
|
RegionEntry* current = _regions == NULL ? NULL
|
|
: _regions->findByName(entry.target_region_name);
|
|
snprintf(action, sizeof(action), "region=%s%s",
|
|
entry.target_region_name, current == NULL ? "?" : "");
|
|
}
|
|
rate[0] = 0;
|
|
if (entry.rate_limit_enabled) {
|
|
snprintf(rate, sizeof(rate), "%srate=%u/min",
|
|
action[0] == 0 ? "" : " ",
|
|
(unsigned int)entry.rate_per_minute);
|
|
}
|
|
|
|
int written = snprintf(
|
|
reply, reply_len,
|
|
"> %d type=%s hops=%s channel=%s prefix=%s in=%s %s%s priority=%u%s%s%s",
|
|
index + 1, payloadTypeName(entry.payload_type), hops,
|
|
entry.channel_key_len == 0 ? "*" : entry.channel_name,
|
|
prefix, incoming, action, rate,
|
|
(unsigned int)entry.priority,
|
|
entry.stop_on_match ? " stop" : "",
|
|
entry.scope_uses_slow_timing ? " tx=slow" : "",
|
|
entry.suspend_on_temp_radio ? " suspend=tempradio" : "");
|
|
if (written >= 0 && (size_t)written < reply_len) return;
|
|
|
|
char compact_type[4];
|
|
if (entry.payload_type == ANY_TYPE) copyString(compact_type, "any", sizeof(compact_type));
|
|
else snprintf(compact_type, sizeof(compact_type), "%u",
|
|
(unsigned int)entry.payload_type);
|
|
char compact_incoming[44];
|
|
switch (entry.incoming_scope_kind) {
|
|
case FloodFilterPolicy::RULE_IN_NONE:
|
|
copyString(compact_incoming, "n", sizeof(compact_incoming));
|
|
break;
|
|
case FloodFilterPolicy::RULE_IN_SCOPED:
|
|
copyString(compact_incoming, "s", sizeof(compact_incoming));
|
|
break;
|
|
case FloodFilterPolicy::RULE_IN_ALLOWED:
|
|
copyString(compact_incoming, "a", sizeof(compact_incoming));
|
|
break;
|
|
case FloodFilterPolicy::RULE_IN_UNKNOWN:
|
|
copyString(compact_incoming, "u", sizeof(compact_incoming));
|
|
break;
|
|
case FloodFilterPolicy::RULE_IN_SCOPE:
|
|
snprintf(compact_incoming, sizeof(compact_incoming), "s:%s",
|
|
entry.incoming_scope_name);
|
|
break;
|
|
case FloodFilterPolicy::RULE_IN_REGION:
|
|
snprintf(compact_incoming, sizeof(compact_incoming), "r:%s",
|
|
entry.incoming_scope_name);
|
|
break;
|
|
default:
|
|
copyString(compact_incoming, "*", sizeof(compact_incoming));
|
|
break;
|
|
}
|
|
char compact_action[44];
|
|
if (entry.drop_on_match) {
|
|
copyString(compact_action, " drop", sizeof(compact_action));
|
|
} else if (entry.scope_name[0] != 0) {
|
|
snprintf(compact_action, sizeof(compact_action), " scope=%s",
|
|
entry.scope_name);
|
|
} else if (entry.target_region_name[0] != 0) {
|
|
snprintf(compact_action, sizeof(compact_action), " region=%s",
|
|
entry.target_region_name);
|
|
} else {
|
|
compact_action[0] = 0;
|
|
}
|
|
char compact_rate[12];
|
|
compact_rate[0] = 0;
|
|
if (entry.rate_limit_enabled) {
|
|
snprintf(compact_rate, sizeof(compact_rate), " q=%u",
|
|
(unsigned int)entry.rate_per_minute);
|
|
}
|
|
char compact_flags[8];
|
|
compact_flags[0] = 0;
|
|
if (entry.scope_uses_slow_timing || entry.suspend_on_temp_radio) {
|
|
snprintf(compact_flags, sizeof(compact_flags), " f=%s%s",
|
|
entry.scope_uses_slow_timing ? "s" : "",
|
|
entry.suspend_on_temp_radio ? "t" : "");
|
|
}
|
|
snprintf(reply, reply_len,
|
|
">%d %s %s c=%s p=%s i=%s%s%s pri=%u%s%s",
|
|
index + 1, compact_type, hops,
|
|
entry.channel_key_len == 0 ? "*" : entry.channel_name,
|
|
prefix, compact_incoming, compact_action, compact_rate,
|
|
(unsigned int)entry.priority,
|
|
entry.stop_on_match ? " stop" : "",
|
|
compact_flags);
|
|
}
|
|
|
|
void FloodRuleEngine::format(const char* args, char* reply) const {
|
|
const char* selector = skipSpaces(args);
|
|
if (*selector == '.') selector = skipSpaces(selector + 1);
|
|
if (*selector != 0) {
|
|
uint32_t slot = 0;
|
|
if (!parseUnsigned(selector, RULE_SLOTS, slot) || slot == 0) {
|
|
snprintf(reply, 160, "Err - filter slot must be 1-%u",
|
|
(unsigned int)RULE_SLOTS);
|
|
return;
|
|
}
|
|
formatDetail((int)slot - 1, reply, 160);
|
|
return;
|
|
}
|
|
|
|
size_t used = (size_t)snprintf(reply, 160, ">");
|
|
int active_count = 0;
|
|
bool truncated = false;
|
|
for (int i = 0; i < RULE_SLOTS; i++) {
|
|
const Entry& entry = _entries[i];
|
|
if (!entry.active) continue;
|
|
active_count++;
|
|
char hops[12];
|
|
char item[120];
|
|
char target[48];
|
|
char priority[8];
|
|
formatHopSpec(hops, sizeof(hops), entry.min_hops, entry.max_hops);
|
|
if (entry.drop_on_match) {
|
|
copyString(target, "!drop", sizeof(target));
|
|
} else if (entry.scope_name[0] != 0) {
|
|
snprintf(target, sizeof(target), ">%s", entry.scope_name);
|
|
} else if (entry.target_region_name[0] != 0) {
|
|
snprintf(target, sizeof(target), ">r:%s",
|
|
entry.target_region_name);
|
|
} else {
|
|
target[0] = 0;
|
|
}
|
|
priority[0] = 0;
|
|
if (entry.priority != 0) {
|
|
snprintf(priority, sizeof(priority), "^%u",
|
|
(unsigned int)entry.priority);
|
|
}
|
|
snprintf(item, sizeof(item), " %d=%s@%s%s%s%s%s%s%s%s",
|
|
i + 1, payloadTypeName(entry.payload_type), hops,
|
|
entry.match_blacklisted_path ? "?blacklist" : "", target,
|
|
priority, entry.stop_on_match ? "~stop" : "",
|
|
entry.rate_limit_enabled ? "~rate" : "",
|
|
entry.scope_uses_slow_timing ? "~slow" : "",
|
|
entry.suspend_on_temp_radio ? "~tempradio" : "");
|
|
size_t item_len = strlen(item);
|
|
if (used + item_len >= 156) {
|
|
truncated = true;
|
|
break;
|
|
}
|
|
memcpy(&reply[used], item, item_len + 1);
|
|
used += item_len;
|
|
}
|
|
if (active_count == 0) copyString(reply, "> off", 160);
|
|
else if (truncated) copyString(&reply[used], " ...", 160 - used);
|
|
}
|
|
|
|
void FloodRuleEngine::set(const char* args, char* reply,
|
|
bool require_explicit_action) {
|
|
const char* cursor = skipSpaces(args);
|
|
int requested_slot = -1;
|
|
if (*cursor == '.') {
|
|
cursor++;
|
|
const char* slot_start = cursor;
|
|
while (*cursor >= '0' && *cursor <= '9') cursor++;
|
|
size_t slot_len = (size_t)(cursor - slot_start);
|
|
char slot_text[8];
|
|
if (slot_len == 0 || slot_len >= sizeof(slot_text)) {
|
|
snprintf(reply, 160, "Err - filter slot must be 1-%u",
|
|
(unsigned int)RULE_SLOTS);
|
|
return;
|
|
}
|
|
memcpy(slot_text, slot_start, slot_len);
|
|
slot_text[slot_len] = 0;
|
|
uint32_t slot = 0;
|
|
if (!parseUnsigned(slot_text, RULE_SLOTS, slot) || slot == 0) {
|
|
snprintf(reply, 160, "Err - filter slot must be 1-%u",
|
|
(unsigned int)RULE_SLOTS);
|
|
return;
|
|
}
|
|
requested_slot = (int)slot - 1;
|
|
if (*cursor != ' ') {
|
|
copyString(reply, RULE_USAGE, 160);
|
|
return;
|
|
}
|
|
}
|
|
cursor = skipSpaces(cursor);
|
|
if (strlen(cursor) >= 192) {
|
|
copyString(reply, "Err - rule parameters too long", 160);
|
|
return;
|
|
}
|
|
|
|
char params[192];
|
|
copyString(params, cursor, sizeof(params));
|
|
char* tokens[18];
|
|
int token_count = 0;
|
|
char* token = params;
|
|
while (*token != 0) {
|
|
if (token_count >= 18) {
|
|
copyString(reply, "Err - too many rule parameters", 160);
|
|
return;
|
|
}
|
|
tokens[token_count++] = token;
|
|
char* separator = strchr(token, ' ');
|
|
if (separator == NULL) break;
|
|
*separator++ = 0;
|
|
while (*separator == ' ') separator++;
|
|
token = separator;
|
|
}
|
|
if (token_count == 0) {
|
|
copyString(reply, RULE_USAGE, 160);
|
|
return;
|
|
}
|
|
|
|
uint8_t payload_type = 0;
|
|
const char* type_text = asciiStartsWith(tokens[0], "type=")
|
|
? tokens[0] + strlen("type=") : tokens[0];
|
|
if (!parsePayloadType(type_text, payload_type)) {
|
|
copyString(reply,
|
|
"Err - packet type must be name, any, 0-15, or 0x00-0x0F",
|
|
160);
|
|
return;
|
|
}
|
|
|
|
uint8_t min_hops = 0;
|
|
uint8_t max_hops = MAX_HOPS;
|
|
bool hops_set = false;
|
|
bool suspend_on_temp_radio = false;
|
|
bool path_set = false;
|
|
uint8_t path_hash_size = 0;
|
|
uint8_t path_hops = 0;
|
|
uint8_t path[PATH_PREFIX_BYTES_MAX];
|
|
memset(path, 0, sizeof(path));
|
|
uint8_t incoming_scope_kind = FloodFilterPolicy::RULE_IN_ANY;
|
|
char incoming_scope_name[NAME_LEN];
|
|
memset(incoming_scope_name, 0, sizeof(incoming_scope_name));
|
|
bool incoming_set = false;
|
|
uint8_t channel_key_len = 0;
|
|
uint8_t channel_hash = 0;
|
|
uint8_t channel_secret[PUB_KEY_SIZE];
|
|
memset(channel_secret, 0, sizeof(channel_secret));
|
|
char channel_name[NAME_LEN];
|
|
memset(channel_name, 0, sizeof(channel_name));
|
|
bool channel_set = false;
|
|
bool scope_timing_set = false;
|
|
bool scope_uses_slow_timing = false;
|
|
char scope_name[NAME_LEN];
|
|
memset(scope_name, 0, sizeof(scope_name));
|
|
char target_region_name[NAME_LEN];
|
|
memset(target_region_name, 0, sizeof(target_region_name));
|
|
bool target_set = false;
|
|
bool drop_on_match = false;
|
|
bool drop_set = false;
|
|
bool rate_limit_enabled = false;
|
|
uint16_t rate_per_minute = 0;
|
|
uint8_t priority = 0;
|
|
bool priority_set = false;
|
|
bool stop_on_match = false;
|
|
|
|
for (int i = 1; i < token_count; i++) {
|
|
if (asciiEqual(tokens[i], "suspend=tempradio")) {
|
|
if (suspend_on_temp_radio) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
suspend_on_temp_radio = true;
|
|
} else if (asciiStartsWith(tokens[i], "f=")) {
|
|
const char* flags = tokens[i] + 2;
|
|
if (*flags == 0) {
|
|
copyString(reply, "Err - compact flags are s and/or t", 160);
|
|
return;
|
|
}
|
|
while (*flags != 0) {
|
|
if (*flags == 's' || *flags == 'S') {
|
|
if (scope_timing_set) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
scope_timing_set = true;
|
|
scope_uses_slow_timing = true;
|
|
} else if (*flags == 't' || *flags == 'T') {
|
|
if (suspend_on_temp_radio) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
suspend_on_temp_radio = true;
|
|
} else {
|
|
copyString(reply, "Err - compact flags are s and/or t", 160);
|
|
return;
|
|
}
|
|
flags++;
|
|
}
|
|
} else if (asciiEqual(tokens[i], "path=blacklist")) {
|
|
copyString(reply,
|
|
"Err - path=blacklist is repeater-only; use prefix= on rooms",
|
|
160);
|
|
return;
|
|
} else if (asciiEqual(tokens[i], "require=region")) {
|
|
if (incoming_set) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
incoming_set = true;
|
|
incoming_scope_kind = FloodFilterPolicy::RULE_IN_ALLOWED;
|
|
} else if (asciiStartsWith(tokens[i], "in=")
|
|
|| asciiStartsWith(tokens[i], "i=")) {
|
|
if (incoming_set) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
incoming_set = true;
|
|
bool compact = asciiStartsWith(tokens[i], "i=");
|
|
const char* value = tokens[i] + (compact ? 2 : strlen("in="));
|
|
if (asciiEqual(value, "any") || (compact && asciiEqual(value, "*"))) {
|
|
incoming_scope_kind = FloodFilterPolicy::RULE_IN_ANY;
|
|
} else if (asciiEqual(value, "none") || asciiEqual(value, "unscoped")
|
|
|| (compact && asciiEqual(value, "n"))) {
|
|
incoming_scope_kind = FloodFilterPolicy::RULE_IN_NONE;
|
|
} else if (asciiEqual(value, "scoped")
|
|
|| (compact && asciiEqual(value, "s"))) {
|
|
incoming_scope_kind = FloodFilterPolicy::RULE_IN_SCOPED;
|
|
} else if (asciiEqual(value, "allowed") || asciiEqual(value, "known")
|
|
|| (compact && asciiEqual(value, "a"))) {
|
|
incoming_scope_kind = FloodFilterPolicy::RULE_IN_ALLOWED;
|
|
} else if (asciiEqual(value, "unknown")
|
|
|| (compact && asciiEqual(value, "u"))) {
|
|
incoming_scope_kind = FloodFilterPolicy::RULE_IN_UNKNOWN;
|
|
} else if (asciiStartsWith(value, "scope:")
|
|
|| (compact && asciiStartsWith(value, "s:"))) {
|
|
incoming_scope_kind = FloodFilterPolicy::RULE_IN_SCOPE;
|
|
const char* name = value + (asciiStartsWith(value, "scope:")
|
|
? strlen("scope:") : 2);
|
|
if (!normalizeScopeName(name, incoming_scope_name,
|
|
sizeof(incoming_scope_name))) {
|
|
copyString(reply, "Err - bad incoming scope name", 160);
|
|
return;
|
|
}
|
|
} else if (asciiStartsWith(value, "region:")
|
|
|| (compact && asciiStartsWith(value, "r:"))) {
|
|
const char* name = value + (asciiStartsWith(value, "region:")
|
|
? strlen("region:") : 2);
|
|
RegionEntry* region = _regions == NULL ? NULL
|
|
: _regions->findByNamePrefix(name);
|
|
if (region == NULL || region->isWildcard()) {
|
|
copyString(reply, "Err - bad incoming region", 160);
|
|
return;
|
|
}
|
|
incoming_scope_kind = FloodFilterPolicy::RULE_IN_REGION;
|
|
copyString(incoming_scope_name, region->name,
|
|
sizeof(incoming_scope_name));
|
|
} else {
|
|
copyString(reply,
|
|
"Err - in=any|none|scoped|allowed|unknown|scope:name|region:name",
|
|
160);
|
|
return;
|
|
}
|
|
} else if (asciiStartsWith(tokens[i], "priority=")
|
|
|| asciiStartsWith(tokens[i], "pri=")) {
|
|
if (priority_set) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
const char* value = strchr(tokens[i], '=') + 1;
|
|
uint32_t parsed = 0;
|
|
if (!parseUnsigned(value, 255, parsed)) {
|
|
copyString(reply, "Err - priority must be 0-255", 160);
|
|
return;
|
|
}
|
|
priority = (uint8_t)parsed;
|
|
priority_set = true;
|
|
} else if (asciiEqual(tokens[i], "stop")
|
|
|| asciiEqual(tokens[i], "action=stop")) {
|
|
if (stop_on_match) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
stop_on_match = true;
|
|
} else if (asciiStartsWith(tokens[i], "tx=")) {
|
|
if (scope_timing_set) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
if (asciiEqual(tokens[i], "tx=slow")) {
|
|
scope_uses_slow_timing = true;
|
|
} else if (!asciiEqual(tokens[i], "tx=fast")) {
|
|
copyString(reply, "Err - tx timing must be fast or slow", 160);
|
|
return;
|
|
}
|
|
scope_timing_set = true;
|
|
} else if (asciiStartsWith(tokens[i], "scope=")) {
|
|
if (target_set || drop_set) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
if (!normalizeScopeName(tokens[i] + strlen("scope="), scope_name,
|
|
sizeof(scope_name))) {
|
|
copyString(reply,
|
|
"Err - scope must be a public name of at most 30 characters",
|
|
160);
|
|
return;
|
|
}
|
|
target_set = true;
|
|
} else if (asciiStartsWith(tokens[i], "region=")) {
|
|
if (target_set || drop_set) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
RegionEntry* region = _regions == NULL ? NULL
|
|
: _regions->findByNamePrefix(tokens[i] + strlen("region="));
|
|
TransportKey target_scope;
|
|
if (region == NULL || region->isWildcard()
|
|
|| (region->flags & REGION_DENY_FLOOD) != 0
|
|
|| _regions->getTransportKeysFor(*region, &target_scope, 1) <= 0
|
|
|| target_scope.isNull()) {
|
|
copyString(reply, "Err - bad target region", 160);
|
|
return;
|
|
}
|
|
copyString(target_region_name, region->name,
|
|
sizeof(target_region_name));
|
|
target_set = true;
|
|
} else if (asciiEqual(tokens[i], "drop")
|
|
|| asciiEqual(tokens[i], "action=drop")) {
|
|
if (drop_set || target_set) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
drop_on_match = true;
|
|
drop_set = true;
|
|
} else if (asciiStartsWith(tokens[i], "rate=")
|
|
|| asciiStartsWith(tokens[i], "q=")) {
|
|
if (rate_limit_enabled) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
bool compact = asciiStartsWith(tokens[i], "q=");
|
|
char rate_text[24];
|
|
copyString(rate_text,
|
|
tokens[i] + (compact ? 2 : strlen("rate=")),
|
|
sizeof(rate_text));
|
|
char* slash = strchr(rate_text, '/');
|
|
if ((!compact && slash == NULL)
|
|
|| (slash != NULL && !(strcmp(slash, "/min") == 0
|
|
|| strcmp(slash, "/m") == 0))) {
|
|
copyString(reply, "Err - rate format is X/min", 160);
|
|
return;
|
|
}
|
|
if (slash != NULL) *slash = 0;
|
|
uint32_t parsed = 0;
|
|
if (!parseUnsigned(rate_text, RATE_UNLIMITED - 1, parsed)) {
|
|
copyString(reply, "Err - rate must be 0-65534/min", 160);
|
|
return;
|
|
}
|
|
rate_per_minute = (uint16_t)parsed;
|
|
rate_limit_enabled = true;
|
|
} else if (asciiStartsWith(tokens[i], "channel=")
|
|
|| asciiStartsWith(tokens[i], "c=")) {
|
|
if (channel_set) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
channel_set = true;
|
|
const char* value = tokens[i] + (asciiStartsWith(tokens[i], "c=")
|
|
? 2 : strlen("channel="));
|
|
if (!asciiEqual(value, "*")) {
|
|
if (!parseChannel(value, channel_secret, channel_key_len,
|
|
channel_hash, channel_name,
|
|
sizeof(channel_name))) {
|
|
copyString(reply,
|
|
"Err - channel must be *, public, #channel, or a key",
|
|
160);
|
|
return;
|
|
}
|
|
}
|
|
} else if (asciiStartsWith(tokens[i], "prefix=")
|
|
|| asciiStartsWith(tokens[i], "p=")
|
|
|| asciiStartsWith(tokens[i], "path=")) {
|
|
if (path_set) {
|
|
copyString(reply, DUPLICATE_OPTION, 160);
|
|
return;
|
|
}
|
|
const char* value = strchr(tokens[i], '=') + 1;
|
|
if (!parsePathPrefix(value, path_hash_size, path_hops, path)) {
|
|
copyString(reply,
|
|
"Err - prefix is * or 1-3 comma-separated 1/2/3-byte IDs",
|
|
160);
|
|
return;
|
|
}
|
|
path_set = true;
|
|
} else if (asciiStartsWith(tokens[i], "hops=")) {
|
|
if (hops_set || !parseHopSpec(tokens[i] + strlen("hops="),
|
|
min_hops, max_hops)) {
|
|
copyString(reply,
|
|
"Err - hops must be all, N, N+, or N-M (0-63)", 160);
|
|
return;
|
|
}
|
|
hops_set = true;
|
|
} else if (!hops_set
|
|
&& parseHopSpec(tokens[i], min_hops, max_hops)) {
|
|
hops_set = true;
|
|
} else {
|
|
copyString(reply, RULE_USAGE, 160);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (scope_timing_set && !target_set) {
|
|
copyString(reply, "Err - tx timing requires scope= or region=", 160);
|
|
return;
|
|
}
|
|
if (drop_on_match && rate_limit_enabled) {
|
|
copyString(reply, "Err - drop cannot be combined with rate", 160);
|
|
return;
|
|
}
|
|
if (channel_key_len != 0 && payload_type != ANY_TYPE
|
|
&& payload_type != PAYLOAD_TYPE_GRP_TXT
|
|
&& payload_type != PAYLOAD_TYPE_GRP_DATA) {
|
|
copyString(reply,
|
|
"Err - channel matcher requires type=any|grp_txt|grp_data",
|
|
160);
|
|
return;
|
|
}
|
|
bool action_set = drop_set || target_set || rate_limit_enabled
|
|
|| stop_on_match;
|
|
if (require_explicit_action && !action_set) {
|
|
copyString(reply,
|
|
"Err - flood.rule requires drop, scope=, region=, rate=, or stop",
|
|
160);
|
|
return;
|
|
}
|
|
if (!action_set) drop_on_match = true;
|
|
|
|
Entry candidate;
|
|
memset(&candidate, 0, sizeof(candidate));
|
|
candidate.active = true;
|
|
candidate.payload_type = payload_type;
|
|
candidate.min_hops = min_hops;
|
|
candidate.max_hops = max_hops;
|
|
candidate.suspend_on_temp_radio = suspend_on_temp_radio;
|
|
candidate.scope_uses_slow_timing = scope_uses_slow_timing;
|
|
candidate.incoming_scope_kind = incoming_scope_kind;
|
|
copyString(candidate.incoming_scope_name, incoming_scope_name,
|
|
sizeof(candidate.incoming_scope_name));
|
|
candidate.channel_key_len = channel_key_len;
|
|
candidate.channel_hash = channel_hash;
|
|
memcpy(candidate.channel_secret, channel_secret,
|
|
sizeof(candidate.channel_secret));
|
|
copyString(candidate.channel_name, channel_name,
|
|
sizeof(candidate.channel_name));
|
|
candidate.path_hash_size = path_hash_size;
|
|
candidate.path_hops = path_hops;
|
|
memcpy(candidate.path, path, sizeof(candidate.path));
|
|
copyString(candidate.scope_name, scope_name, sizeof(candidate.scope_name));
|
|
copyString(candidate.target_region_name, target_region_name,
|
|
sizeof(candidate.target_region_name));
|
|
candidate.drop_on_match = drop_on_match;
|
|
candidate.rate_limit_enabled = rate_limit_enabled;
|
|
candidate.rate_per_minute = rate_per_minute;
|
|
candidate.priority = priority;
|
|
candidate.stop_on_match = stop_on_match;
|
|
|
|
int slot = requested_slot;
|
|
if (slot < 0) {
|
|
for (int i = 0; i < RULE_SLOTS; i++) {
|
|
if (_entries[i].active
|
|
&& memcmp(&_entries[i], &candidate,
|
|
offsetof(Entry, rate_window_started)) == 0) {
|
|
slot = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (slot < 0) {
|
|
for (int i = 0; i < RULE_SLOTS; i++) {
|
|
if (!_entries[i].active) {
|
|
slot = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (slot < 0) {
|
|
copyString(reply, "Err - filter table full", 160);
|
|
return;
|
|
}
|
|
|
|
Entry previous = _entries[slot];
|
|
_entries[slot] = candidate;
|
|
if (!save()) {
|
|
_entries[slot] = previous;
|
|
copyString(reply, "Err - unable to save flood filter", 160);
|
|
return;
|
|
}
|
|
char detail[160];
|
|
formatDetail(slot, detail, sizeof(detail));
|
|
const char* text = detail[0] == '>' ? skipSpaces(detail + 1) : detail;
|
|
snprintf(reply, 160, "OK - %s", text);
|
|
}
|
|
|
|
void FloodRuleEngine::remove(const char* args, char* reply) {
|
|
const char* selector = skipSpaces(args);
|
|
if (*selector == '.') selector = skipSpaces(selector + 1);
|
|
if (asciiEqual(selector, "all")) {
|
|
Entry previous[RULE_SLOTS];
|
|
memcpy(previous, _entries, sizeof(previous));
|
|
memset(_entries, 0, sizeof(_entries));
|
|
if (!save()) {
|
|
memcpy(_entries, previous, sizeof(_entries));
|
|
copyString(reply, "Err - unable to save flood filter", 160);
|
|
} else {
|
|
copyString(reply, "OK - all flood filters removed", 160);
|
|
}
|
|
return;
|
|
}
|
|
|
|
uint32_t slot = 0;
|
|
if (!parseUnsigned(selector, RULE_SLOTS, slot) || slot == 0) {
|
|
snprintf(reply, 160, "Err - use: del flood.rule.<1-%u>|all",
|
|
(unsigned int)RULE_SLOTS);
|
|
return;
|
|
}
|
|
int index = (int)slot - 1;
|
|
if (!_entries[index].active) {
|
|
copyString(reply, "Err - empty filter slot", 160);
|
|
return;
|
|
}
|
|
Entry previous = _entries[index];
|
|
memset(&_entries[index], 0, sizeof(_entries[index]));
|
|
if (!save()) {
|
|
_entries[index] = previous;
|
|
copyString(reply, "Err - unable to save flood filter", 160);
|
|
} else {
|
|
copyString(reply, "OK", 160);
|
|
}
|
|
}
|
|
|
|
bool FloodRuleEngine::handleCommand(const char* command, char* reply) {
|
|
if (command == NULL || reply == NULL) return false;
|
|
if (commandMatches(command, "get flood.filter.blacklist")
|
|
|| commandMatches(command, "set flood.filter.blacklist")
|
|
|| commandMatches(command, "del flood.filter.blacklist")) {
|
|
copyString(reply,
|
|
"Err - flood.filter.blacklist is repeater-only; use prefix=",
|
|
160);
|
|
return true;
|
|
}
|
|
if (commandMatches(command, "get flood.rule")) {
|
|
format(command + strlen("get flood.rule"), reply);
|
|
return true;
|
|
}
|
|
if (commandMatches(command, "set flood.rule")) {
|
|
set(command + strlen("set flood.rule"), reply, true);
|
|
return true;
|
|
}
|
|
if (commandMatches(command, "del flood.rule")) {
|
|
remove(command + strlen("del flood.rule"), reply);
|
|
return true;
|
|
}
|
|
if (commandMatches(command, "get flood.filter")) {
|
|
format(command + strlen("get flood.filter"), reply);
|
|
return true;
|
|
}
|
|
if (commandMatches(command, "set flood.filter")) {
|
|
set(command + strlen("set flood.filter"), reply, false);
|
|
return true;
|
|
}
|
|
if (commandMatches(command, "del flood.filter")) {
|
|
remove(command + strlen("del flood.filter"), reply);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endif
|