summaryrefslogtreecommitdiff
path: root/src/core/security_ban.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/security_ban.c')
-rw-r--r--src/core/security_ban.c1213
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);
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com