[PATCH 3/7] RADIUS: Reassemble RADIUS/TLS stream by RADIUS Length
Seb Adamski
seb at bionic.network
Sun Oct 18 23:38:10 PDT 2026
The RADIUS/TLS receive path did one recvmsg(), one decrypt call and one
RADIUS message parse per socket event. RADIUS/TLS is a byte stream: the
packet boundaries are determined from the RADIUS Length field (RFC 6614,
Section 3.4, item (1); draft-ietf-radext-radiusdtls-bis, Section 4.1),
so a single read can carry several TLS records, a TLS record can carry
more than one RADIUS message, and a RADIUS message can be split over
records. With the OpenSSL wrapper, a decrypt call returns at most one
TLS record, so a second record that arrived in the same read stayed
undecrypted until the next socket event. Since tls_connection_encrypt()
resets the TLS input buffer, that record was lost as soon as anything
was sent on the connection in the meantime. A second RADIUS message
inside one record was ignored as extra bytes, and a message split over
two records was dropped as malformed.
These cases were mostly hidden as long as the server only answered
requests one at a time, but a server that sends several messages back
to back, e.g., a proxy forwarding responses for several stations, can
hit them, and a burst of messages could make the connection fail.
Decrypt all received TLS data before processing any of it: call the
decrypt function again without new input until it returns no more data,
so that no undecrypted input is left when a response or a new request is
encrypted. Only a failure of the first call is treated as a decryption
error, so that a TLS library that does not accept a call without new
input stops the loop instead of closing the connection. Append the
plaintext to a per-connection buffer and process every complete RADIUS
message in it, in order, keeping an incomplete message for the next
socket event. Close the connection if the Length field is less than 20
or more than 4096 (RFC 6613, Section 2.6.4). Stop processing buffered
messages if handling one of them closed the connection. The buffer is
freed when the socket is closed.
Move the processing of a single received message into a separate
function that both the UDP and the TLS paths use. There is no change in
behavior for RADIUS/UDP.
Signed-off-by: Seb Adamski <seb at bionic.network>
---
src/radius/radius_client.c | 310 ++++++++++++++++++++++++++-----------
1 file changed, 220 insertions(+), 90 deletions(-)
diff --git a/src/radius/radius_client.c b/src/radius/radius_client.c
index 16284e1fe..ff85265a6 100644
--- a/src/radius/radius_client.c
+++ b/src/radius/radius_client.c
@@ -249,6 +249,19 @@ struct radius_client_data {
void *tls_ctx;
struct tls_connection *auth_tls_conn;
struct tls_connection *acct_tls_conn;
+
+ /**
+ * auth_tls_rx - Start of an incomplete RADIUS message received on the
+ * RADIUS/TLS authentication connection
+ */
+ struct wpabuf *auth_tls_rx;
+
+ /**
+ * acct_tls_rx - Start of an incomplete RADIUS message received on the
+ * RADIUS/TLS accounting connection
+ */
+ struct wpabuf *acct_tls_rx;
+
unsigned int tls_reconnect_attempts;
unsigned int tls_reconnect_max_attempts;
unsigned int tls_reconnect_max_delay;
@@ -875,6 +888,10 @@ static void radius_close_auth_socket(struct radius_client_data *radius)
close(radius->auth_sock);
radius->auth_sock = -1;
}
+#ifdef CONFIG_RADIUS_TLS
+ wpabuf_free(radius->auth_tls_rx);
+ radius->auth_tls_rx = NULL;
+#endif /* CONFIG_RADIUS_TLS */
}
@@ -890,6 +907,10 @@ static void radius_close_acct_socket(struct radius_client_data *radius)
close(radius->acct_sock);
radius->acct_sock = -1;
}
+#ifdef CONFIG_RADIUS_TLS
+ wpabuf_free(radius->acct_tls_rx);
+ radius->acct_tls_rx = NULL;
+#endif /* CONFIG_RADIUS_TLS */
}
@@ -1183,15 +1204,12 @@ fail:
#endif /* CONFIG_RADIUS_TLS */
-static void radius_client_receive(int sock, void *eloop_ctx, void *sock_ctx)
+static void radius_client_handle_msg(struct radius_client_data *radius,
+ RadiusType msg_type,
+ const u8 *buf, size_t len)
{
- struct radius_client_data *radius = eloop_ctx;
struct hostapd_radius_servers *conf = radius->conf;
- RadiusType msg_type = (uintptr_t) sock_ctx;
- int len, roundtrip;
- unsigned char buf[RADIUS_MAX_MSG_LEN];
- struct msghdr msghdr = {0};
- struct iovec iov;
+ int roundtrip;
struct radius_msg *msg;
struct radius_hdr *hdr;
struct radius_rx_handler *handlers;
@@ -1200,99 +1218,17 @@ static void radius_client_receive(int sock, void *eloop_ctx, void *sock_ctx)
struct os_reltime now;
struct hostapd_radius_server *rconf;
int invalid_authenticator = 0;
-#ifdef CONFIG_RADIUS_TLS
- struct tls_connection *conn = NULL;
- bool tls, tls_ready;
-#endif /* CONFIG_RADIUS_TLS */
if (msg_type == RADIUS_ACCT) {
-#ifdef CONFIG_RADIUS_TLS
- if (radius->acct_tls)
- conn = radius->acct_tls_conn;
- tls = radius->acct_tls;
- tls_ready = radius->acct_tls_ready;
-#endif /* CONFIG_RADIUS_TLS */
handlers = radius->acct_handlers;
num_handlers = radius->num_acct_handlers;
rconf = conf->acct_server;
} else {
-#ifdef CONFIG_RADIUS_TLS
- if (radius->auth_tls)
- conn = radius->auth_tls_conn;
- tls = radius->auth_tls;
- tls_ready = radius->auth_tls_ready;
-#endif /* CONFIG_RADIUS_TLS */
handlers = radius->auth_handlers;
num_handlers = radius->num_auth_handlers;
rconf = conf->auth_server;
}
- iov.iov_base = buf;
- iov.iov_len = RADIUS_MAX_MSG_LEN;
- msghdr.msg_iov = &iov;
- msghdr.msg_iovlen = 1;
- msghdr.msg_flags = 0;
- len = recvmsg(sock, &msghdr, MSG_DONTWAIT);
- if (len < 0) {
- wpa_printf(MSG_INFO, "recvmsg[RADIUS]: %s", strerror(errno));
- return;
- }
-#ifdef CONFIG_RADIUS_TLS
- if (tls && len == 0) {
- wpa_printf(MSG_DEBUG, "RADIUS: No TCP data available");
- goto close_tcp;
- }
-
- if (tls && !tls_ready) {
- radius_client_process_tls_handshake(radius, sock, msg_type,
- buf, len);
- return;
- }
-
- if (conn) {
- struct wpabuf *out, *in;
-
- in = wpabuf_alloc_copy(buf, len);
- if (!in)
- return;
- wpa_printf(MSG_DEBUG,
- "RADIUS: Process %d bytes of encrypted TLS data",
- len);
- out = tls_connection_decrypt(radius->tls_ctx, conn, in);
- wpabuf_free(in);
- if (!out) {
- wpa_printf(MSG_INFO,
- "RADIUS: Failed to decrypt TLS data");
- goto close_tcp;
- }
- if (wpabuf_len(out) == 0) {
- wpa_printf(MSG_DEBUG,
- "RADIUS: Full message not yet received - continue waiting for additional TLS data");
- wpabuf_free(out);
- return;
- }
- if (wpabuf_len(out) > RADIUS_MAX_MSG_LEN) {
- wpa_printf(MSG_INFO,
- "RADIUS: Too long RADIUS message from TLS: %zu",
- wpabuf_len(out));
- wpabuf_free(out);
- goto close_tcp;
- }
- os_memcpy(buf, wpabuf_head(out), wpabuf_len(out));
- len = wpabuf_len(out);
- wpabuf_free(out);
- }
-#endif /* CONFIG_RADIUS_TLS */
-
- hostapd_logger(radius->ctx, NULL, HOSTAPD_MODULE_RADIUS,
- HOSTAPD_LEVEL_DEBUG, "Received %d bytes from RADIUS "
- "server", len);
-
- if (msghdr.msg_flags & MSG_TRUNC) {
- wpa_printf(MSG_INFO, "RADIUS: Possibly too long UDP frame for our buffer - dropping it");
- return;
- }
-
msg = radius_msg_parse(buf, len);
if (msg == NULL) {
wpa_printf(MSG_INFO, "RADIUS: Parsing incoming frame failed");
@@ -1401,12 +1337,206 @@ static void radius_client_receive(int sock, void *eloop_ctx, void *sock_ctx)
fail:
radius_msg_free(msg);
- return;
+}
+
#ifdef CONFIG_RADIUS_TLS
+
+static bool radius_client_tls_conn_open(struct radius_client_data *radius,
+ int sock, RadiusType msg_type)
+{
+ if (msg_type == RADIUS_ACCT)
+ return radius->acct_sock == sock && radius->acct_tls_ready;
+ return radius->auth_sock == sock && radius->auth_tls_ready;
+}
+
+
+/*
+ * RADIUS/TLS is a byte stream (RFC 6614, Section 3.4): a TLS record can carry
+ * a part of a RADIUS message or several of them, and several records can
+ * arrive in a single read. Decrypt everything that has been received before
+ * processing any of it, since sending on the connection (which can happen
+ * while a message is processed) discards undecrypted TLS input. Then split
+ * the plaintext into RADIUS messages based on the Length field and keep an
+ * incomplete message for the next call.
+ */
+static void radius_client_tls_receive(struct radius_client_data *radius,
+ int sock, RadiusType msg_type,
+ struct tls_connection *conn,
+ const u8 *data, size_t data_len)
+{
+ struct wpabuf *in, *out, *rx, **pending;
+ struct hostapd_radius_server *rconf;
+ const u8 *pos;
+ size_t left, mlen;
+ u8 buf[RADIUS_MAX_MSG_LEN];
+ bool first = true;
+
+ if (msg_type == RADIUS_ACCT) {
+ pending = &radius->acct_tls_rx;
+ rconf = radius->conf->acct_server;
+ } else {
+ pending = &radius->auth_tls_rx;
+ rconf = radius->conf->auth_server;
+ }
+
+ wpa_printf(MSG_DEBUG, "RADIUS: Process %zu bytes of encrypted TLS data",
+ data_len);
+ in = wpabuf_alloc_copy(data, data_len);
+ if (!in)
+ return;
+
+ /* Continue after the incomplete message from the previous call */
+ rx = *pending;
+ *pending = NULL;
+
+ /*
+ * A decrypt call returns at most one TLS record with some TLS
+ * libraries, so call it again with no new input until it returns
+ * nothing. Only a failure of the first call is treated as a TLS error;
+ * a TLS library that cannot be called without input stops the loop.
+ */
+ while (in) {
+ out = tls_connection_decrypt(radius->tls_ctx, conn, in);
+ wpabuf_free(in);
+ in = NULL;
+ if (!out && first) {
+ wpa_printf(MSG_INFO,
+ "RADIUS: Failed to decrypt TLS data");
+ goto close_tcp;
+ }
+ first = false;
+ if (!out || wpabuf_len(out) == 0) {
+ wpabuf_free(out);
+ break;
+ }
+ if (wpabuf_resize(&rx, wpabuf_len(out)) < 0) {
+ wpabuf_free(out);
+ goto close_tcp;
+ }
+ wpabuf_put_buf(rx, out);
+ wpabuf_free(out);
+ in = wpabuf_alloc(0);
+ }
+
+ pos = rx ? wpabuf_head(rx) : NULL;
+ left = rx ? wpabuf_len(rx) : 0;
+ while (left >= 4) {
+ mlen = WPA_GET_BE16(pos + 2);
+ if (mlen < sizeof(struct radius_hdr) ||
+ mlen > RADIUS_MAX_MSG_LEN) {
+ wpa_printf(MSG_INFO,
+ "RADIUS: Invalid RADIUS message length %zu in TLS stream",
+ mlen);
+ rconf->malformed_responses++;
+ goto close_tcp;
+ }
+ if (left < mlen)
+ break;
+
+ os_memcpy(buf, pos, mlen);
+ pos += mlen;
+ left -= mlen;
+
+ hostapd_logger(radius->ctx, NULL, HOSTAPD_MODULE_RADIUS,
+ HOSTAPD_LEVEL_DEBUG,
+ "Received %d bytes from RADIUS server",
+ (int) mlen);
+ radius_client_handle_msg(radius, msg_type, buf, mlen);
+
+ /* Processing the message may have closed the connection */
+ if (!radius_client_tls_conn_open(radius, sock, msg_type)) {
+ wpa_printf(MSG_DEBUG,
+ "RADIUS: TLS connection closed while processing received data");
+ wpabuf_free(rx);
+ return;
+ }
+ }
+
+ if (left) {
+ wpa_printf(MSG_DEBUG,
+ "RADIUS: Full message not yet received - continue waiting for additional TLS data (%zu bytes buffered)",
+ left);
+ *pending = wpabuf_alloc_copy(pos, left);
+ if (!*pending)
+ goto close_tcp;
+ }
+ wpabuf_free(rx);
+ return;
+
close_tcp:
+ wpabuf_free(rx);
radius_client_close_tcp(radius, sock, msg_type);
+}
+
#endif /* CONFIG_RADIUS_TLS */
+
+
+static void radius_client_receive(int sock, void *eloop_ctx, void *sock_ctx)
+{
+ struct radius_client_data *radius = eloop_ctx;
+ RadiusType msg_type = (uintptr_t) sock_ctx;
+ int len;
+ unsigned char buf[RADIUS_MAX_MSG_LEN];
+ struct msghdr msghdr = {0};
+ struct iovec iov;
+#ifdef CONFIG_RADIUS_TLS
+ struct tls_connection *conn = NULL;
+ bool tls, tls_ready;
+
+ if (msg_type == RADIUS_ACCT) {
+ if (radius->acct_tls)
+ conn = radius->acct_tls_conn;
+ tls = radius->acct_tls;
+ tls_ready = radius->acct_tls_ready;
+ } else {
+ if (radius->auth_tls)
+ conn = radius->auth_tls_conn;
+ tls = radius->auth_tls;
+ tls_ready = radius->auth_tls_ready;
+ }
+#endif /* CONFIG_RADIUS_TLS */
+
+ iov.iov_base = buf;
+ iov.iov_len = RADIUS_MAX_MSG_LEN;
+ msghdr.msg_iov = &iov;
+ msghdr.msg_iovlen = 1;
+ msghdr.msg_flags = 0;
+ len = recvmsg(sock, &msghdr, MSG_DONTWAIT);
+ if (len < 0) {
+ wpa_printf(MSG_INFO, "recvmsg[RADIUS]: %s", strerror(errno));
+ return;
+ }
+#ifdef CONFIG_RADIUS_TLS
+ if (tls && len == 0) {
+ wpa_printf(MSG_DEBUG, "RADIUS: No TCP data available");
+ radius_client_close_tcp(radius, sock, msg_type);
+ return;
+ }
+
+ if (tls && !tls_ready) {
+ radius_client_process_tls_handshake(radius, sock, msg_type,
+ buf, len);
+ return;
+ }
+
+ if (conn) {
+ radius_client_tls_receive(radius, sock, msg_type, conn,
+ buf, len);
+ return;
+ }
+#endif /* CONFIG_RADIUS_TLS */
+
+ hostapd_logger(radius->ctx, NULL, HOSTAPD_MODULE_RADIUS,
+ HOSTAPD_LEVEL_DEBUG, "Received %d bytes from RADIUS "
+ "server", len);
+
+ if (msghdr.msg_flags & MSG_TRUNC) {
+ wpa_printf(MSG_INFO, "RADIUS: Possibly too long UDP frame for our buffer - dropping it");
+ return;
+ }
+
+ radius_client_handle_msg(radius, msg_type, buf, len);
}
--
2.54.0 (Apple Git-157)
More information about the Hostap
mailing list