CI / Build and Test (gcc, g++, ubuntu-latest) (push) Failing after 5m2s
CI / Code Coverage (push) Skipped
Build and Publish Docker Image / build-and-test (push) Failing after 6m13s
Build and Publish Docker Image / build-and-push-image (push) Skipped
Build and Publish Docker Image / security-scan (push) Skipped
The per-IP ban tracker treats every globally-routable client equally, but an aggregating front-end like the finger-web proxy funnels the whole internet's federated lookups through a single IP. A burst from any one client of the proxy (or a load test) is then attributed to the proxy's IP and, once it crosses the failure threshold, the daemon blocks the proxy — taking out finger lookups for everyone. Per-client abuse protection for the proxied path belongs in the proxy (which now rate-limits per real client IP), so the daemon should trust it. Add a FINGER_BAN_ALLOWLIST env var (comma-separated IPs). Allowlisted addresses are marked non-trackable in the listener, so their connections are never blocked and never recorded as offenses. Unset = unchanged behaviour. - parse_ip_allowlist() in ban.cpp (trims entries, skips blanks) + unit tests - listener() consults the set when computing 'trackable' - documented in docker-compose.yml and DOCKER.md
106 lines
3.1 KiB
C++
106 lines
3.1 KiB
C++
#include "ban.hpp"
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
|
|
bool is_bannable_address(const boost::asio::ip::address &addr) {
|
|
if (addr.is_loopback() || addr.is_unspecified() || addr.is_multicast()) {
|
|
return false;
|
|
}
|
|
|
|
if (addr.is_v4()) {
|
|
const std::uint32_t a = addr.to_v4().to_uint();
|
|
if ((a & 0xFF000000u) == 0x0A000000u) return false; // 10.0.0.0/8
|
|
if ((a & 0xFFF00000u) == 0xAC100000u) return false; // 172.16.0.0/12
|
|
if ((a & 0xFFFF0000u) == 0xC0A80000u) return false; // 192.168.0.0/16
|
|
if ((a & 0xFFFF0000u) == 0xA9FE0000u) return false; // 169.254.0.0/16 link-local
|
|
if ((a & 0xFFC00000u) == 0x64400000u) return false; // 100.64.0.0/10 CGNAT / Tailscale
|
|
return true;
|
|
}
|
|
|
|
// IPv6: drop link-local (fe80::/10) and unique-local (fc00::/7).
|
|
const auto v6 = addr.to_v6();
|
|
if (v6.is_link_local()) {
|
|
return false;
|
|
}
|
|
if ((v6.to_bytes()[0] & 0xFEu) == 0xFCu) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::unordered_set<std::string> parse_ip_allowlist(std::string_view csv) {
|
|
std::unordered_set<std::string> out;
|
|
std::size_t start = 0;
|
|
while (start <= csv.size()) {
|
|
const std::size_t comma = csv.find(',', start);
|
|
const std::size_t end =
|
|
(comma == std::string_view::npos) ? csv.size() : comma;
|
|
std::string_view tok = csv.substr(start, end - start);
|
|
const std::size_t a = tok.find_first_not_of(" \t\r\n");
|
|
if (a != std::string_view::npos) {
|
|
const std::size_t b = tok.find_last_not_of(" \t\r\n");
|
|
out.emplace(tok.substr(a, b - a + 1));
|
|
}
|
|
if (comma == std::string_view::npos) {
|
|
break;
|
|
}
|
|
start = comma + 1;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|