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
10 changes: 10 additions & 0 deletions include/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ struct Connection
struct Timer con_sasl_timer; /**< SASL timeout timer */
char* con_rexmit; /**< TLS retransmission data */
size_t con_rexmit_len; /**, TLS retransmission length */
unsigned char con_tls_want_rd; /**< enum ircd_tls_want: a TLS read's blocked direction */
unsigned char con_tls_want_wr; /**< enum ircd_tls_want: a TLS write's blocked direction */
};

/** Magic constant to identify valid Connection structures. */
Expand Down Expand Up @@ -413,6 +415,10 @@ struct Client {
#define cli_buffer(cli) con_buffer(cli_connect(cli))
/** Get the Socket structure for sending to a client. */
#define cli_socket(cli) con_socket(cli_connect(cli))
/** Blocked direction (enum ircd_tls_want) of a TLS read for a client. */
#define cli_tls_want_rd(cli) con_tls_want_rd(cli_connect(cli))
/** Blocked direction (enum ircd_tls_want) of a TLS write for a client. */
#define cli_tls_want_wr(cli) con_tls_want_wr(cli_connect(cli))
/** Get Timer for processing waiting messages from the client. */
#define cli_proc(cli) con_proc(cli_connect(cli))
/** Get auth request for client. */
Expand Down Expand Up @@ -498,6 +504,10 @@ struct Client {
#define con_buffer(con) ((con)->con_buffer)
/** Get the Socket for the connection. */
#define con_socket(con) ((con)->con_socket)
/** Blocked direction (enum ircd_tls_want) of a TLS read on the connection. */
#define con_tls_want_rd(con) ((con)->con_tls_want_rd)
/** Blocked direction (enum ircd_tls_want) of a TLS write on the connection. */
#define con_tls_want_wr(con) ((con)->con_tls_want_wr)
/** Get the Timer for processing more data from the connection. */
#define con_proc(con) ((con)->con_proc)
/** Get the oper privilege set for the connection. */
Expand Down
108 changes: 84 additions & 24 deletions include/ircd_tls.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ static inline int ircd_tls_trust_verifies_ca(ircd_tls_trust_policy policy)
/** Size of the human-readable reason buffer filled by ircd_tls_negotiate(). */
#define TLS_REASON_LEN 128

/** Which socket direction a TLS operation is blocked on.
*
* TLS breaks the plaintext assumption that a read waits on readable and a
* write waits on writable: a TLS *write* can be blocked waiting to *read* the
* socket (and vice versa). Backends report the blocked direction with these
* values; the core (tls_io.c) turns them into socket event interest. This is
* the single source of truth for cross-direction I/O — there are no separate
* ad-hoc flags. */
enum ircd_tls_want {
IRCD_TLS_WANT_NONE = 0, /**< not blocked (or blocked on its natural direction) */
IRCD_TLS_WANT_READ, /**< the operation needs the socket to become readable */
IRCD_TLS_WANT_WRITE /**< the operation needs the socket to become writable */
};

/* The following variables and functions are provided by ircu2's core
* code, not by the TLS interface.
*/
Expand Down Expand Up @@ -226,49 +240,95 @@ void ircd_tls_listen_free(struct Listener *listener);
/** ircd_tls_negotiate() attempts to continue an initial TLS handshake
* for \a cptr. If the handshake completes, this function calls
* \a ClearNegotiatingTLS(cptr) and returns 1. If the handshake failed,
* this function returns -1. Otherwise it updates event flags for the
* client's socket and returns 0.
* this function returns -1. Otherwise it returns 0 and reports through
* \a want which socket direction the handshake is blocked on, so the caller
* can set the socket's event interest. The backend never touches socket
* events itself, and it does not enforce the handshake deadline (a core
* timer does).
*
* @param[in] cptr Locally connected client to perform handshake for.
* @param[out] reason If non-NULL, receives a human-readable failure reason
* on a -1 return (empty otherwise). Intended for operator notices and
* the disconnect log, not for the peer (a categorical ERROR line is sent
* to the peer instead).
* the disconnect log, not for the peer.
* @param[in] reasonlen Size of the \a reason buffer (see TLS_REASON_LEN).
* @param[out] want If non-NULL, set on a 0 return to the socket direction the
* handshake is waiting on (IRCD_TLS_WANT_READ / IRCD_TLS_WANT_WRITE);
* IRCD_TLS_WANT_NONE otherwise.
* \returns 1 on completed handshake, 0 on continuing handshake, -1 on
* error.
*/
int ircd_tls_negotiate(struct Client *cptr, char *reason, size_t reasonlen);
int ircd_tls_negotiate(struct Client *cptr, char *reason, size_t reasonlen,
enum ircd_tls_want *want);

/** ircd_tls_recv() performs a non-blocking receive of TLS application
* data from \a cptr into \a buf.
/** tls_backend_read() reads TLS application data from \a cptr into \a buf.
*
* Thin per-backend primitive (tls_io_recv() in the core wraps it and records
* the blocked direction).
*
* @param[in] cptr Locally connected client to read from.
* @param[out] buf Buffer to receive application data into.
* @param[in] length Length of \a buf.
* @param[out] count_out Number of bytes actually read into \a buf.
* \returns IO_FAILURE on error, IO_BLOCKED if no data is available, or
* IO_SUCCESS if any data was read into \a buf.
* @param[out] count_out Number of bytes read (0 unless IO_SUCCESS).
* @param[out] want On IO_BLOCKED, the socket direction the read is waiting on.
* \returns IO_FAILURE on a fatal error (session torn down), IO_BLOCKED if no
* data is available (with \a want set), or IO_SUCCESS if data was read.
*/
/** Raw peer material a backend hands back after a completed handshake, for the
* core (tls_io.c) to apply trust policy to. The backend does no policy of its
* own beyond what the TLS library enforces during the handshake. */
struct tls_peer {
int have_cert; /**< peer presented a certificate */
int verified; /**< PKIX/CA verification passed */
unsigned char digest[32]; /**< SHA-256 of the peer cert */
unsigned int digest_len; /**< bytes in \a digest (0 if none) */
char fp_hex[65]; /**< pre-formatted hex, for libtls */
char verify_err[TLS_REASON_LEN]; /**< backend-specific verify reason */
};

/** tls_backend_handshake() advances the TLS handshake for \a cptr.
*
* Thin per-backend primitive (ircd_tls_negotiate() in the core wraps it and
* applies the cert-required / verifypeer trust policy and fingerprint storage).
* It performs no teardown and touches no client flags.
*
* @param[out] peer On IO_SUCCESS, filled with the peer's raw material.
* @param[out] reason On IO_FAILURE, a human-readable failure reason.
* @param[out] want On IO_BLOCKED, the socket direction to wait on.
* \returns IO_SUCCESS (handshake complete, \a peer filled), IO_BLOCKED (in
* progress), or IO_FAILURE (fatal; \a reason set, session left for the caller
* to drop).
*/
IOResult ircd_tls_recv(struct Client *cptr, char *buf,
unsigned int length, unsigned int *count_out);
IOResult tls_backend_handshake(struct Client *cptr, struct tls_peer *peer,
char *reason, size_t reasonlen,
enum ircd_tls_want *want);

/** tls_backend_drop() hard-frees \a cptr's TLS session after a fatal error and
* NULLs the socket's session pointer. Unlike ircd_tls_close() it sends no
* close_notify (the session is unusable). The core teardown (tls_io.c) calls
* this; the backend touches no client flags or connection state itself. */
void tls_backend_drop(struct Client *cptr);

IOResult tls_backend_read(struct Client *cptr, char *buf, unsigned int length,
unsigned int *count_out, enum ircd_tls_want *want);

/** ircd_tls_sendv() performs a non-blocking send of TLS application
* data from \a buf to \a cptr.
/** tls_backend_write() writes one contiguous buffer to \a cptr's TLS session.
*
* This function must accomodate changes to \a buf for successive calls
* to \a cptr. The connection's \a con_rexmit and \a con_rexmit_len
* fields are provided to support this requirement.
* This is a thin per-backend primitive: it does no message-queue or
* retransmit bookkeeping (tls_io_sendv() in the core owns that). It performs
* a single non-blocking record write and classifies the outcome.
*
* @param[in] cptr Locally connected client to send to.
* @param[in] buf Client's message queue.
* @param[out] count_in Total number of bytes in \a buf at entry.
* @param[out] count_out Number of bytes consumed from \a buf.
* \returns IO_FAILURE on error, IO_BLOCKED if no data could be sent, or
* IO_SUCCESS if any data was written from \a buf.
* @param[in] buf Bytes to write.
* @param[in] len Number of bytes in \a buf.
* @param[out] written Number of bytes accepted (only meaningful on IO_SUCCESS).
* @param[out] want On IO_BLOCKED, the socket direction the write is waiting on.
* \returns IO_SUCCESS if any bytes were written, IO_BLOCKED if none could be
* (with \a want set), or IO_FAILURE on a fatal error (the backend has torn
* the session down).
*/
IOResult ircd_tls_sendv(struct Client *cptr, struct MsgQ *buf,
unsigned int *count_in, unsigned int *count_out);
IOResult tls_backend_write(struct Client *cptr, const char *buf,
unsigned int len, unsigned int *written,
enum ircd_tls_want *want);

/** Compute base64(SHA1(\a data)) into \a out.
* Used for RFC 6455 WebSocket handshakes and similar protocols.
Expand Down
84 changes: 84 additions & 0 deletions include/tls_io.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* IRC - Internet Relay Chat, include/tls_io.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 2, 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.
*/
/** @file
* @brief Core TLS I/O orchestration shared by all backends.
*
* This module owns the mapping from a connection's I/O state to its socket
* event interest. TLS breaks the plaintext assumption that "readable = want
* to read, writable = want to send": a TLS write can be blocked waiting to
* read the socket and vice versa (renegotiation, TLS1.3 KeyUpdate, a partial
* record). Rather than sprinkle special cases through the event loop, the
* desired interest is computed from state in exactly one place here, so the
* socket interest can never drift out of sync with what the TLS layer needs.
*/
#ifndef INCLUDED_tls_io_h
#define INCLUDED_tls_io_h

#ifndef INCLUDED_ircd_osdep_h
#include "ircd_osdep.h" /* IOResult */
#endif

struct Client;
struct MsgQ;

/** tls_io_sendv() sends as much of \a cptr's message queue as the TLS session
* will accept, owning the partial-write / retransmit bookkeeping so no backend
* has to. It drives the thin per-backend tls_backend_write() primitive.
*
* @param[in] cptr Locally connected TLS client to send to.
* @param[in] buf Client's message queue.
* @param[out] count_in Total number of bytes mapped from \a buf.
* @param[out] count_out Number of bytes consumed from \a buf.
* \returns IO_FAILURE on a fatal error, IO_BLOCKED if nothing could be sent,
* or IO_SUCCESS if any data was written.
*/
IOResult tls_io_sendv(struct Client *cptr, struct MsgQ *buf,
unsigned int *count_in, unsigned int *count_out);

/** tls_io_recv() reads TLS application data into \a buf, recording the blocked
* direction so the event loop waits on the right event. Drives the thin
* per-backend tls_backend_read() primitive. */
IOResult tls_io_recv(struct Client *cptr, char *buf, unsigned int length,
unsigned int *count_out);

/** Record \a cptr's peer-certificate fingerprint from a raw SHA-256 \a digest
* (\a len bytes): store the lowercase hex, or clear it if the digest is not a
* 32-byte SHA-256 or the port suppresses fingerprints (Cloudflare). */
void tls_io_store_fingerprint(struct Client *cptr, const unsigned char *digest,
unsigned int len);

/** As tls_io_store_fingerprint(), but from an already-hex fingerprint string
* \a hex (or NULL to clear), for backends that expose the hash pre-formatted. */
void tls_io_store_fingerprint_hex(struct Client *cptr, const char *hex);

/** Non-zero if the connection currently wants writable events.
*
* The plaintext rule is "there is queued output or a /LIST in progress". TLS
* overrides it: a write blocked waiting to read must NOT assert writable (the
* level-triggered writable event would spin), and a read blocked waiting to
* write must assert it even with an empty send queue.
*/
int tls_want_writable(struct Client *cptr);

/** Full socket event interest mask (SOCK_EVENT_*) the connection should hold,
* accounting for TLS cross-direction blocking. Used by the unified event
* driver; readable is always wanted for a live connection. */
unsigned int tls_desired_events(struct Client *cptr);

#endif /* INCLUDED_tls_io_h */
1 change: 1 addition & 0 deletions ircd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ ircd_SOURCES = \
sasl.c \
send.c \
sline.c \
tls_io.c \
uping.c \
userload.c \
websocket.c \
Expand Down
10 changes: 9 additions & 1 deletion ircd/engine_kqueue.c
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,15 @@ engine_loop(struct Generators* gen)
case SS_CONNECTED:
if (evt->filter == EVFILT_READ) { /* data on socket */
Debug((DEBUG_ENGINE, "kqueue: EOF or data to be read"));
event_generate(evt->flags & EV_EOF ? ET_EOF : ET_READ, sock, 0);
/* EV_EOF is set as soon as the peer's FIN arrives, even while
* evt->data bytes are still unread (typically the peer's final
* ERROR/SQUIT line). Deliver those as ET_READ first; the filter is
* level-triggered, so once the buffer is drained the next kevent()
* returns EV_EOF with data == 0 and becomes the real ET_EOF. */
if ((evt->flags & EV_EOF) && evt->data <= 0)
event_generate(ET_EOF, sock, 0);
else
event_generate(ET_READ, sock, 0);
}
if (evt->filter == EVFILT_WRITE) { /* socket writable */
Debug((DEBUG_ENGINE, "kqueue: Data can be written"));
Expand Down
Loading
Loading