/src/freeradius-server/src/lib/server/state.c
Line | Count | Source |
1 | | /* |
2 | | * This program is free software; you can redistribute it and/or modify |
3 | | * it under the terms of the GNU General Public License as published by |
4 | | * the Free Software Foundation; either version 2 of the License, or |
5 | | * (at your option) any later version. |
6 | | * |
7 | | * This program is distributed in the hope that it will be useful, |
8 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
9 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
10 | | * GNU General Public License for more details. |
11 | | * |
12 | | * You should have received a copy of the GNU General Public License |
13 | | * along with this program; if not, write to the Free Software |
14 | | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA |
15 | | */ |
16 | | |
17 | | /** |
18 | | * $Id: 6e25bc080183af5250b4793f935c91a474e4d786 $ |
19 | | * |
20 | | * @brief Multi-packet state handling |
21 | | * @file src/lib/server/state.c |
22 | | * |
23 | | * @ingroup AVP |
24 | | * |
25 | | * For each round of a multi-round authentication method such as EAP, |
26 | | * or a 2FA method such as OTP, a state entry will be created. The state |
27 | | * entry holds data that should be available during the complete lifecycle |
28 | | * of the authentication attempt. |
29 | | * |
30 | | * When a request is complete, #fr_state_store is called to transfer |
31 | | * ownership of the state fr_pair_ts and state_ctx (which the fr_pair_ts |
32 | | * are allocated in) to a #fr_state_entry_t. This #fr_state_entry_t holds the |
33 | | * value of the State attribute, that will be send out in the response. |
34 | | * |
35 | | * When the next request is received, #fr_state_restore is called to transfer |
36 | | * the fr_pair_ts and state ctx to the new request. |
37 | | * |
38 | | * The ownership of the state_ctx and state fr_pair_ts is transferred as below: |
39 | | * |
40 | | * @verbatim |
41 | | request -> state_entry -> request -> state_entry -> request -> free() |
42 | | \-> reply \-> reply \-> access-reject/access-accept |
43 | | * @endverbatim |
44 | | * |
45 | | * @copyright 2014 The FreeRADIUS server project |
46 | | */ |
47 | | RCSID("$Id: 6e25bc080183af5250b4793f935c91a474e4d786 $") |
48 | | |
49 | | #include <freeradius-devel/server/request.h> |
50 | | #include <freeradius-devel/server/request_data.h> |
51 | | #include <freeradius-devel/server/state.h> |
52 | | |
53 | | #include <freeradius-devel/io/listen.h> |
54 | | |
55 | | #include <freeradius-devel/util/debug.h> |
56 | | #include <freeradius-devel/util/md5.h> |
57 | | #include <freeradius-devel/util/rand.h> |
58 | | |
59 | | const conf_parser_t state_session_config[] = { |
60 | | { FR_CONF_OFFSET("timeout", fr_state_config_t, timeout), .dflt = "15" }, |
61 | | { FR_CONF_OFFSET("max", fr_state_config_t, max_sessions), .dflt = "4096" }, |
62 | | { FR_CONF_OFFSET("max_rounds", fr_state_config_t, max_rounds), .dflt = "50" }, |
63 | | { FR_CONF_OFFSET("state_server_id", fr_state_config_t, server_id) }, |
64 | | { FR_CONF_OFFSET("dedup_key", fr_state_config_t, dedup_key) }, |
65 | | |
66 | | CONF_PARSER_TERMINATOR |
67 | | }; |
68 | | |
69 | | |
70 | | /** Holds a state value, and associated fr_pair_ts and data |
71 | | * |
72 | | */ |
73 | | typedef struct { |
74 | | uint64_t id; //!< State number |
75 | | fr_rb_node_t node; //!< Entry in the state rbtree. |
76 | | union { |
77 | | /** Server ID components |
78 | | * |
79 | | * State values should be unique to a given server |
80 | | */ |
81 | | struct state_comp { |
82 | | uint8_t tries; //!< Number of rounds so far in this state sequence. |
83 | | uint8_t tx; //!< Bits changed in the tries counter for this round. |
84 | | uint8_t r_0; //!< Random component. |
85 | | uint8_t server_id; //!< Configured server ID. Used for debugging |
86 | | //!< to locate authentication sessions originating |
87 | | //!< from a particular backend authentication server. |
88 | | |
89 | | uint32_t context_id; //!< Hash of the current virtual server, xor'd with |
90 | | //!< r1, r2, r3, r4 after the original state value |
91 | | //!< is sent, but before the state entry is inserted |
92 | | //!< into the tree. The receiving virtual server |
93 | | //!< xor's its hash with the received state before |
94 | | //!< performing the lookup. This means one virtual |
95 | | //!< server can't act on a state entry generated by |
96 | | //!< another, even though the state tree is global |
97 | | //!< to all virtual servers. |
98 | | |
99 | | uint8_t vx_0; //!< Random component. |
100 | | uint8_t r_1; //!< Random component. |
101 | | uint8_t vx_1; //!< Random component. |
102 | | uint8_t r_2; //!< Random component. |
103 | | |
104 | | uint8_t vx_2; //!< Random component. |
105 | | uint8_t vx_3; //!< Random component. |
106 | | uint8_t r_3; //!< Random component. |
107 | | uint8_t r_4; //!< Random component. |
108 | | } state_comp; |
109 | | |
110 | | uint8_t state[sizeof(struct state_comp)]; //!< State value in binary. |
111 | | }; |
112 | | |
113 | | uint64_t seq_start; //!< Number of first request in this sequence. |
114 | | fr_time_t cleanup; //!< When this entry should be cleaned up. |
115 | | |
116 | | /* |
117 | | * Should only even be in one at a time |
118 | | */ |
119 | | union { |
120 | | fr_dlist_t expire_entry; //!< Entry in the list of things to expire. |
121 | | fr_dlist_t free_entry; //!< Entry in the list of things to free. |
122 | | }; |
123 | | |
124 | | unsigned int tries; |
125 | | |
126 | | fr_pair_t *ctx; //!< for all session specific data. |
127 | | |
128 | | fr_dlist_head_t data; //!< Persistable request data, also parented by ctx. |
129 | | |
130 | | request_t *thawed; //!< The request that thawed this entry. |
131 | | |
132 | | fr_value_box_t const *dedup_key; //!< Key for dedup |
133 | | fr_rb_node_t dedup_node; //!< Entry in the dedup rbtree |
134 | | } fr_state_entry_t; |
135 | | |
136 | | /** A child of a fr_state_entry_t |
137 | | * |
138 | | * Children are tracked using the request data of parents. |
139 | | * |
140 | | * request data is added with identifiers that uniquely identify the |
141 | | * subrequest it should be restored to. |
142 | | * |
143 | | * In this way a top level fr_state_entry_t can hold the session |
144 | | * information for multiple children, and the children may hold |
145 | | * state_child_entry_ts for grandchildren. |
146 | | */ |
147 | | typedef struct { |
148 | | fr_pair_t *ctx; //!< for all session specific data. |
149 | | |
150 | | fr_dlist_head_t data; //!< Persistable request data, also parented by ctx. |
151 | | |
152 | | request_t *thawed; //!< The request that thawed this entry. |
153 | | } state_child_entry_t; |
154 | | |
155 | | struct fr_state_tree_s { |
156 | | uint64_t id; //!< Next ID to assign. |
157 | | uint64_t timed_out; //!< Number of states that were cleaned up due to |
158 | | //!< timeout. |
159 | | fr_state_config_t config; //!< a local copy |
160 | | |
161 | | fr_rb_tree_t *tree; //!< rbtree used to lookup state value. |
162 | | fr_rb_tree_t *dedup_tree; //!< rbtree used to do dedups |
163 | | fr_dlist_head_t to_expire; //!< Linked list of entries to free. |
164 | | |
165 | | pthread_mutex_t mutex; //!< Synchronisation mutex. |
166 | | |
167 | | fr_dict_attr_t const *da; //!< Attribute where the state is stored. |
168 | | }; |
169 | | |
170 | 0 | #define PTHREAD_MUTEX_LOCK if (state->config.thread_safe) pthread_mutex_lock |
171 | 0 | #define PTHREAD_MUTEX_UNLOCK if (state->config.thread_safe) pthread_mutex_unlock |
172 | | |
173 | | static void state_entry_unlink(fr_state_tree_t *state, fr_state_entry_t *entry); |
174 | | |
175 | | /** Compare two fr_state_entry_t based on their state value i.e. the value of the attribute |
176 | | * |
177 | | */ |
178 | | static fr_cmp_ret_t state_entry_cmp(void const *one, void const *two) |
179 | 0 | { |
180 | 0 | fr_state_entry_t const *a = one, *b = two; |
181 | 0 | int ret; |
182 | |
|
183 | 0 | ret = memcmp(a->state, b->state, sizeof(a->state)); |
184 | 0 | return CMP(ret, 0); |
185 | 0 | } |
186 | | |
187 | | /** Compare two fr_state_entry_t based on their dedup key |
188 | | * |
189 | | */ |
190 | | static fr_cmp_ret_t state_dedup_cmp(void const *one, void const *two) |
191 | 0 | { |
192 | 0 | fr_state_entry_t const *a = one, *b = two; |
193 | |
|
194 | 0 | return fr_value_box_cmp(a->dedup_key, b->dedup_key); |
195 | 0 | } |
196 | | |
197 | | |
198 | | /** Free the state tree |
199 | | * |
200 | | */ |
201 | | static int _state_tree_free(fr_state_tree_t *state) |
202 | 0 | { |
203 | 0 | fr_state_entry_t *entry; |
204 | |
|
205 | 0 | if (state->config.thread_safe) pthread_mutex_destroy(&state->mutex); |
206 | |
|
207 | 0 | DEBUG4("Freeing state tree %p", state); |
208 | |
|
209 | 0 | while ((entry = fr_dlist_head(&state->to_expire)) != NULL) { |
210 | 0 | state_entry_unlink(state, entry); |
211 | 0 | DEBUG4("Freeing state entry %p (%" PRIu64 ")", entry, entry->id); |
212 | 0 | talloc_free(entry); |
213 | 0 | } |
214 | | |
215 | | /* |
216 | | * Free the rbtree |
217 | | */ |
218 | 0 | talloc_free(state->tree); |
219 | |
|
220 | 0 | return 0; |
221 | 0 | } |
222 | | |
223 | | /** Initialise a new state tree |
224 | | * |
225 | | * @param[in] ctx to link the lifecycle of the state tree to. |
226 | | * @param[in] da Attribute used to store and retrieve state from. |
227 | | * @param[in] config the configuration data |
228 | | * @return |
229 | | * - A new state tree. |
230 | | * - NULL on failure. |
231 | | */ |
232 | | fr_state_tree_t *fr_state_tree_init(TALLOC_CTX *ctx, fr_dict_attr_t const *da, fr_state_config_t const *config) |
233 | 0 | { |
234 | 0 | fr_state_tree_t *state; |
235 | | |
236 | | /* |
237 | | * We can only handle 'octets' types. |
238 | | */ |
239 | 0 | if (da->type != FR_TYPE_OCTETS) { |
240 | 0 | fr_strerror_printf("Input state attribute '%s' has data type %s instead of 'octets'", |
241 | 0 | da->name, fr_type_to_str(da->type)); |
242 | 0 | return NULL; |
243 | 0 | } |
244 | | |
245 | 0 | state = talloc_zero(NULL, fr_state_tree_t); |
246 | 0 | if (!state) return 0; |
247 | | |
248 | 0 | state->config = *config; |
249 | 0 | state->da = da; /* Remember which attribute we use to load/store state */ |
250 | | |
251 | | /* |
252 | | * Some systems may start a new session before closing |
253 | | * out the old one. The dedup key lets us find |
254 | | * pre-existing sessions, and close them out. |
255 | | */ |
256 | 0 | if (config->dedup_key) { |
257 | 0 | if ((!tmpl_is_attr(config->dedup_key) && |
258 | 0 | !tmpl_is_xlat(config->dedup_key)) || |
259 | 0 | tmpl_needs_resolving(config->dedup_key)) { |
260 | 0 | fr_strerror_const("Invalid value for \"dedup_key\" - it must be an attribute reference or a simple expansion"); |
261 | 0 | talloc_free(state); |
262 | 0 | return NULL; |
263 | 0 | } |
264 | | |
265 | 0 | if (tmpl_async_required(config->dedup_key)) { |
266 | 0 | fr_strerror_const("Invalid value for \"dedup_key\" - it must be a simple expansion, and cannot query external systems such as databases"); |
267 | 0 | talloc_free(state); |
268 | 0 | return NULL; |
269 | 0 | } |
270 | 0 | } |
271 | | |
272 | | /* |
273 | | * Create a break in the contexts. |
274 | | * We still want this to be freed at the same time |
275 | | * as the parent, but we also need it to be thread |
276 | | * safe, and multiple threads could be using the |
277 | | * tree. |
278 | | */ |
279 | 0 | talloc_link_ctx(ctx, state); |
280 | |
|
281 | 0 | if (state->config.thread_safe && (pthread_mutex_init(&state->mutex, NULL) != 0)) { |
282 | 0 | talloc_free(state); |
283 | 0 | return NULL; |
284 | 0 | } |
285 | | |
286 | 0 | fr_dlist_talloc_init(&state->to_expire, fr_state_entry_t, free_entry); |
287 | | |
288 | | /* |
289 | | * We need to do controlled freeing of the |
290 | | * rbtree, so that all the state entries |
291 | | * are freed before it's destroyed. Hence |
292 | | * it being parented from the NULL ctx. |
293 | | */ |
294 | 0 | state->tree = fr_rb_inline_talloc_alloc(NULL, fr_state_entry_t, node, state_entry_cmp, NULL); |
295 | 0 | if (!state->tree) { |
296 | 0 | talloc_free(state); |
297 | 0 | return NULL; |
298 | 0 | } |
299 | 0 | talloc_set_destructor(state, _state_tree_free); |
300 | |
|
301 | 0 | if (config->dedup_key) { |
302 | 0 | state->dedup_tree = fr_rb_inline_talloc_alloc(state->tree, fr_state_entry_t, dedup_node, state_dedup_cmp, NULL); |
303 | 0 | if (!state->dedup_tree) { |
304 | 0 | talloc_free(state); |
305 | 0 | return NULL; |
306 | 0 | } |
307 | 0 | } |
308 | | |
309 | 0 | return state; |
310 | 0 | } |
311 | | |
312 | | /** Unlink an entry and remove if from the tree |
313 | | * |
314 | | */ |
315 | | static inline CC_HINT(always_inline) |
316 | | void state_entry_unlink(fr_state_tree_t *state, fr_state_entry_t *entry) |
317 | 0 | { |
318 | | /* |
319 | | * Check the memory is still valid |
320 | | */ |
321 | 0 | (void) talloc_get_type_abort(entry, fr_state_entry_t); |
322 | |
|
323 | 0 | fr_dlist_remove(&state->to_expire, entry); |
324 | 0 | fr_rb_delete(state->tree, entry); |
325 | 0 | if (state->dedup_tree) fr_rb_delete(state->dedup_tree, entry); |
326 | |
|
327 | 0 | DEBUG4("State ID %" PRIu64 " unlinked", entry->id); |
328 | 0 | } |
329 | | |
330 | | /** Frees any data associated with a state |
331 | | * |
332 | | */ |
333 | | static int _state_entry_free(fr_state_entry_t *entry) |
334 | 0 | { |
335 | 0 | #ifdef WITH_VERIFY_PTR |
336 | | /* |
337 | | * Verify all state attributes are parented |
338 | | * by the state context. |
339 | | */ |
340 | 0 | if (entry->ctx) { |
341 | 0 | fr_pair_list_foreach(&entry->ctx->children, vp) { |
342 | 0 | fr_assert(entry->ctx == talloc_parent(vp)); |
343 | 0 | } |
344 | 0 | } |
345 | | |
346 | | /* |
347 | | * Ensure any request data is parented by us |
348 | | * so we know it'll be cleaned up. |
349 | | */ |
350 | 0 | (void)fr_cond_assert(request_data_verify_parent(entry->ctx, &entry->data)); |
351 | 0 | #endif |
352 | | |
353 | | /* |
354 | | * Should also free any state attributes |
355 | | */ |
356 | 0 | if (entry->ctx) TALLOC_FREE(entry->ctx); |
357 | |
|
358 | 0 | DEBUG4("State ID %" PRIu64 " freed", entry->id); |
359 | |
|
360 | 0 | return 0; |
361 | 0 | } |
362 | | |
363 | | static void state_entry_fill(fr_state_entry_t *entry, fr_value_box_t const *vb) |
364 | 0 | { |
365 | |
|
366 | 0 | uint64_t hash; |
367 | | |
368 | | /* |
369 | | * Use the supplied State if it's the correct size. |
370 | | */ |
371 | 0 | if (vb->vb_length == sizeof(entry->state)) { |
372 | 0 | memcpy(&entry->state, vb->vb_octets, vb->vb_length); |
373 | 0 | return; |
374 | 0 | } |
375 | | |
376 | | /* |
377 | | * Otherwise hash the data. |
378 | | */ |
379 | 0 | memset(&entry->state, 0, sizeof(entry->state)); |
380 | |
|
381 | 0 | hash = fr_hash64(vb->vb_octets, vb->vb_length); |
382 | 0 | memcpy(&entry->state, &hash, sizeof(hash)); |
383 | 0 | } |
384 | | |
385 | | /** Create a new state entry, and store state_ctx and data in it |
386 | | * |
387 | | * @note Takes the mutex, and releases it before returning, on success and on |
388 | | * failure. The mutex is released while the entry is allocated, so it is |
389 | | * taken and released twice. |
390 | | */ |
391 | | static fr_state_entry_t *state_entry_create(fr_state_tree_t *state, request_t *request, |
392 | | fr_pair_list_t *reply_list, fr_state_entry_t *old, |
393 | | fr_value_box_t const *dedup_key, |
394 | | fr_pair_t *state_ctx, fr_dlist_head_t *data) |
395 | 0 | { |
396 | 0 | fr_time_t now = fr_time(); |
397 | 0 | fr_pair_t *vp; |
398 | 0 | fr_state_entry_t *entry; |
399 | |
|
400 | 0 | uint64_t timed_out = 0; |
401 | 0 | bool too_many = false; |
402 | 0 | fr_dlist_head_t to_free; |
403 | |
|
404 | 0 | PTHREAD_MUTEX_LOCK(&state->mutex); |
405 | | |
406 | | /* |
407 | | * If we have a previous entry, then it can't be in an |
408 | | * expiry list, and it can't be in the list of states |
409 | | * where we have sent a reply. |
410 | | */ |
411 | 0 | fr_assert(!old || |
412 | 0 | (!fr_dlist_entry_in_list(&old->expire_entry) && |
413 | 0 | !fr_rb_node_inline_in_tree(&old->node))); |
414 | | |
415 | | /* |
416 | | * If we have a previous entry and a dedup_tree, then we |
417 | | * must have a dedup key, AND the entry must be in the |
418 | | * dedup tree. |
419 | | */ |
420 | 0 | fr_assert(!old || !state->dedup_tree || (old->dedup_key && fr_rb_node_inline_in_tree(&old->dedup_node))); |
421 | | |
422 | | /* |
423 | | * If there is an old entry, we can't have a dedup_key. |
424 | | */ |
425 | 0 | fr_assert(!old || !dedup_key); |
426 | | |
427 | | /* |
428 | | * We track a separate free list, as we have to check |
429 | | * expiration with the mutex locked. But we want to free |
430 | | * things with the mutex unlocked. |
431 | | */ |
432 | 0 | fr_dlist_init(&to_free, fr_state_entry_t, free_entry); |
433 | | |
434 | | /* |
435 | | * Clean up expired entries which have not finished. If |
436 | | * the request fails, then the corresponding entry is |
437 | | * discarded. So the expiration list is only for entries |
438 | | * which have been half-started, and then (many seconds |
439 | | * later) haven't seen a "next" packet. |
440 | | */ |
441 | 0 | fr_dlist_foreach(&state->to_expire, fr_state_entry_t, expires) { |
442 | 0 | (void)talloc_get_type_abort(expires, fr_state_entry_t); /* Allow examination */ |
443 | | |
444 | | /* |
445 | | * It's active (and asserted so above), so it can't be in the expiry list. |
446 | | */ |
447 | 0 | fr_assert(expires != old); |
448 | | |
449 | | /* |
450 | | * Too old, we can delete it. |
451 | | */ |
452 | 0 | if (fr_time_lt(expires->cleanup, now)) { |
453 | 0 | state_entry_unlink(state, expires); |
454 | 0 | fr_dlist_insert_tail(&to_free, expires); |
455 | 0 | timed_out++; |
456 | 0 | continue; |
457 | 0 | } |
458 | | |
459 | 0 | break; |
460 | 0 | } |
461 | |
|
462 | 0 | if (!old) { |
463 | | /* |
464 | | * We're inserting a new session. Limit the |
465 | | * number of sessions based on how many are in |
466 | | * the RB tree. If at least one session has |
467 | | * timed out, then we can definitely add a new |
468 | | * session. |
469 | | * |
470 | | * Note that sessions being processed are removed |
471 | | * from the tree. This means that the maximum |
472 | | * number of sessions might actually be |
473 | | * max_session+num_workers. In practice this |
474 | | * shouldn't be a problem. |
475 | | */ |
476 | 0 | too_many = (fr_rb_num_elements(state->tree) >= state->config.max_sessions) && (timed_out == 0); |
477 | | |
478 | | /* |
479 | | * If there is a previous session for the same dedup key, then remove the old one from |
480 | | * the dedup tree. |
481 | | */ |
482 | 0 | if (dedup_key) { |
483 | 0 | fr_state_entry_t *unfinished; |
484 | | |
485 | | /* |
486 | | * A comparator error means we can't tell whether a |
487 | | * previous session exists. The insert into the dedup |
488 | | * tree below fails for the same reason, refusing the |
489 | | * new entry, so here we only report. |
490 | | */ |
491 | 0 | if (unlikely(fr_rb_find((void **)&unfinished, state->dedup_tree, |
492 | 0 | &(fr_state_entry_t) { .dedup_key = dedup_key }) < 0)) { |
493 | 0 | RPEDEBUG("Failed checking for an existing session with the same dedup_key"); |
494 | 0 | } else if (unfinished) { |
495 | 0 | state_entry_unlink(state, unfinished); |
496 | 0 | fr_dlist_insert_tail(&to_free, unfinished); |
497 | 0 | } |
498 | 0 | } |
499 | 0 | } |
500 | |
|
501 | 0 | PTHREAD_MUTEX_UNLOCK(&state->mutex); |
502 | |
|
503 | 0 | if (timed_out > 0) { |
504 | 0 | RWDEBUG("Cleaning up %"PRIu64" timed out state entries", timed_out); |
505 | 0 | state->timed_out += timed_out; |
506 | | |
507 | | /* |
508 | | * Now free the unlinked entries. |
509 | | * |
510 | | * We do it here as freeing may involve significantly more |
511 | | * work than just freeing the data. |
512 | | * |
513 | | * If there's request data that was persisted it will now |
514 | | * be freed also, and it may have complex destructors associated |
515 | | * with it. |
516 | | */ |
517 | 0 | fr_dlist_talloc_free(&to_free); |
518 | |
|
519 | 0 | } else if (too_many) { |
520 | 0 | talloc_const_free(dedup_key); |
521 | 0 | RERROR("Failed inserting state entry - At maximum ongoing session limit (%u)", |
522 | 0 | state->config.max_sessions); |
523 | 0 | return NULL; |
524 | 0 | } |
525 | | |
526 | | /* |
527 | | * Allocation doesn't need to occur inside the critical region |
528 | | * and would add significantly to contention. |
529 | | */ |
530 | 0 | if (!old) { |
531 | 0 | MEM(entry = talloc_zero(NULL, fr_state_entry_t)); |
532 | 0 | talloc_set_destructor(entry, _state_entry_free); |
533 | |
|
534 | 0 | entry->id = state->id++; |
535 | |
|
536 | 0 | } else { |
537 | 0 | fr_assert(!old->ctx); |
538 | 0 | entry = old; |
539 | 0 | } |
540 | |
|
541 | 0 | request_data_list_init(&entry->data); |
542 | | |
543 | | /* |
544 | | * Limit the lifetime of this entry based on how long the |
545 | | * server takes to process a request. Doing it this way |
546 | | * isn't perfect, but it's reasonable, and it's one less |
547 | | * thing for an administrator to configure. |
548 | | */ |
549 | 0 | entry->cleanup = fr_time_add(now, state->config.timeout); |
550 | | |
551 | | /* |
552 | | * Some modules either create their own state, or need to |
553 | | * synthesize it from data in a packet header. If we |
554 | | * have such a state, then use that in preference to |
555 | | * creating a random one. |
556 | | */ |
557 | 0 | vp = fr_pair_find_by_da(reply_list, NULL, state->da); |
558 | 0 | if (vp && vp->vp_length) { |
559 | 0 | state_entry_fill(entry, &vp->data); |
560 | |
|
561 | 0 | } else { |
562 | 0 | if (old) { |
563 | | /* |
564 | | * Just re-use the old state. |
565 | | */ |
566 | 0 | entry->tries++; |
567 | |
|
568 | 0 | if (entry->tries > state->config.max_rounds) { |
569 | 0 | RERROR("Failed tracking state entry - too many rounds (%u)", entry->tries); |
570 | 0 | goto fail; |
571 | 0 | } |
572 | 0 | } else { |
573 | 0 | size_t i; |
574 | 0 | uint32_t hash; |
575 | |
|
576 | 0 | if (dedup_key) entry->dedup_key = talloc_steal(entry, dedup_key); |
577 | | |
578 | | /* |
579 | | * Get a bunch of random numbers. |
580 | | */ |
581 | 0 | for (i = 0; i < sizeof(entry->state); i+= 4) { |
582 | 0 | hash = fr_rand(); |
583 | 0 | memcpy(&entry->state[i], &hash, sizeof(hash)); |
584 | 0 | } |
585 | | |
586 | | /* |
587 | | * Add in a server ID. This lets a "FreeRADIUS |
588 | | * aware" load balancer direct the packet based |
589 | | * on the contents of the State attribute. |
590 | | */ |
591 | 0 | entry->state_comp.server_id = state->config.server_id; |
592 | | |
593 | | /* |
594 | | * Add our own custom brand of magic. |
595 | | */ |
596 | 0 | entry->state_comp.vx_0 = entry->state_comp.r_0 ^ |
597 | 0 | ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 24) & 0xff); |
598 | 0 | entry->state_comp.vx_1 = entry->state_comp.r_0 ^ |
599 | 0 | ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 16) & 0xff); |
600 | 0 | entry->state_comp.vx_2 = entry->state_comp.r_0 ^ |
601 | 0 | ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 8) & 0xff); |
602 | 0 | entry->state_comp.vx_3 = entry->state_comp.r_0 ^ |
603 | 0 | (((uint32_t) HEXIFY(RADIUSD_VERSION)) & 0xff); |
604 | 0 | } |
605 | | |
606 | | /* |
607 | | * Track the number of round trips, too. |
608 | | */ |
609 | 0 | entry->state_comp.tx ^= entry->tries; |
610 | 0 | entry->state_comp.tries = entry->tries ^ entry->state_comp.r_3; |
611 | |
|
612 | 0 | MEM(vp = fr_pair_afrom_da(request->reply_ctx, state->da)); |
613 | 0 | fr_pair_value_memdup(vp, entry->state, sizeof(entry->state), false); |
614 | 0 | fr_pair_append(reply_list, vp); |
615 | 0 | } |
616 | | |
617 | 0 | DEBUG4("State ID %" PRIu64 " created, value 0x%pH, expires %pV", |
618 | 0 | entry->id, &vp->data, |
619 | 0 | fr_box_time_delta(fr_time_sub(entry->cleanup, now))); |
620 | | |
621 | | /* |
622 | | * XOR the server hash with four bytes of random context |
623 | | * ID after adding it to the reply, but before inserting |
624 | | * it into the RB rtree. We XOR is again before looking |
625 | | * it up in the tree, to ensure state lookups only |
626 | | * succeed in the virtual server that created the state |
627 | | * value. |
628 | | */ |
629 | 0 | entry->state_comp.context_id ^= state->config.context_id; |
630 | |
|
631 | 0 | PTHREAD_MUTEX_LOCK(&state->mutex); |
632 | |
|
633 | 0 | if (fr_rb_insert(state->tree, entry) != 0) { |
634 | 0 | fail_unlock: |
635 | 0 | PTHREAD_MUTEX_UNLOCK(&state->mutex); |
636 | 0 | RERROR("Failed inserting state entry - Insertion into state tree failed"); |
637 | 0 | fail: |
638 | 0 | fr_pair_delete_by_da(reply_list, state->da); |
639 | 0 | talloc_free(entry); |
640 | 0 | return NULL; |
641 | 0 | } |
642 | | |
643 | | /* |
644 | | * Ensure that we can de-duplicate things if the supplicant is misbehaving. |
645 | | */ |
646 | 0 | if (state->dedup_tree && !old) { |
647 | 0 | if (fr_rb_insert(state->dedup_tree, entry) != 0) { |
648 | 0 | (void) fr_rb_remove(NULL, state->tree, entry); |
649 | 0 | goto fail_unlock; |
650 | 0 | } |
651 | 0 | } |
652 | | |
653 | | /* |
654 | | * Link it to the end of the list, which is implicitly |
655 | | * ordered by cleanup time. |
656 | | */ |
657 | 0 | fr_dlist_insert_tail(&state->to_expire, entry); |
658 | |
|
659 | 0 | entry->thawed = NULL; |
660 | |
|
661 | 0 | fr_assert(entry->ctx == NULL); |
662 | 0 | fr_assert(request->session_state_ctx); |
663 | |
|
664 | 0 | entry->seq_start = request->seq_start; |
665 | 0 | entry->ctx = state_ctx; |
666 | 0 | fr_dlist_move(&entry->data, data); |
667 | |
|
668 | 0 | PTHREAD_MUTEX_UNLOCK(&state->mutex); |
669 | |
|
670 | 0 | return entry; |
671 | 0 | } |
672 | | |
673 | | /** Find the entry based on the State attribute and remove it from the state tree |
674 | | * |
675 | | */ |
676 | | static fr_state_entry_t *state_entry_find_and_unlink(fr_state_tree_t *state, fr_value_box_t const *vb) |
677 | 0 | { |
678 | 0 | fr_state_entry_t *entry, my_entry; |
679 | |
|
680 | 0 | state_entry_fill(&my_entry, vb); |
681 | | |
682 | | /* |
683 | | * Make it unique for different virtual servers handling the same request |
684 | | */ |
685 | 0 | my_entry.state_comp.context_id ^= state->config.context_id; |
686 | |
|
687 | 0 | fr_rb_remove((void **)&entry, state->tree, &my_entry); |
688 | 0 | if (entry) { |
689 | 0 | (void) talloc_get_type_abort(entry, fr_state_entry_t); |
690 | 0 | fr_dlist_remove(&state->to_expire, entry); |
691 | 0 | } |
692 | |
|
693 | 0 | return entry; |
694 | 0 | } |
695 | | |
696 | | |
697 | | /** Called when sending an Access-Accept/Access-Reject to discard state information |
698 | | * |
699 | | * @param[in] state tree to lookup state in. |
700 | | * @param[in] request to discard state for. |
701 | | */ |
702 | | void fr_state_discard(fr_state_tree_t *state, request_t *request) |
703 | 0 | { |
704 | 0 | fr_state_entry_t *entry; |
705 | | |
706 | | /* |
707 | | * The caller MUST have called fr_state_restore() before |
708 | | * calling this function. If so, there is request data |
709 | | * that points to the state entry. |
710 | | * |
711 | | * This function should only be called from the "outer" |
712 | | * request. Any child request should call |
713 | | * fr_state_discard_child() |
714 | | * |
715 | | * Relying on request data also means that the user can |
716 | | * nuke request.State, and the code will still work. |
717 | | * |
718 | | * Find a pointer to the entry, but leave the request |
719 | | * data associated with the entry. That way when the |
720 | | * request is freed, the entry will also be freed. |
721 | | */ |
722 | 0 | entry = request_data_reference(request, state, 0); |
723 | 0 | if (!entry) return; |
724 | | |
725 | | /* |
726 | | * Unlink the entry to shrink the state tree, and make |
727 | | * sure that the state is never re-used. |
728 | | * |
729 | | * However, we don't wipe the session-state list, as the |
730 | | * request can still be processed through a "finally" |
731 | | * section. And we want the session state data to be |
732 | | * usable from there. |
733 | | */ |
734 | 0 | PTHREAD_MUTEX_LOCK(&state->mutex); |
735 | 0 | state_entry_unlink(state, entry); |
736 | 0 | PTHREAD_MUTEX_UNLOCK(&state->mutex); |
737 | |
|
738 | 0 | return; |
739 | 0 | } |
740 | | |
741 | | /** Called to discard a state which is synthesized |
742 | | * |
743 | | * Some protocols synthesize a state, which means that there is the |
744 | | * possibility for conflict. i.e. an old state exists which needs to |
745 | | * be discarded. The current request can't restore the old state, it |
746 | | * instead needs to discard it. |
747 | | * |
748 | | * @param[in] state tree to lookup state in. |
749 | | * @param[in] request to discard state for. |
750 | | */ |
751 | | void fr_state_discard_by_state(fr_state_tree_t *state, request_t *request) |
752 | 0 | { |
753 | 0 | fr_pair_t *vp; |
754 | 0 | fr_state_entry_t *entry; |
755 | |
|
756 | 0 | vp = fr_pair_find_by_da(&request->request_pairs, NULL, state->da); |
757 | 0 | if (!vp) return; |
758 | | |
759 | 0 | PTHREAD_MUTEX_LOCK(&state->mutex); |
760 | 0 | entry = state_entry_find_and_unlink(state, &vp->data); |
761 | 0 | PTHREAD_MUTEX_UNLOCK(&state->mutex); |
762 | |
|
763 | 0 | if (entry) talloc_free(entry); |
764 | 0 | } |
765 | | |
766 | | /** Copy a pointer to the head of the list of state fr_pair_ts (and their ctx) into the request |
767 | | * |
768 | | * @note Does not copy the actual fr_pair_ts. The fr_pair_ts and their context |
769 | | * are transferred between state entries as the conversation progresses. |
770 | | * |
771 | | * @note Called with the mutex free. |
772 | | * |
773 | | * @param[in] state tree to lookup state in. |
774 | | * @param[in] request to restore state for. |
775 | | * @return |
776 | | * - 2 if the state attribute didn't match any known states. |
777 | | * - 1 if no state attribute existed. |
778 | | * - 0 on success (state restored) |
779 | | * - -1 if a state entry has already been thawed by a another request. |
780 | | */ |
781 | | int fr_state_restore(fr_state_tree_t *state, request_t *request) |
782 | 0 | { |
783 | 0 | fr_state_entry_t *entry; |
784 | 0 | fr_pair_t *vp; |
785 | | |
786 | | /* |
787 | | * No State, don't do anything. |
788 | | */ |
789 | 0 | vp = fr_pair_find_by_da(&request->request_pairs, NULL, state->da); |
790 | 0 | if (!vp) { |
791 | 0 | RDEBUG3("No request.%s attribute, can't restore session-state", state->da->name); |
792 | 0 | if (request->seq_start == 0) request->seq_start = request->number; /* Need check for fake requests */ |
793 | 0 | return 1; |
794 | 0 | } |
795 | | |
796 | 0 | PTHREAD_MUTEX_LOCK(&state->mutex); |
797 | 0 | entry = state_entry_find_and_unlink(state, &vp->data); |
798 | 0 | PTHREAD_MUTEX_UNLOCK(&state->mutex); |
799 | 0 | if (!entry) { |
800 | 0 | RDEBUG2("No state entry matching request.%pP found", vp); |
801 | 0 | return 2; |
802 | 0 | } |
803 | | |
804 | | /* Probably impossible in the current code */ |
805 | 0 | if (unlikely(entry->thawed && (entry->thawed != request))) { |
806 | 0 | RERROR("State entry has already been thawed by a request %"PRIu64, entry->thawed->number); |
807 | 0 | return -2; |
808 | 0 | } |
809 | | |
810 | | /* |
811 | | * Discard any existing session state, and replace it |
812 | | * with the cached one. |
813 | | */ |
814 | 0 | fr_assert(entry->ctx); |
815 | 0 | talloc_free(request_state_replace(request, entry->ctx)); |
816 | 0 | entry->ctx = NULL; |
817 | |
|
818 | 0 | request->seq_start = entry->seq_start; |
819 | | |
820 | | /* |
821 | | * Associate old state with the request |
822 | | * |
823 | | * If the request is freed, it's freed immediately. |
824 | | * |
825 | | * Otherwise, if there's another round, we reuse |
826 | | * the state entry and insert it back into the |
827 | | * tree. |
828 | | */ |
829 | 0 | request_data_add(request, state, 0, entry, true, true, false); |
830 | 0 | request_data_restore(request, &entry->data); |
831 | |
|
832 | 0 | entry->thawed = request; |
833 | |
|
834 | 0 | if (!fr_pair_list_empty(&request->session_state_pairs)) { |
835 | 0 | RDEBUG2("Restored session-state"); |
836 | 0 | log_request_pair_list(L_DBG_LVL_2, request, NULL, &request->session_state_pairs, "session-state."); |
837 | 0 | } |
838 | |
|
839 | 0 | RDEBUG3("%s - restored", state->da->name); |
840 | | |
841 | | /* |
842 | | * Set sequence so that we can prioritize ongoing multi-packet sessions. |
843 | | */ |
844 | 0 | request->sequence = entry->tries; |
845 | 0 | REQUEST_VERIFY(request); |
846 | 0 | return 0; |
847 | 0 | } |
848 | | |
849 | | |
850 | | /** Transfer ownership of the state fr_pair_ts and ctx, back to a state entry |
851 | | * |
852 | | * Put request->session_state_pairs into the State attribute. Put the State attribute |
853 | | * into the vps list. Delete the original entry, if it exists |
854 | | * |
855 | | * Also creates a new state entry. |
856 | | */ |
857 | | int fr_state_store(fr_state_tree_t *state, request_t *request) |
858 | 0 | { |
859 | 0 | fr_state_entry_t *entry, *old; |
860 | 0 | fr_dlist_head_t data; |
861 | 0 | fr_pair_t *state_ctx; |
862 | 0 | fr_value_box_t *dedup_key = NULL; |
863 | |
|
864 | 0 | old = request_data_get(request, state, 0); |
865 | 0 | request_data_list_init(&data); |
866 | 0 | request_data_by_persistance(&data, request, true); |
867 | |
|
868 | 0 | if (fr_pair_list_empty(&request->session_state_pairs) && fr_dlist_empty(&data)) return 0; |
869 | | |
870 | 0 | if (!fr_pair_list_empty(&request->session_state_pairs)) { |
871 | 0 | RDEBUG2("Saving session-state"); |
872 | 0 | log_request_pair_list(L_DBG_LVL_2, request, NULL, &request->session_state_pairs, "session-state."); |
873 | |
|
874 | 0 | #ifdef WITH_VERIFY_PTR |
875 | | /* |
876 | | * Double check all the session state pairs |
877 | | * are parented correctly, else we'll get |
878 | | * memory errors when we restore. |
879 | | */ |
880 | 0 | fr_pair_list_verify(__FILE__, __LINE__, request->session_state_ctx, &request->session_state_pairs, true); |
881 | 0 | #endif |
882 | 0 | } |
883 | | |
884 | | /* |
885 | | * If there's a dedup tree, then we need to expand the |
886 | | * key, but only if we don't already have a pre-existing state. |
887 | | */ |
888 | 0 | if (state->dedup_tree && !old) { |
889 | 0 | fr_value_box_list_t list; |
890 | |
|
891 | 0 | fr_value_box_list_init(&list); |
892 | |
|
893 | 0 | if (tmpl_eval(NULL, &list, request, state->config.dedup_key) < 0) { |
894 | 0 | REDEBUG("Failed expanding dedup_key - not doing dedup"); |
895 | 0 | } else { |
896 | 0 | dedup_key = fr_value_box_list_pop_head(&list); |
897 | 0 | if (!dedup_key) { |
898 | 0 | RDEBUG("Failed expanding dedup_key - not doing dedup due to empty output"); |
899 | 0 | } |
900 | 0 | fr_value_box_list_talloc_free_head(&list); |
901 | 0 | } |
902 | 0 | } |
903 | |
|
904 | 0 | MEM(state_ctx = request_state_replace(request, NULL)); |
905 | | |
906 | | /* |
907 | | * Reuses old if possible. On failure, data is still ours. |
908 | | */ |
909 | 0 | entry = state_entry_create(state, request, &request->reply_pairs, old, dedup_key, state_ctx, &data); |
910 | 0 | if (!entry) { |
911 | 0 | talloc_free(request_state_replace(request, state_ctx)); |
912 | 0 | request_data_restore(request, &data); /* Put it back again */ |
913 | 0 | return -1; |
914 | 0 | } |
915 | | |
916 | 0 | RDEBUG3("%s - saved", state->da->name); |
917 | 0 | REQUEST_VERIFY(request); |
918 | |
|
919 | 0 | return 0; |
920 | 0 | } |
921 | | |
922 | | /** Free any subrequest request data if the dlist head is freed |
923 | | * |
924 | | */ |
925 | | static int _free_child_data(state_child_entry_t *child_entry) |
926 | 0 | { |
927 | 0 | fr_dlist_talloc_free(&child_entry->data); |
928 | 0 | talloc_free(child_entry->ctx); /* Free the child's session_state_ctx if we own it */ |
929 | |
|
930 | 0 | return 0; |
931 | 0 | } |
932 | | |
933 | | /** Store subrequest's session-state list and persistable request data in its parent |
934 | | * |
935 | | * @param[in] child The child request to retrieve state from. |
936 | | * @param[in] unique_ptr A parent may have multiple subrequests spawned |
937 | | * by different modules. This identifies the module |
938 | | * or other facility that spawned the subrequest. |
939 | | * @param[in] unique_int Further identification. |
940 | | */ |
941 | | void fr_state_store_in_parent(request_t *child, void const *unique_ptr, int unique_int) |
942 | 0 | { |
943 | 0 | state_child_entry_t *child_entry; |
944 | 0 | request_t *request = child; /* Stupid logging */ |
945 | |
|
946 | 0 | if (!fr_cond_assert_msg(child->parent, |
947 | 0 | "Child request must have request->parent set when storing state")) return; |
948 | | |
949 | 0 | RDEBUG3("Storing subrequest state in request %s", child->parent->name); |
950 | |
|
951 | 0 | if ((request_data_by_persistance_count(request, true) > 0) || |
952 | 0 | !fr_pair_list_empty(&request->session_state_pairs)) { |
953 | 0 | MEM(child_entry = talloc_zero(request->parent->session_state_ctx, state_child_entry_t)); |
954 | 0 | request_data_list_init(&child_entry->data); |
955 | 0 | talloc_set_destructor(child_entry, _free_child_data); |
956 | |
|
957 | 0 | child_entry->ctx = request_state_replace(child, NULL); |
958 | | |
959 | | /* |
960 | | * Pull everything out of the child, |
961 | | * add it to our temporary list head... |
962 | | * |
963 | | * request_data_add allocs persistable |
964 | | * request dta in the session_state_ctx |
965 | | * which is why we don't need to copy or |
966 | | * reparent any of this. |
967 | | */ |
968 | 0 | request_data_by_persistance(&child_entry->data, request, true); |
969 | | |
970 | | /* |
971 | | * ...and add the request_data from |
972 | | * the child back into the parent. |
973 | | */ |
974 | 0 | request_data_talloc_add(request->parent, unique_ptr, unique_int, |
975 | 0 | state_child_entry_t, child_entry, true, false, true); |
976 | 0 | } |
977 | 0 | } |
978 | | |
979 | | /** Restore subrequest data from a parent request |
980 | | * |
981 | | * @param[in] child The child request to restore state to. |
982 | | * @param[in] unique_ptr A parent may have multiple subrequests spawned |
983 | | * by different modules. This identifies the module |
984 | | * or other facility that spawned the subrequest. |
985 | | * @param[in] unique_int Further identification. |
986 | | */ |
987 | | void fr_state_restore_from_parent(request_t *child, void const *unique_ptr, int unique_int) |
988 | 0 | { |
989 | 0 | state_child_entry_t *child_entry; |
990 | 0 | request_t *request = child; /* Stupid logging */ |
991 | |
|
992 | 0 | if (!fr_cond_assert_msg(child->parent, |
993 | 0 | "Child request must have request->parent set when restoring state")) return; |
994 | | |
995 | | |
996 | 0 | child_entry = request_data_get(child->parent, unique_ptr, unique_int); |
997 | 0 | if (!child_entry) { |
998 | 0 | RDEBUG3("No child state found in parent %s", child->parent->name); |
999 | 0 | return; |
1000 | 0 | } |
1001 | | |
1002 | | /* |
1003 | | * Shouldn't really be possible unless |
1004 | | * there's a logic bug in this API. |
1005 | | */ |
1006 | 0 | if (!fr_cond_assert_msg(!child_entry->thawed, |
1007 | 0 | "Child state entry already thawed by %s - %p", |
1008 | 0 | child_entry->thawed->name, child_entry->thawed)) return; |
1009 | | |
1010 | 0 | RDEBUG3("Restoring subrequest state from request %s", child->parent->name); |
1011 | | |
1012 | | /* |
1013 | | * If we can restore from the parent, do so |
1014 | | */ |
1015 | 0 | fr_assert_msg(child_entry->ctx, "session child entry missing ctx"); |
1016 | 0 | talloc_free(request_state_replace(child, child_entry->ctx)); |
1017 | 0 | child_entry->ctx = NULL; /* No longer owns the ctx */ |
1018 | 0 | child_entry->thawed = child; |
1019 | |
|
1020 | 0 | request_data_restore(child, &child_entry->data); /* Put all the request data back */ |
1021 | |
|
1022 | 0 | talloc_free(child_entry); |
1023 | 0 | } |
1024 | | |
1025 | | /** Remove state from a child |
1026 | | * |
1027 | | * This is useful for modules like EAP, where we keep a persistent eap_session |
1028 | | * but may call multiple EAP method modules during negotiation, and need to |
1029 | | * discard the state between each module call. |
1030 | | * |
1031 | | * @param[in] parent Holding the child's state. |
1032 | | * @param[in] unique_ptr A parent may have multiple subrequests spawned |
1033 | | * by different modules. This identifies the module |
1034 | | * or other facility that spawned the subrequest. |
1035 | | * @param[in] unique_int Further identification. |
1036 | | */ |
1037 | | void fr_state_discard_child(request_t *parent, void const *unique_ptr, int unique_int) |
1038 | 0 | { |
1039 | 0 | state_child_entry_t *child_entry; |
1040 | 0 | request_t *request = parent; /* Stupid logging */ |
1041 | |
|
1042 | 0 | child_entry = request_data_get(parent, unique_ptr, unique_int); |
1043 | 0 | if (!child_entry) { |
1044 | 0 | RDEBUG3("No child state found in parent %s", parent->name); |
1045 | 0 | return; |
1046 | 0 | } |
1047 | | |
1048 | 0 | talloc_free(child_entry); |
1049 | 0 | } |