zynk/net.h
hanez 3b97b4d902 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().
2026-07-25 20:37:07 +02:00

155 lines
4.9 KiB
C

#ifndef NET_H
#define NET_H
#include "zynk.h"
/* ---- outgoing throttle ---- */
static struct timespec last_send_time;
static int throttle_initialized;
/* ---- network I/O ---- */
/*
* net_connect - Establish a TCP connection to a remote host.
*
* Performs DNS resolution via getaddrinfo() with AI_ADDRCONFIG (restricts
* to address families available on the local system), then iterates through
* resolved addresses trying socket()+connect() on each. The socket is
* non-blocking by default since this function doesn't set O_NONBLOCK.
*
* Parameters:
* host - The hostname or IP address to connect to.
* port - The TCP port number.
*
* Returns: A connected file descriptor on success, or -1 on failure.
*/
int net_connect(const char *host, int port) {
struct addrinfo hints, *ai;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_ADDRCONFIG;
char pbuf[16];
snprintf(pbuf, sizeof pbuf, "%d", port);
int err = getaddrinfo(host, pbuf, &hints, &ai);
if (err) { fprintf(stderr, CLR_RED "DNS:" CLR_RESET " %s\n", gai_strerror(err)); return -1; }
int fd = -1;
for (struct addrinfo *rp = ai; rp; rp = rp->ai_next) {
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd < 0) continue;
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break;
close(fd); fd = -1;
}
freeaddrinfo(ai);
return fd;
}
/*
* net_read - Read data from the session's socket (TLS-aware).
*
* If the session has an active SSL connection, reads via SSL_read();
* otherwise uses plain read(). Translates SSL_ERROR_WANT_READ/WANT_WRITE
* into errno=EAGAIN for consistent non-blocking behavior. Returns 0 on
* clean TLS shutdown (SSL_ERROR_ZERO_RETURN).
*
* Parameters:
* s - The IRC session with fd and optional SSL context.
* buf - Destination buffer for the read data.
* size - Maximum number of bytes to read.
*
* Returns: Number of bytes read, 0 on EOF/shutdown, -1 on error (check
* errno for EAGAIN to distinguish transient from fatal errors).
*/
int net_read(Session *s, char *buf, int size) {
if (s->ssl) {
int r = SSL_read(s->ssl, buf, size);
if (r > 0) return r;
int e = SSL_get_error(s->ssl, r);
if (e == SSL_ERROR_WANT_READ || e == SSL_ERROR_WANT_WRITE) { errno = EAGAIN; return -1; }
if (e == SSL_ERROR_ZERO_RETURN) return 0;
errno = EIO;
return -1;
}
return read(s->fd, buf, size);
}
/*
* net_write - Write data to the session's socket (TLS-aware).
*
* If the session has an active SSL connection, writes via SSL_write();
* otherwise uses plain write(). Translates SSL_ERROR_WANT_READ/WANT_WRITE
* into errno=EAGAIN for consistent non-blocking behavior.
*
* Parameters:
* s - The IRC session with fd and optional SSL context.
* buf - Source buffer containing data to send.
* size - Number of bytes to write.
*
* Returns: Number of bytes written on success, -1 on error.
*/
int net_write(Session *s, const char *buf, int size) {
if (s->ssl) {
int r = SSL_write(s->ssl, buf, size);
if (r > 0) return r;
int e = SSL_get_error(s->ssl, r);
if (e == SSL_ERROR_WANT_READ || e == SSL_ERROR_WANT_WRITE) { errno = EAGAIN; return -1; }
return -1;
}
return write(s->fd, buf, size);
}
/*
* net_send - Format and send an IRC protocol message with retry logic.
*
* Uses vsnprintf to format the message into a 4096-byte stack buffer, then
* writes it via net_write() in a loop to handle partial writes. Retries on
* EINTR (signal interruption) and EAGAIN (would-block). The caller should
* include "\r\n" in the format string per IRC protocol requirements.
*
* Parameters:
* s - The IRC session.
* fmt - printf-style format string for the message.
* ... - Format arguments.
*
* Returns: Number of bytes sent on success, 0 if nothing to send,
* -1 on truncation or write failure.
*/
int net_send(Session *s, const char *fmt, ...) {
char buf[4096];
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(buf, sizeof buf, fmt, ap);
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) {
int w = net_write(s, p, left);
if (w < 0) {
if (errno == EINTR || errno == EAGAIN) continue;
return -1;
}
if (w == 0) return -1;
left -= w; p += w;
}
clock_gettime(CLOCK_MONOTONIC, &last_send_time);
return n;
}
#endif