Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
automake \
libc6-dev \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /build/ircu2
Expand All @@ -28,6 +29,7 @@ FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
perl \
libssl3 \
&& rm -rf /var/lib/apt/lists/*

RUN useradd -r -m -d /opt/ircu ircu
Expand Down
13 changes: 13 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,19 @@ AC_MSG_RESULT([$unet_cv_with_maxcon])
AC_DEFINE_UNQUOTED(MAXCONNECTIONS, $unet_cv_with_maxcon,
[Maximum number of network connections])

# Check for OpenSSL (libssl, libcrypto)
# Use pkg-config to find OpenSSL if available
PKG_CHECK_MODULES([OPENSSL], [openssl], [
CPPFLAGS="$OPENSSL_CFLAGS $CPPFLAGS"
LIBS="$OPENSSL_LIBS $LIBS"
], [
AC_CHECK_LIB([ssl], [SSL_library_init], [SSL_LIBS="-lssl"], [AC_MSG_ERROR([OpenSSL libssl not found])])
AC_CHECK_LIB([crypto], [SHA1], [CRYPTO_LIBS="-lcrypto"], [AC_MSG_ERROR([OpenSSL libcrypto not found])])
AC_SUBST([SSL_LIBS])
AC_SUBST([CRYPTO_LIBS])
LIBS="$SSL_LIBS $CRYPTO_LIBS $LIBS"
])

dnl Finally really generate all output files:
AC_CONFIG_FILES([Makefile ircd/Makefile ircd/test/Makefile])
AC_OUTPUT
Expand Down
10 changes: 10 additions & 0 deletions doc/example.conf
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,16 @@ Port {
hidden = no;
};

# WebSocket listener (RFC 6455). This example uses port 6080 so the ircd
# does not need root: binding to port 80 (typical for ws:// in browsers)
# is a privileged range on Unix. For standard ports, run the ircd on a
# high port like this and put a reverse proxy (e.g. nginx, haproxy, etc.)
# in front that listens on 80.
Port {
port = 6080;
websocket = yes;
};

# Quarantine blocks disallow operators from using OPMODE and CLEARMODE
# on certain channels. Opers with the force_opmode (for local
# channels, force_local_opmode) privilege may override the quarantine
Expand Down
9 changes: 9 additions & 0 deletions doc/readme.features
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,15 @@ This is the maximum number of seconds to wait for the ident lookup and
the DNS query to succeed. On older (pre 2.10.11.06) servers this was
hard coded to 60 seconds.

WEBSOCKET_KEEPALIVE
* Type: integer
* Default: 0 (disabled)

If greater than zero, the server sends an RFC 6455 WebSocket Ping frame
to registered clients on WebSocket listener connections at this interval
(in seconds). This is for transport / idle TCP keepalive only; it does
not replace IRC PING/PONG (see PINGFREQUENCY and related settings).

IPCHECK_CLONE_LIMIT
* Type: integer
* Default: 4
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ services:
ports:
- "6667:6667"
- "4400:4400"
- "7000:7000"
networks:
ircu-test-net:
ipv4_address: 10.55.0.10
Expand Down
27 changes: 23 additions & 4 deletions include/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
#ifndef INCLUDED_res_h
#include "res.h"
#endif
#ifndef INCLUDED_websocket_h
#include "websocket.h"
#endif
#ifndef INCLUDED_sys_types_h
#include <sys/types.h> /* time_t, size_t */
#define INCLUDED_sys_types_h
Expand Down Expand Up @@ -198,6 +201,11 @@ struct Connection
int con_error; /**< last socket level error for client */
int con_sentalong; /**< sentalong marker for connection */
unsigned int con_snomask; /**< mask for server messages */
enum ws_mode_t {
WS_NONE = 0,
WS_TEXT = 1,
WS_BINARY = 2
} ws_mode; /**< WebSocket mode */
HandlerType con_handler; /**< Message index into command table
for parsing. */
time_t con_nextnick; /**< Next time a nick change is allowed */
Expand Down Expand Up @@ -231,6 +239,9 @@ struct Connection
clients socket to close. */
struct Socket con_socket; /**< socket descriptor for
client */
char con_ws_handshake[WEBSOCKET_MAX_HEADER + 1]; /**< Buffer for accumulating WebSocket handshake data */
size_t con_ws_handshake_len; /**< Length of handshake buffer */
time_t con_ws_last_keepalive; /**< Last time we sent RFC6455 Ping (not IRC PING); 0 = not set */
struct Timer con_proc; /**< process latent messages from
client */
struct Privs con_privs; /**< Oper privileges */
Expand Down Expand Up @@ -395,6 +406,8 @@ struct Client {
#define cli_sasl(cli) con_sasl(cli_connect(cli))
/** Get SASL timeout timer for client. */
#define cli_sasl_timer(cli) (&con_sasl_timer(cli_connect(cli)))
/** Get the WebSocket mode for the client. */
#define cli_ws_mode(cli) con_ws_mode(cli_connect(cli))

/** Verify that a connection is valid. */
#define con_verify(con) ((con)->con_magic == CONNECTION_MAGIC)
Expand Down Expand Up @@ -482,6 +495,8 @@ struct Client {
#define con_sasl(con) ((con)->con_sasl)
/** Get the SASL timeout timer for the connection. */
#define con_sasl_timer(con) ((con)->con_sasl_timer)
/** Get the WebSocket mode for the connection. */
#define con_ws_mode(con) ((con)->ws_mode)

#define STAT_CONNECTING 0x001 /**< connecting to another server */
#define STAT_HANDSHAKE 0x002 /**< pass - server sent */
Expand All @@ -492,7 +507,7 @@ struct Client {
#define STAT_SERVER 0x040 /**< fully registered server */
#define STAT_USER 0x080 /**< fully registered user */
#define STAT_WEBIRC 0x100 /**< connection on a webirc port */

#define STAT_WEBSOCKET 0x200 /**< connection on a websocket port */
/*
* status macros.
*/
Expand All @@ -508,7 +523,7 @@ struct Client {
#define IsMe(x) (cli_status(x) == STAT_ME)
/** Return non-zero if the client has not yet registered. */
#define IsUnknown(x) (cli_status(x) & \
(STAT_UNKNOWN | STAT_UNKNOWN_USER | STAT_UNKNOWN_SERVER | STAT_WEBIRC))
(STAT_UNKNOWN | STAT_UNKNOWN_USER | STAT_UNKNOWN_SERVER | STAT_WEBIRC | STAT_WEBSOCKET))
/** Return non-zero if the client is an unregistered connection on a
* server port. */
#define IsServerPort(x) (cli_status(x) == STAT_UNKNOWN_SERVER )
Expand All @@ -518,10 +533,13 @@ struct Client {
/** Return non-zero if the client is an unregistered connection on a
* WebIRC port that has not yet sent WEBIRC. */
#define IsWebircPort(x) (cli_status(x) == STAT_WEBIRC)
/** Return non-zero if the client is an unregistered connection on a
* websocket port that has not yet completed the handshake. */
#define IsWebsocketPort(x) (cli_status(x) == STAT_WEBSOCKET)
/** Return non-zero if the client is a real client connection. */
#define IsClient(x) (cli_status(x) & \
(STAT_HANDSHAKE | STAT_ME | STAT_UNKNOWN |\
STAT_UNKNOWN_USER | STAT_UNKNOWN_SERVER | STAT_SERVER | STAT_USER))
STAT_UNKNOWN_USER | STAT_UNKNOWN_SERVER | STAT_SERVER | STAT_USER | STAT_WEBSOCKET))
/** Return non-zero if the client ignores flood limits. */
#define IsTrusted(x) (cli_status(x) & \
(STAT_CONNECTING | STAT_HANDSHAKE | STAT_ME | STAT_SERVER))
Expand Down Expand Up @@ -615,6 +633,8 @@ struct Client {
/** Return non-zero if the client has an active PING request. */
#define IsPingSent(x) HasFlag(x, FLAG_PINGSENT)

/** Return non-zero if the client has completed the handshake for a WebSocket connection. */
#define IsWebsocket(x) (cli_ws_mode(x) != WS_NONE)
/** Return non-zero if the client has operator or server privileges. */
#define IsPrivileged(x) (IsAnOper(x) || IsServer(x))
/** Return non-zero if the client's host is hidden. */
Expand Down Expand Up @@ -774,4 +794,3 @@ extern void client_set_privs(struct Client *client, struct ConfItem *oper,
extern int client_report_privs(struct Client* to, struct Client* client);

#endif /* INCLUDED_client_h */

1 change: 1 addition & 0 deletions include/dbuf.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ extern int dbuf_put(struct DBuf *dyn, const char *buf, unsigned int length);
extern const char *dbuf_map(const struct DBuf *dyn, unsigned int *length);
extern unsigned int dbuf_get(struct DBuf *dyn, char *buf, unsigned int length);
extern unsigned int dbuf_getmsg(struct DBuf *dyn, char *buf, unsigned int length);
extern unsigned int dbuf_getframe(struct DBuf *dyn, char *buf, unsigned int length);
extern void dbuf_count_memory(size_t *allocated, size_t *used);


Expand Down
1 change: 1 addition & 0 deletions include/ircd_features.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ enum Feature {
FEAT_IRCD_RES_RETRIES,
FEAT_IRCD_RES_TIMEOUT,
FEAT_AUTH_TIMEOUT,
FEAT_WEBSOCKET_KEEPALIVE,
FEAT_ANNOUNCE_INVITES,

/* features that affect all operators */
Expand Down
3 changes: 3 additions & 0 deletions include/listener.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ enum ListenerFlag {
LISTEN_IPV6,
/** Port accepts only webirc connections. */
LISTEN_WEBIRC,
/** Port accepts websocket connections. */
LISTEN_WEBSOCKET,
/** Sentinel for counting listener flags. */
LISTEN_LAST_FLAG
};
Expand All @@ -79,6 +81,7 @@ struct Listener {
#define listener_server(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_SERVER)
#define listener_active(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_ACTIVE)
#define listener_webirc(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_WEBIRC)
#define listener_websocket(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_WEBSOCKET)

extern void add_listener(int port, const char* vaddr_ip,
const char* mask,
Expand Down
13 changes: 12 additions & 1 deletion include/msgq.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,17 @@ struct Client;
struct StatDesc;

struct Msg;
struct MsgBuf;

/** Buffer for a single message. */
struct MsgBuf {
struct MsgBuf *next; /**< next msg in global queue */
struct MsgBuf **prev_p; /**< what points to us in linked list */
struct MsgBuf *real; /**< the actual MsgBuf we're attaching */
unsigned int ref; /**< reference count */
unsigned int length; /**< length of message */
unsigned int power; /**< size of buffer (power of 2) */
char msg[1]; /**< the message */
};

/** Queue of individual messages. */
struct MsgQList {
Expand Down Expand Up @@ -75,6 +85,7 @@ extern void msgq_delete(struct MsgQ *mq, unsigned int length);
extern int msgq_mapiov(const struct MsgQ *mq, struct iovec *iov, int count,
unsigned int *len);
extern struct MsgBuf *msgq_make(struct Client *dest, const char *format, ...);
extern struct MsgBuf *msgq_raw_alloc(struct Client *dest, unsigned int minbytes);
extern struct MsgBuf *msgq_vmake(struct Client *dest, const char *format,
va_list args);
extern void msgq_append(struct Client *dest, struct MsgBuf *mb,
Expand Down
1 change: 1 addition & 0 deletions include/s_auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ struct AuthRequest;
struct StatDesc;

extern void start_auth(struct Client *);
extern void start_dns_ident(struct Client *client);
extern int auth_ping_timeout(struct Client *);
extern int auth_set_pong(struct AuthRequest *auth, unsigned int cookie);
extern int auth_set_user(struct AuthRequest *auth, const char *username, const char *hostname, const char *servername, const char *userinfo);
Expand Down
35 changes: 35 additions & 0 deletions include/websocket.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* IRC - Internet Relay Chat, include/websocket.h
* Copyright (C) 2026 MrIron <mriron@undernet.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 1, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#ifndef INCLUDED_WEBSOCKET_H
#define INCLUDED_WEBSOCKET_H

#include <stddef.h>
#include "client.h"

/* Maximum size of a WebSocket header */
#define WEBSOCKET_MAX_HEADER 4096

int websocket_handshake_handler(struct Client *cptr);
int websocket_parse_frame(struct Client *cptr, const char *buf, size_t buflen);
struct MsgBuf *websocket_frame_msgbuf(struct Client *cptr, const char *line, size_t linelen);
/** RFC 6455 Ping (not IRC PING); 0 on success, -1 on write failure. */
int websocket_send_keepalive_ping(struct Client *cptr);

#endif /* INCLUDED_WEBSOCKET_H */
1 change: 1 addition & 0 deletions ircd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ ircd_SOURCES = \
send.c \
uping.c \
userload.c \
websocket.c \
whocmds.c \
whowas.c \
ircd_parser.y \
Expand Down
15 changes: 15 additions & 0 deletions ircd/dbuf.c
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,21 @@ static unsigned int dbuf_flush(struct DBuf *dyn)
return dyn->length;
}

/** Extract the entire buffer as a single frame (for WebSocket).
* Copies up to 'length' bytes from the buffer, regardless of EOL.
* Deletes the bytes from the buffer after copying.
* Returns the number of bytes copied.
*/
unsigned int dbuf_getframe(struct DBuf *dyn, char *buf, unsigned int length)
{
unsigned int to_copy = (length < dyn->length) ? length : dyn->length;
if (to_copy == 0)
return 0;
unsigned int copied = dbuf_get(dyn, buf, to_copy);
buf[copied] = '\0';
return copied;
}

/** Copy a single line from a data buffer.
* If the output buffer cannot hold the whole line, or if there is no
* EOL in the buffer, return 0.
Expand Down
20 changes: 20 additions & 0 deletions ircd/ircd.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
#include "uping.h"
#include "userload.h"
#include "version.h"
#include "websocket.h"
#include "whowas.h"

/* #include <assert.h> -- Now using assert in ircd_log.h */
Expand Down Expand Up @@ -379,6 +380,25 @@ static void check_pings(struct Event* ev) {

max_ping = client_get_ping(cptr);

/* RFC6455 WebSocket Ping keepalive (not IRC PING); interval from features */
if (MyConnect(cptr) && IsWebsocket(cptr) && IsRegistered(cptr)) {
int ws_ka = feature_int(FEAT_WEBSOCKET_KEEPALIVE);
if (ws_ka > 0) {
struct Connection *wcon = cli_connect(cptr);
int expire_ws;

if (wcon->con_ws_last_keepalive == 0)
wcon->con_ws_last_keepalive = CurrentTime;
expire_ws = wcon->con_ws_last_keepalive + ws_ka;
if (expire_ws < next_check)
next_check = expire_ws;
if (CurrentTime - wcon->con_ws_last_keepalive >= ws_ka) {
if (websocket_send_keepalive_ping(cptr) == 0)
wcon->con_ws_last_keepalive = CurrentTime;
}
}
}

/* If it's a server and we have not sent an AsLL lately, do so. */
if (IsServer(cptr)) {
if (CurrentTime - cli_serv(cptr)->asll_last >= max_ping) {
Expand Down
1 change: 1 addition & 0 deletions ircd/ircd_features.c
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ static struct FeatureDesc {
F_I(IRCD_RES_RETRIES, 0, 2, 0),
F_I(IRCD_RES_TIMEOUT, 0, 4, 0),
F_I(AUTH_TIMEOUT, 0, 9, 0),
F_I(WEBSOCKET_KEEPALIVE, 0, 0, 0),
F_B(ANNOUNCE_INVITES, 0, 0, 0),

/* features that affect all operators */
Expand Down
1 change: 1 addition & 0 deletions ircd/ircd_lexer.c
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ static const struct lexer_token tokens[] = {
{ "vhost", VHOST },
{ "walk_lchan", TPRIV_WALK_LCHAN },
{ "webirc", WEBIRC },
{ "websocket", WEBSOCKET },
{ "weeks", WEEKS },
{ "whox", TPRIV_WHOX },
{ "wide_gline", TPRIV_WIDE_GLINE },
Expand Down
11 changes: 10 additions & 1 deletion ircd/ircd_parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ static void free_slist(struct SLink **link) {
%token TOK_IPV4 TOK_IPV6
%token DNS
%token WEBIRC
%token WEBSOCKET
%token IPCHECK
%token EXCEPT
%token INCLUDE
Expand Down Expand Up @@ -800,7 +801,7 @@ portblock: PORT {
port = 0;
};
portitems: portitem portitems | portitem;
portitem: portnumber | portvhost | portvhostnumber | portmask | portserver | portwebirc | porthidden;
portitem: portnumber | portvhost | portvhostnumber | portmask | portserver | portwebirc | portwebsocket | porthidden;
portnumber: PORT '=' address_family NUMBER ';'
{
if ($4 < 1 || $4 > 65535) {
Expand Down Expand Up @@ -853,6 +854,14 @@ portserver: SERVER '=' YES ';'
FlagClr(&listen_flags, LISTEN_SERVER);
};

portwebsocket: WEBSOCKET '=' YES ';'
{
FlagSet(&listen_flags, LISTEN_WEBSOCKET);
} | WEBSOCKET '=' NO ';'
{
FlagClr(&listen_flags, LISTEN_WEBSOCKET);
};

porthidden: HIDDEN '=' YES ';'
{
FlagSet(&listen_flags, LISTEN_HIDDEN);
Expand Down
Loading
Loading