[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