Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/simple_repeater/Filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -775,8 +775,8 @@ bool Filter::load(FILESYSTEM* fs) {
_prefs.payload_prefs[i].hops_max = constrain(_prefs.payload_prefs[i].hops_max, 0, 64);
// a soft cutoff at/above the hard limit leaves no ramp room -> treat as off
if (_prefs.soft_limit[i] >= _prefs.payload_prefs[i].rate_limit) _prefs.soft_limit[i] = 0;
_limiters[i].init(_prefs.payload_prefs[i].rate_limit, _prefs.payload_prefs[i].rate_secs, _prefs.soft_limit[i]);
}
initLimiters();
_prefs.minimal_hash_bytes = constrain(_prefs.minimal_hash_bytes, 1, 3);
_prefs.filter_malformed = constrain(_prefs.filter_malformed, 0, 1);
_prefs.dryrun = constrain(_prefs.dryrun, 0, 1);
Expand Down
11 changes: 11 additions & 0 deletions examples/simple_repeater/Filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ class Filter {
// to every reason alike. Returns the forwarding verdict.
bool drop(const mesh::Packet *packet);

// Rebuild the per-type rate limiters from _prefs. Every path that replaces
// the prefs (defaults, reset, load) must end here, or the limiters keep
// enforcing whatever they held before (all-zero = no limit at all).
void initLimiters() {
for (uint8_t i = 0; i < PAYLOAD_TYPE_COUNT; ++i) {
_limiters[i].init(_prefs.payload_prefs[i].rate_limit, _prefs.payload_prefs[i].rate_secs, _prefs.soft_limit[i]);
}
}

// xorshift32: cheap, dependency-free source of a random byte for the soft
// cutoff. State is never allowed to reach 0 (that would freeze the sequence).
uint8_t nextRandom() {
Expand All @@ -106,6 +115,7 @@ class Filter {
Filter(ClientACL &acl, mesh::RTCClock &rtc) : _acl(&acl), _rtc(&rtc), _radio(nullptr), _rng_state(0xA5A5F00D) {
memset(_sender_last, 0, sizeof(_sender_last));
memset(_text_last, 0, sizeof(_text_last));
initLimiters();
}
void setRadio(mesh::Radio *radio) { _radio = radio; }
void resetPrefs(void) {
Expand All @@ -114,6 +124,7 @@ class Filter {
_advert.clear();
memset(_sender_last, 0, sizeof(_sender_last));
memset(_text_last, 0, sizeof(_text_last));
initLimiters();
}
void resetStats(void) { _cnt.reset(); }
bool allowPacketForward(const mesh::Packet *packet);
Expand Down
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ build_src_filter =
+<../src/helpers/DynamicConfigSerializer.cpp>
+<../src/helpers/DutyCycleLimits.cpp>
+<../examples/companion_radio/CompanionFilter.cpp>
+<../examples/simple_repeater/Filter.cpp>

lib_deps =
google/googletest @ 1.17.0
Expand Down
5 changes: 5 additions & 0 deletions test/mocks/Arduino.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ inline void delay(uint32_t ms) {
g_mock_millis += ms;
}

// Same definition as the Arduino core macro.
#ifndef constrain
#define constrain(amt, low, high) ((amt) < (low) ? (low) : ((amt) > (high) ? (high) : (amt)))
#endif

// ---- Arduino string helpers not present in the host libc --------------------
// (MinGW/Windows already declares ltoa in <stdlib.h>, so only define it where
// the host libc lacks it — e.g. glibc on Linux.)
Expand Down
85 changes: 85 additions & 0 deletions test/test_filter_limits/test_filter_limits.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include <gtest/gtest.h>

// Drives the repeater Filter through its own interface (load, handleCommand,
// allowPacketForward) to check that the per-type rate limiters always match
// the active prefs, whichever way those prefs were arrived at.
#include "../../examples/simple_repeater/Filter.h"

// src/Identity.cpp needs the ed25519 library, which the native env does not
// build. ClientACL only needs the default constructor.
mesh::Identity::Identity() { memset(pub_key, 0, sizeof(pub_key)); }

namespace {

class FixedClock : public mesh::RTCClock {
uint32_t _now = 1700000000;
public:
uint32_t getCurrentTime() override { return _now; }
void setCurrentTime(uint32_t time) override { _now = time; }
};

// A flood ACK: not a priority type, so it reaches the per-type rate limiter.
// The default limit for ACK is 5 per 60 s (FilterPrefs in Filter.h).
mesh::Packet floodAck() {
mesh::Packet p;
p.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT);
p.setPathHashSizeAndCount(1, 0);
p.payload_len = 4;
memset(p.payload, 0, sizeof(p.payload));
return p;
}

// Number of `sent` identical flood ACKs (same second) the filter forwards.
int forwarded(Filter& filter, int sent) {
mesh::Packet p = floodAck();
int n = 0;
for (int i = 0; i < sent; i++) {
if (filter.allowPacketForward(&p)) n++;
}
return n;
}

void command(Filter& filter, FILESYSTEM& fs, const char* cmd) {
char buf[64];
char reply[FILTER_REPLY_SIZE] = "";
strncpy(buf, cmd, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
filter.handleCommand(&fs, buf, reply);
}

} // namespace

// A repeater with no /filter_prefs yet (the mock FS has no files) must still
// enforce the default rate limits once the filter is switched on.
TEST(FilterLimits, FreshDeviceAppliesDefaultRateLimit) {
ClientACL acl;
FixedClock clock;
FsMock fs;
Filter filter(acl, clock);

ASSERT_TRUE(filter.load(&fs));
command(filter, fs, "filter on");

EXPECT_EQ(forwarded(filter, 6), 5);
}

// `filter reset` restores the default prefs, so it must restore the default
// limiter too instead of keeping the last `filter rate` setting active.
TEST(FilterLimits, ResetRestoresDefaultRateLimit) {
ClientACL acl;
FixedClock clock;
FsMock fs;
Filter filter(acl, clock);

ASSERT_TRUE(filter.load(&fs));
command(filter, fs, "filter rate 3 100 60");
command(filter, fs, "filter reset");
command(filter, fs, "filter on");

EXPECT_EQ(forwarded(filter, 6), 5);
}

int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Loading