Moved generic RADIUS protocol support to a dedicated libradius
[strongswan.git] / src / libcharon / plugins / eap_radius / eap_radius_dae.c
1 /*
2 * Copyright (C) 2012 Martin Willi
3 * Copyright (C) 2012 revosec AG
4 *
5 * This program is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the
7 * Free Software Foundation; either version 2 of the License, or (at your
8 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 * for more details.
14 */
15
16 #include "eap_radius_dae.h"
17
18 #include <radius_message.h>
19
20 #include <sys/types.h>
21 #include <sys/stat.h>
22 #include <sys/socket.h>
23 #include <unistd.h>
24 #include <errno.h>
25
26 #include <daemon.h>
27 #include <threading/thread.h>
28 #include <processing/jobs/callback_job.h>
29 #include <processing/jobs/delete_ike_sa_job.h>
30
31 #define RADIUS_DAE_PORT 3799
32
33 typedef struct private_eap_radius_dae_t private_eap_radius_dae_t;
34
35 /**
36 * Private data of an eap_radius_dae_t object.
37 */
38 struct private_eap_radius_dae_t {
39
40 /**
41 * Public eap_radius_dae_t interface.
42 */
43 eap_radius_dae_t public;
44
45 /**
46 * RADIUS session state
47 */
48 eap_radius_accounting_t *accounting;
49
50 /**
51 * Socket to listen on authorization extension port
52 */
53 int fd;
54
55 /**
56 * Listen job
57 */
58 callback_job_t *job;
59
60 /**
61 * RADIUS shared secret for DAE exchanges
62 */
63 chunk_t secret;
64
65 /**
66 * MD5 hasher
67 */
68 hasher_t *hasher;
69
70 /**
71 * HMAC MD5 signer, with secret set
72 */
73 signer_t *signer;
74
75 /**
76 * List of responses for retransmission, as entry_t
77 */
78 linked_list_t *responses;
79 };
80
81 /**
82 * Entry to store responses for retransmit
83 */
84 typedef struct {
85 /** stored response */
86 radius_message_t *response;
87 /** client that sent the request */
88 host_t *client;
89 } entry_t;
90
91 /**
92 * Clean up an entry
93 */
94 static void entry_destroy(entry_t *entry)
95 {
96 entry->response->destroy(entry->response);
97 entry->client->destroy(entry->client);
98 free(entry);
99 }
100
101 /**
102 * Save/Replace response for retransmission
103 */
104 static void save_retransmit(private_eap_radius_dae_t *this,
105 radius_message_t *response, host_t *client)
106 {
107 enumerator_t *enumerator;
108 entry_t *entry;
109 bool found = FALSE;
110
111 enumerator = this->responses->create_enumerator(this->responses);
112 while (enumerator->enumerate(enumerator, &entry))
113 {
114 if (client->equals(client, entry->client))
115 {
116 entry->response->destroy(entry->response);
117 entry->response = response;
118 found = TRUE;
119 break;
120 }
121 }
122 enumerator->destroy(enumerator);
123
124 if (!found)
125 {
126 INIT(entry,
127 .response = response,
128 .client = client->clone(client),
129 );
130 this->responses->insert_first(this->responses, entry);
131 }
132 }
133
134 /**
135 * Send a RADIUS message to client
136 */
137 static void send_message(private_eap_radius_dae_t *this,
138 radius_message_t *message, host_t *client)
139 {
140 chunk_t data;
141
142 data = message->get_encoding(message);
143 if (sendto(this->fd, data.ptr, data.len, 0, client->get_sockaddr(client),
144 *client->get_sockaddr_len(client)) != data.len)
145 {
146 DBG1(DBG_CFG, "sending RADIUS DAE response failed: %s", strerror(errno));
147 }
148 }
149
150 /**
151 * Check if we request is a retransmit, retransmit stored response
152 */
153 static bool send_retransmit(private_eap_radius_dae_t *this,
154 radius_message_t *request, host_t *client)
155 {
156 enumerator_t *enumerator;
157 entry_t *entry;
158 bool found = FALSE;
159
160 enumerator = this->responses->create_enumerator(this->responses);
161 while (enumerator->enumerate(enumerator, &entry))
162 {
163 if (client->equals(client, entry->client) &&
164 request->get_identifier(request) ==
165 entry->response->get_identifier(entry->response))
166 {
167 DBG1(DBG_CFG, "received retransmit of RADIUS %N, retransmitting %N "
168 "to %H", radius_message_code_names, request->get_code(request),
169 radius_message_code_names,
170 entry->response->get_code(entry->response), client);
171 send_message(this, entry->response, client);
172 found = TRUE;
173 break;
174 }
175 }
176 enumerator->destroy(enumerator);
177
178 return found;
179 }
180
181 /**
182 * Send an ACK/NAK response for a request
183 */
184 static void send_response(private_eap_radius_dae_t *this,
185 radius_message_t *request, radius_message_code_t code,
186 host_t *client)
187 {
188 radius_message_t *response;
189
190 response = radius_message_create(code);
191 response->set_identifier(response, request->get_identifier(request));
192 response->sign(response, request->get_authenticator(request),
193 this->secret, this->hasher, this->signer, NULL);
194
195 send_message(this, response, client);
196 save_retransmit(this, response, client);
197 }
198
199 /**
200 * Add all IKE_SAs matching to user to a list
201 */
202 static void add_matching_ike_sas(linked_list_t *list, identification_t *user)
203 {
204 enumerator_t *enumerator;
205 ike_sa_t *ike_sa;
206 ike_sa_id_t *id;
207
208 enumerator = charon->ike_sa_manager->create_enumerator(
209 charon->ike_sa_manager, FALSE);
210 while (enumerator->enumerate(enumerator, &ike_sa))
211 {
212 if (user->matches(user, ike_sa->get_other_eap_id(ike_sa)))
213 {
214 id = ike_sa->get_id(ike_sa);
215 list->insert_last(list, id->clone(id));
216 }
217 }
218 enumerator->destroy(enumerator);
219 }
220
221 /**
222 * Get list of IKE_SAs matching a Disconnect/CoA request
223 */
224 static linked_list_t *get_matching_ike_sas(private_eap_radius_dae_t *this,
225 radius_message_t *request, host_t *client)
226 {
227 enumerator_t *enumerator;
228 identification_t *user;
229 linked_list_t *ids;
230 chunk_t data;
231 int type;
232
233 ids = linked_list_create();
234
235 enumerator = request->create_enumerator(request);
236 while (enumerator->enumerate(enumerator, &type, &data))
237 {
238 if (type == RAT_USER_NAME && data.len)
239 {
240 user = identification_create_from_data(data);
241 DBG1(DBG_CFG, "received RADIUS DAE %N for %Y from %H",
242 radius_message_code_names, request->get_code(request),
243 user, client);
244 add_matching_ike_sas(ids, user);
245 user->destroy(user);
246 }
247 }
248 enumerator->destroy(enumerator);
249
250 return ids;
251 }
252
253 /**
254 * Process a DAE disconnect request, send response
255 */
256 static void process_disconnect(private_eap_radius_dae_t *this,
257 radius_message_t *request, host_t *client)
258 {
259 enumerator_t *enumerator;
260 linked_list_t *ids;
261 ike_sa_id_t *id;
262
263 ids = get_matching_ike_sas(this, request, client);
264
265 if (ids->get_count(ids))
266 {
267 DBG1(DBG_CFG, "closing %d IKE_SA%s matching %N, sending %N",
268 ids->get_count(ids), ids->get_count(ids) > 1 ? "s" : "",
269 radius_message_code_names, RMC_DISCONNECT_REQUEST,
270 radius_message_code_names, RMC_DISCONNECT_ACK);
271
272 enumerator = ids->create_enumerator(ids);
273 while (enumerator->enumerate(enumerator, &id))
274 {
275 lib->processor->queue_job(lib->processor, (job_t*)
276 delete_ike_sa_job_create(id, TRUE));
277 }
278 enumerator->destroy(enumerator);
279
280 send_response(this, request, RMC_DISCONNECT_ACK, client);
281 }
282 else
283 {
284 DBG1(DBG_CFG, "no IKE_SA matches %N, sending %N",
285 radius_message_code_names, RMC_DISCONNECT_REQUEST,
286 radius_message_code_names, RMC_DISCONNECT_NAK);
287 send_response(this, request, RMC_DISCONNECT_NAK, client);
288 }
289 ids->destroy_offset(ids, offsetof(ike_sa_id_t, destroy));
290 }
291
292 /**
293 * Apply a new lifetime to an IKE_SA
294 */
295 static void apply_lifetime(private_eap_radius_dae_t *this, ike_sa_id_t *id,
296 u_int32_t lifetime)
297 {
298 ike_sa_t *ike_sa;
299
300 ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager, id);
301 if (ike_sa)
302 {
303 if (ike_sa->set_auth_lifetime(ike_sa, lifetime) == DESTROY_ME)
304 {
305 charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager,
306 ike_sa);
307 }
308 else
309 {
310 charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
311 }
312 }
313 }
314
315 /**
316 * Process a DAE CoA request, send response
317 */
318 static void process_coa(private_eap_radius_dae_t *this,
319 radius_message_t *request, host_t *client)
320 {
321 enumerator_t *enumerator;
322 linked_list_t *ids;
323 ike_sa_id_t *id;
324 chunk_t data;
325 int type;
326 u_int32_t lifetime = 0;
327 bool lifetime_seen = FALSE;
328
329 ids = get_matching_ike_sas(this, request, client);
330
331 if (ids->get_count(ids))
332 {
333 enumerator = request->create_enumerator(request);
334 while (enumerator->enumerate(enumerator, &type, &data))
335 {
336 if (type == RAT_SESSION_TIMEOUT && data.len == 4)
337 {
338 lifetime = untoh32(data.ptr);
339 lifetime_seen = TRUE;
340 break;
341 }
342 }
343 enumerator->destroy(enumerator);
344
345 if (lifetime_seen)
346 {
347 DBG1(DBG_CFG, "applying %us lifetime to %d IKE_SA%s matching %N, "
348 "sending %N", lifetime, ids->get_count(ids),
349 ids->get_count(ids) > 1 ? "s" : "",
350 radius_message_code_names, RMC_COA_REQUEST,
351 radius_message_code_names, RMC_COA_ACK);
352
353 enumerator = ids->create_enumerator(ids);
354 while (enumerator->enumerate(enumerator, &id))
355 {
356 apply_lifetime(this, id, lifetime);
357 }
358 enumerator->destroy(enumerator);
359 send_response(this, request, RMC_COA_ACK, client);
360 }
361 else
362 {
363 DBG1(DBG_CFG, "no Session-Timeout attribute found in %N, sending %N",
364 radius_message_code_names, RMC_COA_REQUEST,
365 radius_message_code_names, RMC_COA_NAK);
366 send_response(this, request, RMC_COA_NAK, client);
367 }
368 }
369 else
370 {
371 DBG1(DBG_CFG, "no IKE_SA matches %N, sending %N",
372 radius_message_code_names, RMC_COA_REQUEST,
373 radius_message_code_names, RMC_COA_NAK);
374 send_response(this, request, RMC_COA_NAK, client);
375 }
376 ids->destroy_offset(ids, offsetof(ike_sa_id_t, destroy));
377 }
378
379 /**
380 * Receive RADIUS DAE requests
381 */
382 static job_requeue_t receive(private_eap_radius_dae_t *this)
383 {
384 struct sockaddr_storage addr;
385 socklen_t addr_len = sizeof(addr);
386 radius_message_t *request;
387 char buf[2048];
388 ssize_t len;
389 bool oldstate;
390 host_t *client;
391
392 oldstate = thread_cancelability(TRUE);
393 len = recvfrom(this->fd, buf, sizeof(buf), 0,
394 (struct sockaddr*)&addr, &addr_len);
395 thread_cancelability(oldstate);
396
397 if (len > 0)
398 {
399 request = radius_message_parse(chunk_create(buf, len));
400 if (request)
401 {
402 client = host_create_from_sockaddr((struct sockaddr*)&addr);
403 if (client)
404 {
405 if (!send_retransmit(this, request, client))
406 {
407 if (request->verify(request, NULL, this->secret,
408 this->hasher, this->signer))
409 {
410 switch (request->get_code(request))
411 {
412 case RMC_DISCONNECT_REQUEST:
413 process_disconnect(this, request, client);
414 break;
415 case RMC_COA_REQUEST:
416 process_coa(this, request, client);
417 break;
418 default:
419 DBG1(DBG_CFG, "ignoring unsupported RADIUS DAE "
420 "%N message from %H",
421 radius_message_code_names,
422 request->get_code(request), client);
423 break;
424 }
425 }
426 }
427 client->destroy(client);
428 }
429 request->destroy(request);
430 }
431 else
432 {
433 DBG1(DBG_NET, "ignoring invalid RADIUS DAE request");
434 }
435 }
436 else
437 {
438 DBG1(DBG_NET, "receving RADIUS DAE request failed: %s", strerror(errno));
439 }
440 return JOB_REQUEUE_DIRECT;
441 }
442
443 /**
444 * Open DAE socket
445 */
446 static bool open_socket(private_eap_radius_dae_t *this)
447 {
448 host_t *host;
449
450 this->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
451 if (this->fd == -1)
452 {
453 DBG1(DBG_CFG, "unable to open RADIUS DAE socket: %s", strerror(errno));
454 return FALSE;
455 }
456
457 host = host_create_from_string(
458 lib->settings->get_str(lib->settings,
459 "charon.plugins.eap-radius.dae.listen", "0.0.0.0"),
460 lib->settings->get_int(lib->settings,
461 "charon.plugins.eap-radius.dae.port", RADIUS_DAE_PORT));
462 if (!host)
463 {
464 DBG1(DBG_CFG, "invalid RADIUS DAE listen address");
465 return FALSE;
466 }
467
468 if (bind(this->fd, host->get_sockaddr(host),
469 *host->get_sockaddr_len(host)) == -1)
470 {
471 DBG1(DBG_CFG, "unable to bind RADIUS DAE socket: %s", strerror(errno));
472 host->destroy(host);
473 return FALSE;
474 }
475 host->destroy(host);
476 return TRUE;
477 }
478
479 METHOD(eap_radius_dae_t, destroy, void,
480 private_eap_radius_dae_t *this)
481 {
482 if (this->job)
483 {
484 this->job->cancel(this->job);
485 }
486 if (this->fd != -1)
487 {
488 close(this->fd);
489 }
490 DESTROY_IF(this->signer);
491 DESTROY_IF(this->hasher);
492 this->responses->destroy_function(this->responses, (void*)entry_destroy);
493 free(this);
494 }
495
496 /**
497 * See header
498 */
499 eap_radius_dae_t *eap_radius_dae_create(eap_radius_accounting_t *accounting)
500 {
501 private_eap_radius_dae_t *this;
502
503 INIT(this,
504 .public = {
505 .destroy = _destroy,
506 },
507 .accounting = accounting,
508 .fd = -1,
509 .secret = {
510 .ptr = lib->settings->get_str(lib->settings,
511 "charon.plugins.eap-radius.dae.secret", NULL),
512 },
513 .hasher = lib->crypto->create_hasher(lib->crypto, HASH_MD5),
514 .signer = lib->crypto->create_signer(lib->crypto, AUTH_HMAC_MD5_128),
515 .responses = linked_list_create(),
516 );
517
518 if (!this->hasher || !this->signer)
519 {
520 destroy(this);
521 return NULL;
522 }
523 if (!this->secret.ptr)
524 {
525 DBG1(DBG_CFG, "missing RADIUS DAE secret, disabled");
526 destroy(this);
527 return NULL;
528 }
529 this->secret.len = strlen(this->secret.ptr);
530 this->signer->set_key(this->signer, this->secret);
531
532 if (!open_socket(this))
533 {
534 destroy(this);
535 return NULL;
536 }
537
538 this->job = callback_job_create_with_prio((callback_job_cb_t)receive,
539 this, NULL, NULL, JOB_PRIO_CRITICAL);
540 lib->processor->queue_job(lib->processor, (job_t*)this->job);
541
542 return &this->public;
543 }