diff options
| author | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
|---|---|---|
| committer | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
| commit | 20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch) | |
| tree | 2857f41513a56ad41af97b57362639298aa7f033 /src/core/security_ban.c | |
#2
Diffstat (limited to 'src/core/security_ban.c')
| -rw-r--r-- | src/core/security_ban.c | 1213 |
1 files changed, 1213 insertions, 0 deletions
diff --git a/src/core/security_ban.c b/src/core/security_ban.c new file mode 100644 index 0000000..8fc660f --- /dev/null +++ b/src/core/security_ban.c @@ -0,0 +1,1213 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/security_ban.h" +#include "hybbx/security.h" +#include "hybbx/config.h" +#include "hybbx/limits.h" +#include "hybbx/socket.h" +#include "hybbx/storage.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <ctype.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <time.h> + +typedef struct hybbx_security_cfg { + int enabled; + unsigned maxretry; + unsigned findtime_sec; + unsigned bantime_sec; + unsigned abuse_maxretry; + unsigned abuse_findtime_sec; + int telnet; + int ssh; + int websocket; + int circuit; + unsigned rate_limit; + unsigned rate_window_sec; + hybbx_ban_backend_t backend; +} hybbx_security_cfg_t; + +typedef struct ban_entry { + char ip[HYBBX_REMOTE_ADDR_MAX]; + time_t expire_at; + int active; +} ban_entry_t; + +typedef struct fail_entry { + char ip[HYBBX_REMOTE_ADDR_MAX]; + time_t stamps[HYBBX_SECURITY_DEFAULT_MAXRETRY]; + unsigned count; + int active; +} fail_entry_t; + +typedef struct rate_entry { + char ip[HYBBX_REMOTE_ADDR_MAX]; + time_t stamps[32]; + unsigned count; + int active; +} rate_entry_t; + +typedef struct ban_callid_entry { + char callid[HYBBX_CALLID_MAX]; + time_t expire_at; + int active; + int permanent; +} ban_callid_entry_t; + +typedef struct callid_track_entry { + char callid[HYBBX_CALLID_MAX]; + time_t stamps[HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY]; + unsigned count; + int active; +} callid_track_entry_t; + +static hybbx_security_cfg_t g_cfg; +static ban_entry_t g_bans[HYBBX_SECURITY_BAN_MAX]; +static ban_callid_entry_t g_callid_bans[HYBBX_SECURITY_BAN_MAX]; +static fail_entry_t g_fails[HYBBX_SECURITY_TRACK_MAX]; +static fail_entry_t g_abuse[HYBBX_SECURITY_TRACK_MAX]; +static callid_track_entry_t g_callid_fails[HYBBX_SECURITY_TRACK_MAX]; +static callid_track_entry_t g_callid_abuse[HYBBX_SECURITY_TRACK_MAX]; +static rate_entry_t g_rates[HYBBX_SECURITY_TRACK_MAX]; +static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; + +static void security_cfg_defaults(hybbx_security_cfg_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->enabled = 1; + cfg->maxretry = HYBBX_SECURITY_DEFAULT_MAXRETRY; + cfg->findtime_sec = HYBBX_SECURITY_DEFAULT_FINDTIME_SEC; + cfg->bantime_sec = HYBBX_SECURITY_DEFAULT_BANTIME_SEC; + cfg->abuse_maxretry = HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY; + cfg->abuse_findtime_sec = HYBBX_SECURITY_DEFAULT_ABUSE_FINDTIME_SEC; + cfg->telnet = 1; + cfg->ssh = 1; + cfg->websocket = 1; + cfg->circuit = 1; + cfg->rate_limit = HYBBX_SECURITY_DEFAULT_RATE_LIMIT; + cfg->rate_window_sec = HYBBX_SECURITY_DEFAULT_RATE_WINDOW_SEC; + cfg->backend = HYBBX_BAN_BACKEND_INTERNAL; +} + +static hybbx_ban_backend_t parse_ban_backend(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_BAN_BACKEND_INTERNAL; + } + + if (strcasecmp(value, "log") == 0) { + return HYBBX_BAN_BACKEND_LOG; + } + if (strcasecmp(value, "iptables") == 0) { + return HYBBX_BAN_BACKEND_IPTABLES; + } + if (strcasecmp(value, "nftables") == 0) { + return HYBBX_BAN_BACKEND_NFTABLES; + } + if (strcasecmp(value, "hosts") == 0) { + return HYBBX_BAN_BACKEND_HOSTS; + } + + return HYBBX_BAN_BACKEND_INTERNAL; +} + +static unsigned parse_uint_clamp(const char *value, unsigned default_value, + unsigned max_value) +{ + char *end = NULL; + unsigned long n; + + if (value == NULL || value[0] == '\0') { + return default_value; + } + + n = strtoul(value, &end, 10); + if (end == value || (end != NULL && *end != '\0')) { + return default_value; + } + + if (n > max_value) { + return max_value; + } + + return (unsigned)n; +} + +static int ip_valid(const char *ip) +{ + return ip != NULL && ip[0] != '\0' && strcmp(ip, "?") != 0; +} + +int hybbx_security_callid_normalize(const char *in, char *out, size_t out_cap) +{ + size_t len; + size_t i; + int has_alpha = 0; + + if (in == NULL || out == NULL || out_cap < 2u) { + return 0; + } + + while (*in == ' ' || *in == '\t') { + in++; + } + + len = 0; + for (i = 0; in[i] != '\0'; i++) { + unsigned char ch = (unsigned char)in[i]; + + if (ch == ' ' || ch == '\t') { + break; + } + + if (len + 1 >= out_cap) { + return 0; + } + + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { + out[len++] = (char)toupper(ch); + has_alpha = 1; + } else if (ch >= '0' && ch <= '9') { + out[len++] = (char)ch; + } else if (ch == '-' || ch == '_' || ch == '.') { + out[len++] = (char)ch; + } else { + return 0; + } + } + + out[len] = '\0'; + return len > 0 && has_alpha; +} + +static ban_callid_entry_t *callid_ban_find(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_callid_bans[i].active && + strcmp(g_callid_bans[i].callid, callid) == 0) { + return &g_callid_bans[i]; + } + } + + return NULL; +} + +static ban_callid_entry_t *callid_ban_alloc(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (!g_callid_bans[i].active) { + hybbx_strlcpy(g_callid_bans[i].callid, callid, + sizeof(g_callid_bans[i].callid)); + g_callid_bans[i].active = 1; + return &g_callid_bans[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_fail_find(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_callid_fails[i].active && + strcmp(g_callid_fails[i].callid, callid) == 0) { + return &g_callid_fails[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_fail_alloc(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_callid_fails[i].active) { + hybbx_strlcpy(g_callid_fails[i].callid, callid, + sizeof(g_callid_fails[i].callid)); + g_callid_fails[i].count = 0; + g_callid_fails[i].active = 1; + return &g_callid_fails[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_abuse_find(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_callid_abuse[i].active && + strcmp(g_callid_abuse[i].callid, callid) == 0) { + return &g_callid_abuse[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_abuse_alloc(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_callid_abuse[i].active) { + hybbx_strlcpy(g_callid_abuse[i].callid, callid, + sizeof(g_callid_abuse[i].callid)); + g_callid_abuse[i].count = 0; + g_callid_abuse[i].active = 1; + return &g_callid_abuse[i]; + } + } + + return NULL; +} + +static void prune_callid_fail_window(callid_track_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= (time_t)g_cfg.findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->callid[0] = '\0'; + } +} + +static void prune_callid_abuse_window(callid_track_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= + (time_t)g_cfg.abuse_findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->callid[0] = '\0'; + } +} + +static void backend_apply_callid(const char *callid, const char *reason) +{ + hybbx_security_log_write("ban callid=%s reason=%s backend=internal", + callid, reason != NULL ? reason : "abuse"); + (void)g_cfg.backend; +} + +static void apply_callid_ban_locked(const char *callid, const char *reason, + time_t now, int permanent) +{ + ban_callid_entry_t *ban; + + ban = callid_ban_find(callid); + if (ban == NULL) { + ban = callid_ban_alloc(callid); + } + + if (ban == NULL) { + return; + } + + ban->permanent = permanent ? 1 : 0; + ban->expire_at = permanent ? (time_t)0 : + now + (time_t)g_cfg.bantime_sec; + backend_apply_callid(callid, reason); +} + +static void config_clear_permanent_callid_bans_locked(void) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_callid_bans[i].active && g_callid_bans[i].permanent) { + g_callid_bans[i].active = 0; + g_callid_bans[i].callid[0] = '\0'; + g_callid_bans[i].permanent = 0; + } + } +} + +static void config_load_callid_bans_locked(const char *list) +{ + char buf[HYBBX_PATH_MAX]; + char norm[HYBBX_CALLID_MAX]; + char *save = NULL; + char *token; + + if (list == NULL || list[0] == '\0') { + return; + } + + hybbx_strlcpy(buf, list, sizeof(buf)); + token = strtok_r(buf, ",", &save); + while (token != NULL) { + while (*token == ' ' || *token == '\t') { + token++; + } + if (hybbx_security_callid_normalize(token, norm, sizeof(norm))) { + apply_callid_ban_locked(norm, "config", time(NULL), 1); + } + token = strtok_r(NULL, ",", &save); + } +} + +static void record_callid_failure_locked(const char *callid, time_t now) +{ + callid_track_entry_t *entry; + + entry = callid_fail_find(callid); + if (entry == NULL) { + entry = callid_fail_alloc(callid); + } + + if (entry == NULL) { + return; + } + + prune_callid_fail_window(entry, now); + + if (!entry->active) { + entry = callid_fail_alloc(callid); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.maxretry) { + apply_callid_ban_locked(callid, "link_auth_fail", now, 0); + entry->active = 0; + entry->count = 0; + entry->callid[0] = '\0'; + } +} + +static void record_callid_abuse_locked(const char *callid, const char *category, + time_t now) +{ + callid_track_entry_t *entry; + char reason[64]; + + entry = callid_abuse_find(callid); + if (entry == NULL) { + entry = callid_abuse_alloc(callid); + } + + if (entry == NULL) { + return; + } + + prune_callid_abuse_window(entry, now); + + if (!entry->active) { + entry = callid_abuse_alloc(callid); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.abuse_maxretry) { + snprintf(reason, sizeof(reason), "abuse:%s", + category != NULL && category[0] != '\0' ? category : "flood"); + apply_callid_ban_locked(callid, reason, now, 0); + entry->active = 0; + entry->count = 0; + entry->callid[0] = '\0'; + } +} + +static int callid_is_banned_locked(const char *callid, time_t now) +{ + ban_callid_entry_t *ban; + + ban = callid_ban_find(callid); + if (ban == NULL) { + return 0; + } + + if (ban->permanent) { + return 1; + } + + if (now < ban->expire_at) { + return 1; + } + + ban->active = 0; + ban->callid[0] = '\0'; + ban->permanent = 0; + return 0; +} + +static int transport_enabled(const char *transport) +{ + if (!g_cfg.enabled || transport == NULL || transport[0] == '\0') { + return 0; + } + + if (strcmp(transport, "telnet") == 0) { + return g_cfg.telnet; + } + if (strcmp(transport, "ssh") == 0) { + return g_cfg.ssh; + } + if (strcmp(transport, "websocket") == 0) { + return g_cfg.websocket; + } + if (strcmp(transport, "circuit") == 0) { + return g_cfg.circuit; + } + + return 1; +} + +static ban_entry_t *ban_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_bans[i].active && strcmp(g_bans[i].ip, ip) == 0) { + return &g_bans[i]; + } + } + + return NULL; +} + +static ban_entry_t *ban_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (!g_bans[i].active) { + hybbx_strlcpy(g_bans[i].ip, ip, sizeof(g_bans[i].ip)); + g_bans[i].active = 1; + return &g_bans[i]; + } + } + + return NULL; +} + +static fail_entry_t *fail_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_fails[i].active && strcmp(g_fails[i].ip, ip) == 0) { + return &g_fails[i]; + } + } + + return NULL; +} + +static fail_entry_t *fail_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_fails[i].active) { + hybbx_strlcpy(g_fails[i].ip, ip, sizeof(g_fails[i].ip)); + g_fails[i].count = 0; + g_fails[i].active = 1; + return &g_fails[i]; + } + } + + return NULL; +} + +static fail_entry_t *abuse_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_abuse[i].active && strcmp(g_abuse[i].ip, ip) == 0) { + return &g_abuse[i]; + } + } + + return NULL; +} + +static fail_entry_t *abuse_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_abuse[i].active) { + hybbx_strlcpy(g_abuse[i].ip, ip, sizeof(g_abuse[i].ip)); + g_abuse[i].count = 0; + g_abuse[i].active = 1; + return &g_abuse[i]; + } + } + + return NULL; +} + +static rate_entry_t *rate_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_rates[i].active && strcmp(g_rates[i].ip, ip) == 0) { + return &g_rates[i]; + } + } + + return NULL; +} + +static rate_entry_t *rate_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_rates[i].active) { + hybbx_strlcpy(g_rates[i].ip, ip, sizeof(g_rates[i].ip)); + g_rates[i].count = 0; + g_rates[i].active = 1; + return &g_rates[i]; + } + } + + return NULL; +} + +static void prune_fail_window(fail_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= (time_t)g_cfg.findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->ip[0] = '\0'; + } +} + +static void prune_abuse_window(fail_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= + (time_t)g_cfg.abuse_findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->ip[0] = '\0'; + } +} + +static void prune_rate_window(rate_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= (time_t)g_cfg.rate_window_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->ip[0] = '\0'; + } +} + +static int run_backend_cmd(const char *cmd) +{ + int rc; + + if (cmd == NULL || cmd[0] == '\0') { + return -1; + } + + rc = system(cmd); + return rc; +} + +static void backend_apply(const char *ip, const char *reason) +{ + char cmd[HYBBX_PATH_MAX]; + + switch (g_cfg.backend) { + case HYBBX_BAN_BACKEND_LOG: + hybbx_security_log_write("ban ip=%s reason=%s backend=log", + ip, reason != NULL ? reason : "abuse"); + break; + + case HYBBX_BAN_BACKEND_IPTABLES: + snprintf(cmd, sizeof(cmd), + "iptables -I INPUT -s %s -j DROP 2>/dev/null", ip); + if (run_backend_cmd(cmd) != 0) { + hybbx_security_log_write( + "ban_backend_fail ip=%s backend=iptables", ip); + } else { + hybbx_security_log_write( + "ban ip=%s reason=%s backend=iptables", + ip, reason != NULL ? reason : "abuse"); + } + break; + + case HYBBX_BAN_BACKEND_NFTABLES: + snprintf(cmd, sizeof(cmd), + "nft add rule inet filter input ip saddr %s drop " + "2>/dev/null", + ip); + if (run_backend_cmd(cmd) != 0) { + hybbx_security_log_write( + "ban_backend_fail ip=%s backend=nftables", ip); + } else { + hybbx_security_log_write( + "ban ip=%s reason=%s backend=nftables", + ip, reason != NULL ? reason : "abuse"); + } + break; + + case HYBBX_BAN_BACKEND_HOSTS: + hybbx_security_log_write( + "ban ip=%s reason=%s backend=hosts (stub — internal only)", + ip, reason != NULL ? reason : "abuse"); + break; + + case HYBBX_BAN_BACKEND_INTERNAL: + default: + hybbx_security_log_write("ban ip=%s reason=%s backend=internal", + ip, reason != NULL ? reason : "abuse"); + break; + } +} + +static void apply_ban_locked(const char *ip, const char *reason, time_t now) +{ + ban_entry_t *ban; + + ban = ban_find(ip); + if (ban == NULL) { + ban = ban_alloc(ip); + } + + if (ban == NULL) { + return; + } + + ban->expire_at = now + (time_t)g_cfg.bantime_sec; + backend_apply(ip, reason); +} + +static void record_failure_locked(const char *ip, time_t now) +{ + fail_entry_t *entry; + + entry = fail_find(ip); + if (entry == NULL) { + entry = fail_alloc(ip); + } + + if (entry == NULL) { + return; + } + + prune_fail_window(entry, now); + + if (!entry->active) { + entry = fail_alloc(ip); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.maxretry) { + apply_ban_locked(ip, "login_fail", now); + entry->active = 0; + entry->count = 0; + entry->ip[0] = '\0'; + } +} + +static void record_abuse_locked(const char *ip, const char *category, time_t now) +{ + fail_entry_t *entry; + char reason[64]; + + entry = abuse_find(ip); + if (entry == NULL) { + entry = abuse_alloc(ip); + } + + if (entry == NULL) { + return; + } + + prune_abuse_window(entry, now); + + if (!entry->active) { + entry = abuse_alloc(ip); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.abuse_maxretry) { + snprintf(reason, sizeof(reason), "abuse:%s", + category != NULL && category[0] != '\0' ? category : "flood"); + apply_ban_locked(ip, reason, now); + entry->active = 0; + entry->count = 0; + entry->ip[0] = '\0'; + } +} + +static void record_rate_locked(const char *ip, time_t now) +{ + rate_entry_t *entry; + + entry = rate_find(ip); + if (entry == NULL) { + entry = rate_alloc(ip); + } + + if (entry == NULL) { + return; + } + + prune_rate_window(entry, now); + + if (!entry->active) { + entry = rate_alloc(ip); + if (entry == NULL) { + return; + } + } + + if (entry->count < (unsigned)(sizeof(entry->stamps) / sizeof(entry->stamps[0]))) { + entry->stamps[entry->count++] = now; + } +} + +void hybbx_security_ban_config_apply(const struct hybbx_config *config) +{ + const char *value; + + pthread_mutex_lock(&g_lock); + + security_cfg_defaults(&g_cfg); + + if (config != NULL) { + value = hybbx_config_get(config, "security", "enabled", NULL); + if (value != NULL) { + g_cfg.enabled = hybbx_parse_bool(value, g_cfg.enabled); + } + + value = hybbx_config_get(config, "security", "maxretry", NULL); + g_cfg.maxretry = parse_uint_clamp(value, g_cfg.maxretry, 100u); + if (g_cfg.maxretry < 1u) { + g_cfg.maxretry = 1u; + } + + value = hybbx_config_get(config, "security", "findtime", NULL); + g_cfg.findtime_sec = + parse_uint_clamp(value, g_cfg.findtime_sec, 86400u); + + value = hybbx_config_get(config, "security", "bantime", NULL); + g_cfg.bantime_sec = + parse_uint_clamp(value, g_cfg.bantime_sec, 86400u); + + value = hybbx_config_get(config, "security", "abuse_maxretry", NULL); + g_cfg.abuse_maxretry = + parse_uint_clamp(value, g_cfg.abuse_maxretry, 1000u); + if (g_cfg.abuse_maxretry < 1u) { + g_cfg.abuse_maxretry = 1u; + } + + value = hybbx_config_get(config, "security", "abuse_findtime", NULL); + g_cfg.abuse_findtime_sec = + parse_uint_clamp(value, g_cfg.abuse_findtime_sec, 86400u); + + value = hybbx_config_get(config, "security", "telnet", NULL); + if (value != NULL) { + g_cfg.telnet = hybbx_parse_bool(value, g_cfg.telnet); + } + + value = hybbx_config_get(config, "security", "ssh", NULL); + if (value != NULL) { + g_cfg.ssh = hybbx_parse_bool(value, g_cfg.ssh); + } + + value = hybbx_config_get(config, "security", "websocket", NULL); + if (value != NULL) { + g_cfg.websocket = hybbx_parse_bool(value, g_cfg.websocket); + } + + value = hybbx_config_get(config, "security", "circuit", NULL); + if (value != NULL) { + g_cfg.circuit = hybbx_parse_bool(value, g_cfg.circuit); + } + + value = hybbx_config_get(config, "security", "rate_limit", NULL); + g_cfg.rate_limit = + parse_uint_clamp(value, g_cfg.rate_limit, 10000u); + + value = hybbx_config_get(config, "security", "rate_window", NULL); + g_cfg.rate_window_sec = + parse_uint_clamp(value, g_cfg.rate_window_sec, 3600u); + + value = hybbx_config_get(config, "security", "ban_backend", NULL); + g_cfg.backend = parse_ban_backend(value); + + config_clear_permanent_callid_bans_locked(); + value = hybbx_config_get(config, "security", "ban_callid", NULL); + config_load_callid_bans_locked(value); + } + + pthread_mutex_unlock(&g_lock); + + if (g_cfg.enabled) { + hybbx_log_info("[security] ban enabled maxretry=%u findtime=%us bantime=%us " + "abuse_maxretry=%u abuse_findtime=%us " + "rate_limit=%u/%us backend=%d", + g_cfg.maxretry, g_cfg.findtime_sec, g_cfg.bantime_sec, + g_cfg.abuse_maxretry, g_cfg.abuse_findtime_sec, + g_cfg.rate_limit, g_cfg.rate_window_sec, (int)g_cfg.backend); + } +} + +void hybbx_security_ban_shutdown(void) +{ + pthread_mutex_lock(&g_lock); + + memset(g_bans, 0, sizeof(g_bans)); + memset(g_callid_bans, 0, sizeof(g_callid_bans)); + memset(g_fails, 0, sizeof(g_fails)); + memset(g_abuse, 0, sizeof(g_abuse)); + memset(g_callid_fails, 0, sizeof(g_callid_fails)); + memset(g_callid_abuse, 0, sizeof(g_callid_abuse)); + memset(g_rates, 0, sizeof(g_rates)); + security_cfg_defaults(&g_cfg); + + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_tick(void) +{ + time_t now = time(NULL); + size_t i; + + if (!g_cfg.enabled) { + return; + } + + pthread_mutex_lock(&g_lock); + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_bans[i].active && now >= g_bans[i].expire_at) { + hybbx_security_log_write("unban ip=%s", g_bans[i].ip); + g_bans[i].active = 0; + g_bans[i].ip[0] = '\0'; + } + if (g_callid_bans[i].active && !g_callid_bans[i].permanent && + g_callid_bans[i].expire_at > (time_t)0 && + now >= g_callid_bans[i].expire_at) { + hybbx_security_log_write("unban callid=%s", g_callid_bans[i].callid); + g_callid_bans[i].active = 0; + g_callid_bans[i].callid[0] = '\0'; + g_callid_bans[i].permanent = 0; + } + } + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_fails[i].active) { + prune_fail_window(&g_fails[i], now); + } + if (g_abuse[i].active) { + prune_abuse_window(&g_abuse[i], now); + } + if (g_callid_fails[i].active) { + prune_callid_fail_window(&g_callid_fails[i], now); + } + if (g_callid_abuse[i].active) { + prune_callid_abuse_window(&g_callid_abuse[i], now); + } + if (g_rates[i].active) { + prune_rate_window(&g_rates[i], now); + } + } + + pthread_mutex_unlock(&g_lock); +} + +int hybbx_security_ban_is_banned(const char *ip) +{ + ban_entry_t *ban; + time_t now = time(NULL); + int banned = 0; + + if (!g_cfg.enabled || !ip_valid(ip)) { + return 0; + } + + pthread_mutex_lock(&g_lock); + + ban = ban_find(ip); + if (ban != NULL) { + if (now < ban->expire_at) { + banned = 1; + } else { + ban->active = 0; + ban->ip[0] = '\0'; + } + } + + pthread_mutex_unlock(&g_lock); + return banned; +} + +int hybbx_security_ban_accept(const char *ip) +{ + rate_entry_t *entry; + ban_entry_t *ban; + time_t now = time(NULL); + int allow = 1; + + if (!g_cfg.enabled || !ip_valid(ip)) { + return 1; + } + + pthread_mutex_lock(&g_lock); + + ban = ban_find(ip); + if (ban != NULL) { + if (now < ban->expire_at) { + allow = 0; + } else { + ban->active = 0; + ban->ip[0] = '\0'; + } + } + + if (allow && g_cfg.rate_limit > 0u && g_cfg.rate_window_sec > 0u) { + entry = rate_find(ip); + if (entry != NULL) { + prune_rate_window(entry, now); + } + + if (entry != NULL && entry->active && + entry->count >= g_cfg.rate_limit) { + apply_ban_locked(ip, "rate_limit", now); + allow = 0; + } else { + record_rate_locked(ip, now); + } + } + + pthread_mutex_unlock(&g_lock); + return allow; +} + +int hybbx_security_ban_accept_fd(int fd) +{ + char ip[HYBBX_REMOTE_ADDR_MAX]; + + if (fd < 0) { + return 1; + } + + if (hybbx_socket_peer_name(fd, ip, sizeof(ip)) != HYBBX_OK) { + return 1; + } + + return hybbx_security_ban_accept(ip); +} + +void hybbx_security_ban_login_fail(const char *ip, const char *transport) +{ + time_t now = time(NULL); + + if (!g_cfg.enabled || !ip_valid(ip) || !transport_enabled(transport)) { + return; + } + + pthread_mutex_lock(&g_lock); + record_failure_locked(ip, now); + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_link_auth_fail(const char *ip) +{ + time_t now = time(NULL); + + if (!g_cfg.enabled || !ip_valid(ip) || !g_cfg.circuit) { + return; + } + + pthread_mutex_lock(&g_lock); + record_failure_locked(ip, now); + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_abuse_report(const char *ip, const char *category) +{ + time_t now = time(NULL); + + if (!g_cfg.enabled || !ip_valid(ip)) { + return; + } + + pthread_mutex_lock(&g_lock); + record_abuse_locked(ip, category, now); + pthread_mutex_unlock(&g_lock); +} + +int hybbx_security_ban_callid_is_banned(const char *callid) +{ + char norm[HYBBX_CALLID_MAX]; + time_t now = time(NULL); + int banned = 0; + + if (!g_cfg.enabled || !hybbx_security_callid_normalize(callid, norm, + sizeof(norm))) { + return 0; + } + + pthread_mutex_lock(&g_lock); + banned = callid_is_banned_locked(norm, now); + pthread_mutex_unlock(&g_lock); + return banned; +} + +int hybbx_security_ban_callid_accept(const char *callid) +{ + return !hybbx_security_ban_callid_is_banned(callid); +} + +void hybbx_security_ban_link_auth_fail_callid(const char *callid) +{ + char norm[HYBBX_CALLID_MAX]; + time_t now = time(NULL); + + if (!g_cfg.enabled || !g_cfg.circuit || + !hybbx_security_callid_normalize(callid, norm, sizeof(norm))) { + return; + } + + pthread_mutex_lock(&g_lock); + record_callid_failure_locked(norm, now); + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_callid_abuse_report(const char *callid, + const char *category) +{ + char norm[HYBBX_CALLID_MAX]; + time_t now = time(NULL); + + if (!g_cfg.enabled || !hybbx_security_callid_normalize(callid, norm, + sizeof(norm))) { + return; + } + + pthread_mutex_lock(&g_lock); + record_callid_abuse_locked(norm, category, now); + pthread_mutex_unlock(&g_lock); +} |
