[PATCH 5/7] RADIUS: Accept Disconnect-Request and CoA-Request over RADIUS/TLS

Seb Adamski seb at bionic.network
Sun Oct 18 23:38:12 PDT 2026


A RADIUS/TLS server, typically a proxy, can send a Disconnect-Request or
CoA-Request to the NAS over the TLS connection that the NAS opened to it
instead of to a separate UDP DAS listener that the proxy may not be able
to reach (reverse CoA). draft-ietf-radext-reverse-coa-08, Section 4
requires a static per-server configuration flag on the client to enable
this; there is no negotiation on the connection, and without the flag
such packets are silently discarded. Section 6 of the draft extends the
routing of RFC 8559, Section 2.1 so that the last proxy forwards these
requests over that connection.

Add a reverse_coa flag to struct hostapd_radius_server and
radius_client_set_das_handler() for registering a handler for these
requests. The handler gets the parsed request and the shared secret of
the server the connection belongs to, and returns the response, which
is sent on the same connection. A callback is used so that the RADIUS
client does not depend on the DAS code, which is not linked into
eapol_test.

A request is accepted only on the RADIUS/TLS connection of a server that
has reverse_coa set, i.e., from a peer that was authenticated in the TLS
handshake, and is validated and answered with the shared secret of that
server (normally "radsec", RFC 6614, Section 2.3) and the normal DAS
rules for authenticators and Event-Timestamp.

A server-initiated request is never a response to a pending request,
so check the packet code before looking for a pending request: a
Disconnect-Request or CoA-Request from a server without the flag, or
from a RADIUS/UDP authentication or accounting server, is now dropped
with a log message and counted as an unknown type, instead of being
matched by identifier against pending requests and then rejected for its
authenticator.

The response is sent on a non-blocking socket. Send all RADIUS/TLS data
through a common function that uses MSG_NOSIGNAL (hostapd does not
ignore SIGPIPE), queues data that cannot be sent right away and sends it
when the socket becomes writable, so that a partial TLS record is never
followed by other data, and closes the connection on other errors or if
more than 64 kB would be queued. If sending the response fails, open a
new connection from an immediate timeout instead of waiting for the next
request.

Allow radius_das_init() to create a DAS context without a UDP listener
(port 0). Such a context has no shared secret of its own; requests are
passed to it with radius_das_process() together with the shared secret
to use.

When CoA is not supported (no CoA callback), the CoA-NAK used
Error-Cause 405 (Unsupported Service), which RFC 5176, Section 3.5
defines for an unsupported Service-Type value. Use 406 (Unsupported
Extension), which RFC 6614, Section 2.5 names for this case. The test
case expectation is updated accordingly.

Signed-off-by: Seb Adamski <seb at bionic.network>
---
 src/radius/radius_client.c | 317 ++++++++++++++++++++++++++++++++++++-
 src/radius/radius_client.h |  20 +++
 src/radius/radius_das.c    |  11 +-
 src/radius/radius_das.h    |   3 +
 tests/hwsim/test_radius.py |   2 +-
 5 files changed, 346 insertions(+), 7 deletions(-)

diff --git a/src/radius/radius_client.c b/src/radius/radius_client.c
index ff85265a6..ec3ca999a 100644
--- a/src/radius/radius_client.c
+++ b/src/radius/radius_client.c
@@ -16,6 +16,18 @@
 #include "radius.h"
 #include "radius_client.h"
 
+#ifndef MSG_NOSIGNAL
+#define MSG_NOSIGNAL 0
+#endif /* MSG_NOSIGNAL */
+
+/**
+ * RADIUS_CLIENT_TLS_MAX_TX_QUEUE - Maximum amount of queued RADIUS/TLS data
+ *
+ * Data is queued when the server does not read what is sent to it; the
+ * connection is closed if more than this would be queued.
+ */
+#define RADIUS_CLIENT_TLS_MAX_TX_QUEUE 65536
+
 /* Defaults for RADIUS retransmit values (exponential backoff) */
 
 /**
@@ -262,6 +274,32 @@ struct radius_client_data {
 	 */
 	struct wpabuf *acct_tls_rx;
 
+	/**
+	 * auth_tls_tx - TLS data waiting for the authentication connection to
+	 * become writable
+	 */
+	struct wpabuf *auth_tls_tx;
+
+	/**
+	 * acct_tls_tx - TLS data waiting for the accounting connection to
+	 * become writable
+	 */
+	struct wpabuf *acct_tls_tx;
+
+	/**
+	 * das_handler - Handler for DAS requests received on RADIUS/TLS
+	 */
+	struct radius_msg * (*das_handler)(struct radius_msg *msg,
+					   const u8 *shared_secret,
+					   size_t shared_secret_len,
+					   const char *from_addr,
+					   int from_port, void *ctx);
+
+	/**
+	 * das_handler_ctx - Context data for das_handler()
+	 */
+	void *das_handler_ctx;
+
 	unsigned int tls_reconnect_attempts;
 	unsigned int tls_reconnect_max_attempts;
 	unsigned int tls_reconnect_max_delay;
@@ -279,6 +317,10 @@ static int radius_client_init_acct(struct radius_client_data *radius);
 static int radius_client_init_auth(struct radius_client_data *radius);
 static void radius_client_auth_failover(struct radius_client_data *radius);
 static void radius_client_acct_failover(struct radius_client_data *radius);
+#ifdef CONFIG_RADIUS_TLS
+static void radius_client_close_tcp(struct radius_client_data *radius,
+				    int sock, RadiusType msg_type);
+#endif /* CONFIG_RADIUS_TLS */
 
 
 static void radius_client_msg_free(struct radius_msg_list *req)
@@ -357,6 +399,104 @@ void radius_client_set_interim_error_cb(struct radius_client_data *radius,
 }
 
 
+#ifdef CONFIG_RADIUS_TLS
+/**
+ * radius_client_set_das_handler - Register a handler for reverse CoA requests
+ * @radius: RADIUS client context from radius_client_init()
+ * @handler: Handler for a Disconnect-Request or CoA-Request received from a
+ *	RADIUS/TLS server; returns the response to send or %NULL to drop the
+ *	request
+ * @ctx: Context pointer for handler callbacks
+ *
+ * The handler is called for Disconnect-Request and CoA-Request messages that
+ * a server with reverse_coa set sends on its RADIUS/TLS connection (reverse
+ * CoA, draft-ietf-radext-reverse-coa). The shared secret of that server is
+ * passed to the handler for validating the request and for authenticating the
+ * response, which is sent on the same connection.
+ */
+void radius_client_set_das_handler(struct radius_client_data *radius,
+				   struct radius_msg * (*handler)
+				   (struct radius_msg *msg,
+				    const u8 *shared_secret,
+				    size_t shared_secret_len,
+				    const char *from_addr, int from_port,
+				    void *ctx),
+				   void *ctx)
+{
+	radius->das_handler = handler;
+	radius->das_handler_ctx = ctx;
+}
+
+
+static void radius_client_tls_flush(int sock, void *eloop_ctx, void *sock_ctx);
+
+/*
+ * Send TLS data on a RADIUS/TLS connection. The socket is non-blocking and a
+ * partial TLS record would corrupt the stream, so data that cannot be sent
+ * right away is queued and sent when the socket becomes writable; later data
+ * is queued behind it. On other errors, or if the queue would grow too large,
+ * the connection is closed. Returns 0 if the data was sent or queued, -1 on
+ * failure (errno set).
+ */
+static int radius_client_tls_send(struct radius_client_data *radius, int sock,
+				  RadiusType msg_type,
+				  const struct wpabuf *data)
+{
+	bool acct = msg_type == RADIUS_ACCT || msg_type == RADIUS_ACCT_INTERIM;
+	struct wpabuf **pending = acct ? &radius->acct_tls_tx :
+		&radius->auth_tls_tx;
+	const u8 *pos = wpabuf_head(data);
+	size_t left = wpabuf_len(data);
+	ssize_t res;
+	int err;
+
+	if (!*pending) {
+		res = send(sock, pos, left, MSG_NOSIGNAL);
+		if (res < 0) {
+			if (errno != EAGAIN && errno != EWOULDBLOCK)
+				goto fail;
+			res = 0;
+		}
+		pos += res;
+		left -= res;
+		if (!left)
+			return 0;
+		if (eloop_register_sock(sock, EVENT_TYPE_WRITE,
+					radius_client_tls_flush, radius,
+					acct ? (void *) RADIUS_ACCT :
+					(void *) RADIUS_AUTH) < 0) {
+			errno = ENOMEM;
+			goto fail;
+		}
+	}
+
+	wpa_printf(MSG_DEBUG,
+		   "RADIUS: Queue %zu bytes of TLS data until the socket is writable",
+		   left);
+	if ((*pending ? wpabuf_len(*pending) : 0) + left >
+	    RADIUS_CLIENT_TLS_MAX_TX_QUEUE) {
+		errno = ENOBUFS;
+		goto fail;
+	}
+	if (wpabuf_resize(pending, left) < 0) {
+		errno = ENOMEM;
+		goto fail;
+	}
+	wpabuf_put_data(*pending, pos, left);
+	return 0;
+
+fail:
+	err = errno;
+	wpa_printf(MSG_INFO, "RADIUS: send (TLS, sock %d): %s",
+		   sock, strerror(err));
+	radius_client_close_tcp(radius, sock,
+				acct ? RADIUS_ACCT : RADIUS_AUTH);
+	errno = err;
+	return -1;
+}
+#endif /* CONFIG_RADIUS_TLS */
+
+
 /*
  * Returns >0 if message queue was flushed (i.e., the message that triggered
  * the error is not available anymore)
@@ -398,7 +538,7 @@ static int radius_client_retransmit(struct radius_client_data *radius,
 	size_t prev_num_msgs;
 	u8 *acct_delay_time;
 	size_t acct_delay_time_len;
-	int num_servers;
+	int num_servers, res;
 #ifdef CONFIG_RADIUS_TLS
 	struct wpabuf *out = NULL;
 	struct tls_connection *conn = NULL;
@@ -541,7 +681,13 @@ static int radius_client_retransmit(struct radius_client_data *radius,
 
 	wpa_printf(MSG_DEBUG, "RADIUS: Send %zu bytes to the server",
 		   wpabuf_len(buf));
-	if (send(s, wpabuf_head(buf), wpabuf_len(buf), 0) < 0) {
+#ifdef CONFIG_RADIUS_TLS
+	if (conn)
+		res = radius_client_tls_send(radius, s, entry->msg_type, buf);
+	else
+#endif /* CONFIG_RADIUS_TLS */
+		res = send(s, wpabuf_head(buf), wpabuf_len(buf), 0);
+	if (res < 0) {
 		if (radius_client_handle_send_error(radius, s, entry->msg_type)
 		    > 0) {
 #ifdef CONFIG_RADIUS_TLS
@@ -891,6 +1037,8 @@ static void radius_close_auth_socket(struct radius_client_data *radius)
 #ifdef CONFIG_RADIUS_TLS
 	wpabuf_free(radius->auth_tls_rx);
 	radius->auth_tls_rx = NULL;
+	wpabuf_free(radius->auth_tls_tx);
+	radius->auth_tls_tx = NULL;
 #endif /* CONFIG_RADIUS_TLS */
 }
 
@@ -910,6 +1058,8 @@ static void radius_close_acct_socket(struct radius_client_data *radius)
 #ifdef CONFIG_RADIUS_TLS
 	wpabuf_free(radius->acct_tls_rx);
 	radius->acct_tls_rx = NULL;
+	wpabuf_free(radius->acct_tls_tx);
+	radius->acct_tls_tx = NULL;
 #endif /* CONFIG_RADIUS_TLS */
 }
 
@@ -1048,7 +1198,12 @@ int radius_client_send(struct radius_client_data *radius,
 #endif /* CONFIG_RADIUS_TLS */
 	wpa_printf(MSG_DEBUG, "RADIUS: Send %zu bytes to the server",
 		   wpabuf_len(buf));
-	res = send(s, wpabuf_head(buf), wpabuf_len(buf), 0);
+#ifdef CONFIG_RADIUS_TLS
+	if (conn)
+		res = radius_client_tls_send(radius, s, msg_type, buf);
+	else
+#endif /* CONFIG_RADIUS_TLS */
+		res = send(s, wpabuf_head(buf), wpabuf_len(buf), 0);
 #ifdef CONFIG_RADIUS_TLS
 	wpabuf_free(out);
 #endif /* CONFIG_RADIUS_TLS */
@@ -1082,6 +1237,46 @@ static void radius_client_close_tcp(struct radius_client_data *radius,
 }
 
 
+static void radius_client_tls_flush(int sock, void *eloop_ctx, void *sock_ctx)
+{
+	struct radius_client_data *radius = eloop_ctx;
+	RadiusType msg_type = (uintptr_t) sock_ctx;
+	struct wpabuf **pending, *rest;
+	ssize_t res;
+
+	pending = msg_type == RADIUS_ACCT ? &radius->acct_tls_tx :
+		&radius->auth_tls_tx;
+	if (*pending) {
+		res = send(sock, wpabuf_head(*pending), wpabuf_len(*pending),
+			   MSG_NOSIGNAL);
+		if (res < 0) {
+			if (errno == EAGAIN || errno == EWOULDBLOCK)
+				return;
+			wpa_printf(MSG_INFO,
+				   "RADIUS: send (queued TLS data, sock %d): %s",
+				   sock, strerror(errno));
+			radius_client_close_tcp(radius, sock, msg_type);
+			return;
+		}
+		wpa_printf(MSG_DEBUG,
+			   "RADIUS: Sent %zd of %zu bytes of queued TLS data",
+			   res, wpabuf_len(*pending));
+		if ((size_t) res < wpabuf_len(*pending)) {
+			rest = wpabuf_alloc_copy(wpabuf_head_u8(*pending) + res,
+						 wpabuf_len(*pending) - res);
+			wpabuf_free(*pending);
+			*pending = rest;
+			if (!rest)
+				radius_client_close_tcp(radius, sock, msg_type);
+			return;
+		}
+		wpabuf_free(*pending);
+		*pending = NULL;
+	}
+	eloop_unregister_sock(sock, EVENT_TYPE_WRITE);
+}
+
+
 static void
 radius_client_process_tls_handshake(struct radius_client_data *radius,
 				    int sock, RadiusType msg_type,
@@ -1242,6 +1437,18 @@ static void radius_client_handle_msg(struct radius_client_data *radius,
 	if (conf->msg_dumps)
 		radius_msg_dump(msg);
 
+	if (hdr->code == RADIUS_CODE_DISCONNECT_REQUEST ||
+	    hdr->code == RADIUS_CODE_COA_REQUEST) {
+		/* Not a response to a pending request. Such a request from
+		 * the server is accepted only on a RADIUS/TLS connection with
+		 * reverse_coa enabled and it does not reach this point. */
+		wpa_printf(MSG_INFO,
+			   "RADIUS: Reverse CoA not enabled for this server - drop request (code=%d id=%d)",
+			   hdr->code, hdr->identifier);
+		rconf->unknown_types++;
+		goto fail;
+	}
+
 	switch (hdr->code) {
 	case RADIUS_CODE_ACCESS_ACCEPT:
 		rconf->access_accepts++;
@@ -1351,6 +1558,101 @@ static bool radius_client_tls_conn_open(struct radius_client_data *radius,
 }
 
 
+static void radius_client_tls_reopen(void *eloop_ctx, void *timeout_ctx)
+{
+	struct radius_client_data *radius = eloop_ctx;
+	RadiusType msg_type = (uintptr_t) timeout_ctx;
+
+	if (msg_type == RADIUS_ACCT) {
+		if (radius->acct_sock < 0)
+			radius_client_init_acct(radius);
+	} else if (radius->auth_sock < 0) {
+		radius_client_init_auth(radius);
+	}
+}
+
+
+/*
+ * Process a Disconnect-Request or CoA-Request that a server with reverse_coa
+ * set sent on its RADIUS/TLS connection and send the response on the same
+ * connection (draft-ietf-radext-reverse-coa).
+ */
+static void radius_client_tls_das_req(struct radius_client_data *radius,
+				      int sock, RadiusType msg_type,
+				      struct tls_connection *conn,
+				      struct hostapd_radius_server *rconf,
+				      const u8 *buf, size_t len)
+{
+	struct radius_msg *msg, *reply;
+	struct radius_hdr *hdr;
+	struct wpabuf *out;
+	char abuf[50];
+
+	hostapd_ip_txt(&rconf->addr, abuf, sizeof(abuf));
+
+	msg = radius_msg_parse(buf, len);
+	if (!msg) {
+		wpa_printf(MSG_INFO,
+			   "RADIUS: Parsing incoming DAS request from %s:%d failed",
+			   abuf, rconf->port);
+		return;
+	}
+	hdr = radius_msg_get_hdr(msg);
+	wpa_printf(MSG_DEBUG,
+		   "RADIUS: Received DAS request (code=%d id=%d) from %s:%d over RADIUS/TLS",
+		   hdr->code, hdr->identifier, abuf, rconf->port);
+	if (radius->conf->msg_dumps)
+		radius_msg_dump(msg);
+
+	if (!rconf->shared_secret) {
+		wpa_printf(MSG_INFO,
+			   "RADIUS: No shared secret for server %s:%d - drop DAS request",
+			   abuf, rconf->port);
+		radius_msg_free(msg);
+		return;
+	}
+
+	reply = radius->das_handler(msg, rconf->shared_secret,
+				    rconf->shared_secret_len, abuf, rconf->port,
+				    radius->das_handler_ctx);
+	radius_msg_free(msg);
+	if (!reply)
+		return;
+
+	/* Processing the request may have closed the connection */
+	if (!radius_client_tls_conn_open(radius, sock, msg_type)) {
+		wpa_printf(MSG_INFO,
+			   "RADIUS: TLS connection closed before the DAS response could be sent");
+		radius_msg_free(reply);
+		return;
+	}
+
+	out = tls_connection_encrypt(radius->tls_ctx, conn,
+				     radius_msg_get_buf(reply));
+	radius_msg_free(reply);
+	if (!out) {
+		wpa_printf(MSG_INFO,
+			   "RADIUS: Failed to encrypt DAS response (TLS)");
+		radius_client_close_tcp(radius, sock, msg_type);
+		return;
+	}
+
+	if (radius_client_tls_send(radius, sock, msg_type, out) < 0) {
+		/* The connection was closed; reverse CoA needs one, so do not
+		 * wait for the next request to reconnect. Do it from a timeout
+		 * so that the new socket is not handled together with events
+		 * for the old one. */
+		eloop_register_timeout(0, 0, radius_client_tls_reopen, radius,
+				       (void *) (uintptr_t) msg_type);
+	} else {
+		wpa_printf(MSG_DEBUG,
+			   "RADIUS: Sent DAS response (%zu bytes of TLS data) to %s:%d",
+			   wpabuf_len(out), abuf, rconf->port);
+	}
+	wpabuf_free(out);
+}
+
+
 /*
  * 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
@@ -1442,7 +1744,13 @@ static void radius_client_tls_receive(struct radius_client_data *radius,
 			       HOSTAPD_LEVEL_DEBUG,
 			       "Received %d bytes from RADIUS server",
 			       (int) mlen);
-		radius_client_handle_msg(radius, msg_type, buf, mlen);
+		if ((buf[0] == RADIUS_CODE_DISCONNECT_REQUEST ||
+		     buf[0] == RADIUS_CODE_COA_REQUEST) &&
+		    rconf->reverse_coa && radius->das_handler)
+			radius_client_tls_das_req(radius, sock, msg_type, conn,
+						  rconf, buf, mlen);
+		else
+			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)) {
@@ -2149,6 +2457,7 @@ void radius_client_deinit(struct radius_client_data *radius)
 	eloop_cancel_timeout(radius_retry_primary_timer, radius, NULL);
 #ifdef CONFIG_RADIUS_TLS
 	eloop_cancel_timeout(radius_tls_reconnect, radius, NULL);
+	eloop_cancel_timeout(radius_client_tls_reopen, radius, ELOOP_ALL_CTX);
 #endif /* CONFIG_RADIUS_TLS */
 
 	radius_client_flush(radius, 0);
diff --git a/src/radius/radius_client.h b/src/radius/radius_client.h
index db40637ea..ea8c89cca 100644
--- a/src/radius/radius_client.h
+++ b/src/radius/radius_client.h
@@ -70,6 +70,16 @@ struct hostapd_radius_server {
 	 */
 	char *private_key_passwd;
 
+	/**
+	 * reverse_coa - Accept DAS requests on the RADIUS/TLS connection
+	 *
+	 * Whether Disconnect-Request and CoA-Request messages that this server
+	 * sends on its RADIUS/TLS connection are processed (reverse CoA,
+	 * draft-ietf-radext-reverse-coa); used only with tls=true. When not
+	 * set, such messages are discarded.
+	 */
+	bool reverse_coa;
+
 	/* Dynamic (not from configuration file) MIB data */
 
 	/**
@@ -274,6 +284,16 @@ int radius_client_register(struct radius_client_data *radius,
 void radius_client_set_interim_error_cb(struct radius_client_data *radius,
 					void (*cb)(const u8 *addr, void *ctx),
 					void *ctx);
+#ifdef CONFIG_RADIUS_TLS
+void radius_client_set_das_handler(struct radius_client_data *radius,
+				   struct radius_msg * (*handler)
+				   (struct radius_msg *msg,
+				    const u8 *shared_secret,
+				    size_t shared_secret_len,
+				    const char *from_addr, int from_port,
+				    void *ctx),
+				   void *ctx);
+#endif /* CONFIG_RADIUS_TLS */
 int radius_client_send(struct radius_client_data *radius,
 		       struct radius_msg *msg,
 		       RadiusType msg_type, const u8 *addr);
diff --git a/src/radius/radius_das.c b/src/radius/radius_das.c
index 5210bf27b..e04b14df6 100644
--- a/src/radius/radius_das.c
+++ b/src/radius/radius_das.c
@@ -233,6 +233,7 @@ static struct radius_msg * radius_das_coa(struct radius_das_data *das,
 
 	if (!das->coa) {
 		wpa_printf(MSG_INFO, "DAS: CoA not supported");
+		error = 406;
 		goto fail;
 	}
 
@@ -574,8 +575,8 @@ radius_das_init(struct radius_das_conf *conf)
 {
 	struct radius_das_data *das;
 
-	if (conf->port == 0 || conf->shared_secret == NULL ||
-	    conf->client_addr == NULL)
+	if (conf->port &&
+	    (conf->shared_secret == NULL || conf->client_addr == NULL))
 		return NULL;
 
 	das = os_zalloc(sizeof(*das));
@@ -591,6 +592,12 @@ radius_das_init(struct radius_das_conf *conf)
 	das->disconnect = conf->disconnect;
 	das->coa = conf->coa;
 
+	if (!conf->port) {
+		/* No UDP listener; requests are passed in with
+		 * radius_das_process() together with their shared secret. */
+		return das;
+	}
+
 	os_memcpy(&das->client_addr, conf->client_addr,
 		  sizeof(das->client_addr));
 
diff --git a/src/radius/radius_das.h b/src/radius/radius_das.h
index e7a611586..de54a4548 100644
--- a/src/radius/radius_das.h
+++ b/src/radius/radius_das.h
@@ -43,6 +43,9 @@ struct radius_das_attrs {
 };
 
 struct radius_das_conf {
+	/* UDP port for the DAS listener; 0 = no listener (requests are passed
+	 * in with radius_das_process() and shared_secret and client_addr are
+	 * not used) */
 	int port;
 	const u8 *shared_secret;
 	size_t shared_secret_len;
diff --git a/tests/hwsim/test_radius.py b/tests/hwsim/test_radius.py
index d1a24ae67..c4a6f05e8 100644
--- a/tests/hwsim/test_radius.py
+++ b/tests/hwsim/test_radius.py
@@ -986,7 +986,7 @@ def test_radius_das_coa(dev, apdev):
     req = radius_das.CoAPacket(dict=dict, secret=b"secret",
                                Acct_Session_Id=id,
                                Event_Timestamp=int(time.time()))
-    send_and_check_reply(srv, req, pyrad.packet.CoANAK, error_cause=405)
+    send_and_check_reply(srv, req, pyrad.packet.CoANAK, error_cause=406)
 
 def test_radius_ipv6(dev, apdev):
     """RADIUS connection over IPv6"""
-- 
2.54.0 (Apple Git-157)





More information about the Hostap mailing list