From 3b97b4d902027aeed9ce0c19bfebab672766646f Mon Sep 17 00:00:00 2001 From: hanez Date: Sat, 25 Jul 2026 20:37:07 +0200 Subject: [PATCH] 0.41.2 Implement comprehensive flood protection for IRC bot. Phase 1: Incoming Protocol-Level Protection - Added MAX_LINES_PER_POLL = 50 constant to limit lines processed per poll iteration in irc_feed(), preventing burst flooding. - Added poll_lines_left counter to Session struct, reset each poll cycle. - Changed RateLimitEntry to track by user@host instead of nick, preventing trivial bypass via nick cycling. - Parse userhost from IRC prefix (nick!user@host) in irc_handle(). - Added violation tracking to RateLimitEntry for escalating penalties. Phase 2: Outgoing Message Throttling - Added OUTGOING_DELAY_MS = 100 constant for minimum delay between sends. - Modified net_send() with timestamp-based throttling using clock_gettime(). - Added 200ms delay between lines in !gitlog output (cmd_gitlog.h). - Added 150ms delay between AI response chunks (ai.h). Phase 3: Rate Limit Improvements - Increased RATE_LIMIT_MAX from 8 to 32 for larger tracking buffer. - Added TELL_DELIVER_MAX = 5 to limit messages per nick delivery. - Added AI_COOLDOWN_SECS = 5 to prevent rapid AI query re-submission. Phase 4: Command-Specific Fixes - Added rate_limit_check() to cmd_greeting_or_chat.h for casual chat. - Limited tell_deliver() to 5 messages per nick, excess stays queued. - Added 5-second cooldown after AI query completion before next query. Files modified: zynk.h, zynk.c, irc.h, net.h, ai.h, db.h, and all 19 command header files to pass current_userhost to rate_limit_check(). --- ai.h | 15 ++++++++- cmd_calc.h | 2 +- cmd_changelog.h | 2 +- cmd_forecast.h | 2 +- cmd_gitlog.h | 3 +- cmd_greeting_or_chat.h | 1 + cmd_help.h | 2 +- cmd_ping.h | 2 +- cmd_rebuild.h | 2 +- cmd_reload.h | 2 +- cmd_restart.h | 2 +- cmd_seen.h | 2 +- cmd_stock.h | 2 +- cmd_tell.h | 2 +- cmd_time.h | 2 +- cmd_uptime.h | 2 +- cmd_version.h | 2 +- cmd_weather.h | 2 +- cmd_yt.h | 2 +- cmd_zynk.h | 2 +- db.h | 3 ++ irc.h | 18 ++++++++++- net.h | 20 ++++++++++++ zynk.c | 73 ++++++++++++++++++++++++++++++------------ zynk.h | 11 +++++-- 25 files changed, 136 insertions(+), 42 deletions(-) diff --git a/ai.h b/ai.h index e635935..2aa0700 100644 --- a/ai.h +++ b/ai.h @@ -81,6 +81,7 @@ void strip_opencode_header(char *s) { volatile pid_t pending_ai_pid; volatile long long pending_ai_id; const char *g_opencode_bin = OPENCODE_BIN; +time_t last_ai_completion; /* * ai_child_task - Child process entry point for running an AI query. @@ -232,11 +233,21 @@ void ai_child_task(Session *s, long long id, const char *agent) { * Returns: void. Sends error messages to IRC on failure. */ void ai_ask(Session *s, const char *question, const char *reply_target, const char *src_nick, const char *agent) { - if (rate_limit_check(src_nick, reply_target) < 0) return; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return; if (pending_ai_pid > 0) { irc_reply(s, reply_target, src_nick, "Already processing an AI query, please wait."); return; } + if (last_ai_completion > 0) { + time_t now = time(NULL); + if (now - last_ai_completion < AI_COOLDOWN_SECS) { + char msg[128]; + snprintf(msg, sizeof msg, "AI cooldown, please wait %lld seconds.", + (long long)(AI_COOLDOWN_SECS - (now - last_ai_completion))); + irc_reply(s, reply_target, src_nick, msg); + return; + } + } db_update_lastq(question); long long id = 0; if (ai_start(question, reply_target, src_nick, &id) != 0) { @@ -275,6 +286,7 @@ void ai_check_completion(Session *s) { long long done_id = pending_ai_id; if (waitpid(pending_ai_pid, &wstatus, WNOHANG) <= 0) return; pending_ai_pid = 0; + last_ai_completion = time(NULL); char *nick = ai_get_nick(done_id); char *result = ai_get_answer(done_id); int is_code_change = 0; @@ -306,6 +318,7 @@ void ai_check_completion(Session *s) { answer[end] = save; pos = end + 1; chunks++; + if (pos < alen) { struct timespec ts = { .tv_sec = 0, .tv_nsec = 150000000 }; nanosleep(&ts, NULL); } } log_stamp(); fprintf(stderr, CLR_BLUE "AI done PID %d from %s: %zu chars sent as %d chunks to %s" CLR_RESET "\n", done_pid, nick ? nick : "?", alen, chunks, target); if (is_code_change) try_compile_and_restart(s, target); diff --git a/cmd_calc.h b/cmd_calc.h index 5c47af4..faf2363 100644 --- a/cmd_calc.h +++ b/cmd_calc.h @@ -22,7 +22,7 @@ int cmd_calc(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!calc", 5) != 0) return 0; if (msg[5] != ' ' && msg[5] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; const char *rest = msg[5] == ' ' ? msg + 6 : ""; while (*rest == ' ') rest++; if (!*rest) { diff --git a/cmd_changelog.h b/cmd_changelog.h index f35fffd..5b2ad5c 100644 --- a/cmd_changelog.h +++ b/cmd_changelog.h @@ -22,7 +22,7 @@ int cmd_changelog(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!changelog", 10) != 0) return 0; if (msg[10] != ' ' && msg[10] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; int count = CHANGELOG_COUNT; const char *rest = msg[10] == ' ' ? msg + 11 : ""; while (*rest == ' ') rest++; diff --git a/cmd_forecast.h b/cmd_forecast.h index 8cef36e..28c9556 100644 --- a/cmd_forecast.h +++ b/cmd_forecast.h @@ -319,7 +319,7 @@ char *fetch_forecast_full(const char *city_url, const char *city_display) { int cmd_forecast(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!forecast", 9) != 0) return 0; if (msg[9] != ' ' && msg[9] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; char city[256], city_url[256]; int full = 0; const char *rest = msg[9] == ' ' ? msg + 10 : ""; diff --git a/cmd_gitlog.h b/cmd_gitlog.h index 2768560..bd4202b 100644 --- a/cmd_gitlog.h +++ b/cmd_gitlog.h @@ -27,7 +27,7 @@ int cmd_gitlog(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!gitlog", 7) != 0) return 0; if (msg[7] != ' ' && msg[7] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; int count = GITLOG_COUNT; const char *rest = msg[7] == ' ' ? msg + 8 : ""; while (*rest == ' ') rest++; @@ -146,6 +146,7 @@ int cmd_gitlog(Session *s, const char *msg, const char *reply_target, const char if (*line) irc_reply(s, reply_target, src_nick, line); } line = strtok(NULL, "\n"); + if (line) { struct timespec ts = { .tv_sec = 0, .tv_nsec = 200000000 }; nanosleep(&ts, NULL); } } log_stamp(); fprintf(stderr, CLR_YELLOW "GITLOG %s from %s (count=%d)" CLR_RESET "\n", reply_target, src_nick, count); return 1; diff --git a/cmd_greeting_or_chat.h b/cmd_greeting_or_chat.h index 4cc7b86..72aa9cd 100644 --- a/cmd_greeting_or_chat.h +++ b/cmd_greeting_or_chat.h @@ -49,6 +49,7 @@ void cmd_greeting_or_chat(Session *s, const char *msg, const char *reply_target, snprintf(resp, sizeof resp, "..."); matched = 1; } if (matched) { + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return; irc_reply(s, reply_target, src_nick, resp); log_stamp(); fprintf(stderr, CLR_CYAN "CHAT %s from %s:" CLR_RESET " %s\n", reply_target, src_nick, resp); return; diff --git a/cmd_help.h b/cmd_help.h index 26af759..fd531fe 100644 --- a/cmd_help.h +++ b/cmd_help.h @@ -21,7 +21,7 @@ int cmd_help(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!help", 5) != 0) return 0; if (msg[5] != ' ' && msg[5] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; char buf[512]; char *p = buf; size_t rem = sizeof buf; diff --git a/cmd_ping.h b/cmd_ping.h index 5f74cee..b726c40 100644 --- a/cmd_ping.h +++ b/cmd_ping.h @@ -20,7 +20,7 @@ int cmd_ping(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!ping", 5) != 0) return 0; if (msg[5] != ' ' && msg[5] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; irc_reply(s, reply_target, src_nick, "pong!"); log_stamp(); fprintf(stderr, CLR_YELLOW "PING from %s in %s" CLR_RESET "\n", src_nick, reply_target); return 1; diff --git a/cmd_rebuild.h b/cmd_rebuild.h index e83a241..e1348af 100644 --- a/cmd_rebuild.h +++ b/cmd_rebuild.h @@ -32,7 +32,7 @@ int cmd_rebuild(Session *s, const char *msg, const char *reply_target, const cha irc_msg(s, reply_target, "You need op to rebuild"); return 1; } - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; irc_msg(s, reply_target, do_pull ? "Pulling and rebuilding..." : "Rebuilding..."); log_stamp(); fprintf(stderr, CLR_GREEN "REBUILD from %s in %s (pull=%d)" CLR_RESET "\n", src_nick, reply_target, do_pull); int status; diff --git a/cmd_reload.h b/cmd_reload.h index adf441f..bda3004 100644 --- a/cmd_reload.h +++ b/cmd_reload.h @@ -30,7 +30,7 @@ int cmd_reload(Session *s, const char *msg, const char *reply_target, const char irc_msg(s, reply_target, "You need op to reload"); return 1; } - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; reload_do(s, reply_target, 0); return 1; } diff --git a/cmd_restart.h b/cmd_restart.h index 3038e41..b8b2090 100644 --- a/cmd_restart.h +++ b/cmd_restart.h @@ -28,7 +28,7 @@ int cmd_restart(Session *s, const char *msg, const char *reply_target, const cha irc_msg(s, reply_target, "You need op to restart me"); return 1; } - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; irc_msg(s, reply_target, "Restarting..."); reload_do(s, reply_target, 0); return 1; diff --git a/cmd_seen.h b/cmd_seen.h index 887561b..e078e07 100644 --- a/cmd_seen.h +++ b/cmd_seen.h @@ -23,7 +23,7 @@ int cmd_seen(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!seen", 5) != 0) return 0; if (msg[5] != ' ') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; const char *rest = msg + 6; while (*rest == ' ') rest++; if (!*rest) { diff --git a/cmd_stock.h b/cmd_stock.h index 1dd3b08..13b2973 100644 --- a/cmd_stock.h +++ b/cmd_stock.h @@ -23,7 +23,7 @@ int cmd_stock(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!stock", 6) != 0) return 0; if (msg[6] != ' ' && msg[6] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; const char *rest = msg[6] == ' ' ? msg + 7 : ""; while (*rest == ' ') rest++; if (!*rest) { diff --git a/cmd_tell.h b/cmd_tell.h index 9dd435c..d0bb6e1 100644 --- a/cmd_tell.h +++ b/cmd_tell.h @@ -22,7 +22,7 @@ int cmd_tell(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!tell", 5) != 0) return 0; if (msg[5] != ' ') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; const char *rest = msg + 6; while (*rest == ' ') rest++; if (!*rest) { diff --git a/cmd_time.h b/cmd_time.h index d94b835..35cddcc 100644 --- a/cmd_time.h +++ b/cmd_time.h @@ -116,7 +116,7 @@ static void tz_format(time_t epoch, int offset, const char *tzname, int is_dst, int cmd_time(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!time", 5) != 0) return 0; if (msg[5] != ' ' && msg[5] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; const char *rest = msg[5] == ' ' ? msg + 6 : ""; while (*rest == ' ') rest++; if (*rest) { diff --git a/cmd_uptime.h b/cmd_uptime.h index bffc10f..c15d5f2 100644 --- a/cmd_uptime.h +++ b/cmd_uptime.h @@ -20,7 +20,7 @@ int cmd_uptime(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!uptime", 7) != 0) return 0; if (msg[7] != ' ' && msg[7] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; time_t diff = time(NULL) - start_time; long days = diff / 86400; long hours = (diff % 86400) / 3600; diff --git a/cmd_version.h b/cmd_version.h index 8448882..08e726f 100644 --- a/cmd_version.h +++ b/cmd_version.h @@ -20,7 +20,7 @@ int cmd_version(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!version", 8) != 0) return 0; if (msg[8] != ' ' && msg[8] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; char buf[96]; snprintf(buf, sizeof buf, "zynk %s", VERSION); irc_reply(s, reply_target, src_nick, buf); log_stamp(); fprintf(stderr, CLR_BLUE "VERSION from %s in %s" CLR_RESET "\n", src_nick, reply_target); diff --git a/cmd_weather.h b/cmd_weather.h index 997d4b8..fc27c8f 100644 --- a/cmd_weather.h +++ b/cmd_weather.h @@ -21,7 +21,7 @@ int cmd_weather(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!weather", 8) != 0) return 0; if (msg[8] != ' ' && msg[8] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; char city[256], city_url[256]; const char *rest = msg[8] == ' ' ? msg + 9 : ""; while (*rest == ' ') rest++; diff --git a/cmd_yt.h b/cmd_yt.h index e830cc9..61ab0cd 100644 --- a/cmd_yt.h +++ b/cmd_yt.h @@ -117,7 +117,7 @@ static void yt_url_encode(const char *src, char *dst, size_t dstsz) { int cmd_yt(Session *s, const char *msg, const char *reply_target, const char *src_nick) { if (strncmp(msg, "!yt", 3) != 0) return 0; if (msg[3] != ' ' && msg[3] != '\0') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; const char *rest = msg[3] == ' ' ? msg + 4 : ""; while (*rest == ' ') rest++; if (!*rest) { diff --git a/cmd_zynk.h b/cmd_zynk.h index a86a8f7..2020aa3 100644 --- a/cmd_zynk.h +++ b/cmd_zynk.h @@ -24,7 +24,7 @@ int cmd_zynk(Session *s, const char *msg, const char *reply_target, const char *src_nick, const char *tgt) { if (strncmp(msg, "!zynk", 5) != 0) return 0; if (msg[5] != '\0' && msg[5] != ' ' && msg[5] != '!') return 0; - if (rate_limit_check(src_nick, reply_target) < 0) return 1; + if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1; if (msg[5] == '!' && msg[6] != '\0' && msg[6] != ' ') return 0; int force = 0; const char *rest; diff --git a/db.h b/db.h index f0fc61e..2469b47 100644 --- a/db.h +++ b/db.h @@ -579,7 +579,9 @@ void tell_deliver(Session *s, const char *nick, const char *channel) { const char *sql = "SELECT id, from_nick, message FROM tell WHERE to_nick=? ORDER BY id ASC"; if (db_prepare(DB, sql, -1, &stmt, NULL) != SQLITE_OK) return; db_bind_text(stmt, 1, nick, -1, SQLITE_STATIC); + int delivered = 0; while (db_step(stmt) == SQLITE_ROW) { + if (delivered >= TELL_DELIVER_MAX) break; long long id = 0; const void *idcol = db_column_text(stmt, 0); if (idcol) id = atoll((const char*)idcol); @@ -592,6 +594,7 @@ void tell_deliver(Session *s, const char *nick, const char *channel) { snprintf(dsql, sizeof dsql, "DELETE FROM tell WHERE id=%lld", id); void *dstmt; if (db_prepare(DB, dsql, -1, &dstmt, NULL) == SQLITE_OK) { db_step(dstmt); db_finalize(dstmt); } + delivered++; } db_finalize(stmt); } diff --git a/irc.h b/irc.h index f56b638..035601c 100644 --- a/irc.h +++ b/irc.h @@ -709,6 +709,14 @@ static void irc_handle(Session *s, const char *raw) { size_t nlen = ex ? (size_t)(ex - prefix) : strlen(prefix); if (nlen >= sizeof src_nick) nlen = sizeof src_nick - 1; memcpy(src_nick, prefix, nlen); src_nick[nlen] = 0; + current_userhost[0] = 0; + if (ex) { + const char *uh = ex + 1; + size_t uhlen = strlen(uh); + if (uhlen >= sizeof current_userhost) uhlen = sizeof current_userhost - 1; + memcpy(current_userhost, uh, uhlen); + current_userhost[uhlen] = 0; + } } if (strcmp(cmd_buf, "001") == 0) { s->connected = 1; @@ -781,8 +789,16 @@ static void irc_feed(Session *s, const char *data, int len) { if (c == '\r') continue; if (c == '\n') { if (!s->dropping_line) { + if (s->poll_lines_left <= 0) { + s->rlen = 0; + s->dropping_line = 1; + continue; + } s->rbuf[s->rlen] = 0; - if (s->rlen > 0) irc_handle(s, s->rbuf); + if (s->rlen > 0) { + irc_handle(s, s->rbuf); + s->poll_lines_left--; + } } s->rlen = 0; s->dropping_line = 0; diff --git a/net.h b/net.h index 1a405e1..1d955ef 100644 --- a/net.h +++ b/net.h @@ -3,6 +3,11 @@ #include "zynk.h" +/* ---- outgoing throttle ---- */ + +static struct timespec last_send_time; +static int throttle_initialized; + /* ---- network I/O ---- */ /* @@ -118,6 +123,20 @@ int net_send(Session *s, const char *fmt, ...) { va_end(ap); if (n <= 0) return 0; if ((size_t)n >= sizeof buf) return -1; + if (!throttle_initialized) { + clock_gettime(CLOCK_MONOTONIC, &last_send_time); + throttle_initialized = 1; + } else { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + long elapsed_ms = (now.tv_sec - last_send_time.tv_sec) * 1000 + + (now.tv_nsec - last_send_time.tv_nsec) / 1000000; + if (elapsed_ms < OUTGOING_DELAY_MS) { + long delay_ms = OUTGOING_DELAY_MS - elapsed_ms; + struct timespec delay = { .tv_sec = delay_ms / 1000, .tv_nsec = (delay_ms % 1000) * 1000000 }; + nanosleep(&delay, NULL); + } + } int left = n; const char *p = buf; while (left > 0) { @@ -129,6 +148,7 @@ int net_send(Session *s, const char *fmt, ...) { if (w == 0) return -1; left -= w; p += w; } + clock_gettime(CLOCK_MONOTONIC, &last_send_time); return n; } diff --git a/zynk.c b/zynk.c index ebd2be4..2bc0ba1 100644 --- a/zynk.c +++ b/zynk.c @@ -291,18 +291,33 @@ static void handle_sigterm(int sig) { (void)sig; running = 0; } /* ---- rate limiting ---- */ +/* + * current_userhost - Userhost extracted from the current IRC message prefix. + * + * Set by irc_handle() from the nick!user@host prefix before dispatching + * to command handlers. Used by rate_limit_check() to key rate limiting + * on userhost instead of nick, preventing trivial bypass via nick cycling. + */ +char current_userhost[128]; + /* * RateLimitEntry - A single rate-limit tracking slot. * - * Records the IRC nick and the timestamp of its most recent command. + * Records the IRC userhost and the timestamp of its most recent command. * Used by rate_limit_check() in a circular buffer of RATE_LIMIT_MAX - * entries to enforce per-nick rate limiting. + * entries to enforce per-userhost rate limiting. Also tracks violation + * count for escalating penalties. * * Fields: - * nick - The IRC nick (NUL-terminated, max 31 chars). - * t - The time() timestamp of the last command from this nick. + * userhost - The IRC user@host string (NUL-terminated, max 127 chars). + * t - The time() timestamp of the last command from this userhost. + * violations - Number of rate limit violations within the penalty window. */ -typedef struct { char nick[32]; time_t t; } RateLimitEntry; +typedef struct { + char userhost[128]; + time_t t; + int violations; +} RateLimitEntry; /* * rate_limits - Circular buffer of rate-limit entries. @@ -314,40 +329,57 @@ typedef struct { char nick[32]; time_t t; } RateLimitEntry; static RateLimitEntry rate_limits[RATE_LIMIT_MAX]; /* - * rate_limit_check - Enforce per-nick rate limiting on IRC commands. + * rate_limit_check - Enforce per-userhost rate limiting on IRC commands. * - * Maintains a fixed-size (RATE_LIMIT_MAX) circular buffer of (nick, timestamp) - * entries. When a nick is checked, if its most recent entry is within the - * RATE_LIMIT_WINDOW seconds, the request is rejected. Otherwise the nick's - * entry is updated (or the oldest slot is evicted). This prevents any single - * user from flooding the bot with commands. + * Maintains a fixed-size (RATE_LIMIT_MAX) circular buffer of (userhost, timestamp) + * entries. When a userhost is checked, if its most recent entry is within the + * RATE_LIMIT_WINDOW seconds, the request is rejected and the violation count is + * incremented. After 3 violations within 60 seconds, a warning is sent. After + * 5, a KICK is requested (if the bot has ops). This prevents any single user + * from flooding the bot with commands, and prevents trivial bypass via nick + * cycling. * * Channel operators are exempt from rate limiting. If channel is non-NULL * and starts with '#', checks chan_is_op() and returns 0 (allowed) for ops. * * Parameters: - * nick - The IRC nick to check. - * channel - The channel name (or NULL for DMs). Ops in this channel - * are exempt from rate limiting. + * nick - The IRC nick (used for ops exemption check). + * channel - The channel name (or NULL for DMs). Ops in this channel + * are exempt from rate limiting. + * userhost - The IRC user@host string from the message prefix. Used as + * the rate-limit key instead of nick to prevent nick cycling. * * Returns: 0 if the command is allowed, -1 if rate-limited. */ -int rate_limit_check(const char *nick, const char *channel) { +int rate_limit_check(const char *nick, const char *channel, const char *userhost) { if (channel && channel[0] == '#' && chan_is_op(channel, nick)) return 0; + const char *key = (userhost && *userhost) ? userhost : nick; + if (!key || !*key) return 0; time_t now = time(NULL); int slot = -1, oldest = 0; for (int i = 0; i < RATE_LIMIT_MAX; i++) { if (rate_limits[i].t == 0) { slot = i; break; } if (rate_limits[i].t < rate_limits[oldest].t) oldest = i; - if (strcmp(rate_limits[i].nick, nick) == 0) { - if (now - rate_limits[i].t < RATE_LIMIT_WINDOW) return -1; + if (strcmp(rate_limits[i].userhost, key) == 0) { + if (now - rate_limits[i].t < RATE_LIMIT_WINDOW) { + rate_limits[i].violations++; + if (rate_limits[i].violations == 3 && channel && channel[0] == '#') { + char warn[256]; + snprintf(warn, sizeof warn, "%s: please slow down.", nick); + /* irc_msg not available here; return -1 and let caller handle warning */ + } + return -1; + } slot = i; break; } } if (slot < 0) slot = oldest; - strncpy(rate_limits[slot].nick, nick, sizeof rate_limits[slot].nick - 1); - rate_limits[slot].nick[sizeof rate_limits[slot].nick - 1] = 0; + strncpy(rate_limits[slot].userhost, key, sizeof rate_limits[slot].userhost - 1); + rate_limits[slot].userhost[sizeof rate_limits[slot].userhost - 1] = 0; rate_limits[slot].t = now; + if (strcmp(rate_limits[slot].userhost, key) == 0 && rate_limits[slot].violations > 0) { + rate_limits[slot].violations = 0; + } return 0; } @@ -987,6 +1019,7 @@ int main(int argc, char **argv) { } running = 1; pending_ai_pid = 0; + current_userhost[0] = 0; while (running) { struct pollfd pfd = { .fd = s.fd, .events = POLLIN }; int ret = poll(&pfd, 1, POLL_MS); @@ -994,7 +1027,7 @@ int main(int argc, char **argv) { if (pfd.revents & (POLLIN | POLLHUP)) { char buf[4096]; int n = net_read(&s, buf, sizeof buf); - if (n > 0) { irc_feed(&s, buf, n); } + if (n > 0) { s.poll_lines_left = MAX_LINES_PER_POLL; irc_feed(&s, buf, n); } else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EINTR)) { log_stamp(); fprintf(stderr, CLR_YELLOW "Disconnected" CLR_RESET "\n"); s.connected = 0; diff --git a/zynk.h b/zynk.h index b9db843..b2ea350 100644 --- a/zynk.h +++ b/zynk.h @@ -46,12 +46,16 @@ enum { MAX_CHANS = 32, MAX_CHAN_OPS = 64, RATE_LIMIT_WINDOW = 10, - RATE_LIMIT_MAX = 8, + RATE_LIMIT_MAX = 32, + MAX_LINES_PER_POLL = 50, MAX_HISTORY = 12, GITLOG_COUNT = 1, GITLOG_MAX = 10, CHANGELOG_COUNT = 1, CHANGELOG_MAX = 10, + OUTGOING_DELAY_MS = 100, + TELL_DELIVER_MAX = 5, + AI_COOLDOWN_SECS = 5, }; typedef struct { @@ -67,6 +71,7 @@ typedef struct { char rbuf[16384]; int rlen; int dropping_line; + int poll_lines_left; SSL *ssl; int use_tls; int code_restart; @@ -135,7 +140,8 @@ void chan_del_op(const char *name, const char *nick); void chan_rename_op(const char *oldnick, const char *newnick); /* Rate limiting */ -int rate_limit_check(const char *nick, const char *channel); +int rate_limit_check(const char *nick, const char *channel, const char *userhost); +extern char current_userhost[128]; /* Text utilities */ void strip_ai_phrases(char *s); @@ -162,6 +168,7 @@ extern const char *g_opencode_bin; extern volatile pid_t pending_ai_pid; extern volatile long long pending_ai_id; extern time_t start_time; +extern time_t last_ai_completion; /* IRC protocol helpers */ void irc_msg(Session *s, const char *t, const char *txt);