Port 79 mostly attracts HTTP/SIP probes, TLS handshakes, and username guessers -- none of which resolve to a plan file. Treat any request that fails to read a plan as an "offense" and timestamp it against the source IP. Add BanTracker (ban.hpp/ban.cpp): a per-IP rolling-window offender list. When an IP has more than 3 offenses still inside a 24h window, its connections are dropped without being read or answered; timestamps older than the window are pruned so a blocked IP frees itself automatically. State is in-memory (single io_context thread, no locking); the clock is injected for testability. A periodic sweeper keeps the map bounded. Legitimate lookups that hit a real plan never count, which also frustrates username enumeration. Unit tests in test_ban.cpp.
56 lines
1.5 KiB
C++
56 lines
1.5 KiB
C++
#include "ban.hpp"
|
|
|
|
namespace {
|
|
// Count timestamps that fall within (now - window, now]. The deque is kept in
|
|
// ascending order, so the in-window entries are always a suffix.
|
|
int count_in_window(const std::deque<BanTracker::clock::time_point> &ts,
|
|
BanTracker::clock::time_point now,
|
|
BanTracker::clock::duration window) {
|
|
const auto cutoff = now - window;
|
|
int count = 0;
|
|
for (auto it = ts.rbegin(); it != ts.rend() && *it > cutoff; ++it) {
|
|
++count;
|
|
}
|
|
return count;
|
|
}
|
|
} // namespace
|
|
|
|
bool BanTracker::is_blocked(const std::string &ip, clock::time_point now) const {
|
|
auto it = offenders_.find(ip);
|
|
if (it == offenders_.end()) {
|
|
return false;
|
|
}
|
|
return count_in_window(it->second, now, cfg_.window) > cfg_.threshold;
|
|
}
|
|
|
|
BanTracker::OffenseResult
|
|
BanTracker::record_offense(const std::string &ip, clock::time_point now) {
|
|
auto &ts = offenders_[ip];
|
|
const auto cutoff = now - cfg_.window;
|
|
|
|
// Drop this IP's timestamps that have aged out of the window.
|
|
while (!ts.empty() && ts.front() <= cutoff) {
|
|
ts.pop_front();
|
|
}
|
|
|
|
ts.push_back(now);
|
|
|
|
const int count = static_cast<int>(ts.size());
|
|
return {count, count > cfg_.threshold};
|
|
}
|
|
|
|
void BanTracker::sweep(clock::time_point now) {
|
|
const auto cutoff = now - cfg_.window;
|
|
for (auto it = offenders_.begin(); it != offenders_.end();) {
|
|
auto &ts = it->second;
|
|
while (!ts.empty() && ts.front() <= cutoff) {
|
|
ts.pop_front();
|
|
}
|
|
if (ts.empty()) {
|
|
it = offenders_.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|