Radcli library 2.0.0
A simple radius library -- new API reference
Loading...
Searching...
No Matches
request.c
1/*
2 * Copyright (C) 2026 Nikos Mavrogiannopoulos
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
14 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
15 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
16 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
17 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
18 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
19 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
20 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
22 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 */
24
28
29/* radcli2.h's request/reply entry point (radcli_request_new()/_perform()/
30 * etc.): builds a wire packet with radcli_avp_encode(), sends it with the
31 * same radcli_transport_exchange() rc_send_server_ctx() uses (so failover,
32 * retries, Response Authenticator, and Message-Authenticator/Blast-RADIUS
33 * verification are exactly the code already proven against rc_send_server_ctx(),
34 * not a second copy), and decodes the reply with radcli_avp_decode().
35 *
36 * Uses only the first configured server for the request's type, by design:
37 * the new API carries one
38 * server per context, redundancy delegated to DNS/address-level fail-over
39 * (already inside radcli_transport_exchange()) rather than a configured
40 * list of distinct servers each with its own secret -- that remains
41 * rc_auth()/rc_acct()'s job alone. radcli_request_new() logs a warning,
42 * not an error, if more than one entry is configured. */
43
44#include <config.h>
45#include <includes.h>
46#include <radcli/radcli.h>
47#include <radcli/radcli2.h>
48#include "avp.h"
49#include "util.h"
50#include "options.h"
51#include "rc-crypto.h"
52#include "rc-random.h"
53
54struct radcli_request_st {
55 rc_handle *rh;
56 uint8_t code;
57 radcli_avp_list *send;
58 char server[AUTH_ID_LEN + 1];
59 uint16_t svc_port;
60 char secret[MAX_SECRET_LENGTH + 1];
61 int timeout;
62 int retries;
63 rc_type type;
64
65 int performed;
66 uint8_t reply_code;
67 radcli_avp_list *reply_attrs;
68
69 /* Set by radcli_request_perform(r, RADCLI_REQUEST_SENDONLY); driven to
70 * completion by radcli_ctx_get_poll()/radcli_ctx_dispatch() (lib/dae.c),
71 * read back with radcli_request_done(). See lib/includes.h's struct
72 * radcli_async_send_st. */
73 struct radcli_async_send_st async;
74};
75
97radcli_request *radcli_request_new(radcli_ctx *ctx, radcli_code code, const radcli_avp_list *send)
98{
99 rc_handle *rh = (rc_handle *)ctx;
100 struct radcli_request_st *r;
101 SERVER *servers;
102 const char *optname;
103 rc_type type;
104 radcli_avp_list *send_copy;
106 const radcli_avp *a;
107
108 if (rh == NULL)
109 return NULL;
110
111 if (code != RADCLI_CODE_ACCESS_REQUEST && code != RADCLI_CODE_ACCOUNTING_REQUEST) {
112 rc_log(LOG_ERR, "radcli_request_new: code must be RADCLI_CODE_ACCESS_REQUEST "
113 "or RADCLI_CODE_ACCOUNTING_REQUEST");
114 return NULL;
115 }
116
117 /* Matches rc_select_aaa_server()'s (lib/buildreq.c) rule: an
118 * Accounting-Request goes to authserver, not acctserver, over
119 * TLS/DTLS, which carry both request types over one connection. */
120 if (rh->so_type == RC_SOCKET_TLS || rh->so_type == RC_SOCKET_DTLS ||
121 code == RADCLI_CODE_ACCESS_REQUEST) {
122 optname = "authserver";
123 type = AUTH;
124 } else {
125 optname = "acctserver";
126 type = ACCT;
127 }
128
129 servers = radcli2_priv_conf_srv(rh, optname);
130 if (servers == NULL || servers->max == 0) {
131 rc_log(LOG_ERR, "radcli_request_new: no %s configured", optname);
132 return NULL;
133 }
134 /* By design, the new API carries one server per handle, redundancy
135 * delegated to DNS/address-level
136 * fail-over (already inside radcli_transport_exchange()), not a
137 * second, per-server-list failover loop -- that job is rc_auth()/
138 * rc_acct()'s alone. Only the first configured entry is ever used;
139 * this only warns, rather than rejecting outright, so a caller
140 * migrating a legacy multi-server config one entry point at a time
141 * isn't broken by the leftover entries. */
142 if (servers->max > 1)
143 rc_log(LOG_WARNING, "radcli_request_new: %d %s entries configured; "
144 "the new API uses only the first (%s) -- redundancy is "
145 "address-level (DNS), not server-list-level",
146 servers->max, optname, servers->name[0]);
147
148 /* Copied in -- radcli2.h documents that "send" may be freed or reused
149 * by the caller immediately after this call returns; only aliasing
150 * the caller's pointer here would make that a use-after-free the
151 * first time radcli_request_perform() later reads it. */
152 send_copy = radcli_avp_list_new();
153 if (send_copy == NULL)
154 return NULL;
155 it = radcli_avp_list_iter(send);
156 while ((a = radcli_avp_iter_next(&it)) != NULL) {
157 const void *data;
158 size_t len;
159
160 if (radcli_avp_get_bytes(a, &data, &len) != 0 ||
161 radcli_avp_add_bytes(send_copy, radcli_avp_def(a), data, len) != 0) {
162 radcli_avp_list_free(send_copy);
163 return NULL;
164 }
165 }
166
167 r = calloc(1, sizeof(*r));
168 if (r == NULL) {
169 radcli_avp_list_free(send_copy);
170 return NULL;
171 }
172
173 r->type = type;
174
175 strlcpy(r->server, servers->name[0], sizeof(r->server));
176 r->svc_port = servers->port[0];
177 if (servers->secret[0] != NULL)
178 strlcpy(r->secret, servers->secret[0], sizeof(r->secret));
179
180 r->timeout = rc_conf_int_id(rh, OPT_RADIUS_TIMEOUT);
181 r->retries = rc_conf_int_id(rh, OPT_RADIUS_RETRIES);
182
183 r->rh = rh;
184 r->code = (uint8_t)code;
185 r->send = send_copy;
186
187 return r;
188}
189
190/*- Builds a wire packet for (code, send) into send_buffer (capacity
191 * RC_BUFFER_LEN): Request Authenticator (or the Accounting-Request
192 * zero-vector/trailing-MD5 variant), and -- for any code other than
193 * Accounting-Request -- a Message-Authenticator via add_msg_auth_attr().
194 * Factored out of radcli_do_exchange() so radcli_request_perform()'s
195 * RADCLI_REQUEST_SENDONLY path (which hands the packet to
196 * radcli_transport_send_async() instead of radcli_transport_exchange())
197 * builds the exact same Response Authenticator / Message-Authenticator wire
198 * logic, not a second copy -- and shared, too, by lib/dae.c's
199 * radcli2_priv_dae_send_watchdog() (RFC 5997 Status-Server over an
200 * established RadSec session). send may be an empty list (Status-Server
201 * needs no attributes but Message-Authenticator) but not NULL --
202 * radcli_avp_encode() rejects that outright. For a TLS/DTLS rh, secret is
203 * overridden with the RFC 6614/7360 fixed RadSec secret before it is used
204 * for anything (REQ-NET2-SEND-015) -- the caller's resolved secret is
205 * irrelevant for that transport regardless of what it is.
206 *
207 * @param rh a handle to parsed configuration.
208 * @param code the RADIUS packet code to send.
209 * @param send the attributes to encode; must not be NULL.
210 * @param secret the shared secret (overridden for TLS/DTLS, see above).
211 * @param send_buffer destination, capacity RC_BUFFER_LEN.
212 * @param id the packet's Identifier, always supplied by the caller -- this
213 * function does not draw one itself, so every call site states explicitly
214 * where its id comes from: rc_get_random_byte() for every caller not
215 * sharing a socket with any other concurrently in-flight exchange (the
216 * blocking radcli_do_exchange()/radcli_transport_exchange() path,
217 * radcli_aaa(), radcli2_priv_dae_send_watchdog() -- REQ-NET2-SEND-010), or,
218 * for RADCLI_REQUEST_SENDONLY, the Identifier ctx's in-flight registry
219 * already reserved (REQ-NET2-SEND-016) -- reserved and passed in before
220 * this call, never patched into the wire packet afterward, since id is
221 * itself covered by the Message-Authenticator HMAC.
222 * @param vector_out set to the request authenticator vector used.
223 * @param out_len set to the total encoded length, including the appended
224 * Message-Authenticator attribute.
225 * @return 0 on success, -1 on encoding failure.
226 -*/
227int radcli_encode_request(rc_handle *rh, uint8_t code, const radcli_avp_list *send,
228 char secret[MAX_SECRET_LENGTH + 1],
229 uint8_t send_buffer[RC_BUFFER_LEN], uint8_t id,
230 unsigned char vector_out[AUTH_VECTOR_LEN], int *out_len)
231{
232 AUTH_HDR *auth = (AUTH_HDR *)send_buffer;
233 int encoded_len, total_length;
234
235 /* RFC 6614 SS2.3/RFC 7360 SS3.2 fix the RadSec shared secret; whatever
236 * the caller resolved from authserver/acctserver's configured secret
237 * (empty, ordinarily -- REQ-CONFIG-CFG-019) is not it. Overriding here,
238 * before secret is used for anything below, is the same override
239 * lib/legacy/send.c's rc_send_server_ctx() and lib/sendserver.c's
240 * radcli_transport_exchange()/radcli_transport_send_async() already
241 * apply -- those three just apply it too late to affect an already-
242 * encoded packet, since encoding (this function) always runs first. */
243 if (rh->so.static_secret)
244 strlcpy(secret, rh->so.static_secret, MAX_SECRET_LENGTH + 1);
245
246 auth->code = code;
247 auth->id = id;
248
249 if (code == RADCLI_CODE_ACCOUNTING_REQUEST) {
250 size_t secretlen;
251 uint16_t tlen;
252
253 memset(vector_out, 0, AUTH_VECTOR_LEN);
254 memcpy(auth->vector, vector_out, AUTH_VECTOR_LEN);
255
256 encoded_len = radcli_avp_encode(rh, send, secret, vector_out,
257 send_buffer + AUTH_HDR_LEN,
258 RC_MAX_PACKET_LEN - AUTH_HDR_LEN, NULL);
259 if (encoded_len < 0)
260 return -1;
261 total_length = AUTH_HDR_LEN + encoded_len;
262
263 tlen = htons((uint16_t)total_length);
264 memcpy(&auth->length, &tlen, sizeof(tlen));
265
266 secretlen = rc_secret_len(secret);
267 memcpy(send_buffer + total_length, secret, secretlen);
268 rc_md5_calc(vector_out, send_buffer, (size_t)total_length + secretlen);
269 memcpy(auth->vector, vector_out, AUTH_VECTOR_LEN);
270 } else {
271 rc_get_random_bytes(vector_out, AUTH_VECTOR_LEN);
272 memcpy(auth->vector, vector_out, AUTH_VECTOR_LEN);
273
274 /* Leave 2+MD5_DIGEST_SIZE bytes for Message-Authenticator (added below). */
275 encoded_len = radcli_avp_encode(rh, send, secret, vector_out,
276 send_buffer + AUTH_HDR_LEN,
277 RC_MAX_PACKET_LEN - AUTH_HDR_LEN - (2 + MD5_DIGEST_SIZE), NULL);
278 if (encoded_len < 0)
279 return -1;
280 total_length = AUTH_HDR_LEN + encoded_len;
281
282 total_length = add_msg_auth_attr(rh, secret, auth, total_length);
283 auth->length = htons((uint16_t)total_length);
284 }
285
286 *out_len = total_length;
287 return 0;
288}
289
290/*- Builds a wire packet for (code, send) and hands it to
291 * radcli_transport_exchange() against (server, svc_port, secret). Shared by
292 * radcli_request_perform() and lib/aaa2.c's radcli_aaa(), so both build the
293 * exact same Response Authenticator / Message-Authenticator wire logic
294 * instead of two independently-written copies of security-sensitive code.
295 *
296 * @param rh a handle to parsed configuration.
297 * @param code the RADIUS packet code to send.
298 * @param send the attributes to encode.
299 * @param server the server to resolve and contact.
300 * @param svc_port overrides the resolved port when non-zero.
301 * @param secret the shared secret.
302 * @param timeout per-attempt reply wait, in seconds.
303 * @param retries additional retransmit attempts after the first.
304 * @param no_wait nonzero to send once and return without waiting for a
305 * reply.
306 * @param type AUTH or ACCT.
307 * @param recv_buffer filled with the reply's attributes on a terminal
308 * result.
309 * @param recv_buffer_cap recv_buffer's capacity in bytes.
310 * @param recv_len set to the number of attribute bytes written to
311 * recv_buffer.
312 * @param vector_out set to the Request Authenticator vector used, for the
313 * caller to decode the reply against.
314 * @param out_reply_code if non-NULL, set to the reply's RADIUS code on a
315 * terminal result.
316 * @return whatever radcli_transport_exchange() itself returns.
317 -*/
318int radcli_do_exchange(rc_handle *rh, uint8_t code, const radcli_avp_list *send,
319 char *server, uint16_t svc_port, char secret[MAX_SECRET_LENGTH + 1],
320 int timeout, int retries, int no_wait, rc_type type,
321 uint8_t *recv_buffer, size_t recv_buffer_cap, size_t *recv_len,
322 unsigned char vector_out[AUTH_VECTOR_LEN], uint8_t *out_reply_code)
323{
324 uint8_t send_buffer[RC_BUFFER_LEN];
325 int total_length;
326
327 /* Own per-call socket via radcli_transport_exchange() below -- no other
328 * concurrently in-flight exchange to collide with, so a CSPRNG draw is
329 * sufficient (REQ-NET2-SEND-010). */
330 if (radcli_encode_request(rh, code, send, secret, send_buffer, rc_get_random_byte(),
331 vector_out, &total_length) < 0)
332 return ERROR_RC;
333
334 return radcli_transport_exchange(rh, NULL, server, svc_port,
335 secret, 0, timeout, retries, no_wait, type,
336 send_buffer, total_length,
337 recv_buffer, recv_buffer_cap, recv_len, out_reply_code);
338}
339
364int radcli_request_perform(radcli_request *r, unsigned flags)
365{
366 uint8_t recv_buffer[RC_BUFFER_LEN];
367 unsigned char vector[AUTH_VECTOR_LEN];
368 size_t recv_len = 0;
369 int result;
370
371 if (r == NULL || r->performed)
372 return RADCLI_ERROR;
373 r->performed = 1;
374
375 if (flags & RADCLI_REQUEST_SENDONLY) {
376 uint8_t send_buffer[RC_BUFFER_LEN];
377 int send_len;
378 uint8_t id;
379 int slot;
380
381 /* Reserve the Identifier before encoding: it is itself covered
382 * by the Message-Authenticator HMAC below, so it cannot be
383 * patched in afterward (REQ-NET2-SEND-016). */
384 slot = radcli2_priv_reqreg_reserve(r->rh, &r->async, &id);
385 if (slot < 0)
386 return RADCLI_ERROR;
387
388 if (radcli_encode_request(r->rh, r->code, r->send, r->secret,
389 send_buffer, id, vector, &send_len) < 0) {
390 radcli2_priv_reqreg_release(r->rh, slot);
391 return RADCLI_ERROR;
392 }
393
394 result = radcli_transport_send_async(r->rh, slot, r->server, r->svc_port, r->secret,
395 r->type, send_buffer, send_len,
396 r->timeout, r->retries, &r->async);
397 if (result != OK_RC) {
398 radcli2_priv_reqreg_release(r->rh, slot);
399 return RADCLI_ERROR;
400 }
401 return RADCLI_OK;
402 }
403
404 result = radcli_do_exchange(r->rh, r->code, r->send, r->server, r->svc_port, r->secret,
405 r->timeout, r->retries, 0, r->type,
406 recv_buffer, sizeof(recv_buffer), &recv_len, vector, &r->reply_code);
407
408 switch (result) {
409 case OK_RC:
410 case REJECT_RC:
411 case CHALLENGE_RC:
412 if (recv_len > 0) {
413 if (radcli_avp_decode(r->rh, r->secret, vector, recv_buffer, recv_len, 0,
414 &r->reply_attrs) != 0)
415 return RADCLI_ERROR;
416 }
417 return RADCLI_OK;
418 case TIMEOUT_RC:
419 return RADCLI_TIMEOUT;
420 default:
421 return RADCLI_ERROR;
422 }
423}
424
444int radcli_request_done(radcli_request *r)
445{
446 if (r == NULL || !r->async.active)
447 return RADCLI_ERROR;
448 if (!r->async.delivered)
449 return RADCLI_AGAIN;
450
451 r->reply_code = r->async.reply_code;
452 r->reply_attrs = r->async.reply_attrs;
453 r->async.reply_attrs = NULL; /* ownership moved to r */
454 r->async.active = 0;
455
456 switch (r->async.result) {
457 case OK_RC:
458 case REJECT_RC:
459 case CHALLENGE_RC:
460 return RADCLI_OK;
461 case TIMEOUT_RC:
462 return RADCLI_TIMEOUT;
463 default:
464 return RADCLI_ERROR;
465 }
466}
467
473radcli_code radcli_request_code(const radcli_request *r)
474{
475 if (r == NULL || !r->performed)
476 return 0;
477 return (radcli_code)r->reply_code;
478}
479
486const radcli_avp_list *radcli_request_attrs(const radcli_request *r)
487{
488 if (r == NULL)
489 return NULL;
490 return r->reply_attrs;
491}
492
497const char *radcli_request_server(const radcli_request *r)
498{
499 static const char empty[] = "";
500
501 if (r == NULL)
502 return empty;
503 return r->server;
504}
505
517void radcli_request_free(radcli_request *r)
518{
519 if (r == NULL)
520 return;
521 radcli_transport_async_abort(&r->async);
522 radcli_avp_list_free(r->send);
523 radcli_avp_list_free(r->reply_attrs);
524 memset(r->secret, 0, sizeof(r->secret));
525 free(r);
526}
void radcli_avp_list_free(radcli_avp_list *list)
Free a list and every attribute it holds.
Definition avp.c:159
radcli_avp_iter radcli_avp_list_iter(const radcli_avp_list *list)
Begin iterating list.
Definition avp.c:666
int radcli_avp_add_bytes(radcli_avp_list *list, const radcli_attr_def *def, const void *value, size_t len)
Append an attribute holding an arbitrary byte string.
Definition avp.c:224
radcli_avp_list * radcli_avp_list_new(void)
Create an empty attribute-value pair list.
Definition avp.c:145
const radcli_attr_def * radcli_avp_def(const radcli_avp *a)
Return the attribute definition of a.
Definition avp.c:703
const radcli_avp * radcli_avp_iter_next(radcli_avp_iter *it)
Return the current attribute and advance.
Definition avp.c:680
int radcli_avp_get_bytes(const radcli_avp *a, const void **out, size_t *len)
Read an attribute's value as raw bytes.
Definition avp.c:853
int radcli_request_perform(radcli_request *r, unsigned flags)
Send a request, optionally waiting for the reply.
Definition request.c:364
const radcli_avp_list * radcli_request_attrs(const radcli_request *r)
Return the reply's decoded attributes.
Definition request.c:486
const char * radcli_request_server(const radcli_request *r)
Return the name of the server a request was (or will be) sent to.
Definition request.c:497
int radcli_request_done(radcli_request *r)
Report r's outcome once radcli_ctx_dispatch() has resolved it – a pure state query,...
Definition request.c:444
radcli_code
Definition radcli2.h:506
radcli_request * radcli_request_new(radcli_ctx *ctx, radcli_code code, const radcli_avp_list *send)
Create a request to send.
Definition request.c:97
void radcli_request_free(radcli_request *r)
Release a request.
Definition request.c:517
radcli_code radcli_request_code(const radcli_request *r)
Return the reply's RADIUS code.
Definition request.c:473
@ RADCLI_TIMEOUT
No reply from any address the server name resolved to.
Definition radcli2.h:565
@ RADCLI_AGAIN
Definition radcli2.h:570
@ RADCLI_ERROR
Malformed input, a verification failure, or no server configured.
Definition radcli2.h:564
@ RADCLI_OK
A validated reply was received; see radcli_request_code() for which one.
Definition radcli2.h:563
@ RADCLI_REQUEST_SENDONLY
Definition radcli2.h:603