Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c96736fa6a | ||
|
|
f8ec4ae924 | ||
|
|
fe7304ac7f | ||
|
|
e5d39c984b | ||
|
|
5e45f21e35 | ||
|
|
5321a238b8 | ||
|
|
083bc14972 | ||
|
|
11aaccba9b |
+31
-2
@@ -1338,6 +1338,23 @@ async function subscribeToConfiguration() {
|
||||
console.log('No configuration events were received');
|
||||
}
|
||||
},
|
||||
async onauth(challenge) {
|
||||
// NIP-42 authentication: relay sent AUTH challenge, sign and respond
|
||||
console.log('🔐 NIP-42 AUTH challenge received:', challenge);
|
||||
try {
|
||||
if (!window.nostr) {
|
||||
console.error('NIP-42 auth required but no nostr extension available');
|
||||
return;
|
||||
}
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const authEvent = window.NostrTools.nip42.makeAuthEvent(url, challenge);
|
||||
const signedAuthEvent = await window.nostr.signEvent(authEvent);
|
||||
console.log('✅ NIP-42 AUTH event signed, responding to challenge');
|
||||
return signedAuthEvent;
|
||||
} catch (authError) {
|
||||
console.error('NIP-42 AUTH failed:', authError.message);
|
||||
}
|
||||
},
|
||||
onclose(reason) {
|
||||
console.log('Subscription closed:', reason);
|
||||
// Reset subscription state to allow re-subscription
|
||||
@@ -5467,9 +5484,21 @@ async function sendAdminCommand(commandArray) {
|
||||
throw new Error('Event signing failed');
|
||||
}
|
||||
|
||||
// Publish via SimplePool with detailed error diagnostics
|
||||
// Publish via SimplePool with NIP-42 auth support
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, {
|
||||
async onauth(challenge) {
|
||||
console.log('🔐 NIP-42 AUTH challenge during publish:', challenge);
|
||||
try {
|
||||
const authEvent = window.NostrTools.nip42.makeAuthEvent(url, challenge);
|
||||
const signedAuthEvent = await window.nostr.signEvent(authEvent);
|
||||
console.log('✅ NIP-42 AUTH signed for publish');
|
||||
return signedAuthEvent;
|
||||
} catch (authError) {
|
||||
console.error('NIP-42 AUTH failed during publish:', authError.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
|
||||
File diff suppressed because one or more lines are too long
+230
-71
@@ -12,18 +12,27 @@
|
||||
// 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 // slot is unused
|
||||
#define IP_BAN_ACTIVE 1 // slot has an entry
|
||||
#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
|
||||
int failure_count; // total failures in current window
|
||||
time_t first_failure; // start of current failure window
|
||||
time_t banned_until; // 0 = not banned; >0 = banned until this time
|
||||
int ban_count; // how many times this IP has been banned (for backoff)
|
||||
int state; // IP_BAN_EMPTY or IP_BAN_ACTIVE
|
||||
char ip[46]; // IPv4 or IPv6 string
|
||||
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)
|
||||
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];
|
||||
@@ -41,19 +50,36 @@ static unsigned int ip_hash(const char* ip) {
|
||||
}
|
||||
|
||||
// Find slot for IP (open addressing with linear probing)
|
||||
// Returns index of existing entry or first empty slot, -1 if table full
|
||||
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; // empty slot — can insert here
|
||||
return (int)idx;
|
||||
}
|
||||
if (strcmp(g_ban_table[idx].ip, ip) == 0) {
|
||||
return (int)idx; // found existing entry
|
||||
return (int)idx;
|
||||
}
|
||||
}
|
||||
return -1; // table full (shouldn't happen with 4096 slots)
|
||||
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) {
|
||||
@@ -64,18 +90,147 @@ void ip_ban_init(void) {
|
||||
DEBUG_LOG("IP ban table initialized (%d slots)", IP_BAN_TABLE_SIZE);
|
||||
}
|
||||
|
||||
void ip_ban_load_from_db(sqlite3* db) {
|
||||
if (!db || !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 (strftime('%s', 'now'))"
|
||||
");";
|
||||
char* err = NULL;
|
||||
if (sqlite3_exec(db, create_sql, NULL, NULL, &err) != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to create ip_bans table: %s", err ? err : "unknown");
|
||||
if (err) sqlite3_free(err);
|
||||
return;
|
||||
}
|
||||
|
||||
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"
|
||||
" FROM ip_bans";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to prepare ip_bans load query");
|
||||
return;
|
||||
}
|
||||
|
||||
int loaded = 0;
|
||||
pthread_mutex_lock(&g_ban_mutex);
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const char* ip = (const char*)sqlite3_column_text(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 = sqlite3_column_int(stmt, 1);
|
||||
entry->ban_count = sqlite3_column_int(stmt, 2);
|
||||
entry->banned_until = (time_t)sqlite3_column_int64(stmt, 3);
|
||||
entry->first_failure = (time_t)sqlite3_column_int64(stmt, 4);
|
||||
entry->has_authed_successfully = sqlite3_column_int(stmt, 5);
|
||||
entry->last_success_at = (time_t)sqlite3_column_int64(stmt, 6);
|
||||
entry->total_connections = sqlite3_column_int(stmt, 7);
|
||||
entry->total_failures = sqlite3_column_int(stmt, 8);
|
||||
entry->total_successes = sqlite3_column_int(stmt, 9);
|
||||
entry->first_seen = (time_t)sqlite3_column_int64(stmt, 10);
|
||||
loaded++;
|
||||
}
|
||||
pthread_mutex_unlock(&g_ban_mutex);
|
||||
sqlite3_finalize(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(sqlite3* db) {
|
||||
if (!db || !g_initialized) return;
|
||||
|
||||
const char* upsert_sql =
|
||||
"INSERT OR REPLACE 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)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'))";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
if (sqlite3_prepare_v2(db, upsert_sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to prepare ip_bans save query");
|
||||
return;
|
||||
}
|
||||
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, NULL);
|
||||
|
||||
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;
|
||||
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_bind_text(stmt, 1, entry->ip, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int(stmt, 2, entry->failure_count);
|
||||
sqlite3_bind_int(stmt, 3, entry->ban_count);
|
||||
sqlite3_bind_int64(stmt, 4, (sqlite3_int64)entry->banned_until);
|
||||
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)entry->first_failure);
|
||||
sqlite3_bind_int(stmt, 6, entry->has_authed_successfully);
|
||||
sqlite3_bind_int64(stmt, 7, (sqlite3_int64)entry->last_success_at);
|
||||
sqlite3_bind_int(stmt, 8, entry->total_connections);
|
||||
sqlite3_bind_int(stmt, 9, entry->total_failures);
|
||||
sqlite3_bind_int(stmt, 10, entry->total_successes);
|
||||
sqlite3_bind_int64(stmt, 11, (sqlite3_int64)entry->first_seen);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
DEBUG_WARN("Failed to save ip_ban entry for %s", entry->ip);
|
||||
} else {
|
||||
saved++;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_ban_mutex);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
|
||||
|
||||
DEBUG_TRACE("IP ban table saved: %d entries written to DB", saved);
|
||||
}
|
||||
|
||||
int ip_ban_is_banned(const char* ip) {
|
||||
if (!ip || !g_initialized) return 0;
|
||||
|
||||
// Check if feature is enabled
|
||||
if (!get_config_bool("auth_fail_ban_enabled", 1)) 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; // no entry — not banned
|
||||
return 0;
|
||||
}
|
||||
|
||||
ip_ban_entry_t* entry = &g_ban_table[idx];
|
||||
@@ -84,10 +239,10 @@ int ip_ban_is_banned(const char* ip) {
|
||||
if (entry->banned_until > 0 && now < entry->banned_until) {
|
||||
pthread_mutex_unlock(&g_ban_mutex);
|
||||
DEBUG_TRACE("IP %s is banned for %ld more seconds", ip, entry->banned_until - now);
|
||||
return 1; // still banned
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Ban expired — clear it but keep the entry for failure tracking
|
||||
// Ban expired — clear it but keep the entry
|
||||
if (entry->banned_until > 0 && now >= entry->banned_until) {
|
||||
entry->banned_until = 0;
|
||||
entry->failure_count = 0;
|
||||
@@ -98,6 +253,17 @@ int ip_ban_is_banned(const char* ip) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -107,41 +273,29 @@ void ip_ban_record_failure(const char* ip) {
|
||||
int ban_duration = get_config_int("auth_fail_ban_duration_sec", 300);
|
||||
|
||||
pthread_mutex_lock(&g_ban_mutex);
|
||||
|
||||
int idx = find_slot(ip);
|
||||
if (idx < 0) {
|
||||
// Table full — can't track this IP, just log and return
|
||||
DEBUG_WARN("IP ban table full, cannot track %s", ip);
|
||||
ip_ban_entry_t* entry = get_or_create_entry(ip);
|
||||
if (!entry) {
|
||||
pthread_mutex_unlock(&g_ban_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
ip_ban_entry_t* entry = &g_ban_table[idx];
|
||||
time_t now = time(NULL);
|
||||
|
||||
if (entry->state == IP_BAN_EMPTY) {
|
||||
// New entry
|
||||
entry->state = IP_BAN_ACTIVE;
|
||||
strncpy(entry->ip, ip, sizeof(entry->ip) - 1);
|
||||
entry->ip[sizeof(entry->ip) - 1] = '\0';
|
||||
entry->failure_count = 0;
|
||||
entry->first_failure = now;
|
||||
entry->banned_until = 0;
|
||||
entry->ban_count = 0;
|
||||
}
|
||||
|
||||
// Reset window if it's expired
|
||||
// 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) {
|
||||
// Apply exponential backoff: ban_duration * 2^ban_count, capped at 24 hours
|
||||
int duration = ban_duration;
|
||||
for (int i = 0; i < entry->ban_count && duration < 86400; i++) {
|
||||
duration *= 2;
|
||||
@@ -150,7 +304,7 @@ void ip_ban_record_failure(const char* ip) {
|
||||
|
||||
entry->banned_until = now + duration;
|
||||
entry->ban_count++;
|
||||
entry->failure_count = 0; // reset counter after ban
|
||||
entry->failure_count = 0;
|
||||
entry->first_failure = 0;
|
||||
|
||||
DEBUG_WARN("IP %s banned for %d seconds (ban #%d) after %d auth failures",
|
||||
@@ -164,16 +318,15 @@ void ip_ban_record_success(const char* ip) {
|
||||
if (!ip || !g_initialized) return;
|
||||
|
||||
pthread_mutex_lock(&g_ban_mutex);
|
||||
|
||||
int idx = find_slot(ip);
|
||||
if (idx >= 0 && g_ban_table[idx].state == IP_BAN_ACTIVE) {
|
||||
// Clear failure count on successful auth — reward good behavior
|
||||
g_ban_table[idx].failure_count = 0;
|
||||
g_ban_table[idx].first_failure = 0;
|
||||
// Note: we keep ban_count so backoff persists across sessions
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -181,36 +334,49 @@ 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 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];
|
||||
|
||||
// Clear entries where ban has expired AND failure window has expired AND no recent activity
|
||||
int ban_expired = (entry->banned_until == 0 || now >= entry->banned_until);
|
||||
int window_expired = (entry->first_failure == 0 || (now - entry->first_failure) > window_sec * 10);
|
||||
|
||||
if (ban_expired && window_expired && entry->failure_count == 0) {
|
||||
memset(entry, 0, sizeof(ip_ban_entry_t));
|
||||
cleaned++;
|
||||
int retain_sec = 86400; // 24 hours
|
||||
int last_ban_expired_long_ago = (entry->banned_until == 0 ||
|
||||
(now - entry->banned_until) > retain_sec);
|
||||
|
||||
if (last_ban_expired_long_ago && !entry->has_authed_successfully &&
|
||||
entry->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;
|
||||
if (last_ban_expired_long_ago) {
|
||||
entry->banned_until = 0;
|
||||
// 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;
|
||||
@@ -226,7 +392,6 @@ int ip_ban_get_banned_count(void) {
|
||||
|
||||
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++) {
|
||||
@@ -236,41 +401,35 @@ int ip_ban_get_tracked_count(void) {
|
||||
return count;
|
||||
}
|
||||
|
||||
// Emit a periodic log summary of banned IPs.
|
||||
// Call from the connection age checker (runs every ~30s).
|
||||
// Only logs when there are active bans or when the interval has elapsed.
|
||||
void ip_ban_log_stats(void) {
|
||||
void ip_ban_log_stats(sqlite3* db) {
|
||||
if (!g_initialized) return;
|
||||
|
||||
static time_t last_log = 0;
|
||||
time_t now = time(NULL);
|
||||
|
||||
// Log every 5 minutes
|
||||
if (now - last_log < 300) return;
|
||||
last_log = now;
|
||||
|
||||
pthread_mutex_lock(&g_ban_mutex);
|
||||
// Save to DB every 5 minutes
|
||||
if (db) {
|
||||
ip_ban_save_to_db(db);
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_ban_mutex);
|
||||
int 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) {
|
||||
banned_count++;
|
||||
DEBUG_WARN("IP BAN: %s banned for %ld more seconds (ban #%d, failures: %d)",
|
||||
g_ban_table[i].ip,
|
||||
g_ban_table[i].banned_until - now,
|
||||
g_ban_table[i].ban_count,
|
||||
g_ban_table[i].failure_count);
|
||||
}
|
||||
if (g_ban_table[i].banned_until > now) banned_count++;
|
||||
if (g_ban_table[i].has_authed_successfully) trusted_count++;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_ban_mutex);
|
||||
|
||||
if (banned_count > 0 || tracked_count > 0) {
|
||||
DEBUG_WARN("IP BAN SUMMARY: %d IPs currently banned, %d IPs tracked",
|
||||
banned_count, tracked_count);
|
||||
DEBUG_WARN("IP BAN SUMMARY: %d banned, %d tracked, %d trusted (ever authed)",
|
||||
banned_count, tracked_count, trusted_count);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-7
@@ -5,7 +5,8 @@
|
||||
//
|
||||
// Tracks auth failures per IP address and temporarily bans IPs that repeatedly
|
||||
// fail NIP-42 authentication. Uses an in-memory fixed-size hash table — no
|
||||
// database writes, no root required, no persistent state.
|
||||
// database writes on the hot path. State is persisted to the ip_bans table
|
||||
// every 5 minutes and loaded at startup.
|
||||
//
|
||||
// Config keys (all read from the config table at runtime):
|
||||
// auth_fail_ban_enabled bool default: true (0 = disabled)
|
||||
@@ -14,13 +15,22 @@
|
||||
// auth_fail_ban_duration_sec int default: 300 (initial ban duration, doubles each time)
|
||||
|
||||
#include <time.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
// Maximum number of IPs tracked simultaneously (fixed-size, no malloc)
|
||||
#define IP_BAN_TABLE_SIZE 4096
|
||||
|
||||
// Initialize the IP ban table (call once at startup)
|
||||
// Initialize the IP ban table (call once at startup, before loading from DB)
|
||||
void ip_ban_init(void);
|
||||
|
||||
// Load ban state from the ip_bans database table.
|
||||
// Call after ip_ban_init() and after the database is open.
|
||||
void ip_ban_load_from_db(sqlite3* db);
|
||||
|
||||
// Save current ban state to the ip_bans database table.
|
||||
// Called every 5 minutes from the maintenance timer.
|
||||
void ip_ban_save_to_db(sqlite3* db);
|
||||
|
||||
// Check if an IP is currently banned.
|
||||
// Returns 1 if banned (connection should be rejected), 0 if allowed.
|
||||
int ip_ban_is_banned(const char* ip);
|
||||
@@ -31,18 +41,21 @@ int ip_ban_is_banned(const char* ip);
|
||||
void ip_ban_record_failure(const char* ip);
|
||||
|
||||
// Record a successful auth for an IP.
|
||||
// Clears any failure count for this IP (reward good behavior).
|
||||
// Sets has_authed_successfully=1 and clears failure count.
|
||||
void ip_ban_record_success(const char* ip);
|
||||
|
||||
// Record a new connection from an IP (increment total_connections).
|
||||
void ip_ban_record_connection(const char* ip);
|
||||
|
||||
// Periodic cleanup: expire old entries (call from the connection age checker).
|
||||
void ip_ban_cleanup(void);
|
||||
|
||||
// Emit a periodic WARN-level log summary of banned IPs (every 5 minutes).
|
||||
// Also saves state to DB.
|
||||
void ip_ban_log_stats(sqlite3* db);
|
||||
|
||||
// Get stats for logging/monitoring
|
||||
int ip_ban_get_banned_count(void);
|
||||
int ip_ban_get_tracked_count(void);
|
||||
|
||||
// Emit a periodic WARN-level log summary of banned IPs (every 5 minutes).
|
||||
// Call from the connection age checker timer.
|
||||
void ip_ban_log_stats(void);
|
||||
|
||||
#endif // IP_BAN_H
|
||||
|
||||
+4
-2
@@ -162,8 +162,9 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header);
|
||||
// Forward declaration for WebSocket relay server
|
||||
int start_websocket_relay(int port_override, int strict_port);
|
||||
|
||||
// Forward declaration for IP ban system
|
||||
// Forward declarations for IP ban system
|
||||
void ip_ban_init(void);
|
||||
void ip_ban_load_from_db(sqlite3* db);
|
||||
|
||||
|
||||
// Forward declarations for NIP-13 PoW handling (now in nip013.c)
|
||||
@@ -2168,8 +2169,9 @@ int main(int argc, char* argv[]) {
|
||||
// Cleanup orphaned subscriptions from previous runs
|
||||
cleanup_all_subscriptions_on_startup();
|
||||
|
||||
// Initialize IP ban table before starting the relay
|
||||
// Initialize IP ban table and load persisted state from database
|
||||
ip_ban_init();
|
||||
ip_ban_load_from_db(g_db);
|
||||
|
||||
// Start WebSocket Nostr relay server (port from CLI override or configuration)
|
||||
int result = start_websocket_relay(cli_options.port_override, cli_options.strict_port); // Use CLI port override if specified, otherwise config
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@
|
||||
// Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define CRELAY_VERSION_MAJOR 1
|
||||
#define CRELAY_VERSION_MINOR 2
|
||||
#define CRELAY_VERSION_PATCH 19
|
||||
#define CRELAY_VERSION "v1.2.19"
|
||||
#define CRELAY_VERSION_PATCH 27
|
||||
#define CRELAY_VERSION "v1.2.27"
|
||||
|
||||
// Relay metadata (authoritative source for NIP-11 information)
|
||||
#define RELAY_NAME "C-Relay"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include "websockets.h"
|
||||
#include "ip_ban.h"
|
||||
|
||||
|
||||
// Forward declaration for notice message function
|
||||
@@ -129,6 +130,9 @@ void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* ps
|
||||
|
||||
// Cancel the auth timeout — client has authenticated, keep connection open
|
||||
lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0);
|
||||
|
||||
// Record successful auth for this IP — marks it as trusted
|
||||
ip_ban_record_success(pss->client_ip);
|
||||
|
||||
send_notice_message(wsi, pss, "NIP-42 authentication successful");
|
||||
} else {
|
||||
|
||||
+24
-16
@@ -463,17 +463,6 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
|
||||
case LWS_CALLBACK_ESTABLISHED:
|
||||
DEBUG_TRACE("WebSocket connection established");
|
||||
|
||||
// Check IP ban before doing any work — reject banned IPs immediately
|
||||
{
|
||||
char check_ip[CLIENT_IP_MAX_LENGTH] = {0};
|
||||
const char* fwd = lws_get_peer_simple(wsi, check_ip, sizeof(check_ip));
|
||||
(void)fwd;
|
||||
if (ip_ban_is_banned(check_ip)) {
|
||||
DEBUG_LOG("Rejecting banned IP %s at connection establishment", check_ip);
|
||||
return -1; // Close connection immediately
|
||||
}
|
||||
}
|
||||
memset(pss, 0, sizeof(*pss));
|
||||
pthread_mutex_init(&pss->session_lock, NULL);
|
||||
|
||||
@@ -558,6 +547,17 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
pss->challenge_created = 0;
|
||||
pss->challenge_expires = 0;
|
||||
|
||||
// Record connection for stats tracking
|
||||
ip_ban_record_connection(pss->client_ip);
|
||||
|
||||
// Check IP ban using the resolved client IP (which may be from X-Forwarded-For).
|
||||
// This must happen AFTER pss->client_ip is populated so the same IP string
|
||||
// is used for both ban recording (at CLOSED) and ban checking (here).
|
||||
if (ip_ban_is_banned(pss->client_ip)) {
|
||||
DEBUG_LOG("Rejecting banned IP %s at connection establishment", pss->client_ip);
|
||||
return -1; // Close connection immediately — no challenge, no processing
|
||||
}
|
||||
|
||||
// Set libwebsockets auth timeout: if NIP-42 auth is required and the client
|
||||
// doesn't authenticate within nip42_auth_timeout_sec seconds, lws will close
|
||||
// the connection automatically — even if the client never sends a message.
|
||||
@@ -2293,9 +2293,10 @@ static void check_connection_age(int max_connection_seconds) {
|
||||
// Cleanup
|
||||
free(checked_wsis);
|
||||
|
||||
// Periodic IP ban maintenance: cleanup expired entries and log stats
|
||||
// Periodic IP ban maintenance: cleanup expired entries, log stats, save to DB
|
||||
ip_ban_cleanup();
|
||||
ip_ban_log_stats();
|
||||
extern sqlite3* g_db;
|
||||
ip_ban_log_stats(g_db);
|
||||
}
|
||||
|
||||
// WebSocket protocol definition
|
||||
@@ -2492,11 +2493,18 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check connection age limits (every 60 seconds)
|
||||
// Check connection age limits and run IP ban maintenance (every 60 seconds)
|
||||
int max_connection_seconds = get_config_int("max_connection_seconds", 86400);
|
||||
if (max_connection_seconds > 0 && (current_time - last_connection_age_check >= 60)) {
|
||||
if (current_time - last_connection_age_check >= 60) {
|
||||
last_connection_age_check = current_time;
|
||||
check_connection_age(max_connection_seconds);
|
||||
if (max_connection_seconds > 0) {
|
||||
check_connection_age(max_connection_seconds);
|
||||
} else {
|
||||
// Even when connection age limit is disabled, run IP ban maintenance
|
||||
ip_ban_cleanup();
|
||||
extern sqlite3* g_db;
|
||||
ip_ban_log_stats(g_db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user