Only track bannable (globally-routable) source IPs

The ban logic is per source IP, so it only works where the daemon can see
the real client. Behind Docker's default bridge networking every client is
SNAT'd to the bridge gateway (a 172.16/12 address), so a single IP would
stand in for the whole internet -- counting offenses against it would block
everyone at once.

Add is_bannable_address(): only globally-routable unicast addresses are
tracked. Loopback, RFC1918 private, CGNAT (100.64/10), link-local, IPv6
unique-local, and multicast all return false. main.cpp decides trackability
from the accepted endpoint and skips both the block check and offense
recording for non-global sources. Net effect: banning works where the real
IP is visible (FreeBSD jail via pf rdr; Docker with host networking) and is
inert -- not catastrophic -- where it is not (Docker bridge).

Document the Docker client-IP caveat: docker-compose.yml now defaults to
host networking, with the rationale and alternatives in DOCKER.md.
This commit is contained in:
pmb
2026-06-15 16:38:07 -07:00
parent 946c2b9e01
commit 54650af252
6 changed files with 153 additions and 10 deletions
+17 -7
View File
@@ -25,14 +25,17 @@ awaitable<std::string> dofinger(const std::string &username) {
co_return process(username);
}
awaitable<void> echo(tcp::socket socket, std::string client_addr,
awaitable<void> echo(tcp::socket socket, std::string client_addr, bool trackable,
BanTracker &bans) {
try {
auto now = std::chrono::steady_clock::now();
// An IP that has racked up too many failed lookups (scanners, username
// guessers, non-finger junk) is dropped without being read or answered.
if (bans.is_blocked(client_addr, now)) {
// Only globally-routable addresses are tracked: behind Docker's bridge
// every client is SNAT'd to the gateway, so banning there would block
// everyone at once (see is_bannable_address()).
if (trackable && bans.is_blocked(client_addr, now)) {
std::printf("finger drop from %s: blocked\n", client_addr.c_str());
co_return;
}
@@ -58,10 +61,15 @@ awaitable<void> echo(tcp::socket socket, std::string client_addr,
bool plan_served =
response != username && response.rfind("InvalidInput:", 0) != 0;
if (!plan_served) {
auto res = bans.record_offense(client_addr, now);
std::printf("finger miss from %s for '%s' (%d failures in window)%s\n",
client_addr.c_str(), username.c_str(), res.count,
res.blocked ? " -- now blocked" : "");
if (trackable) {
auto res = bans.record_offense(client_addr, now);
std::printf("finger miss from %s for '%s' (%d failures in window)%s\n",
client_addr.c_str(), username.c_str(), res.count,
res.blocked ? " -- now blocked" : "");
} else {
std::printf("finger miss from %s for '%s' (not tracked)\n",
client_addr.c_str(), username.c_str());
}
co_await async_write(
socket, boost::asio::buffer(std::string("No plan found\r\n")),
deferred);
@@ -83,7 +91,9 @@ awaitable<void> listener(BanTracker &bans) {
auto endpoint = socket.remote_endpoint(ec);
std::string client_addr =
ec ? std::string("unknown") : endpoint.address().to_string();
co_spawn(executor, echo(std::move(socket), std::move(client_addr), bans),
bool trackable = !ec && is_bannable_address(endpoint.address());
co_spawn(executor,
echo(std::move(socket), std::move(client_addr), trackable, bans),
detached);
}
}