mirror of
https://github.com/meshcore-dev/MeshCore.git
synced 2026-04-20 22:13:47 +00:00
Merge d04de2cb02 into dee3e26ac0
This commit is contained in:
commit
76550a532e
4 changed files with 236 additions and 2 deletions
|
|
@ -546,6 +546,124 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
|
|||
}
|
||||
|
||||
bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
|
||||
// Per-sender advert jail: drop flood adverts from senders advertising too frequently
|
||||
if (_prefs.advert_jail > 0 && pkt->getPayloadType() == PAYLOAD_TYPE_ADVERT
|
||||
&& pkt->payload_len >= PUB_KEY_SIZE + 4) {
|
||||
unsigned long now = millis();
|
||||
unsigned long jail_interval_ms = (unsigned long)_prefs.advert_jail * 3600UL * 1000UL;
|
||||
const uint8_t* sender_key = &pkt->payload[0]; // pub_key is first in advert payload
|
||||
|
||||
// Extract advert timestamp from payload (at offset PUB_KEY_SIZE)
|
||||
uint32_t advert_time;
|
||||
memcpy(&advert_time, &pkt->payload[PUB_KEY_SIZE], 4);
|
||||
|
||||
// Search for existing entry, track best eviction candidate (lowest count, then oldest)
|
||||
int found = -1;
|
||||
int evict = 0;
|
||||
for (int i = 0; i < MAX_ADVERT_JAIL_ENTRIES; i++) {
|
||||
if (_advert_jail[i].last_seen != 0
|
||||
&& memcmp(_advert_jail[i].pub_key_prefix, sender_key, ADVERT_JAIL_KEY_SIZE) == 0) {
|
||||
found = i;
|
||||
break;
|
||||
}
|
||||
// Prefer empty slots, then lowest count, then oldest last_seen
|
||||
if (_advert_jail[i].last_seen == 0) {
|
||||
evict = i;
|
||||
} else if (_advert_jail[evict].last_seen != 0) {
|
||||
if (_advert_jail[i].count < _advert_jail[evict].count
|
||||
|| (_advert_jail[i].count == _advert_jail[evict].count
|
||||
&& (now - _advert_jail[i].last_seen) > (now - _advert_jail[evict].last_seen))) {
|
||||
evict = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found >= 0) {
|
||||
AdvertJailEntry& entry = _advert_jail[found];
|
||||
|
||||
// Deduplicate: same advert arriving via multiple paths has the same timestamp
|
||||
if (advert_time == entry.last_advert_time) {
|
||||
// Same advert seen again via a different path, don't count it
|
||||
if (entry.jailed) {
|
||||
return true; // still drop if jailed
|
||||
}
|
||||
return false; // allow without counting
|
||||
}
|
||||
|
||||
unsigned long elapsed = now - entry.last_seen;
|
||||
bool interval_ok = (elapsed >= jail_interval_ms);
|
||||
|
||||
// Decrement count by 1 if interval has passed
|
||||
if (interval_ok && entry.count > 0) {
|
||||
entry.count--;
|
||||
}
|
||||
|
||||
// Exit jail when count drops below 2
|
||||
if (entry.jailed && entry.count < 2) {
|
||||
entry.jailed = false;
|
||||
}
|
||||
|
||||
entry.last_seen = now;
|
||||
entry.last_advert_time = advert_time;
|
||||
if (entry.total_adverts < UINT16_MAX) entry.total_adverts++;
|
||||
|
||||
// Arrived too soon? Increment count (cap at threshold to prevent unbounded growth)
|
||||
if (!interval_ok && entry.count < ADVERT_JAIL_THRESHOLD) {
|
||||
entry.count++;
|
||||
}
|
||||
|
||||
// Enter jail when count reaches threshold
|
||||
if (!entry.jailed && entry.count >= ADVERT_JAIL_THRESHOLD) {
|
||||
entry.jailed = true;
|
||||
}
|
||||
|
||||
// While in jail, drop adverts that arrived too soon
|
||||
if (entry.jailed && !interval_ok) {
|
||||
MESH_DEBUG_PRINTLN("%s filterRecvFloodPacket: advert jailed (sender count=%d, jail=%dh)",
|
||||
getLogDateTime(), entry.count, _prefs.advert_jail);
|
||||
return true; // DROP - sender is in jail
|
||||
}
|
||||
} else {
|
||||
// New sender, add to jail table (evict lowest-count/oldest if full)
|
||||
int slot = evict;
|
||||
memset(&_advert_jail[slot], 0, sizeof(AdvertJailEntry));
|
||||
memcpy(_advert_jail[slot].pub_key_prefix, sender_key, ADVERT_JAIL_KEY_SIZE);
|
||||
_advert_jail[slot].last_seen = now;
|
||||
_advert_jail[slot].first_seen = now;
|
||||
_advert_jail[slot].last_advert_time = advert_time;
|
||||
_advert_jail[slot].total_adverts = 1;
|
||||
_advert_jail[slot].count = 0;
|
||||
_advert_jail[slot].jailed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Global rate limit incoming flood adverts (does not affect own adverts)
|
||||
if (_prefs.advert_ratelimit > 0 && pkt->getPayloadType() == PAYLOAD_TYPE_ADVERT) {
|
||||
unsigned long now = millis();
|
||||
|
||||
// Deduplicate: extract advert timestamp and skip if same advert via different path
|
||||
if (pkt->payload_len >= PUB_KEY_SIZE + 4) {
|
||||
uint32_t advert_time;
|
||||
memcpy(&advert_time, &pkt->payload[PUB_KEY_SIZE], 4);
|
||||
if (last_flood_advert_recv != 0 && advert_time == last_flood_advert_time) {
|
||||
// Same advert arriving via another path, don't rate limit
|
||||
} else {
|
||||
if (last_flood_advert_recv != 0 && (now - last_flood_advert_recv) < ((unsigned long)_prefs.advert_ratelimit * 1000)) {
|
||||
MESH_DEBUG_PRINTLN("%s filterRecvFloodPacket: flood advert rate limited (interval %lu ms)", getLogDateTime(), now - last_flood_advert_recv);
|
||||
return true; // DROP this advert
|
||||
}
|
||||
last_flood_advert_recv = now;
|
||||
last_flood_advert_time = advert_time;
|
||||
}
|
||||
} else {
|
||||
if (last_flood_advert_recv != 0 && (now - last_flood_advert_recv) < ((unsigned long)_prefs.advert_ratelimit * 1000)) {
|
||||
MESH_DEBUG_PRINTLN("%s filterRecvFloodPacket: flood advert rate limited (interval %lu ms)", getLogDateTime(), now - last_flood_advert_recv);
|
||||
return true; // DROP this advert
|
||||
}
|
||||
last_flood_advert_recv = now;
|
||||
}
|
||||
}
|
||||
|
||||
// just try to determine region for packet (apply later in allowPacketForward())
|
||||
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
|
||||
recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
|
||||
|
|
@ -861,6 +979,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
|||
next_local_advert = next_flood_advert = 0;
|
||||
dirty_contacts_expiry = 0;
|
||||
set_radio_at = revert_radio_at = 0;
|
||||
last_flood_advert_recv = 0;
|
||||
last_flood_advert_time = 0;
|
||||
memset(_advert_jail, 0, sizeof(_advert_jail));
|
||||
_logging = false;
|
||||
region_load_active = false;
|
||||
|
||||
|
|
@ -887,6 +1008,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
|||
_prefs.flood_advert_interval = 12; // 12 hours
|
||||
_prefs.flood_max = 64;
|
||||
_prefs.interference_threshold = 0; // disabled
|
||||
_prefs.advert_jail = 12; // 12 hours
|
||||
|
||||
// bridge defaults
|
||||
_prefs.bridge_enabled = 1; // enabled
|
||||
|
|
@ -1114,6 +1236,65 @@ void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) {
|
|||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::formatAdvertJailReply(char *reply) {
|
||||
char *dp = reply;
|
||||
unsigned long now = millis();
|
||||
int count = 0;
|
||||
|
||||
// Build sorted index: only jailed entries, highest count first, then most recent last_seen
|
||||
int sorted[MAX_ADVERT_JAIL_ENTRIES];
|
||||
int n = 0;
|
||||
for (int i = 0; i < MAX_ADVERT_JAIL_ENTRIES && n < MAX_ADVERT_JAIL_ENTRIES; i++) {
|
||||
if (_advert_jail[i].last_seen != 0 && _advert_jail[i].jailed) sorted[n++] = i;
|
||||
}
|
||||
// Simple insertion sort (small n, no alloc)
|
||||
for (int i = 1; i < n; i++) {
|
||||
int key = sorted[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0) {
|
||||
bool swap = _advert_jail[sorted[j]].count < _advert_jail[key].count
|
||||
|| (_advert_jail[sorted[j]].count == _advert_jail[key].count
|
||||
&& (now - _advert_jail[sorted[j]].last_seen) > (now - _advert_jail[key].last_seen));
|
||||
if (!swap) break;
|
||||
sorted[j + 1] = sorted[j];
|
||||
j--;
|
||||
}
|
||||
sorted[j + 1] = key;
|
||||
}
|
||||
|
||||
const int REPLY_MAX = 159; // max usable chars in reply buffer (160 - null)
|
||||
|
||||
for (int si = 0; si < n && (dp - reply) < REPLY_MAX - 30; si++) {
|
||||
AdvertJailEntry& entry = _advert_jail[sorted[si]];
|
||||
|
||||
if (count > 0) *dp++ = '\n';
|
||||
|
||||
char hex[10];
|
||||
mesh::Utils::toHex(hex, entry.pub_key_prefix, ADVERT_JAIL_KEY_SIZE);
|
||||
|
||||
int remaining = REPLY_MAX - (int)(dp - reply);
|
||||
// Calculate average interval in hours
|
||||
if (entry.total_adverts > 1) {
|
||||
unsigned long span = now - entry.first_seen;
|
||||
unsigned long avg_secs = (span / (entry.total_adverts - 1)) / 1000;
|
||||
unsigned long avg_h = avg_secs / 3600;
|
||||
unsigned long avg_m = (avg_secs % 3600) / 60;
|
||||
snprintf(dp, remaining + 1, "%s:%d/%d,avg=%luh%02lum", hex, entry.count, ADVERT_JAIL_THRESHOLD,
|
||||
avg_h, avg_m);
|
||||
} else {
|
||||
snprintf(dp, remaining + 1, "%s:%d/%d", hex, entry.count, ADVERT_JAIL_THRESHOLD);
|
||||
}
|
||||
dp[remaining] = 0; // ensure null termination
|
||||
while (*dp) dp++;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
strcpy(dp, "-none-");
|
||||
dp += 6;
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::startRegionsLoad() {
|
||||
temp_map.resetFrom(region_map); // rebuild regions in a temp instance
|
||||
memset(load_stack, 0, sizeof(load_stack));
|
||||
|
|
|
|||
|
|
@ -78,6 +78,20 @@ struct NeighbourInfo {
|
|||
|
||||
#define FIRMWARE_ROLE "repeater"
|
||||
|
||||
#define ADVERT_JAIL_KEY_SIZE 4
|
||||
#define MAX_ADVERT_JAIL_ENTRIES 128
|
||||
#define ADVERT_JAIL_THRESHOLD 6
|
||||
|
||||
struct AdvertJailEntry {
|
||||
uint8_t pub_key_prefix[ADVERT_JAIL_KEY_SIZE]; // first bytes of sender pub_key
|
||||
uint8_t count; // strike count
|
||||
bool jailed; // true once count reaches threshold
|
||||
unsigned long last_seen; // millis() when last advert seen
|
||||
unsigned long first_seen; // millis() when first advert seen
|
||||
uint16_t total_adverts; // total adverts received (for avg calc)
|
||||
uint32_t last_advert_time; // advert timestamp (sender clock) for dedup
|
||||
};
|
||||
|
||||
#define PACKET_LOG_FILE "/packet_log"
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
|
|
@ -108,6 +122,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
|||
#endif
|
||||
CayenneLPP telemetry;
|
||||
unsigned long set_radio_at, revert_radio_at;
|
||||
unsigned long last_flood_advert_recv;
|
||||
uint32_t last_flood_advert_time; // advert timestamp for global rate limit dedup
|
||||
AdvertJailEntry _advert_jail[MAX_ADVERT_JAIL_ENTRIES];
|
||||
float pending_freq;
|
||||
float pending_bw;
|
||||
uint8_t pending_sf;
|
||||
|
|
@ -211,6 +228,7 @@ public:
|
|||
void setTxPower(int8_t power_dbm) override;
|
||||
void formatNeighborsReply(char *reply) override;
|
||||
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
|
||||
void formatAdvertJailReply(char *reply) override;
|
||||
void formatStatsReply(char *reply) override;
|
||||
void formatRadioStatsReply(char *reply) override;
|
||||
void formatPacketStatsReply(char *reply) override;
|
||||
|
|
|
|||
|
|
@ -88,7 +88,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
|||
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
|
||||
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
|
||||
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
|
||||
// next: 291
|
||||
file.read((uint8_t *)&_prefs->advert_ratelimit, sizeof(_prefs->advert_ratelimit)); // 291
|
||||
file.read((uint8_t *)&_prefs->advert_jail, sizeof(_prefs->advert_jail)); // 293
|
||||
// next: 294
|
||||
|
||||
// sanitise bad pref values
|
||||
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
|
||||
|
|
@ -118,6 +120,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
|||
|
||||
// sanitise settings
|
||||
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
|
||||
_prefs->advert_ratelimit = constrain(_prefs->advert_ratelimit, 0, 3600);
|
||||
_prefs->advert_jail = constrain(_prefs->advert_jail, 0, 168);
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
|
@ -179,7 +183,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
|||
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
|
||||
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
|
||||
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
|
||||
// next: 291
|
||||
file.write((uint8_t *)&_prefs->advert_ratelimit, sizeof(_prefs->advert_ratelimit)); // 291
|
||||
file.write((uint8_t *)&_prefs->advert_jail, sizeof(_prefs->advert_jail)); // 293
|
||||
// next: 294
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
|
@ -255,6 +261,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
|||
}
|
||||
} else if (memcmp(command, "neighbors", 9) == 0) {
|
||||
_callbacks->formatNeighborsReply(reply);
|
||||
} else if (memcmp(command, "jail", 4) == 0 && (command[4] == 0 || command[4] == ' ')) {
|
||||
_callbacks->formatAdvertJailReply(reply);
|
||||
} else if (memcmp(command, "neighbor.remove ", 16) == 0) {
|
||||
const char* hex = &command[16];
|
||||
uint8_t pubkey[PUB_KEY_SIZE];
|
||||
|
|
@ -516,6 +524,24 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else if (memcmp(config, "advert.ratelimit ", 17) == 0) {
|
||||
int secs = _atoi(&config[17]);
|
||||
if (secs < 0 || secs > 3600) {
|
||||
strcpy(reply, "Error: range is 0-3600 seconds (0=off)");
|
||||
} else {
|
||||
_prefs->advert_ratelimit = (uint16_t)secs;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else if (memcmp(config, "advert.jail ", 12) == 0) {
|
||||
int hours = _atoi(&config[12]);
|
||||
if (hours < 0 || hours > 168) {
|
||||
strcpy(reply, "Error: range is 0-168 hours (0=off)");
|
||||
} else {
|
||||
_prefs->advert_jail = (uint8_t)hours;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else if (memcmp(config, "guest.password ", 15) == 0) {
|
||||
StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
|
||||
savePrefs();
|
||||
|
|
@ -751,6 +777,10 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
|||
sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
|
||||
} else if (memcmp(config, "advert.interval", 15) == 0) {
|
||||
sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
|
||||
} else if (memcmp(config, "advert.ratelimit", 16) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t) _prefs->advert_ratelimit);
|
||||
} else if (memcmp(config, "advert.jail", 11) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t) _prefs->advert_jail);
|
||||
} else if (memcmp(config, "guest.password", 14) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->guest_password);
|
||||
} else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ struct NodePrefs { // persisted to file
|
|||
uint8_t rx_boosted_gain; // power settings
|
||||
uint8_t path_hash_mode; // which path mode to use when sending
|
||||
uint8_t loop_detect;
|
||||
uint16_t advert_ratelimit; // seconds, 0 = off. Rate limit for incoming flood adverts
|
||||
uint8_t advert_jail; // hours, 0 = off. Per-sender flood advert jail interval
|
||||
};
|
||||
|
||||
class CommonCLICallbacks {
|
||||
|
|
@ -81,6 +83,9 @@ public:
|
|||
virtual void removeNeighbor(const uint8_t* pubkey, int key_len) {
|
||||
// no op by default
|
||||
};
|
||||
virtual void formatAdvertJailReply(char *reply) {
|
||||
strcpy(reply, "-none-");
|
||||
};
|
||||
virtual void formatStatsReply(char *reply) = 0;
|
||||
virtual void formatRadioStatsReply(char *reply) = 0;
|
||||
virtual void formatPacketStatsReply(char *reply) = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue