[PATCH 2/7] RADIUS DAS: Echo Proxy-State in responses

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


A Disconnect-Request or CoA-Request that carries Proxy-State was NAKed
with Error-Cause 401 (Unsupported Attribute), since Proxy-State was not
in the list of allowed attributes, and Proxy-State was not copied into
any response.

RFC 5176, Section 3.1 requires the Dynamic Authorization Server to
include the Proxy-State attributes of the request in its response, and
RFC 8559, Section 4.3.2 states that the NAS must not treat Proxy-State
as mandatory and must ignore it. A request forwarded by a proxy commonly
carries Proxy-State, so these requests could not be served through such
a proxy.

Accept Proxy-State in both request types and copy all instances, in
order, into the ACK or NAK before the response authenticators are
calculated.

Add a hwsim test case with two Proxy-State attributes, checking both a
NAK (unknown session) and an ACK. The test dictionary did not include
Proxy-State, so add it.

Signed-off-by: Seb Adamski <seb at bionic.network>
---
 src/radius/radius_das.c       | 11 ++++++
 tests/hwsim/dictionary.radius |  1 +
 tests/hwsim/test_radius.py    | 69 +++++++++++++++++++++++++++++++++++
 3 files changed, 81 insertions(+)

diff --git a/src/radius/radius_das.c b/src/radius/radius_das.c
index d3d9f75ec..02e9e9f6f 100644
--- a/src/radius/radius_das.c
+++ b/src/radius/radius_das.c
@@ -48,6 +48,7 @@ static struct radius_msg * radius_das_disconnect(struct radius_das_data *das,
 		RADIUS_ATTR_EVENT_TIMESTAMP,
 		RADIUS_ATTR_MESSAGE_AUTHENTICATOR,
 		RADIUS_ATTR_CHARGEABLE_USER_IDENTITY,
+		RADIUS_ATTR_PROXY_STATE,
 #ifdef CONFIG_IPV6
 		RADIUS_ATTR_NAS_IPV6_ADDRESS,
 #endif /* CONFIG_IPV6 */
@@ -210,6 +211,7 @@ static struct radius_msg * radius_das_coa(struct radius_das_data *das,
 		RADIUS_ATTR_EVENT_TIMESTAMP,
 		RADIUS_ATTR_MESSAGE_AUTHENTICATOR,
 		RADIUS_ATTR_CHARGEABLE_USER_IDENTITY,
+		RADIUS_ATTR_PROXY_STATE,
 #ifdef CONFIG_HS20
 		RADIUS_ATTR_VENDOR_SPECIFIC,
 #endif /* CONFIG_HS20 */
@@ -486,6 +488,15 @@ static void radius_das_receive(int sock, void *eloop_ctx, void *sock_ctx)
 	if (reply) {
 		wpa_printf(MSG_DEBUG, "DAS: Reply to %s:%d", abuf, from_port);
 
+		/* RFC 5176, 3.1: Proxy-State attributes are copied unmodified
+		 * and in order from the request to the response. */
+		if (radius_msg_copy_attr(reply, msg,
+					 RADIUS_ATTR_PROXY_STATE) < 0) {
+			wpa_printf(MSG_INFO,
+				   "DAS: Failed to copy Proxy-State attributes - drop");
+			goto fail;
+		}
+
 		if (!radius_msg_add_attr_int32(reply,
 					       RADIUS_ATTR_EVENT_TIMESTAMP,
 					       now.sec)) {
diff --git a/tests/hwsim/dictionary.radius b/tests/hwsim/dictionary.radius
index d2112dad3..051e49675 100644
--- a/tests/hwsim/dictionary.radius
+++ b/tests/hwsim/dictionary.radius
@@ -6,6 +6,7 @@ ATTRIBUTE	Vendor-Specific		26	octets
 ATTRIBUTE	Session-Timeout		27	integer
 ATTRIBUTE	Calling-Station-Id	31	string
 ATTRIBUTE	NAS-Identifier		32	string
+ATTRIBUTE	Proxy-State		33	octets
 ATTRIBUTE	Acct-Session-Id		44	string
 ATTRIBUTE	Acct-Multi-Session-Id	50	string
 ATTRIBUTE	Event-Timestamp		55	date
diff --git a/tests/hwsim/test_radius.py b/tests/hwsim/test_radius.py
index de88b0b18..d1a24ae67 100644
--- a/tests/hwsim/test_radius.py
+++ b/tests/hwsim/test_radius.py
@@ -423,6 +423,23 @@ def send_and_check_reply(srv, req, code, error_cause=0):
             if reply['Error-Cause'][0] != error_cause:
                 raise Exception("Unexpected Error-Cause: {}".format(reply['Error-Cause']))
 
+def check_das_reply(reply, code, error_cause=0, proxy_state=None):
+    if reply is None:
+        raise Exception("No response to DAS request")
+    if reply.code != code:
+        raise Exception("Unexpected response code %d (expected %d)" %
+                        (reply.code, code))
+    if error_cause:
+        if 'Error-Cause' not in reply:
+            raise Exception("Missing Error-Cause")
+        if reply['Error-Cause'][0] != error_cause:
+            raise Exception("Unexpected Error-Cause: {}".format(reply['Error-Cause']))
+    if proxy_state is not None:
+        if 'Proxy-State' not in reply:
+            raise Exception("Proxy-State not included in the response")
+        if reply['Proxy-State'] != proxy_state:
+            raise Exception("Unexpected Proxy-State: {}".format(reply['Proxy-State']))
+
 def test_radius_acct_psk(dev, apdev):
     """RADIUS Accounting - PSK"""
     as_hapd = hostapd.Hostapd("as")
@@ -885,6 +902,58 @@ def test_radius_das_disconnect_time_window(dev, apdev):
     add_message_auth_req(req)
     send_and_check_reply(srv, req, pyrad.packet.DisconnectACK)
 
+def test_radius_das_proxy_state(dev, apdev):
+    """RADIUS Dynamic Authorization Extensions - Proxy-State"""
+    try:
+        import pyrad.client
+        import pyrad.packet
+        import pyrad.dictionary
+        import radius_das
+    except ImportError:
+        raise HwsimSkip("No pyrad modules available")
+
+    params = hostapd.wpa2_eap_params(ssid="radius-das")
+    params['radius_das_port'] = "3799"
+    params['radius_das_client'] = "127.0.0.1 secret"
+    params['radius_das_require_event_timestamp'] = "1"
+    params['own_ip_addr'] = "127.0.0.1"
+    params['nas_identifier'] = "nas.example.com"
+    hapd = hostapd.add_ap(apdev[0], params)
+    connect(dev[0], "radius-das")
+    addr = dev[0].own_addr()
+    hapd.wait_sta(addr=addr)
+    sta = hapd.get_sta(addr)
+    id = sta['dot1xAuthSessionId']
+
+    dict = pyrad.dictionary.Dictionary("dictionary.radius")
+
+    srv = pyrad.client.Client(server="127.0.0.1", acctport=3799,
+                              secret=b"secret", dict=dict)
+    srv.retries = 1
+    srv.timeout = 1
+
+    proxy_state = [b"proxy-state-1", b"\x00\x01\x02\x03"]
+
+    logger.info("Disconnect-Request with Proxy-State for an unknown session")
+    req = radius_das.DisconnectPacket(dict=dict, secret=b"secret",
+                                      Acct_Session_Id="0123456789abcdef",
+                                      Event_Timestamp=int(time.time()))
+    for val in proxy_state:
+        req.AddAttribute("Proxy-State", val)
+    check_das_reply(srv.SendPacket(req), pyrad.packet.DisconnectNAK,
+                    error_cause=503, proxy_state=proxy_state)
+
+    logger.info("Disconnect-Request with Proxy-State for the session")
+    req = radius_das.DisconnectPacket(dict=dict, secret=b"secret",
+                                      Acct_Session_Id=id,
+                                      Event_Timestamp=int(time.time()))
+    for val in proxy_state:
+        req.AddAttribute("Proxy-State", val)
+    check_das_reply(srv.SendPacket(req), pyrad.packet.DisconnectACK,
+                    proxy_state=proxy_state)
+    dev[0].wait_disconnected(timeout=10)
+    hapd.wait_sta_disconnect(addr=addr)
+
 def test_radius_das_coa(dev, apdev):
     """RADIUS Dynamic Authorization Extensions - CoA"""
     try:
-- 
2.54.0 (Apple Git-157)





More information about the Hostap mailing list