about summary refs log tree commit diff
diff options
context:
space:
mode:
authorNakidai <nakidai@disroot.org>2026-01-04 17:41:32 +0300
committerNakidai <nakidai@disroot.org>2026-01-04 17:44:09 +0300
commit78426afe18d9ce730a4d92033ca261f9b2f173a0 (patch)
tree0c511b571e9a37496f170fce08737a849a8ba5ef
downloadlibreircd-78426afe18d9ce730a4d92033ca261f9b2f173a0.tar.gz
libreircd-78426afe18d9ce730a4d92033ca261f9b2f173a0.zip
Add code
-rw-r--r--.gitignore4
-rw-r--r--LICENSE13
-rw-r--r--Makefile16
-rw-r--r--README9
-rw-r--r--handle.c160
-rw-r--r--ircd.h74
-rw-r--r--loop.c134
-rw-r--r--main.c52
-rw-r--r--message.c81
-rw-r--r--peer.c73
-rw-r--r--user.c50
-rw-r--r--writef.c40
12 files changed, 706 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..30d2722
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*.core
+*.o
+*.out
+ircd
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7802b18
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2026 Naidai Perumenei <nakidai at disroot at org>
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5953be1
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+NAME ?= ircd
+
+CFLAGS += -Wall -Wextra
+
+OBJS += handle.o
+OBJS += loop.o
+OBJS += main.o
+OBJS += message.o
+OBJS += peer.o
+OBJS += user.o
+OBJS += writef.o
+
+${NAME}: ${OBJS}
+	${CC} -o ${NAME} ${CFLAGS} ${LDFLAGS} ${OBJS}
+clean:
+	rm -f ${NAME} ${OBJS}
diff --git a/README b/README
new file mode 100644
index 0000000..473e983
--- /dev/null
+++ b/README
@@ -0,0 +1,9 @@
+LibreIRCd
+=========
+Simple IRC server in C
+
+usage: ircd hostname bindaddr port
+
+---------------
+This project is incomplete: channels, modes, a lot of validation is not present
+yet.
diff --git a/handle.c b/handle.c
new file mode 100644
index 0000000..79a3b9a
--- /dev/null
+++ b/handle.c
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "ircd.h"
+
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+
+
+#define ensure(cond, iffalse, doexit) if (!(cond)) { iffalse; if (doexit >= 0) return doexit; }
+
+static int
+nick(struct Message *msg, struct Peer *peer)
+{
+	ensure(msg->params[0] && msg->params[0], writef(
+		peer->fd,
+		":%s 431 %s :No nickname given",
+		hostname,
+		getnick(peer)
+	), 0);
+
+	user_reg(peer, msg->params[0], NULL, NULL);
+
+	return 0;
+}
+
+static int
+privmsg(struct Message *msg, struct Peer *peer)
+{
+	size_t i;
+
+	ensure(peer->type, writef(
+		peer->fd,
+		":%s 451 %s :You have not registered",
+		hostname,
+		getnick(peer)
+	), 0);
+	ensure(msg->params[0] && *msg->params[0], writef(
+		peer->fd,
+		":%s 411 %s :No recipient given (PRIVMSG)",
+		hostname,
+		getnick(peer)
+	), 0);
+	ensure(msg->params[1] && *msg->params[1], writef(
+		peer->fd,
+		":%s 412 %s :No text to send",
+		hostname,
+		getnick(peer)
+	), 0);
+
+	/* TODO: implement channels */
+	ensure(!strchr("!+#&", *msg->params[0]), writef(
+		peer->fd,
+		":%s 421 %s :Channels are not supported yet",
+		hostname,
+		getnick(peer)
+	), 0);
+
+	/* TODO: implement server-to-server communication */
+	for (i = 0; i < PEERS_MAX; ++i)
+		if (peers[i].type == CLIENT && !strcmp(peers[i].nick, msg->params[0]))
+			writef(
+				peers[i].fd,
+				":%s!%s@%s PRIVMSG %s :%s",
+				peer->nick,
+				peer->user,
+				peer->host,
+				peer->nick,
+				msg->params[1]
+			);
+	return 0;
+}
+
+static int
+quit(struct Message *msg, struct Peer *peer)
+{
+	return 1;
+}
+
+static int
+user(struct Message *msg, struct Peer *peer)
+{
+	size_t i;
+
+	ensure(!peer->type, writef(
+		peer->fd,
+		":%s 462 %s :Unauthorized command (already registered)",
+		hostname,
+		getnick(peer)
+	), 0);
+	for (i = 0; i < 4; ++i)
+		ensure(msg->params[i] && *msg->params[i], writef(
+			peer->fd,
+			":%s 461 %s %s :Not enough parameters",
+			hostname,
+			getnick(peer),
+			msg->command
+		), 0);
+
+	user_reg(peer, NULL, msg->params[0], msg->params[3]);
+
+	return 0;
+}
+
+static int
+default_handler(struct Message *msg, struct Peer *peer)
+{
+	ensure(peer->type, 0, 0);
+
+	writef(peer->fd, ":%s 421 %s %s :Unknown command", hostname, getnick(peer), msg->command);
+	return 0;
+}
+
+static struct Handler {
+	const char *name;
+	Handler *handler;
+} handlers[] =
+{
+	{ "nick", nick },
+	{ "privmsg", privmsg },
+	{ "quit", quit },
+	{ "user", user },
+};
+
+Handler *
+find(const char *command)
+{
+	struct Handler *l, *m, *r;
+	int res;
+
+	l = handlers;
+	r = handlers + lengthof(handlers) - 1;
+	while (l <= r)
+	{
+		m = l + (r - l) / 2;
+		res = strcmp(command, m->name);
+		if (res > 0)
+			l = ++m;
+		else if (res < 0)
+			r = --m;
+		else
+			return m->handler;
+	}
+
+	return default_handler;
+}
diff --git a/ircd.h b/ircd.h
new file mode 100644
index 0000000..5e787da
--- /dev/null
+++ b/ircd.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __IRCD_H__
+#define __IRCD_H__
+
+#include <stddef.h>
+
+
+#define PARAM_MAX 15
+#define PEERS_MAX 512
+#define MESSAGE_MAX 512
+
+#define BIT(x) x##_BIT, x = 1 << x##_BIT, x##_BIT_ = x##_BIT
+#define lengthof(X) (sizeof(X) / sizeof(*(X)))
+
+struct Message
+{
+	char *nick, *user, *host, *command, *params[PARAM_MAX];
+};
+
+struct Peer
+{
+	int fd;
+	enum ClientType { UNREGED, CLIENT, SERVER, SERVICE } type;
+	char nick[16], user[16], real[32], host[64];
+	char **channels;
+	size_t channels_count;
+	char modes[52];
+	char buf[MESSAGE_MAX];
+	size_t recvd;
+};
+
+struct Channel
+{
+	enum { GLOBAL, LOCAL, MODELESS, SAFE } type;
+	char name[64];
+	struct {
+		char mode;
+		char param[MESSAGE_MAX - 1];
+	} *modes;
+	size_t modes_count;
+};
+
+typedef int Handler(struct Message *msg, struct Peer *peer);
+
+extern struct Peer peers[PEERS_MAX];
+extern const char *hostname;
+extern const char *host;
+extern int port;
+
+const char *getnick(const struct Peer *peer);
+void user_reg(struct Peer *peer, const char *nick, const char *user, const char *real);
+
+int parse_message(char *buf, struct Message *msg);
+int handle(struct Peer *peer);
+Handler *find(const char *command);
+int writef(int fd, const char *fmt, ...);
+void ircd(void);
+
+#endif /* __IRCD_H__ */
diff --git a/loop.c b/loop.c
new file mode 100644
index 0000000..ba14a4d
--- /dev/null
+++ b/loop.c
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "ircd.h"
+
+#include <string.h>
+
+#include <err.h>
+#include <poll.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+
+struct Peer peers[PEERS_MAX];
+static struct pollfd pfd[PEERS_MAX + 1];
+static size_t remove_schedule[PEERS_MAX];
+
+const char *host;
+int port;
+
+void
+ircd(void)
+{
+	size_t i, schedulei, connected, pid;
+	struct sockaddr_in addr, clientaddr;
+	int res, sfd, client;
+	nfds_t passed;
+	ssize_t recvd;
+
+	addr.sin_family = PF_INET;
+	addr.sin_port = htons(port);
+
+	res = inet_pton(AF_INET, host, &addr.sin_addr);
+	if (res == -1)
+		err(1, "inet_pton()");
+
+	sfd = socket(AF_INET, SOCK_STREAM, 0);
+	if (sfd == -1)
+		err(1, "socket()");
+
+	res = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));
+	if (res == -1)
+		err(1, "setsockopt(SO_REUSEADDR)");
+
+	res = bind(sfd, (void *)&addr, sizeof(addr));
+	if (res == -1)
+		err(1, "bind()");
+
+	res = listen(sfd, 128);
+	if (res == -1)
+		err(1, "listen()");
+
+	signal(SIGPIPE, SIG_IGN);
+	for (connected = 0;;schedulei = 0)
+	{
+		pfd[connected] = (struct pollfd){ .fd = sfd, .events = POLLIN };
+		for (i = 0; i < connected; ++i)
+			pfd[i] = (struct pollfd)
+			{
+				.fd = peers[i].fd,
+				.events = POLLIN,
+			};
+		res = poll(pfd, (passed = connected) + 1, -1);
+		if (res == -1)
+			err(1, "poll()");
+
+		if (pfd[connected].revents & POLLIN)
+		{
+			client = accept(sfd, (void *)&clientaddr, &(int){sizeof(clientaddr)});
+			if (client == -1)
+			{
+				warn("accept(sfd)");
+			}
+			else if (connected == PEERS_MAX)
+			{
+				/*
+				 * TODO: maybe send 005 or smth?
+				 */
+				close(client);
+			}
+			else
+			{
+				peers[connected++] = (struct Peer){ .fd = client };
+				strlcpy(
+					peers[connected - 1].host,
+					inet_ntoa(clientaddr.sin_addr),
+					sizeof(peers->host)
+				);
+			}
+		}
+
+		for (i = 0; i < passed; ++i)
+		{
+			if (!(pfd[i].revents & POLLIN))
+				continue;
+
+			recvd = read(
+				peers[i].fd,
+				peers[i].buf + peers[i].recvd,
+				sizeof(peers[i].buf) - peers[i].recvd
+			);
+			peers[i].recvd += recvd;
+			if (recvd == -1 || !recvd || handle(&peers[i]))
+				remove_schedule[schedulei++] = i;
+		}
+		for (i = 0; i < schedulei; ++i)
+		{
+			pid = remove_schedule[i];
+			/* TODO: announce_quit(&peers[pid]); */
+			writef(peers[pid].fd, "ERROR :Closing Link: %s", peers[pid].host);
+			close(peers[pid].fd);
+			peers[pid] = peers[--connected];
+		}
+		schedulei = 0;
+	}
+}
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..8be182a
--- /dev/null
+++ b/main.c
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "ircd.h"
+
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <err.h>
+#include <unistd.h>
+
+
+const char *hostname;
+
+int
+main(int argc, char **argv)
+{
+	size_t i;
+	char *p;
+
+	for (i = 1; i < 4; ++i)
+		if (!argv[i] || !*argv[i])
+			errx(1, "usage: %s hostname bindaddr port", argv[0]);
+	hostname = argv[1];
+	host = argv[2];
+	port = strtod(argv[3], &p);
+	if (*p || port <= 0 || port > 65535)
+		errx(1, "invalid port");
+
+#ifdef __OpenBSD__
+	if (unveil(NULL, NULL))
+		err(1, "unveil()");
+	if (pledge("stdio rpath inet", ""))
+		err(1, "pledge()");
+#endif /* __OpenBSD__ */
+
+	ircd();
+}
diff --git a/message.c b/message.c
new file mode 100644
index 0000000..f394ff7
--- /dev/null
+++ b/message.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "ircd.h"
+
+#include <stddef.h>
+#include <string.h>
+
+
+int
+parse_message(char *buf, struct Message *msg)
+{
+	char *t, *next;
+	size_t i;
+
+	memset(msg, 0, sizeof(*msg));
+
+	if (*buf == ':')
+	{
+		t = ++buf;
+		next = strchr(buf, ' ');
+		if (!next)
+			return 1;
+		*next++ = 0;
+		buf = next;
+
+		next = strchr(t, '@');
+		if (next)
+		{
+			*next++ = 0;
+			if (!*t)
+				return 1;
+
+			msg->nick = t;
+			t = next;
+
+			next = strchr(msg->nick, '!');
+			if (next)
+			{
+				*next++ = 0;
+				if (!*msg->nick || !*next)
+					return 1;
+				msg->user = next;
+			}
+		}
+		if (!*t)
+			return 1;
+		msg->host = t;
+	}
+
+	msg->command = buf;
+	t = strstr(buf, " :");
+	if (t)
+		*((t += 2) - 2) = 0;
+	for (i = 0; i < PARAM_MAX - !!t; ++i, buf = next)
+	{
+		next = strchr(buf, ' ');
+		if (!next)
+			break;
+		*next++ = 0;
+		msg->params[i] = next;
+	}
+	if (strchr(buf, ' '))
+		return 1;
+	if (t)
+		msg->params[i] = t;
+	return 0;
+}
diff --git a/peer.c b/peer.c
new file mode 100644
index 0000000..21dba70
--- /dev/null
+++ b/peer.c
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "ircd.h"
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stddef.h>
+#include <string.h>
+
+#include <err.h>
+
+
+static void
+move(struct Peer *peer, size_t n)
+{
+	memmove(peer->buf, peer->buf + n, sizeof(peer->buf) - n);
+	memset(peer->buf + sizeof(peer->buf) - n, 0, n);
+	peer->recvd -= n;
+}
+
+int
+handle(struct Peer *peer)
+{
+	struct Message msg;
+	char *p, *c;
+
+	while ((p = memmem(peer->buf, sizeof(peer->buf), "\r\n", 2)))
+	{
+		if (memchr(peer->buf, '\0', p - peer->buf))
+		{
+			warnx("got 0 from %d", peer->fd);
+			goto next;
+		}
+
+		*p = 0;
+		if (parse_message(peer->buf, &msg))
+		{
+			warnx("malformed input from %d", peer->fd);
+			goto next;
+		}
+
+		for (c = msg.command; *c; ++c)
+			*c = tolower(*c);
+		if (find(msg.command)(&msg, peer))
+			return 1;
+next:
+		move(peer, p - peer->buf + 2);
+	}
+	if (peer->recvd == sizeof(peer->buf))
+	{
+		/*
+	 	* TODO: maybe somehow be more careful with peer data?
+	 	* or drop them?
+	 	*/
+		memset(peer->buf, 0, sizeof(peer->buf));
+		peer->recvd = 0;
+	}
+	return 0;
+}
diff --git a/user.c b/user.c
new file mode 100644
index 0000000..4411144
--- /dev/null
+++ b/user.c
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "ircd.h"
+
+#include <string.h>
+
+
+const char *
+getnick(const struct Peer *peer)
+{
+	return *peer->nick ? peer->nick : "*";
+}
+
+void
+user_reg(struct Peer *peer, const char *nick, const char *user, const char *real)
+{
+	if (nick)
+		strlcpy(peer->nick, nick, sizeof(peer->nick));
+	if (user)
+		strlcpy(peer->user, user, sizeof(peer->user));
+	if (real)
+		strlcpy(peer->real, real, sizeof(peer->real));
+	if (*peer->nick && *peer->user && *peer->real)
+	{
+		peer->type = CLIENT;
+		writef(
+			peer->fd,
+			":%s 001 %s :Welcome to the Internet Relay Network %s!%s@%s",
+			hostname,
+			getnick(peer),
+			getnick(peer),
+			peer->user,
+			peer->host
+		);
+	}
+}
diff --git a/writef.c b/writef.c
new file mode 100644
index 0000000..7d57e3b
--- /dev/null
+++ b/writef.c
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2026 Nakidai Perumenei <nakidai at disroot dot org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "ircd.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <unistd.h>
+
+
+int
+writef(int fd, const char *fmt, ...)
+{
+	static char buf[MESSAGE_MAX + 1];
+	va_list args;
+	int written;
+
+	va_start(args, fmt);
+	written = vsnprintf(buf, sizeof(buf) - 2, fmt, args);
+	va_end(args);
+	written = written < sizeof(buf) - 2 ? written : sizeof(buf) - 2;
+	memcpy(buf + written, "\r\n", 3);
+
+	return write(fd, buf, written + 3);
+}