Add CLI listings for rate-limited flood adverts

List limited public-key prefixes, reasons, and wait times in bounded pages, with full-key details for targeted clearing. Queries preserve quotas and recovery timers.

Validated with 71 native tests, Full repeater and room-server builds, and a 909.500 MHz RF test covering 10 forwarded adverts, 2 blocked adverts, and forwarding after targeted clear.
This commit is contained in:
mikecarper committed 2026-09-10 04:18:17 -07:00
1 parent 602dbfe3d9
commit 00163f2c2b
9 files changed
+374 -1

No files matched your search

+35
View File
@@ -447,6 +447,41 @@ Elsewhere it replies `Err - neighbors not enabled in this build`. If a
## Statistics
### List Rate-Limited Flood Adverts
**Usage:** `get flood.advert [page]` or `get flood.advert key <index>`
Lists tracked advert origins currently subject to a forwarding limit. Page 1
is the default, with three entries per page so replies fit both USB and admin
LoRa CLI. Each row shows its index, the first 12 public-key hex characters,
the limit reason, and the minimum wait before another new advert can pass
the automatic limiter. Ordinary forwarding filters still apply.
```text
get flood.advert
get flood.advert 2
get flood.advert key 1
```
Reasons are `quota` (the prefix's three-hour forwarding allowance is spent),
`history` (distinct receive history is full), and `bad` (the full key is under
the stricter one-advert-per-12-hours rule). A `bad` key remains listed when
`wait=0s`: one forward is eligible, but its seven-day recovery is not complete.
If several limits apply, `bad` takes precedence over `history`, then `quota`;
the displayed wait accounts for all applicable limits.
The `key` form returns the complete 64-hex public key, reason, wait, shared
prefix forwarding count/quota, shortest received hop count, and remaining
bad-list recovery time. Different full keys with the same 12-hex prefix remain
separate entries. Indices refer to the current live list and can change as
history expires; read the full key before using a targeted clear.
Queries do not clear history, spend quota, or refresh abuse/recovery timers.
Elapsed history expires normally. Origins rejected only because every table
slot protects abuse history are not retained and cannot be enumerated.
Available on repeaters, room servers, and forwarding sensors through their
ordinary local/admin CLI. Companions do not have this table.
### Clear Automatic Flood Advert History
**Usage:** `clear flood.advert all` or `clear flood.advert <64-hex-full-public-key>`
+17
View File
@@ -69,6 +69,23 @@ physical distance.
abuse can therefore extend the restriction indefinitely; it does not expire
automatically seven days after the first offence.
### Inspect the current limits
Use `get flood.advert` to list rate-limited origins, three per page. Continue
with `get flood.advert 2`, etc. Rows show a 12-hex public-key prefix, the reason
(`quota`, `history`, or `bad`), and the wait before another new advert is
eligible under the automatic limiter. `get flood.advert key 1` shows the full
key and counters for row 1, including the remaining bad-list recovery time.
A bad-listed key remains visible with `wait=0s` when its next single forward
is due. Prefix collisions appear as separate full-key entries.
This is a live, read-only view: expiry can change row indices between queries,
but reading never resets quotas or extends recovery. Sources rejected solely
because the protected-history table is full are not retained, so they cannot
be listed. See the [CLI reference](cli_commands.md#list-rate-limited-flood-adverts).
### Clear history
All timers use rollover-safe uptime, so setting the clock forward or backward
cannot clear the restriction. This history is currently RAM-only: rebooting
the relay clears it, and time while powered off is not tracked. A manual clear
+1
View File
@@ -2461,6 +2461,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
// Observer-only top-level commands (ota check/update, tls.bundletest, alert test)
// live in CommonCLI_Observer.cpp.
if (handleObserverCommand(sender_timestamp, command, reply)) return;
if (mesh::cli::handleFloodAdvertGet(_callbacks->getFloodAdvertLimiter(), command, reply, millis())) return;
if (mesh::cli::handleFloodAdvertClear(_callbacks->getFloodAdvertLimiter(), command, reply)) return;
if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) {
_board->powerOff(); // doesn't return
+72
View File
@@ -2,10 +2,82 @@
#include "FloodAdvertLimiter.h"
#include <Utils.h>
#include <stdio.h>
namespace mesh {
namespace cli {
inline const char* floodAdvertLimitReason(uint8_t reasons) {
if (reasons & FloodAdvertLimiter::BadListRule) return "bad";
if (reasons & FloodAdvertLimiter::ReceiveHistory) return "history";
return "quota";
}
// Keep pages inside the 160-byte reply shared by USB and authenticated LoRa CLI.
// Row numbers refer to the current filtered list, not permanent table slots.
inline bool handleFloodAdvertGet(FloodAdvertLimiter* limiter, const char* command,
char* reply, uint32_t now) {
static const char prefix[] = "get flood.advert";
const size_t prefix_len = sizeof(prefix) - 1;
if (strncmp(command, prefix, prefix_len) != 0) return false;
const char* arg = command + prefix_len;
if (*arg && *arg != ' ' && *arg != '\t' && *arg != '\r' && *arg != '\n') return false;
while (*arg == ' ' || *arg == '\t') ++arg;
const bool detail = strncmp(arg, "key", 3) == 0 && (arg[3] == ' ' || arg[3] == '\t');
if (detail) {
arg += 3;
while (*arg == ' ' || *arg == '\t') ++arg;
}
unsigned number = 1;
if (*arg >= '0' && *arg <= '9') {
number = 0;
do {
number = number * 10 + unsigned(*arg++ - '0');
if (number > 65535) break;
} while (*arg >= '0' && *arg <= '9');
} else if (detail) {
number = 0;
}
while (*arg == ' ' || *arg == '\t' || *arg == '\r' || *arg == '\n') ++arg;
if (*arg || number == 0 || number > 65535) {
strcpy(reply, "ERR: get flood.advert [page] | key <index>");
return true;
}
if (!limiter) {
strcpy(reply, "ERR: advert limiter unavailable on this role");
return true;
}
static constexpr size_t PAGE_SIZE = 3;
FloodAdvertLimiter::LimitedEntry rows[PAGE_SIZE];
const size_t offset = detail ? number - 1 : (number - 1) * PAGE_SIZE;
const size_t total = limiter->listLimited(now, offset, rows, detail ? 1 : PAGE_SIZE);
if (total == 0 && !detail && number == 1) {
strcpy(reply, "> no rate-limited adverts");
} else if (offset >= total) {
strcpy(reply, "ERR: advert list index out of range");
} else if (detail) {
const auto& row = rows[0];
char key[PUB_KEY_SIZE * 2 + 1];
Utils::toHex(key, row.key, PUB_KEY_SIZE);
snprintf(reply, 160, "> %s\n%s wait=%lus sent=%u/%u hops=%u recovery=%lus",
key, floodAdvertLimitReason(row.reasons), (unsigned long)((row.wait_ms + 999) / 1000),
unsigned(row.forwarded), unsigned(row.quota), unsigned(row.hops),
(unsigned long)((row.recovery_ms + 999) / 1000));
} else {
size_t used = snprintf(reply, 160, "> page %u/%u limited=%u", number,
unsigned((total + PAGE_SIZE - 1) / PAGE_SIZE), unsigned(total));
for (size_t i = 0; i < PAGE_SIZE && offset + i < total && used < 159; ++i) {
char key[FloodAdvertLimiter::PREFIX_BYTES * 2 + 1];
Utils::toHex(key, rows[i].key, FloodAdvertLimiter::PREFIX_BYTES);
used += snprintf(reply + used, 160 - used, "\n%u %s %s wait=%lus",
unsigned(offset + i + 1), key, floodAdvertLimitReason(rows[i].reasons),
(unsigned long)((rows[i].wait_ms + 999) / 1000));
}
}
return true;
}
// Dispatch only after the role's ordinary CLI authorization checks. Requiring
// an explicit "all" avoids treating missing/malformed keys as a global reset.
inline bool handleFloodAdvertClear(FloodAdvertLimiter* limiter, const char* command, char* reply) {
+47
View File
@@ -183,4 +183,51 @@ bool FloodAdvertLimiter::isBad(const uint8_t* key, uint32_t now) {
return entry && (entry->flags & BAD);
}
size_t FloodAdvertLimiter::listLimited(uint32_t now, size_t offset,
LimitedEntry* dest, size_t count) {
refresh(now);
size_t total = 0;
for (size_t i = 0; i < _capacity; ++i) {
const Entry& entry = _entries[i];
if (!(entry.flags & ACTIVE)) continue;
unsigned forwarded = 0;
uint8_t hops = entry.min_hops;
for (size_t j = 0; j < _capacity; ++j) {
const Entry& other = _entries[j];
if (!(other.flags & ACTIVE) || memcmp(other.key, entry.key, PREFIX_BYTES) != 0) continue;
if (other.min_hops < hops) hops = other.min_hops;
for (uint16_t bits = other.forwarded; bits; bits &= bits - 1) ++forwarded;
}
const uint8_t quota = allowance(hops);
uint8_t reasons = 0;
if (forwarded >= quota) reasons |= WindowQuota;
// Once receive history fills, a new distinct payload is refused even if
// another forwarding filter prevented earlier payloads spending quota.
if (entry.count == HASH_SLOTS) reasons |= ReceiveHistory;
if (entry.flags & BAD) reasons |= BadListRule;
if (!reasons) continue;
if (dest && total >= offset && total - offset < count) {
LimitedEntry& out = dest[total - offset];
memcpy(out.key, entry.key, PUB_KEY_SIZE);
out.forwarded = forwarded;
out.quota = quota;
out.hops = hops;
out.reasons = reasons;
out.wait_ms = reasons & (WindowQuota | ReceiveHistory)
? WINDOW_MS - uint32_t(now - entry.window_start) : 0;
out.recovery_ms = 0;
if (reasons & BadListRule) {
out.recovery_ms = RECOVERY_MS - uint32_t(now - entry.last_violation);
if (entry.flags & HAS_FORWARDED) {
const uint32_t bad_wait = BAD_INTERVAL_MS - uint32_t(now - entry.last_forwarded);
if (bad_wait > out.wait_ms) out.wait_ms = bad_wait;
}
}
}
++total;
}
return total;
}
} // namespace mesh
+15
View File
@@ -41,6 +41,17 @@ public:
uint8_t flags;
};
enum LimitReason : uint8_t { WindowQuota = 1, ReceiveHistory = 2, BadListRule = 4 };
struct LimitedEntry {
uint8_t key[PUB_KEY_SIZE];
uint32_t wait_ms;
uint32_t recovery_ms;
uint16_t forwarded;
uint8_t quota;
uint8_t hops;
uint8_t reasons;
};
void reset();
void clear(const uint8_t* full_key);
void tick(uint32_t now);
@@ -54,6 +65,10 @@ public:
Decision check(const uint8_t* key, const uint8_t* hash, uint32_t now);
void commit(const uint8_t* key, const uint8_t* hash, uint32_t now);
bool isBad(const uint8_t* key, uint32_t now);
// Return the total number of limited full keys and copy the requested slice.
// Queries only expire elapsed history; they never consume quota, refresh
// last-heard, or add abuse evidence. Prefix collisions remain separate keys.
size_t listLimited(uint32_t now, size_t offset, LimitedEntry* dest, size_t count);
protected:
FloodAdvertLimiter(Entry* entries, size_t capacity) : _entries(entries), _capacity(capacity) {}
+2 -1
View File
@@ -150,7 +150,8 @@ does not reflect the GoogleTest count -- run the built binary directly
| `test_wifi_ota_seeder_policy` | `src/helpers/WiFiOtaSeederPolicy.h`, `WiFiOtaSeederStatus.h` | listener state versus network availability, serial/TCP folder ownership, detach detection, and bounded status formatting |
| `test_tls_clock_validity` | `src/helpers/esp32/TlsClockValidity.h` | signed wall-clock minimum and the fresh-proof/WiFi/time conjunction required before certificate-validating downloads |
| `test_ota` | `src/helpers/ota/` | v2 application/v3 bootloader parser separation; legacy XIAO, generic internal, and exact MeshTower V2 SD embedded identity, vector, capability, explicit-confirmation, codec-isolation, scratch-headroom/shared-slot no-EndF gates, and no-autofetch gates; container and EndF integrity; protocol codecs; transfer, resume, and layered apply safety; adaptive 2-to-4 block-request window growth and stall contraction; active-transfer priority classification |
| `test_trace_retry` | `src/Mesh.cpp`, `RTCClock`, `ClockSyncUtils.h`, retry and relay policy | app-v2 and boot-v3 traffic sharing `PAYLOAD_TYPE_OTA=0x0C` and the TempRadio suspend policy; opaque OTA relay behavior; background discovery priority; immediate primary transfer relay, receive-delay bypass, fast CAD retry, and no generic flood retry; trace and non-OTA flood retry timing; backward RTC correction; clock consensus/path policy and the 10-minute default drift threshold |
| `test_flood_advert_limiter` | `FloodAdvertLimiter.h/.cpp`, `FloodAdvertCLI.h` | hop-based quotas, verified duplicates, prefix collisions, abuse escalation/recovery, protected capacity, rollover, exact-key clearing, read-only listing, pagination, malformed selectors, full-key details, and USB/LoRa reply bounds |
| `test_trace_retry` | `src/Mesh.cpp`, `RTCClock`, `ClockSyncUtils.h`, retry and relay policy | app-v2 and boot-v3 traffic sharing `PAYLOAD_TYPE_OTA=0x0C` and the TempRadio suspend policy; opaque OTA relay behavior; background discovery priority; immediate primary transfer relay, receive-delay bypass, fast CAD retry, and no generic flood retry; trace and non-OTA flood retry timing; backward RTC correction; clock consensus/path policy and the 10-minute default drift threshold; advert receive/forward limits and matching read-only CLI listings |
| `test_utils` | `src/Utils.cpp` | `Utils::toHex` (upstream) |
## Conventions (and how to add a suite)
@@ -1,6 +1,7 @@
#include <gtest/gtest.h>
#include <helpers/FloodAdvertLimiter.h>
#include <helpers/FloodAdvertCLI.h>
#include <helpers/RemoteCliReplyCache.h>
#include <array>
using Limiter = mesh::FloodAdvertLimiter;
@@ -304,6 +305,169 @@ TEST_F(FloodAdvertLimit, CliInvalidSelectorsNeverClearAnything) {
EXPECT_STREQ("ERR: advert limiter unavailable on this role", reply);
}
TEST_F(FloodAdvertLimit, CliListsOnlyLimitedKeysAndReturnsFullKeyDetails) {
char reply[160] = {};
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert", reply, 0));
EXPECT_STREQ("> no rate-limited adverts", reply);
receive(1, 8, 0);
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert", reply, 0));
EXPECT_STREQ("> no rate-limited adverts", reply);
receive(2, 8, 0);
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert 1", reply, 1000));
EXPECT_STREQ("> page 1/1 limited=1\n1 BADBEE5C0000 quota wait=10799s", reply);
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert key 1", reply, 1000));
char hex[PUB_KEY_SIZE * 2 + 1];
mesh::Utils::toHex(hex, key.data(), PUB_KEY_SIZE);
EXPECT_EQ(std::string("> ") + hex + "\nquota wait=10799s sent=2/2 hops=8 recovery=0s", reply);
// Reading neither spends more quota nor resets it.
EXPECT_EQ(Decision::Quota, receive(3, 8, 1000));
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert", reply, W));
EXPECT_STREQ("> no rate-limited adverts", reply);
}
TEST_F(FloodAdvertLimit, CliPagesTheWholeListWithoutTruncationOrChangingIt) {
for (unsigned i = 0; i < 4; ++i) {
auto source = key;
source[0] += i;
receive(1, 8, 0, source.data());
receive(2, 8, 0, source.data());
}
struct { char before = '!'; char reply[160] = {}; char after = '?'; } guarded;
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert", guarded.reply, 0));
EXPECT_STREQ("> page 1/2 limited=4\n1 BADBEE5C0000 quota wait=10800s\n2 BBDBEE5C0000 quota wait=10800s\n3 BCDBEE5C0000 quota wait=10800s", guarded.reply);
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert\t2\r\n", guarded.reply, 1));
EXPECT_STREQ("> page 2/2 limited=4\n4 BDDBEE5C0000 quota wait=10800s", guarded.reply);
EXPECT_EQ('!', guarded.before);
EXPECT_EQ('?', guarded.after);
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert 3", guarded.reply, 1));
EXPECT_STREQ("ERR: advert list index out of range", guarded.reply);
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert key 4", guarded.reply, 1));
EXPECT_NE(nullptr, strstr(guarded.reply, "BDDBEE5C0000"));
EXPECT_EQ(4U, limiter.listLimited(1, 0, nullptr, 0));
}
TEST_F(FloodAdvertLimit, ListedPrefixCollisionsPreserveFullKeyIdentity) {
auto collision = key;
collision[31] = 1;
receive(1, 8, 0);
receive(1, 8, 1, collision.data());
Limiter::LimitedEntry rows[2];
ASSERT_EQ(2U, limiter.listLimited(100, 0, rows, 2));
EXPECT_EQ(0, memcmp(rows[0].key, rows[1].key, Limiter::PREFIX_BYTES));
EXPECT_NE(0, memcmp(rows[0].key, rows[1].key, PUB_KEY_SIZE));
for (const auto& row : rows) {
EXPECT_EQ(Limiter::WindowQuota, row.reasons);
EXPECT_EQ(2, row.forwarded);
EXPECT_EQ(2, row.quota);
EXPECT_EQ(W - 100, row.wait_ms);
}
char reply[160];
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert key 2", reply, 100));
char hex[PUB_KEY_SIZE * 2 + 1];
mesh::Utils::toHex(hex, collision.data(), PUB_KEY_SIZE);
EXPECT_NE(nullptr, strstr(reply, hex));
}
TEST_F(FloodAdvertLimit, LargestSupportedTableFitsUsbAndRemoteReplies) {
mesh::StaticFloodAdvertLimiter<128> large;
for (unsigned i = 0; i < 128; ++i) {
auto source = key;
source[0] = i;
for (uint8_t seq = 0; seq < Limiter::HASH_SLOTS; ++seq) {
uint8_t hash[MAX_HASH_SIZE] = {seq};
large.observe(source.data(), hash, 0, 0);
}
}
struct { char before = '!'; char reply[160] = {}; char after = '?'; } guarded;
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&large, "get flood.advert 34", guarded.reply, 0));
EXPECT_STREQ("> page 34/43 limited=128\n100 63DBEE5C0000 history wait=10800s\n101 64DBEE5C0000 history wait=10800s\n102 65DBEE5C0000 history wait=10800s", guarded.reply);
EXPECT_LE(strlen(guarded.reply), mesh::RemoteCliReplyCache::MAX_REPLY_TEXT);
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&large, "get flood.advert key 128", guarded.reply, 0));
EXPECT_NE(nullptr, strstr(guarded.reply, "history wait=10800s sent=0/10 hops=0 recovery=0s"));
EXPECT_LE(strlen(guarded.reply), mesh::RemoteCliReplyCache::MAX_REPLY_TEXT);
EXPECT_EQ('!', guarded.before);
EXPECT_EQ('?', guarded.after);
}
TEST_F(FloodAdvertLimit, ListingUsesTheSameShortestPrefixPathAsForwarding) {
receive(1, 8, 0);
receive(2, 8, 1);
ASSERT_EQ(1U, limiter.listLimited(1, 0, nullptr, 0));
uint8_t hash[MAX_HASH_SIZE] = {1};
limiter.observe(key.data(), hash, 1, 2, true);
EXPECT_EQ(0U, limiter.listLimited(2, 0, nullptr, 0));
EXPECT_EQ(Decision::Allow, receive(3, 8, 2));
}
TEST_F(FloodAdvertLimit, ListingShowsSaturatedReceiveHistoryWithoutSpentQuota) {
for (unsigned i = 0; i < Limiter::HASH_SLOTS; ++i) receive(i, 1, 0, nullptr, false);
Limiter::LimitedEntry row;
ASSERT_EQ(1U, limiter.listLimited(0, 0, &row, 1));
EXPECT_EQ(Limiter::ReceiveHistory, row.reasons);
EXPECT_EQ(0, row.forwarded);
EXPECT_EQ(10, row.quota);
EXPECT_EQ(W, row.wait_ms);
char reply[160];
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert", reply, 0));
EXPECT_NE(nullptr, strstr(reply, "history wait=10800s"));
EXPECT_EQ(Decision::Quota, receive(99, 1, 0));
}
TEST_F(FloodAdvertLimit, ListingReportsBadKeyCooldownAndDoesNotExtendRecovery) {
bad();
Limiter::LimitedEntry row;
ASSERT_EQ(1U, limiter.listLimited(W, 0, &row, 1));
EXPECT_TRUE(row.reasons & Limiter::BadListRule);
EXPECT_EQ(H12, row.wait_ms);
EXPECT_EQ(D7, row.recovery_ms);
char reply[160];
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert", reply, W));
EXPECT_NE(nullptr, strstr(reply, "bad wait=43200s"));
// Bad-list membership is still visible when its next single forward is due.
ASSERT_EQ(1U, limiter.listLimited(W + H12, 0, &row, 1));
EXPECT_TRUE(row.reasons & Limiter::BadListRule);
EXPECT_EQ(0U, row.wait_ms);
EXPECT_EQ(D7 - (H12 - W), row.recovery_ms);
for (uint32_t now = W + H12; now < 2 * W + D7; now += W) {
EXPECT_EQ(1U, limiter.listLimited(now, 0, nullptr, 0));
}
EXPECT_EQ(0U, limiter.listLimited(2 * W + D7, 0, nullptr, 0));
EXPECT_FALSE(limiter.isBad(key.data(), 2 * W + D7));
}
TEST_F(FloodAdvertLimit, ListingWaitsAndExpiryWorkAcrossMillisWrap) {
const uint32_t start = UINT32_MAX - W / 2;
bad(start);
Limiter::LimitedEntry row;
ASSERT_EQ(1U, limiter.listLimited(start + W + 1, 0, &row, 1));
EXPECT_EQ(H12 - 1, row.wait_ms);
EXPECT_EQ(D7 - 1, row.recovery_ms);
EXPECT_EQ(0U, limiter.listLimited(start + 2 * W + D7, 0, &row, 1));
}
TEST_F(FloodAdvertLimit, CliInvalidListSelectorsDoNotChangeHistoryOrStealOtherCommands) {
bad();
const char* invalid[] = {
"get flood.advert 0", "get flood.advert -1", "get flood.advert 65536",
"get flood.advert 99999999999999999999", "get flood.advert 1.5",
"get flood.advert all", "get flood.advert 1 extra", "get flood.advert key",
"get flood.advert key 0", "get flood.advert key -1", "get flood.advert key 1 extra"
};
for (const char* command : invalid) {
char reply[160] = {};
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&limiter, command, reply, W));
EXPECT_EQ(0, strncmp(reply, "ERR:", 4)) << command;
EXPECT_TRUE(limiter.isBad(key.data(), W));
}
char reply[160] = "unchanged";
EXPECT_FALSE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advert.interval", reply, W));
EXPECT_FALSE(mesh::cli::handleFloodAdvertGet(&limiter, "get flood.advertisement", reply, W));
EXPECT_FALSE(mesh::cli::handleFloodAdvertGet(&limiter, "clear flood.advert all", reply, W));
EXPECT_STREQ("unchanged", reply);
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(nullptr, "get flood.advert", reply, W));
EXPECT_STREQ("ERR: advert limiter unavailable on this role", reply);
}
#ifndef FLOOD_ADVERT_COMBINED_TEST
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
@@ -3,6 +3,7 @@
#include <Ed25519.h>
#include <Mesh.h>
#include <helpers/ClockSyncUtils.h>
#include <helpers/FloodAdvertCLI.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/ota/OtaFormat.h>
@@ -1077,6 +1078,26 @@ TEST_F(AdvertReceiveLimit, StopsOnlyForwardingAndKeepsLocalAdvertCallbacks) {
EXPECT_NE(ACTION_RELEASE, node.receivePacket(&message));
}
TEST_F(AdvertReceiveLimit, CliListsTheActualReceiveLimitWithoutChangingForwarding) {
char reply[160];
ASSERT_NE(ACTION_RELEASE, receive(1));
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&node.limiter, "get flood.advert", reply, clock.now));
EXPECT_STREQ("> no rate-limited adverts", reply);
ASSERT_NE(ACTION_RELEASE, receive(2));
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&node.limiter, "get flood.advert", reply, clock.now));
EXPECT_NE(nullptr, strstr(reply, "BA0000000000 quota wait=10800s"));
ASSERT_EQ(ACTION_RELEASE, receive(3));
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&node.limiter, "get flood.advert key 1", reply, clock.now));
EXPECT_NE(nullptr, strstr(reply, "sent=2/2 hops=8"));
EXPECT_EQ(3U, node.advert_callbacks);
EXPECT_EQ(2U, node.forwarding_checks);
// An authenticated shorter duplicate changes the actual allowance and list.
ASSERT_EQ(ACTION_RELEASE, receive(1, 1, true));
ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&node.limiter, "get flood.advert", reply, clock.now));
EXPECT_STREQ("> no rate-limited adverts", reply);
EXPECT_NE(ACTION_RELEASE, receive(4));
}
TEST_F(AdvertReceiveLimit, VerifiedShorterDuplicateRaisesAllowanceWithoutRelayingIt) {
receive(1);
receive(2);