621 lines
22 KiB
C
621 lines
22 KiB
C
#define _GNU_SOURCE
|
|
#include "ip_ban.h"
|
|
#include "debug.h"
|
|
#include "config.h"
|
|
#include "db_ops.h"
|
|
#include "thread_pool.h"
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <pthread.h>
|
|
|
|
// ============================================================
|
|
// IP Auth Failure Ban System
|
|
//
|
|
// Fixed-size open-addressing hash table. No malloc after init.
|
|
// Thread-safe via a single mutex (low contention — only called
|
|
// at connection open/close, not in the hot event path).
|
|
//
|
|
// State is persisted to the ip_bans SQLite table every 5 minutes
|
|
// and loaded at startup so bans survive relay restarts.
|
|
// ============================================================
|
|
|
|
#define IP_BAN_EMPTY 0
|
|
#define IP_BAN_ACTIVE 1
|
|
|
|
typedef struct {
|
|
int state; // IP_BAN_EMPTY or IP_BAN_ACTIVE
|
|
char ip[46]; // IPv4 or IPv6 string
|
|
|
|
// Auth failure tracking (existing)
|
|
int failure_count; // failures in current window
|
|
time_t first_failure; // start of current failure window
|
|
time_t banned_until; // 0 = not banned
|
|
int ban_count; // escalation level (for exponential backoff)
|
|
|
|
// NEW: Idle failure tracking (separate)
|
|
int idle_failure_count;
|
|
time_t idle_first_failure;
|
|
time_t idle_banned_until;
|
|
int idle_ban_count;
|
|
|
|
// Other existing fields
|
|
int has_authed_successfully; // 1 if this IP has ever authenticated
|
|
time_t last_success_at; // timestamp of last successful auth
|
|
int total_connections; // lifetime connection count
|
|
int total_failures; // lifetime auth failure count
|
|
int total_successes; // lifetime successful auth count
|
|
time_t first_seen; // when this IP was first seen
|
|
} ip_ban_entry_t;
|
|
|
|
static ip_ban_entry_t g_ban_table[IP_BAN_TABLE_SIZE];
|
|
static pthread_mutex_t g_ban_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
static int g_initialized = 0;
|
|
|
|
// Simple FNV-1a hash for IP strings
|
|
static unsigned int ip_hash(const char* ip) {
|
|
unsigned int hash = 2166136261u;
|
|
while (*ip) {
|
|
hash ^= (unsigned char)*ip++;
|
|
hash *= 16777619u;
|
|
}
|
|
return hash % IP_BAN_TABLE_SIZE;
|
|
}
|
|
|
|
// Find slot for IP (open addressing with linear probing)
|
|
static int find_slot(const char* ip) {
|
|
unsigned int start = ip_hash(ip);
|
|
for (unsigned int i = 0; i < IP_BAN_TABLE_SIZE; i++) {
|
|
unsigned int idx = (start + i) % IP_BAN_TABLE_SIZE;
|
|
if (g_ban_table[idx].state == IP_BAN_EMPTY) {
|
|
return (int)idx;
|
|
}
|
|
if (strcmp(g_ban_table[idx].ip, ip) == 0) {
|
|
return (int)idx;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Get or create an entry for an IP. Returns NULL if table is full.
|
|
// Caller must hold g_ban_mutex.
|
|
static ip_ban_entry_t* get_or_create_entry(const char* ip) {
|
|
int idx = find_slot(ip);
|
|
if (idx < 0) {
|
|
DEBUG_WARN("IP ban table full, cannot track %s", ip);
|
|
return NULL;
|
|
}
|
|
ip_ban_entry_t* entry = &g_ban_table[idx];
|
|
if (entry->state == IP_BAN_EMPTY) {
|
|
entry->state = IP_BAN_ACTIVE;
|
|
strncpy(entry->ip, ip, sizeof(entry->ip) - 1);
|
|
entry->ip[sizeof(entry->ip) - 1] = '\0';
|
|
entry->first_seen = time(NULL);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
void ip_ban_init(void) {
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
memset(g_ban_table, 0, sizeof(g_ban_table));
|
|
g_initialized = 1;
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
DEBUG_LOG("IP ban table initialized (%d slots)", IP_BAN_TABLE_SIZE);
|
|
}
|
|
|
|
void ip_ban_load_from_db(void) {
|
|
if (!db_is_available() || !g_initialized) return;
|
|
|
|
// Create table if it doesn't exist (handles existing databases)
|
|
const char* create_sql =
|
|
"CREATE TABLE IF NOT EXISTS ip_bans ("
|
|
" ip TEXT PRIMARY KEY,"
|
|
" failure_count INTEGER NOT NULL DEFAULT 0,"
|
|
" ban_count INTEGER NOT NULL DEFAULT 0,"
|
|
" banned_until INTEGER NOT NULL DEFAULT 0,"
|
|
" first_failure INTEGER NOT NULL DEFAULT 0,"
|
|
" has_authed_successfully INTEGER NOT NULL DEFAULT 0,"
|
|
" last_success_at INTEGER NOT NULL DEFAULT 0,"
|
|
" total_connections INTEGER NOT NULL DEFAULT 0,"
|
|
" total_failures INTEGER NOT NULL DEFAULT 0,"
|
|
" total_successes INTEGER NOT NULL DEFAULT 0,"
|
|
" first_seen INTEGER NOT NULL DEFAULT 0,"
|
|
" updated_at INTEGER NOT NULL DEFAULT 0"
|
|
");";
|
|
if (db_exec_sql(create_sql) != 0) {
|
|
DEBUG_ERROR("Failed to create ip_bans table: %s", db_last_error());
|
|
return;
|
|
}
|
|
|
|
// Migration: Add idle_* columns if they don't exist (ignore errors if already exists)
|
|
(void)db_exec_sql("ALTER TABLE ip_bans ADD COLUMN idle_failure_count INTEGER NOT NULL DEFAULT 0");
|
|
(void)db_exec_sql("ALTER TABLE ip_bans ADD COLUMN idle_ban_count INTEGER NOT NULL DEFAULT 0");
|
|
(void)db_exec_sql("ALTER TABLE ip_bans ADD COLUMN idle_banned_until INTEGER NOT NULL DEFAULT 0");
|
|
(void)db_exec_sql("ALTER TABLE ip_bans ADD COLUMN idle_first_failure INTEGER NOT NULL DEFAULT 0");
|
|
|
|
const char* sql =
|
|
"SELECT ip, failure_count, ban_count, banned_until, first_failure,"
|
|
" has_authed_successfully, last_success_at, total_connections,"
|
|
" total_failures, total_successes, first_seen,"
|
|
" idle_failure_count, idle_ban_count, idle_banned_until, idle_first_failure"
|
|
" FROM ip_bans";
|
|
|
|
db_stmt_t* stmt;
|
|
if (db_prepare(sql, &stmt) != DB_OK) {
|
|
DEBUG_ERROR("Failed to prepare ip_bans load query");
|
|
return;
|
|
}
|
|
|
|
int loaded = 0;
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
while (db_step_stmt(stmt) == DB_ROW) {
|
|
const char* ip = db_column_text_value(stmt, 0);
|
|
if (!ip) continue;
|
|
|
|
int idx = find_slot(ip);
|
|
if (idx < 0) continue;
|
|
|
|
ip_ban_entry_t* entry = &g_ban_table[idx];
|
|
entry->state = IP_BAN_ACTIVE;
|
|
strncpy(entry->ip, ip, sizeof(entry->ip) - 1);
|
|
entry->ip[sizeof(entry->ip) - 1] = '\0';
|
|
entry->failure_count = db_column_int_value(stmt, 1);
|
|
entry->ban_count = db_column_int_value(stmt, 2);
|
|
entry->banned_until = (time_t)db_column_int64_value(stmt, 3);
|
|
entry->first_failure = (time_t)db_column_int64_value(stmt, 4);
|
|
entry->has_authed_successfully = db_column_int_value(stmt, 5);
|
|
entry->last_success_at = (time_t)db_column_int64_value(stmt, 6);
|
|
entry->total_connections = db_column_int_value(stmt, 7);
|
|
entry->total_failures = db_column_int_value(stmt, 8);
|
|
entry->total_successes = db_column_int_value(stmt, 9);
|
|
entry->first_seen = (time_t)db_column_int64_value(stmt, 10);
|
|
// Load idle tracking fields (default to 0 if columns don't exist yet)
|
|
entry->idle_failure_count = db_column_int_value(stmt, 11);
|
|
entry->idle_ban_count = db_column_int_value(stmt, 12);
|
|
entry->idle_banned_until = (time_t)db_column_int64_value(stmt, 13);
|
|
entry->idle_first_failure = (time_t)db_column_int64_value(stmt, 14);
|
|
loaded++;
|
|
}
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
db_finalize_stmt(stmt);
|
|
|
|
// Count how many are still actively banned
|
|
time_t now = time(NULL);
|
|
int still_banned = 0;
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
for (int i = 0; i < IP_BAN_TABLE_SIZE; i++) {
|
|
if (g_ban_table[i].state == IP_BAN_ACTIVE &&
|
|
g_ban_table[i].banned_until > now) {
|
|
still_banned++;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
|
|
DEBUG_WARN("IP ban table loaded: %d IPs restored (%d still banned)", loaded, still_banned);
|
|
}
|
|
|
|
void ip_ban_save_to_db(void) {
|
|
if (!db_is_available() || !g_initialized) return;
|
|
|
|
const char* upsert_sql =
|
|
"INSERT INTO ip_bans"
|
|
" (ip, failure_count, ban_count, banned_until, first_failure,"
|
|
" has_authed_successfully, last_success_at, total_connections,"
|
|
" total_failures, total_successes, first_seen, updated_at,"
|
|
" idle_failure_count, idle_ban_count, idle_banned_until, idle_first_failure)"
|
|
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
" ON CONFLICT(ip) DO UPDATE SET"
|
|
" failure_count=excluded.failure_count,"
|
|
" ban_count=excluded.ban_count,"
|
|
" banned_until=excluded.banned_until,"
|
|
" first_failure=excluded.first_failure,"
|
|
" has_authed_successfully=excluded.has_authed_successfully,"
|
|
" last_success_at=excluded.last_success_at,"
|
|
" total_connections=excluded.total_connections,"
|
|
" total_failures=excluded.total_failures,"
|
|
" total_successes=excluded.total_successes,"
|
|
" first_seen=excluded.first_seen,"
|
|
" updated_at=excluded.updated_at,"
|
|
" idle_failure_count=excluded.idle_failure_count,"
|
|
" idle_ban_count=excluded.idle_ban_count,"
|
|
" idle_banned_until=excluded.idle_banned_until,"
|
|
" idle_first_failure=excluded.idle_first_failure";
|
|
|
|
db_stmt_t* stmt;
|
|
if (db_prepare(upsert_sql, &stmt) != DB_OK) {
|
|
DEBUG_ERROR("Failed to prepare ip_bans save query");
|
|
return;
|
|
}
|
|
|
|
(void)db_exec_sql("BEGIN TRANSACTION");
|
|
|
|
int saved = 0;
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
for (int i = 0; i < IP_BAN_TABLE_SIZE; i++) {
|
|
ip_ban_entry_t* entry = &g_ban_table[i];
|
|
if (entry->state != IP_BAN_ACTIVE) continue;
|
|
|
|
(void)db_reset_stmt(stmt);
|
|
db_bind_text_param(stmt, 1, entry->ip);
|
|
db_bind_int_param(stmt, 2, entry->failure_count);
|
|
db_bind_int_param(stmt, 3, entry->ban_count);
|
|
db_bind_int64_param(stmt, 4, (long long)entry->banned_until);
|
|
db_bind_int64_param(stmt, 5, (long long)entry->first_failure);
|
|
db_bind_int_param(stmt, 6, entry->has_authed_successfully);
|
|
db_bind_int64_param(stmt, 7, (long long)entry->last_success_at);
|
|
db_bind_int_param(stmt, 8, entry->total_connections);
|
|
db_bind_int_param(stmt, 9, entry->total_failures);
|
|
db_bind_int_param(stmt, 10, entry->total_successes);
|
|
db_bind_int64_param(stmt, 11, (long long)entry->first_seen);
|
|
db_bind_int64_param(stmt, 12, (long long)time(NULL));
|
|
// Idle tracking fields
|
|
db_bind_int_param(stmt, 13, entry->idle_failure_count);
|
|
db_bind_int_param(stmt, 14, entry->idle_ban_count);
|
|
db_bind_int64_param(stmt, 15, (long long)entry->idle_banned_until);
|
|
db_bind_int64_param(stmt, 16, (long long)entry->idle_first_failure);
|
|
|
|
if (db_step_stmt(stmt) != DB_DONE) {
|
|
DEBUG_WARN("Failed to save ip_ban entry for %s", entry->ip);
|
|
} else {
|
|
saved++;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
|
|
db_finalize_stmt(stmt);
|
|
(void)db_exec_sql("COMMIT");
|
|
|
|
DEBUG_TRACE("IP ban table saved: %d entries written to DB", saved);
|
|
}
|
|
|
|
// Check if an IP is in the idle_ban_whitelist config (comma-separated list)
|
|
static int ip_is_whitelisted(const char* ip) {
|
|
if (!ip) {
|
|
return 0;
|
|
}
|
|
|
|
// Always trust loopback to avoid local test/dev self-bans.
|
|
if (strcmp(ip, "127.0.0.1") == 0 || strcmp(ip, "::1") == 0) {
|
|
return 1;
|
|
}
|
|
|
|
const char* whitelist = get_config_value("idle_ban_whitelist");
|
|
if (!whitelist || whitelist[0] == '\0') {
|
|
if (whitelist) free((char*)whitelist);
|
|
return 0;
|
|
}
|
|
|
|
// Make a mutable copy to tokenize
|
|
char buf[1024];
|
|
strncpy(buf, whitelist, sizeof(buf) - 1);
|
|
buf[sizeof(buf) - 1] = '\0';
|
|
|
|
int is_match = 0;
|
|
char* token = strtok(buf, ",");
|
|
while (token) {
|
|
// Trim leading/trailing spaces
|
|
while (*token == ' ') token++;
|
|
char* end = token + strlen(token) - 1;
|
|
while (end > token && *end == ' ') { *end = '\0'; end--; }
|
|
if (strcmp(token, ip) == 0) {
|
|
is_match = 1;
|
|
break;
|
|
}
|
|
token = strtok(NULL, ",");
|
|
}
|
|
|
|
free((char*)whitelist);
|
|
return is_match;
|
|
}
|
|
|
|
int ip_ban_is_banned(const char* ip) {
|
|
if (!ip || !g_initialized) return 0;
|
|
|
|
// Whitelisted IPs are never banned
|
|
if (ip_is_whitelisted(ip)) return 0;
|
|
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
int idx = find_slot(ip);
|
|
if (idx < 0 || g_ban_table[idx].state == IP_BAN_EMPTY) {
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
return 0;
|
|
}
|
|
|
|
ip_ban_entry_t* entry = &g_ban_table[idx];
|
|
time_t now = time(NULL);
|
|
int banned = 0;
|
|
|
|
// Check auth ban (if enabled)
|
|
if (get_config_bool("auth_fail_ban_enabled", 1) &&
|
|
entry->banned_until > 0 && now < entry->banned_until) {
|
|
banned = 1;
|
|
}
|
|
|
|
// Check idle ban (if enabled)
|
|
if (get_config_bool("idle_ban_enabled", 1) &&
|
|
entry->idle_banned_until > 0 && now < entry->idle_banned_until) {
|
|
banned = 1;
|
|
}
|
|
|
|
// Clear expired bans
|
|
if (!banned) {
|
|
if (entry->banned_until > 0 && now >= entry->banned_until) {
|
|
entry->banned_until = 0;
|
|
entry->failure_count = 0;
|
|
entry->first_failure = 0;
|
|
}
|
|
if (entry->idle_banned_until > 0 && now >= entry->idle_banned_until) {
|
|
entry->idle_banned_until = 0;
|
|
entry->idle_failure_count = 0;
|
|
entry->idle_first_failure = 0;
|
|
}
|
|
}
|
|
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
return banned;
|
|
}
|
|
|
|
void ip_ban_record_connection(const char* ip) {
|
|
if (!ip || !g_initialized) return;
|
|
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
ip_ban_entry_t* entry = get_or_create_entry(ip);
|
|
if (entry) {
|
|
entry->total_connections++;
|
|
}
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
}
|
|
|
|
void ip_ban_record_failure(const char* ip) {
|
|
if (!ip || !g_initialized) return;
|
|
if (!get_config_bool("auth_fail_ban_enabled", 1)) return;
|
|
|
|
int threshold = get_config_int("auth_fail_ban_threshold", 3);
|
|
int window_sec = get_config_int("auth_fail_window_sec", 60);
|
|
int ban_duration = get_config_int("auth_fail_ban_duration_sec", 300);
|
|
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
ip_ban_entry_t* entry = get_or_create_entry(ip);
|
|
if (!entry) {
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
return;
|
|
}
|
|
|
|
time_t now = time(NULL);
|
|
|
|
// Reset window if expired
|
|
if (entry->first_failure > 0 && (now - entry->first_failure) > window_sec) {
|
|
entry->failure_count = 0;
|
|
entry->first_failure = now;
|
|
}
|
|
if (entry->first_failure == 0) {
|
|
entry->first_failure = now;
|
|
}
|
|
|
|
entry->failure_count++;
|
|
entry->total_failures++;
|
|
|
|
DEBUG_TRACE("IP %s auth failure count: %d/%d", ip, entry->failure_count, threshold);
|
|
|
|
if (entry->failure_count >= threshold) {
|
|
int duration = ban_duration;
|
|
for (int i = 0; i < entry->ban_count && duration < 86400; i++) {
|
|
duration *= 2;
|
|
}
|
|
if (duration > 86400) duration = 86400;
|
|
|
|
entry->banned_until = now + duration;
|
|
entry->ban_count++;
|
|
entry->failure_count = 0;
|
|
entry->first_failure = 0;
|
|
|
|
DEBUG_WARN("IP %s banned for %d seconds (ban #%d) after %d auth failures",
|
|
ip, duration, entry->ban_count, threshold);
|
|
}
|
|
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
}
|
|
|
|
// Record an idle/early-disconnect failure for an IP
|
|
void ip_ban_record_idle_failure(const char* ip) {
|
|
if (!ip || !g_initialized) return;
|
|
if (!get_config_bool("idle_ban_enabled", 1)) return;
|
|
if (ip_is_whitelisted(ip)) return; // Never record idle failures for whitelisted IPs
|
|
|
|
int threshold = get_config_int("idle_ban_threshold", 1);
|
|
int window_sec = get_config_int("idle_ban_window_sec", 30);
|
|
int ban_duration = get_config_int("idle_ban_duration_sec", 300);
|
|
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
ip_ban_entry_t* entry = get_or_create_entry(ip);
|
|
if (!entry) {
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
return;
|
|
}
|
|
|
|
time_t now = time(NULL);
|
|
|
|
// Reset window if expired
|
|
if (entry->idle_first_failure > 0 && (now - entry->idle_first_failure) > window_sec) {
|
|
entry->idle_failure_count = 0;
|
|
entry->idle_first_failure = now;
|
|
}
|
|
if (entry->idle_first_failure == 0) {
|
|
entry->idle_first_failure = now;
|
|
}
|
|
|
|
entry->idle_failure_count++;
|
|
entry->total_failures++;
|
|
|
|
DEBUG_TRACE("IP %s idle failure count: %d/%d", ip, entry->idle_failure_count, threshold);
|
|
|
|
if (entry->idle_failure_count >= threshold) {
|
|
int duration = ban_duration;
|
|
for (int i = 0; i < entry->idle_ban_count && duration < 86400; i++) {
|
|
duration *= 2;
|
|
}
|
|
if (duration > 86400) duration = 86400;
|
|
|
|
entry->idle_banned_until = now + duration;
|
|
entry->idle_ban_count++;
|
|
entry->idle_failure_count = 0;
|
|
entry->idle_first_failure = 0;
|
|
|
|
DEBUG_WARN("IP %s banned for %d seconds (idle ban #%d) after %d idle failures",
|
|
ip, duration, entry->idle_ban_count, threshold);
|
|
}
|
|
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
}
|
|
|
|
void ip_ban_record_success(const char* ip) {
|
|
if (!ip || !g_initialized) return;
|
|
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
ip_ban_entry_t* entry = get_or_create_entry(ip);
|
|
if (entry) {
|
|
entry->failure_count = 0;
|
|
entry->first_failure = 0;
|
|
entry->has_authed_successfully = 1;
|
|
entry->last_success_at = time(NULL);
|
|
entry->total_successes++;
|
|
DEBUG_TRACE("IP %s authenticated successfully — failure count cleared", ip);
|
|
}
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
}
|
|
|
|
void ip_ban_cleanup(void) {
|
|
if (!g_initialized) return;
|
|
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
time_t now = time(NULL);
|
|
int window_sec = get_config_int("auth_fail_window_sec", 60);
|
|
int idle_window_sec = get_config_int("idle_ban_window_sec", 60);
|
|
int cleaned = 0;
|
|
|
|
for (int i = 0; i < IP_BAN_TABLE_SIZE; i++) {
|
|
if (g_ban_table[i].state != IP_BAN_ACTIVE) continue;
|
|
ip_ban_entry_t* entry = &g_ban_table[i];
|
|
|
|
// Check auth failure window expiration
|
|
int auth_ban_expired = (entry->banned_until == 0 || now >= entry->banned_until);
|
|
int auth_window_expired = (entry->first_failure == 0 || (now - entry->first_failure) > window_sec * 10);
|
|
|
|
// Check idle failure window expiration
|
|
int idle_ban_expired = (entry->idle_banned_until == 0 || now >= entry->idle_banned_until);
|
|
int idle_window_expired = (entry->idle_first_failure == 0 || (now - entry->idle_first_failure) > idle_window_sec * 10);
|
|
|
|
if (auth_ban_expired && auth_window_expired && entry->failure_count == 0 &&
|
|
idle_ban_expired && idle_window_expired && entry->idle_failure_count == 0) {
|
|
int retain_sec = 86400; // 24 hours
|
|
int last_auth_ban_expired_long_ago = (entry->banned_until == 0 ||
|
|
(now - entry->banned_until) > retain_sec);
|
|
int last_idle_ban_expired_long_ago = (entry->idle_banned_until == 0 ||
|
|
(now - entry->idle_banned_until) > retain_sec);
|
|
|
|
if (last_auth_ban_expired_long_ago && last_idle_ban_expired_long_ago &&
|
|
!entry->has_authed_successfully &&
|
|
entry->ban_count == 0 && entry->idle_ban_count == 0 &&
|
|
entry->total_connections <= 1) {
|
|
// Fully clean — never banned, never authenticated, only seen once
|
|
memset(entry, 0, sizeof(ip_ban_entry_t));
|
|
cleaned++;
|
|
} else {
|
|
// Keep entry permanently — preserve ban_count for escalation.
|
|
// An IP that has been banned before will always get at least a 24-hour ban
|
|
// if it fails auth again, regardless of how long it has been away.
|
|
entry->failure_count = 0;
|
|
entry->first_failure = 0;
|
|
entry->idle_failure_count = 0;
|
|
entry->idle_first_failure = 0;
|
|
if (last_auth_ban_expired_long_ago) {
|
|
entry->banned_until = 0;
|
|
// ban_count intentionally NOT reset — permanent escalation
|
|
}
|
|
if (last_idle_ban_expired_long_ago) {
|
|
entry->idle_banned_until = 0;
|
|
// idle_ban_count intentionally NOT reset — permanent escalation
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cleaned > 0) {
|
|
DEBUG_TRACE("IP ban cleanup: freed %d stale entries", cleaned);
|
|
}
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
}
|
|
|
|
int ip_ban_get_banned_count(void) {
|
|
if (!g_initialized) return 0;
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
time_t now = time(NULL);
|
|
int count = 0;
|
|
for (int i = 0; i < IP_BAN_TABLE_SIZE; i++) {
|
|
if (g_ban_table[i].state != IP_BAN_ACTIVE) continue;
|
|
// Count if either auth banned or idle banned
|
|
if (g_ban_table[i].banned_until > now ||
|
|
g_ban_table[i].idle_banned_until > now) {
|
|
count++;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
return count;
|
|
}
|
|
|
|
int ip_ban_get_tracked_count(void) {
|
|
if (!g_initialized) return 0;
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
int count = 0;
|
|
for (int i = 0; i < IP_BAN_TABLE_SIZE; i++) {
|
|
if (g_ban_table[i].state == IP_BAN_ACTIVE) count++;
|
|
}
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
return count;
|
|
}
|
|
|
|
void ip_ban_log_stats(void) {
|
|
if (!g_initialized) return;
|
|
|
|
static time_t last_log = 0;
|
|
time_t now = time(NULL);
|
|
|
|
if (now - last_log < 300) return;
|
|
last_log = now;
|
|
|
|
// Save to DB every 5 minutes
|
|
if (thread_pool_is_running()) {
|
|
thread_pool_job_t job;
|
|
memset(&job, 0, sizeof(job));
|
|
job.type = THREAD_POOL_JOB_IP_BAN_SAVE;
|
|
|
|
thread_pool_status_t rc = thread_pool_submit_write(&job, NULL);
|
|
if (rc != THREAD_POOL_STATUS_OK) {
|
|
DEBUG_WARN("Failed to queue IP_BAN_SAVE job (%d); saving synchronously", rc);
|
|
ip_ban_save_to_db();
|
|
}
|
|
} else {
|
|
ip_ban_save_to_db();
|
|
}
|
|
|
|
pthread_mutex_lock(&g_ban_mutex);
|
|
int auth_banned_count = 0;
|
|
int idle_banned_count = 0;
|
|
int tracked_count = 0;
|
|
int trusted_count = 0;
|
|
|
|
for (int i = 0; i < IP_BAN_TABLE_SIZE; i++) {
|
|
if (g_ban_table[i].state != IP_BAN_ACTIVE) continue;
|
|
tracked_count++;
|
|
if (g_ban_table[i].banned_until > now) auth_banned_count++;
|
|
if (g_ban_table[i].idle_banned_until > now) idle_banned_count++;
|
|
if (g_ban_table[i].has_authed_successfully) trusted_count++;
|
|
}
|
|
pthread_mutex_unlock(&g_ban_mutex);
|
|
|
|
int total_banned = auth_banned_count + idle_banned_count;
|
|
if (total_banned > 0 || tracked_count > 0) {
|
|
DEBUG_WARN("IP BAN SUMMARY: %d auth-banned, %d idle-banned, %d tracked, %d trusted (ever authed)",
|
|
auth_banned_count, idle_banned_count, tracked_count, trusted_count);
|
|
}
|
|
}
|