24 KiB
🔄 FLUXURI DETALIATE - Underchat IRCD
Data: 23 Februarie 2026
Complement la: ANALIZA_ARHITECTURA_SENIOR.md
📊 DIAGRAMĂ GENERALĂ - DATA FLOW
┌─────────────┐
│ CLIENT │
│ (IRC App) │
└──────┬──────┘
│ TCP/SSL
│ Port 6667/6697
▼
┌─────────────────────────────────────────┐
│ LISTENER (s_bsd.c) │
│ ┌───────────────────────────────┐ │
│ │ accept() → new socket │ │
│ │ set non-blocking │ │
│ │ add to LocalClientArray[] │ │
│ │ register with epoll │ │
│ └───────────────────────────────┘ │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ EVENT LOOP (ircd_events.c) │
│ ┌───────────────────────────────┐ │
│ │ while(running) { │ │
│ │ epoll_wait() → events │ │
│ │ for each event: │ │
│ │ if EPOLLIN → READ │ │
│ │ if EPOLLOUT → WRITE │ │
│ │ if EPOLLERR → ERROR │ │
│ │ } │ │
│ └───────────────────────────────┘ │
└─────────┬───────────────┬───────────────┘
│ │
READ PATH WRITE PATH
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ READ HANDLER │ │ WRITE HANDLER │
│ (s_bsd.c) │ │ (send.c) │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
[Flow A] [Flow B]
🔵 FLOW A: MESSAGE RECEIVING (Client → Server)
Step 1: Socket Read
Fișier: ircd/s_bsd.c - funcția read_packet()
// Pseudocod simplificat
int read_packet(struct Client* cptr, int ready_to_read) {
char readbuf[SERVER_TCP_WINDOW]; // 32KB buffer
// 1. Read from socket
length = recv(cli_fd(cptr), readbuf, sizeof(readbuf), 0);
if (length <= 0) {
// Connection closed or error
return exit_client(cptr);
}
// 2. Append to client's receive buffer (DBuf)
if (dbuf_put(&cli_recvQ(cptr), readbuf, length) < 0) {
// Out of memory
return exit_client_msg(cptr, "Buffer allocation error");
}
// 3. Process complete messages
while (has_complete_line(&cli_recvQ(cptr))) {
dolen = dbuf_get(&cli_recvQ(cptr), readbuf, sizeof(readbuf));
if (IsServer(cptr))
server_dopacket(cptr, readbuf, dolen);
else
connect_dopacket(cptr, readbuf, dolen);
}
return 1;
}
Caracteristici:
- ✅ Non-blocking read
- ✅ Buffering pentru mesaje incomplete
- ✅ Procesare iterativă (multiple mesaje per read)
- ⚠️ RISC: Fără limită pe dimensiunea DBuf
Step 2: Packet Processing
Fișier: ircd/packet.c - funcția connect_dopacket()
int connect_dopacket(struct Client *cptr, const char *buffer, int length) {
const char* src = buffer;
char* endp = cli_buffer(cptr) + cli_count(cptr);
// Procesează byte cu byte
while (length-- > 0) {
*endp = *src++;
// Detectează end-of-line
if (IsEol(*endp)) { // CR sau LF
if (endp == cli_buffer(cptr))
continue; // Skip LF/CR gol
*endp = '\0'; // Null-terminate
// ★ PARSE MESSAGE
if (parse_client(cptr, cli_buffer(cptr), endp) == CPTR_KILLED)
return CPTR_KILLED;
// Reset buffer
endp = cli_buffer(cptr);
}
else if (endp < cli_buffer(cptr) + BUFSIZE) {
++endp;
}
}
cli_count(cptr) = endp - cli_buffer(cptr);
return 1;
}
FLUX:
Buffer: "PRIVMSG #test :Hello\r\nNICK newname\r"
↓
Iterație 1: Găsește \r după "Hello"
→ parse_client("PRIVMSG #test :Hello")
Iterație 2: Găsește \r după "newname"
→ parse_client("NICK newname")
Step 3: Message Parsing
Fișier: ircd/parse.c - funcția parse_client()
int parse_client(struct Client *cptr, char *buffer, char *bufend) {
struct Message* mptr;
char* para[MAXPARA + 2]; // parametrii comenzii
int paramcount;
// 1. Extrage prefix (dacă există)
// Format: :prefix COMMAND param1 param2 :trailing
if (*buffer == ':') {
// Skip prefix pentru client messages
while (*buffer != ' ' && *buffer)
buffer++;
}
// 2. Extrage comanda
char* command = buffer;
while (*buffer != ' ' && *buffer)
buffer++;
*buffer++ = '\0';
// 3. Lookup în trie
mptr = find_command(command); // O(k) lookup
if (!mptr) {
// ERR_UNKNOWNCOMMAND
return send_reply(cptr, ERR_UNKNOWNCOMMAND, command);
}
// 4. Extrage parametrii
paramcount = parse_params(buffer, para, MAXPARA);
// 5. Verificări
if (mptr->parameters < paramcount) {
return send_reply(cptr, ERR_NEEDMOREPARAMS, mptr->cmd);
}
// 6. ★ CALL HANDLER
MessageHandler handler = mptr->handlers[cli_status(cptr)];
return (*handler)(cptr, cptr, paramcount, para);
}
Trie Lookup Example:
Command: "PRIVMSG"
msg_tree:
'P' → 'R' → 'I' → 'V' → 'M' → 'S' → 'G' → [Message*]
↓
{MSG_PRIVATE, handlers[]}
Step 4: Handler Execution
Fișier: ircd/m_privmsg.c - funcția m_privmsg()
int m_privmsg(struct Client* cptr, struct Client* sptr,
int parc, char* parv[]) {
// parv[0] = source nick (implicit)
// parv[1] = target (nick or #channel)
// parv[2] = message text
// 1. Validare parametrii
if (parc < 2 || EmptyString(parv[1])) {
return send_reply(sptr, ERR_NORECIPIENT, "PRIVMSG");
}
if (parc < 3 || EmptyString(parv[2])) {
return send_reply(sptr, ERR_NOTEXTTOSEND);
}
char* target = parv[1];
char* text = parv[2];
// 2. Determine target type
if (IsChannelName(target)) {
// ★ CHANNEL MESSAGE
struct Channel* chptr = FindChannel(target);
if (!chptr)
return send_reply(sptr, ERR_NOSUCHCHANNEL, target);
if (!can_send_to_channel(sptr, chptr))
return send_reply(sptr, ERR_CANNOTSENDTOCHAN, target);
// ★ BROADCAST la toți membrii canalului
sendcmdto_channel_butone(sptr, CMD_PRIVMSG, chptr,
cli_from(sptr), "%s :%s", target, text);
}
else {
// ★ PRIVATE MESSAGE
struct Client* acptr = FindUser(target);
if (!acptr)
return send_reply(sptr, ERR_NOSUCHNICK, target);
// ★ SEND direct
sendcmdto_one(sptr, CMD_PRIVMSG, acptr, "%s :%s", target, text);
}
return 0;
}
🟢 FLOW B: MESSAGE SENDING (Server → Client)
Step 1: Message Queue Addition
Fișier: ircd/send.c - funcția sendcmdto_one()
void sendcmdto_one(struct Client *from, const char *cmd,
struct Client *to, const char *pattern, ...) {
va_list vl;
struct MsgBuf *mb;
// 1. Format message
va_start(vl, pattern);
mb = msgq_vmake(to, pattern, vl);
va_end(vl);
// 2. Prepend command and source
// Format final: ":source COMMAND params\r\n"
msgq_append(to, mb, ":%s %s", cli_name(from), cmd);
// 3. Add to target's send queue
msgq_add(&cli_sendQ(to), mb, 0); // 0 = normal priority
// 4. Mark connection as having data to send
if (MsgQLength(&cli_sendQ(to)) > 0)
update_write(to); // Register for EPOLLOUT
msgq_clean(mb); // Cleanup message buffer
}
Priority Queue:
struct MsgQ {
struct MsgQList prio; // Priority messages (server-to-server)
struct MsgQList queue; // Normal messages (client)
};
// Server messages → prio queue
// Client messages → normal queue
// Send order: prio first, then queue
Step 2: Write Readiness
Fișier: ircd/ircd_events.c + ircd/engine_epoll.c
// Event loop detectează EPOLLOUT
void engine_loop(struct Engine* eng) {
struct epoll_event events[MAXEVENTS];
int nfds = epoll_wait(epoll_fd, events, MAXEVENTS, timeout);
for (int i = 0; i < nfds; i++) {
struct Socket* sock = events[i].data.ptr;
struct Client* cptr = sock->s_data;
if (events[i].events & EPOLLOUT) {
// ★ Socket is writable
event_generate(ET_WRITE, sock, 0);
// Call registered callback
(*sock->s_callback)(sock->s_events);
}
}
}
Step 3: Message Flushing
Fișier: ircd/send.c - funcția send_queued()
void send_queued(struct Client *to) {
if (IsBlocked(to) || !can_send(to))
return;
// Process send queue
while (MsgQLength(&cli_sendQ(to)) > 0) {
unsigned int len;
// ★ DELIVER message
len = deliver_it(to, &cli_sendQ(to));
if (len > 0) {
// Successfully sent 'len' bytes
msgq_delete(&cli_sendQ(to), len);
cli_lastsq(to) = MsgQLength(&cli_sendQ(to)) / 1024;
if (IsBlocked(to)) {
// Socket buffer full, stop for now
update_write(to); // Re-register for EPOLLOUT
return;
}
}
else {
// Error or blocking
if (IsDead(to))
exit_client(to, to, &me, cli_info(to));
return;
}
}
// Send queue empty, no more EPOLLOUT needed
update_write(to);
}
Step 4: Actual Socket Write
Fișier: ircd/s_bsd.c - funcția deliver_it()
unsigned int deliver_it(struct Client *cptr, struct MsgQ *mq) {
struct iovec iov[IOV_MAX]; // Scatter-gather I/O
unsigned int len;
int count, bytes;
// 1. Map MsgQ to iovec array
count = msgq_mapiov(mq, iov, IOV_MAX, &len);
// 2. Write with writev (zero-copy)
bytes = writev(cli_fd(cptr), iov, count);
if (bytes < 0) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
// Socket buffer full
SetBlocked(cptr);
return 0;
}
// Real error
dead_link(cptr, "Write error");
return 0;
}
cli_sendB(cptr) += bytes; // Update statistics
return bytes;
}
writev() Advantages:
Instead of:
send(fd, msg1, len1);
send(fd, msg2, len2);
send(fd, msg3, len3);
Use:
iov[0] = {msg1, len1};
iov[1] = {msg2, len2};
iov[2] = {msg3, len3};
writev(fd, iov, 3); // ★ Single syscall
🔴 FLOW C: CHANNEL BROADCAST
Scenario: User A sends message to #test (100 members)
// m_privmsg.c
void sendcmdto_channel_butone(struct Client *from, const char *cmd,
struct Channel *chptr,
struct Client *one,
const char *pattern, ...) {
struct Membership *member;
struct MsgBuf *mb;
// 1. Format message once
mb = msgq_make(NULL, pattern, ...);
// 2. Iterate all channel members
for (member = chptr->members; member; member = member->next_member) {
struct Client *dest = member->user;
// Skip sender and specified 'one'
if (dest == from || dest == one)
continue;
// ★ Add to each member's sendQ
msgq_add(&cli_sendQ(dest), mb, 0);
// Mark as needing write
if (MsgQLength(&cli_sendQ(dest)) > 0)
update_write(dest);
}
msgq_clean(mb);
}
Performance Impact:
O(N) where N = channel members
For #test with 100 members:
- 1 message format
- 100 msgq_add() calls
- 100 update_write() calls
- Next event loop: 100× send_queued()
Bottleneck: CPU-bound (single thread)
⚡ FLOW D: SERVER-TO-SERVER (P10 Protocol)
Message Format
P10 Numeric Encoding:
Client format: :Nick!user@host PRIVMSG #test :Hello
P10 format: ABCDE P #test :Hello
Where:
AB = Server numeric (2 chars)
CDE = User numeric (3 chars)
P = PRIVMSG token
Encoding Table:
// numnicks.c
static const char convert2y[] = {
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
'w','x','y','z','0','1','2','3','4','5','6','7','8','9','[',']'
};
// Base64-like: 64^3 = 262,144 possible users per server
Server Link Flow
Local Server (AA) Remote Server (BB)
│ │
│ 1. CONNECT initiated │
│ ──────────────────────> │
│ │
│ 2. PASS :password │
│ <────────────────────── │
│ │
│ 3. SERVER name │
│ <────────────────────── │
│ │
│ 4. BURST mode │
│ <══════════════════════ │
│ - All users │
│ - All channels │
│ - All modes │
│ ══════════════════════> │
│ │
│ 5. END_OF_BURST │
│ <────────────────────── │
│ │
│ 6. Normal operation │
│ <═══════════════════════> │
BURST Example:
// Server BB sends its state to AA
N AliceUser 1 1234567890 alice alice.host +i B]AAAB ABAAA :Alice
│ │ │ │ │ │ │ │ └─ realname
│ │ │ │ │ │ │ └─ account
│ │ │ │ │ │ └─ user numeric
│ │ │ │ │ └─ modes
│ │ │ │ └─ hostname
│ │ │ └─ username
│ │ └─ timestamp
│ └─ hopcount
└─ NICK command (token: N)
B #test 1234567890 ABAAA:o,ABAAB
│ │ └─ members (ABAAA is op, ABAAB normal)
│ └─ timestamp
└─ BURST command
🛡️ FLOW E: FLOOD PROTECTION
IPcheck Rate Limiting
// IPcheck.c - ip_registry_check_connect()
int ip_registry_check_connect(const struct irc_in_addr *addr,
time_t *next_target_out) {
struct IPRegistryEntry *entry = ip_registry_find(addr);
if (!entry) {
// First connection from this IP
entry = ip_registry_new_entry();
ip_registry_canonicalize(&entry->addr, addr);
entry->connected = 1;
entry->attempts = 1;
entry->last_connect = NOW;
ip_registry_add(entry);
return 1; // ✅ ALLOW
}
// Check clone limit
if (entry->connected >= IPCHECK_CLONE_LIMIT) {
return 0; // ❌ REJECT: Too many clones
}
// Check connection rate
time_t elapsed = CONNECTED_SINCE(entry->last_connect);
if (elapsed < IPCHECK_CLONE_DELAY) {
// Too fast
if (entry->attempts++ > IPCHECK_CLONE_PERIOD) {
return 0; // ❌ REJECT: Connection flood
}
}
else {
// Reset attempt counter
entry->attempts = 1;
}
entry->connected++;
entry->last_connect = NOW;
return 1; // ✅ ALLOW
}
Decay Algorithm:
Time: 0s 10s 20s 30s 40s
Connect: ✓ ✓ ✓ [decay] ✓
Attempts: 1 2 3 1 2
If attempts > CLONE_PERIOD within CLONE_DELAY → REJECT
Target Rate Limiting
// IPcheck.c - ip_registry_check_target()
int ip_registry_check_target(struct Client *sptr, void *target,
const char *name) {
struct IPRegistryEntry *entry =
ip_registry_find(&cli_ip(sptr));
if (!entry->target)
entry->target = allocate_targets();
// Update free targets (token bucket)
unsigned int free_targets =
entry->target->count +
(CONNECTED_SINCE(entry->last_connect) / TARGET_DELAY);
if (free_targets > STARTTARGETS)
free_targets = STARTTARGETS;
// Check if already talking to this target
for (int i = 0; i < MAXTARGETS; i++) {
if (entry->target->targets[i] == target_hash(target))
return 0; // ✅ Known target, no cost
}
// New target
if (free_targets == 0) {
// ❌ REJECT: No free targets
send_reply(sptr, ERR_TARGETTOOFAST, name,
TARGET_DELAY * (STARTTARGETS - free_targets));
return 1;
}
// Consume one target
free_targets--;
add_target(entry, target);
return 0; // ✅ ALLOW
}
Example:
User starts with 10 targets (STARTTARGETS)
Sends PRIVMSG to:
#channel1 → 9 targets left
#channel2 → 8 targets left
...
#channel10 → 0 targets left
#channel11 → ❌ ERR_TARGETTOOFAST
After TARGET_DELAY seconds:
Targets regenerate: 0 → 1 → 2 → ... → 10 (max)
🐛 FLOW F: ERROR HANDLING
Socket Error Detection
// s_bsd.c - read_packet()
int read_packet(struct Client* cptr, int ready) {
int length = recv(cli_fd(cptr), readbuf, sizeof(readbuf), 0);
if (length < 0) {
// Error handling
int err = errno;
switch (err) {
case EWOULDBLOCK:
case EAGAIN:
// Not actually ready (spurious wakeup)
return 1; // OK, try later
case ECONNRESET:
// Connection reset by peer
return exit_client_msg(cptr, cptr, &me,
"Connection reset by peer");
case ETIMEDOUT:
// Connection timed out
return exit_client_msg(cptr, cptr, &me,
"Connection timed out");
default:
// Other error
return exit_client_msg(cptr, cptr, &me,
"Read error: %s", strerror(err));
}
}
if (length == 0) {
// EOF (connection closed gracefully)
return exit_client_msg(cptr, cptr, &me,
"Connection closed");
}
// Normal processing
// ...
}
Dead Link Marking
// send.c - dead_link()
static void dead_link(struct Client *to, char *notice) {
// ★ Don't exit immediately, mark for cleanup
SetFlag(to, FLAG_DEADSOCKET);
// Clear buffers to prevent further operations
DBufClear(&cli_recvQ(to));
MsgQClear(&cli_sendQ(to));
client_drop_sendq(cli_connect(to));
// Save error message
ircd_strncpy(cli_info(to), notice, REALLEN + 1);
// Notify opers
if (!IsUser(to) && !IsUnknown(to) && !HasFlag(to, FLAG_CLOSING))
sendto_opmask_butone(0, SNO_OLDSNO, "%s for %s",
cli_info(to), cli_name(to));
}
Main Loop Cleanup:
// ircd.c - event_loop()
while (running) {
event_loop_iteration();
// Check for dead sockets
for (i = 0; i <= HighestFd; i++) {
cptr = LocalClientArray[i];
if (cptr && IsDead(cptr)) {
exit_client(cptr, cptr, &me, cli_info(cptr));
}
}
}
📊 TIMING DIAGRAMS
Normal PRIVMSG Latency
Time Event
───── ──────────────────────────────────────
0ms Client A: send("PRIVMSG #test :Hi\r\n")
│
0.1ms Server: recv() → DBuf → parse
│
0.2ms Server: Handler m_privmsg()
│ ├─ Validate
│ ├─ Find channel
│ └─ Broadcast
│
0.3ms Server: 100× msgq_add() (channel members)
│
0.4ms Server: update_write() × 100
│
[Next event loop iteration]
│
10ms Server: epoll_wait() → EPOLLOUT events
│
11ms Server: send_queued() → writev() × 100
│
12ms Clients: recv() message
│
TOTAL: ~12ms latency (local network)
SendQ Overflow Scenario
Time Event
───── ──────────────────────────────────────
0s Client A connected, sendQ = 0 KB
│
1s Server → Client: 100 messages
│ sendQ = 50 KB
│ Client not reading (slow network)
│
2s Server → Client: 100 more messages
│ sendQ = 100 KB
│ ⚠️ WARNING: High sendQ
│
3s Server → Client: 100 more messages
│ sendQ = 150 KB
│ ⚠️ CRITICAL: Very high sendQ
│
4s kill_highest_sendq() triggered
│ ❌ Client A disconnected
│ Reason: "SendQ exceeded"
│
✅ Memory freed, server stable
Prevention:
// Recommended: Per-client hard limit
#define MAX_SENDQ_USER 65536 // 64KB
if (MsgQLength(&cli_sendQ(to)) > MAX_SENDQ_USER) {
dead_link(to, "SendQ exceeded");
return;
}
🔍 DEBUGGING FLOWS
Debug Logging Example
// Enable with /SET DEBUG level
Debug((DEBUG_INFO, "Client %s (%s) connected from %s",
cli_name(cptr), cli_username(cptr), cli_sock_ip(cptr)));
Debug((DEBUG_SEND, "Sending to %s: %s",
cli_name(to), message));
Debug((DEBUG_ERROR, "Failed to send to %s: %s",
cli_name(to), strerror(errno)));
Output (ircd.log):
[12:34:56] [INFO] Client Alice (alice) connected from 192.168.1.100
[12:34:57] [SEND] Sending to Alice: :server.test 001 Alice :Welcome
[12:34:58] [ERROR] Failed to send to Bob: Connection reset by peer
🎯 CRITICAL PATHS (Performance Hotspots)
1. Message Parsing (Most Frequent)
File: parse.c
Frequency: Every message received
Optimization:
- ✅ Trie lookup O(k)
- ✅ Token-based P10 (shorter strings)
- ❌ Still does byte-by-byte parsing
2. Channel Broadcast (Most Expensive)
File: send.c, channel.c
Frequency: Every channel message
Complexity: O(N) where N = members
Optimization:
- ✅ writev() reduces syscalls
- ✅ Message formatted once
- ❌ No message coalescing for multiple broadcasts
3. Event Loop (Always Running)
File: ircd_events.c, engine_epoll.c
Frequency: Continuous
Optimization:
- ✅ epoll() scales to 10K+ connections
- ✅ Edge-triggered mode reduces wakeups
- ❌ Single-threaded CPU bottleneck
📚 REFERINȚE SUPLIMENTARE
Command Flow Examples
USER Registration:
Client → Server: NICK Alice
Server → Client: (no response, waiting for USER)
Client → Server: USER alice 0 * :Alice Smith
Server → Client: :server 001 Alice :Welcome to IRC
:server 002 Alice :Your host is server
:server 003 Alice :This server was created...
:server 004 Alice server version modes
:server 005 Alice FEATURES :are supported
(MOTD)
:server 376 Alice :End of MOTD
Channel JOIN:
Client → Server: JOIN #test
Server processing:
1. Validate channel name
2. Check if channel exists
3. Check bans/invite-only
4. Add user to channel
5. Broadcast JOIN to all members
6. Send channel topic
7. Send NAMES list
Server → Client: :Alice!alice@host JOIN #test
Server → Client: :server 332 Alice #test :Channel topic
Server → Client: :server 333 Alice #test topic_setter 1234567890
Server → Client: :server 353 Alice = #test :@Alice Bob Carol
Server → Client: :server 366 Alice #test :End of NAMES
Document generat de Senior Software Architect - Complement la analiza principală.