538 lines
13 KiB
C
538 lines
13 KiB
C
/*
|
|
* Copyright (C) 2012 Martin Willi
|
|
* Copyright (C) 2012 revosec AG
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* for more details.
|
|
*/
|
|
|
|
#include "eap_radius_dae.h"
|
|
|
|
#include <radius_message.h>
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
|
|
#include <daemon.h>
|
|
#include <threading/thread.h>
|
|
#include <processing/jobs/callback_job.h>
|
|
#include <processing/jobs/delete_ike_sa_job.h>
|
|
|
|
#define RADIUS_DAE_PORT 3799
|
|
|
|
typedef struct private_eap_radius_dae_t private_eap_radius_dae_t;
|
|
|
|
/**
|
|
* Private data of an eap_radius_dae_t object.
|
|
*/
|
|
struct private_eap_radius_dae_t {
|
|
|
|
/**
|
|
* Public eap_radius_dae_t interface.
|
|
*/
|
|
eap_radius_dae_t public;
|
|
|
|
/**
|
|
* RADIUS session state
|
|
*/
|
|
eap_radius_accounting_t *accounting;
|
|
|
|
/**
|
|
* Socket to listen on authorization extension port
|
|
*/
|
|
int fd;
|
|
|
|
/**
|
|
* RADIUS shared secret for DAE exchanges
|
|
*/
|
|
chunk_t secret;
|
|
|
|
/**
|
|
* MD5 hasher
|
|
*/
|
|
hasher_t *hasher;
|
|
|
|
/**
|
|
* HMAC MD5 signer, with secret set
|
|
*/
|
|
signer_t *signer;
|
|
|
|
/**
|
|
* List of responses for retransmission, as entry_t
|
|
*/
|
|
linked_list_t *responses;
|
|
};
|
|
|
|
/**
|
|
* Entry to store responses for retransmit
|
|
*/
|
|
typedef struct {
|
|
/** stored response */
|
|
radius_message_t *response;
|
|
/** client that sent the request */
|
|
host_t *client;
|
|
} entry_t;
|
|
|
|
/**
|
|
* Clean up an entry
|
|
*/
|
|
static void entry_destroy(entry_t *entry)
|
|
{
|
|
entry->response->destroy(entry->response);
|
|
entry->client->destroy(entry->client);
|
|
free(entry);
|
|
}
|
|
|
|
/**
|
|
* Save/Replace response for retransmission
|
|
*/
|
|
static void save_retransmit(private_eap_radius_dae_t *this,
|
|
radius_message_t *response, host_t *client)
|
|
{
|
|
enumerator_t *enumerator;
|
|
entry_t *entry;
|
|
bool found = FALSE;
|
|
|
|
enumerator = this->responses->create_enumerator(this->responses);
|
|
while (enumerator->enumerate(enumerator, &entry))
|
|
{
|
|
if (client->equals(client, entry->client))
|
|
{
|
|
entry->response->destroy(entry->response);
|
|
entry->response = response;
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
if (!found)
|
|
{
|
|
INIT(entry,
|
|
.response = response,
|
|
.client = client->clone(client),
|
|
);
|
|
this->responses->insert_first(this->responses, entry);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a RADIUS message to client
|
|
*/
|
|
static void send_message(private_eap_radius_dae_t *this,
|
|
radius_message_t *message, host_t *client)
|
|
{
|
|
chunk_t data;
|
|
|
|
data = message->get_encoding(message);
|
|
if (sendto(this->fd, data.ptr, data.len, 0, client->get_sockaddr(client),
|
|
*client->get_sockaddr_len(client)) != data.len)
|
|
{
|
|
DBG1(DBG_CFG, "sending RADIUS DAE response failed: %s", strerror(errno));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if we request is a retransmit, retransmit stored response
|
|
*/
|
|
static bool send_retransmit(private_eap_radius_dae_t *this,
|
|
radius_message_t *request, host_t *client)
|
|
{
|
|
enumerator_t *enumerator;
|
|
entry_t *entry;
|
|
bool found = FALSE;
|
|
|
|
enumerator = this->responses->create_enumerator(this->responses);
|
|
while (enumerator->enumerate(enumerator, &entry))
|
|
{
|
|
if (client->equals(client, entry->client) &&
|
|
request->get_identifier(request) ==
|
|
entry->response->get_identifier(entry->response))
|
|
{
|
|
DBG1(DBG_CFG, "received retransmit of RADIUS %N, retransmitting %N "
|
|
"to %H", radius_message_code_names, request->get_code(request),
|
|
radius_message_code_names,
|
|
entry->response->get_code(entry->response), client);
|
|
send_message(this, entry->response, client);
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* Send an ACK/NAK response for a request
|
|
*/
|
|
static void send_response(private_eap_radius_dae_t *this,
|
|
radius_message_t *request, radius_message_code_t code,
|
|
host_t *client)
|
|
{
|
|
radius_message_t *response;
|
|
|
|
response = radius_message_create(code);
|
|
response->set_identifier(response, request->get_identifier(request));
|
|
if (response->sign(response, request->get_authenticator(request),
|
|
this->secret, this->hasher, this->signer, NULL, FALSE))
|
|
{
|
|
send_message(this, response, client);
|
|
save_retransmit(this, response, client);
|
|
}
|
|
else
|
|
{
|
|
response->destroy(response);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add all IKE_SAs matching to user to a list
|
|
*/
|
|
static void add_matching_ike_sas(linked_list_t *list, identification_t *user)
|
|
{
|
|
enumerator_t *enumerator;
|
|
ike_sa_t *ike_sa;
|
|
ike_sa_id_t *id;
|
|
|
|
enumerator = charon->ike_sa_manager->create_enumerator(
|
|
charon->ike_sa_manager, FALSE);
|
|
while (enumerator->enumerate(enumerator, &ike_sa))
|
|
{
|
|
if (user->matches(user, ike_sa->get_other_eap_id(ike_sa)))
|
|
{
|
|
id = ike_sa->get_id(ike_sa);
|
|
list->insert_last(list, id->clone(id));
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
}
|
|
|
|
/**
|
|
* Get list of IKE_SAs matching a Disconnect/CoA request
|
|
*/
|
|
static linked_list_t *get_matching_ike_sas(private_eap_radius_dae_t *this,
|
|
radius_message_t *request, host_t *client)
|
|
{
|
|
enumerator_t *enumerator;
|
|
identification_t *user;
|
|
linked_list_t *ids;
|
|
chunk_t data;
|
|
int type;
|
|
|
|
ids = linked_list_create();
|
|
|
|
enumerator = request->create_enumerator(request);
|
|
while (enumerator->enumerate(enumerator, &type, &data))
|
|
{
|
|
if (type == RAT_USER_NAME && data.len)
|
|
{
|
|
user = identification_create_from_data(data);
|
|
DBG1(DBG_CFG, "received RADIUS DAE %N for %Y from %H",
|
|
radius_message_code_names, request->get_code(request),
|
|
user, client);
|
|
add_matching_ike_sas(ids, user);
|
|
user->destroy(user);
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
return ids;
|
|
}
|
|
|
|
/**
|
|
* Process a DAE disconnect request, send response
|
|
*/
|
|
static void process_disconnect(private_eap_radius_dae_t *this,
|
|
radius_message_t *request, host_t *client)
|
|
{
|
|
enumerator_t *enumerator;
|
|
linked_list_t *ids;
|
|
ike_sa_id_t *id;
|
|
|
|
ids = get_matching_ike_sas(this, request, client);
|
|
|
|
if (ids->get_count(ids))
|
|
{
|
|
DBG1(DBG_CFG, "closing %d IKE_SA%s matching %N, sending %N",
|
|
ids->get_count(ids), ids->get_count(ids) > 1 ? "s" : "",
|
|
radius_message_code_names, RMC_DISCONNECT_REQUEST,
|
|
radius_message_code_names, RMC_DISCONNECT_ACK);
|
|
|
|
enumerator = ids->create_enumerator(ids);
|
|
while (enumerator->enumerate(enumerator, &id))
|
|
{
|
|
lib->processor->queue_job(lib->processor, (job_t*)
|
|
delete_ike_sa_job_create(id, TRUE));
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
send_response(this, request, RMC_DISCONNECT_ACK, client);
|
|
}
|
|
else
|
|
{
|
|
DBG1(DBG_CFG, "no IKE_SA matches %N, sending %N",
|
|
radius_message_code_names, RMC_DISCONNECT_REQUEST,
|
|
radius_message_code_names, RMC_DISCONNECT_NAK);
|
|
send_response(this, request, RMC_DISCONNECT_NAK, client);
|
|
}
|
|
ids->destroy_offset(ids, offsetof(ike_sa_id_t, destroy));
|
|
}
|
|
|
|
/**
|
|
* Apply a new lifetime to an IKE_SA
|
|
*/
|
|
static void apply_lifetime(private_eap_radius_dae_t *this, ike_sa_id_t *id,
|
|
u_int32_t lifetime)
|
|
{
|
|
ike_sa_t *ike_sa;
|
|
|
|
ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager, id);
|
|
if (ike_sa)
|
|
{
|
|
if (ike_sa->set_auth_lifetime(ike_sa, lifetime) == DESTROY_ME)
|
|
{
|
|
charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager,
|
|
ike_sa);
|
|
}
|
|
else
|
|
{
|
|
charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process a DAE CoA request, send response
|
|
*/
|
|
static void process_coa(private_eap_radius_dae_t *this,
|
|
radius_message_t *request, host_t *client)
|
|
{
|
|
enumerator_t *enumerator;
|
|
linked_list_t *ids;
|
|
ike_sa_id_t *id;
|
|
chunk_t data;
|
|
int type;
|
|
u_int32_t lifetime = 0;
|
|
bool lifetime_seen = FALSE;
|
|
|
|
ids = get_matching_ike_sas(this, request, client);
|
|
|
|
if (ids->get_count(ids))
|
|
{
|
|
enumerator = request->create_enumerator(request);
|
|
while (enumerator->enumerate(enumerator, &type, &data))
|
|
{
|
|
if (type == RAT_SESSION_TIMEOUT && data.len == 4)
|
|
{
|
|
lifetime = untoh32(data.ptr);
|
|
lifetime_seen = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
if (lifetime_seen)
|
|
{
|
|
DBG1(DBG_CFG, "applying %us lifetime to %d IKE_SA%s matching %N, "
|
|
"sending %N", lifetime, ids->get_count(ids),
|
|
ids->get_count(ids) > 1 ? "s" : "",
|
|
radius_message_code_names, RMC_COA_REQUEST,
|
|
radius_message_code_names, RMC_COA_ACK);
|
|
|
|
enumerator = ids->create_enumerator(ids);
|
|
while (enumerator->enumerate(enumerator, &id))
|
|
{
|
|
apply_lifetime(this, id, lifetime);
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
send_response(this, request, RMC_COA_ACK, client);
|
|
}
|
|
else
|
|
{
|
|
DBG1(DBG_CFG, "no Session-Timeout attribute found in %N, sending %N",
|
|
radius_message_code_names, RMC_COA_REQUEST,
|
|
radius_message_code_names, RMC_COA_NAK);
|
|
send_response(this, request, RMC_COA_NAK, client);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DBG1(DBG_CFG, "no IKE_SA matches %N, sending %N",
|
|
radius_message_code_names, RMC_COA_REQUEST,
|
|
radius_message_code_names, RMC_COA_NAK);
|
|
send_response(this, request, RMC_COA_NAK, client);
|
|
}
|
|
ids->destroy_offset(ids, offsetof(ike_sa_id_t, destroy));
|
|
}
|
|
|
|
/**
|
|
* Receive RADIUS DAE requests
|
|
*/
|
|
static bool receive(private_eap_radius_dae_t *this)
|
|
{
|
|
struct sockaddr_storage addr;
|
|
socklen_t addr_len = sizeof(addr);
|
|
radius_message_t *request;
|
|
char buf[2048];
|
|
ssize_t len;
|
|
host_t *client;
|
|
|
|
len = recvfrom(this->fd, buf, sizeof(buf), MSG_DONTWAIT,
|
|
(struct sockaddr*)&addr, &addr_len);
|
|
if (len > 0)
|
|
{
|
|
request = radius_message_parse(chunk_create(buf, len));
|
|
if (request)
|
|
{
|
|
client = host_create_from_sockaddr((struct sockaddr*)&addr);
|
|
if (client)
|
|
{
|
|
if (!send_retransmit(this, request, client))
|
|
{
|
|
if (request->verify(request, NULL, this->secret,
|
|
this->hasher, this->signer))
|
|
{
|
|
switch (request->get_code(request))
|
|
{
|
|
case RMC_DISCONNECT_REQUEST:
|
|
process_disconnect(this, request, client);
|
|
break;
|
|
case RMC_COA_REQUEST:
|
|
process_coa(this, request, client);
|
|
break;
|
|
default:
|
|
DBG1(DBG_CFG, "ignoring unsupported RADIUS DAE "
|
|
"%N message from %H",
|
|
radius_message_code_names,
|
|
request->get_code(request), client);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
client->destroy(client);
|
|
}
|
|
request->destroy(request);
|
|
}
|
|
else
|
|
{
|
|
DBG1(DBG_NET, "ignoring invalid RADIUS DAE request");
|
|
}
|
|
}
|
|
else if (errno != EWOULDBLOCK)
|
|
{
|
|
DBG1(DBG_NET, "receiving RADIUS DAE request failed: %s", strerror(errno));
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Open DAE socket
|
|
*/
|
|
static bool open_socket(private_eap_radius_dae_t *this)
|
|
{
|
|
host_t *host;
|
|
|
|
this->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
|
if (this->fd == -1)
|
|
{
|
|
DBG1(DBG_CFG, "unable to open RADIUS DAE socket: %s", strerror(errno));
|
|
return FALSE;
|
|
}
|
|
|
|
host = host_create_from_string(
|
|
lib->settings->get_str(lib->settings,
|
|
"%s.plugins.eap-radius.dae.listen", "0.0.0.0",
|
|
lib->ns),
|
|
lib->settings->get_int(lib->settings,
|
|
"%s.plugins.eap-radius.dae.port", RADIUS_DAE_PORT,
|
|
lib->ns));
|
|
if (!host)
|
|
{
|
|
DBG1(DBG_CFG, "invalid RADIUS DAE listen address");
|
|
return FALSE;
|
|
}
|
|
|
|
if (bind(this->fd, host->get_sockaddr(host),
|
|
*host->get_sockaddr_len(host)) == -1)
|
|
{
|
|
DBG1(DBG_CFG, "unable to bind RADIUS DAE socket: %s", strerror(errno));
|
|
host->destroy(host);
|
|
return FALSE;
|
|
}
|
|
host->destroy(host);
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(eap_radius_dae_t, destroy, void,
|
|
private_eap_radius_dae_t *this)
|
|
{
|
|
if (this->fd != -1)
|
|
{
|
|
lib->watcher->remove(lib->watcher, this->fd);
|
|
close(this->fd);
|
|
}
|
|
DESTROY_IF(this->signer);
|
|
DESTROY_IF(this->hasher);
|
|
this->responses->destroy_function(this->responses, (void*)entry_destroy);
|
|
free(this);
|
|
}
|
|
|
|
/**
|
|
* See header
|
|
*/
|
|
eap_radius_dae_t *eap_radius_dae_create(eap_radius_accounting_t *accounting)
|
|
{
|
|
private_eap_radius_dae_t *this;
|
|
|
|
INIT(this,
|
|
.public = {
|
|
.destroy = _destroy,
|
|
},
|
|
.accounting = accounting,
|
|
.fd = -1,
|
|
.secret = {
|
|
.ptr = lib->settings->get_str(lib->settings,
|
|
"%s.plugins.eap-radius.dae.secret", NULL,
|
|
lib->ns),
|
|
},
|
|
.hasher = lib->crypto->create_hasher(lib->crypto, HASH_MD5),
|
|
.signer = lib->crypto->create_signer(lib->crypto, AUTH_HMAC_MD5_128),
|
|
.responses = linked_list_create(),
|
|
);
|
|
|
|
if (!this->hasher || !this->signer)
|
|
{
|
|
destroy(this);
|
|
return NULL;
|
|
}
|
|
if (!this->secret.ptr)
|
|
{
|
|
DBG1(DBG_CFG, "missing RADIUS DAE secret, disabled");
|
|
destroy(this);
|
|
return NULL;
|
|
}
|
|
this->secret.len = strlen(this->secret.ptr);
|
|
if (!this->signer->set_key(this->signer, this->secret) ||
|
|
!open_socket(this))
|
|
{
|
|
destroy(this);
|
|
return NULL;
|
|
}
|
|
|
|
lib->watcher->add(lib->watcher, this->fd, WATCHER_READ,
|
|
(watcher_cb_t)receive, this);
|
|
|
|
return &this->public;
|
|
}
|