1067 lines
26 KiB
C
1067 lines
26 KiB
C
/*
|
|
* Copyright (C) 2015 Tobias Brunner
|
|
* Hochschule fuer Technik Rapperswil
|
|
*
|
|
* 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_accounting.h"
|
|
#include "eap_radius_plugin.h"
|
|
|
|
#include <time.h>
|
|
|
|
#include <radius_message.h>
|
|
#include <radius_client.h>
|
|
#include <daemon.h>
|
|
#include <collections/array.h>
|
|
#include <collections/hashtable.h>
|
|
#include <threading/mutex.h>
|
|
#include <processing/jobs/callback_job.h>
|
|
|
|
typedef struct private_eap_radius_accounting_t private_eap_radius_accounting_t;
|
|
|
|
/**
|
|
* Private data of an eap_radius_accounting_t object.
|
|
*/
|
|
struct private_eap_radius_accounting_t {
|
|
|
|
/**
|
|
* Public eap_radius_accounting_t interface.
|
|
*/
|
|
eap_radius_accounting_t public;
|
|
|
|
/**
|
|
* Hashtable with sessions, ike_sa_id_t => entry_t
|
|
*/
|
|
hashtable_t *sessions;
|
|
|
|
/**
|
|
* Mutex to lock sessions
|
|
*/
|
|
mutex_t *mutex;
|
|
|
|
/**
|
|
* Session ID prefix
|
|
*/
|
|
uint32_t prefix;
|
|
|
|
/**
|
|
* Format string we use for Called/Calling-Station-Id for a host
|
|
*/
|
|
char *station_id_fmt;
|
|
|
|
/**
|
|
* Disable accounting unless IKE_SA has at least one virtual IP
|
|
*/
|
|
bool acct_req_vip;
|
|
};
|
|
|
|
/**
|
|
* Singleton instance of accounting
|
|
*/
|
|
static private_eap_radius_accounting_t *singleton = NULL;
|
|
|
|
/**
|
|
* Acct-Terminate-Cause
|
|
*/
|
|
typedef enum {
|
|
ACCT_CAUSE_USER_REQUEST = 1,
|
|
ACCT_CAUSE_LOST_CARRIER = 2,
|
|
ACCT_CAUSE_LOST_SERVICE = 3,
|
|
ACCT_CAUSE_IDLE_TIMEOUT = 4,
|
|
ACCT_CAUSE_SESSION_TIMEOUT = 5,
|
|
ACCT_CAUSE_ADMIN_RESET = 6,
|
|
ACCT_CAUSE_ADMIN_REBOOT = 7,
|
|
ACCT_CAUSE_PORT_ERROR = 8,
|
|
ACCT_CAUSE_NAS_ERROR = 9,
|
|
ACCT_CAUSE_NAS_REQUEST = 10,
|
|
ACCT_CAUSE_NAS_REBOOT = 11,
|
|
ACCT_CAUSE_PORT_UNNEEDED = 12,
|
|
ACCT_CAUSE_PORT_PREEMPTED = 13,
|
|
ACCT_CAUSE_PORT_SUSPENDED = 14,
|
|
ACCT_CAUSE_SERVICE_UNAVAILABLE = 15,
|
|
ACCT_CAUSE_CALLBACK = 16,
|
|
ACCT_CAUSE_USER_ERROR = 17,
|
|
ACCT_CAUSE_HOST_REQUEST = 18,
|
|
} radius_acct_terminate_cause_t;
|
|
|
|
/**
|
|
* Usage stats for bytes and packets
|
|
*/
|
|
typedef struct {
|
|
struct {
|
|
uint64_t sent;
|
|
uint64_t received;
|
|
} bytes, packets;
|
|
} usage_t;
|
|
|
|
/**
|
|
* Add usage stats (modifies a)
|
|
*/
|
|
static inline void add_usage(usage_t *a, usage_t b)
|
|
{
|
|
a->bytes.sent += b.bytes.sent;
|
|
a->bytes.received += b.bytes.received;
|
|
a->packets.sent += b.packets.sent;
|
|
a->packets.received += b.packets.received;
|
|
}
|
|
|
|
/**
|
|
* Subtract usage stats (modifies a)
|
|
*/
|
|
static inline void sub_usage(usage_t *a, usage_t b)
|
|
{
|
|
a->bytes.sent -= b.bytes.sent;
|
|
a->bytes.received -= b.bytes.received;
|
|
a->packets.sent -= b.packets.sent;
|
|
a->packets.received -= b.packets.received;
|
|
}
|
|
|
|
/**
|
|
* Usage stats for a cached/migrated SAs
|
|
*/
|
|
typedef struct {
|
|
/** unique CHILD_SA identifier */
|
|
uint32_t id;
|
|
/** usage stats for this SA */
|
|
usage_t usage;
|
|
} sa_entry_t;
|
|
|
|
/**
|
|
* Clone an sa_entry_t
|
|
*/
|
|
static sa_entry_t *clone_sa(sa_entry_t *sa)
|
|
{
|
|
sa_entry_t *this;
|
|
|
|
INIT(this,
|
|
.id = sa->id,
|
|
.usage = sa->usage,
|
|
);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Hashtable entry with usage stats
|
|
*/
|
|
typedef struct {
|
|
/** IKE_SA identifier this entry is stored under */
|
|
ike_sa_id_t *id;
|
|
/** RADIUS accounting session ID */
|
|
char sid[24];
|
|
/** number of sent/received octets/packets for expired SAs */
|
|
usage_t usage;
|
|
/** list of cached SAs, sa_entry_t (sorted by their unique ID) */
|
|
array_t *cached;
|
|
/** list of migrated SAs, sa_entry_t (sorted by their unique ID) */
|
|
array_t *migrated;
|
|
/** session creation time */
|
|
time_t created;
|
|
/** terminate cause */
|
|
radius_acct_terminate_cause_t cause;
|
|
/* interim interval and timestamp of last update */
|
|
struct {
|
|
uint32_t interval;
|
|
time_t last;
|
|
} interim;
|
|
/** did we send Accounting-Start */
|
|
bool start_sent;
|
|
} entry_t;
|
|
|
|
/**
|
|
* Destroy an entry_t
|
|
*/
|
|
static void destroy_entry(entry_t *this)
|
|
{
|
|
array_destroy_function(this->cached, (void*)free, NULL);
|
|
array_destroy_function(this->migrated, (void*)free, NULL);
|
|
this->id->destroy(this->id);
|
|
free(this);
|
|
}
|
|
|
|
/**
|
|
* Accounting message status types
|
|
*/
|
|
typedef enum {
|
|
ACCT_STATUS_START = 1,
|
|
ACCT_STATUS_STOP = 2,
|
|
ACCT_STATUS_INTERIM_UPDATE = 3,
|
|
ACCT_STATUS_ACCOUNTING_ON = 7,
|
|
ACCT_STATUS_ACCOUNTING_OFF = 8,
|
|
} radius_acct_status_t;
|
|
|
|
/**
|
|
* Hashtable hash function
|
|
*/
|
|
static u_int hash(ike_sa_id_t *key)
|
|
{
|
|
return key->get_responder_spi(key);
|
|
}
|
|
|
|
/**
|
|
* Hashtable equals function
|
|
*/
|
|
static bool equals(ike_sa_id_t *a, ike_sa_id_t *b)
|
|
{
|
|
return a->equals(a, b);
|
|
}
|
|
|
|
/**
|
|
* Sort cached SAs
|
|
*/
|
|
static int sa_sort(const void *a, const void *b, void *user)
|
|
{
|
|
const sa_entry_t *ra = a, *rb = b;
|
|
return ra->id - rb->id;
|
|
}
|
|
|
|
/**
|
|
* Find a cached SA
|
|
*/
|
|
static int sa_find(const void *a, const void *b)
|
|
{
|
|
return sa_sort(a, b, NULL);
|
|
}
|
|
|
|
/**
|
|
* Update or create usage counters of a cached SA
|
|
*/
|
|
static void update_sa(entry_t *entry, uint32_t id, usage_t usage)
|
|
{
|
|
sa_entry_t *sa, lookup;
|
|
|
|
lookup.id = id;
|
|
if (array_bsearch(entry->cached, &lookup, sa_find, &sa) == -1)
|
|
{
|
|
INIT(sa,
|
|
.id = id,
|
|
);
|
|
array_insert_create(&entry->cached, ARRAY_TAIL, sa);
|
|
array_sort(entry->cached, sa_sort, NULL);
|
|
}
|
|
sa->usage = usage;
|
|
}
|
|
|
|
/**
|
|
* Update usage counter when a CHILD_SA rekeys/goes down
|
|
*/
|
|
static void update_usage(private_eap_radius_accounting_t *this,
|
|
ike_sa_t *ike_sa, child_sa_t *child_sa)
|
|
{
|
|
usage_t usage;
|
|
entry_t *entry;
|
|
|
|
child_sa->get_usestats(child_sa, TRUE, NULL, &usage.bytes.received,
|
|
&usage.packets.received);
|
|
child_sa->get_usestats(child_sa, FALSE, NULL, &usage.bytes.sent,
|
|
&usage.packets.sent);
|
|
|
|
this->mutex->lock(this->mutex);
|
|
entry = this->sessions->get(this->sessions, ike_sa->get_id(ike_sa));
|
|
if (entry)
|
|
{
|
|
update_sa(entry, child_sa->get_unique_id(child_sa), usage);
|
|
}
|
|
this->mutex->unlock(this->mutex);
|
|
}
|
|
|
|
/**
|
|
* Collect usage stats for all CHILD_SAs of the given IKE_SA, optionally returns
|
|
* the total number of bytes and packets
|
|
*/
|
|
static array_t *collect_stats(ike_sa_t *ike_sa, usage_t *total)
|
|
{
|
|
enumerator_t *enumerator;
|
|
child_sa_t *child_sa;
|
|
array_t *stats;
|
|
sa_entry_t *sa;
|
|
usage_t usage;
|
|
|
|
if (total)
|
|
{
|
|
*total = (usage_t){};
|
|
}
|
|
|
|
stats = array_create(0, 0);
|
|
enumerator = ike_sa->create_child_sa_enumerator(ike_sa);
|
|
while (enumerator->enumerate(enumerator, &child_sa))
|
|
{
|
|
INIT(sa,
|
|
.id = child_sa->get_unique_id(child_sa),
|
|
);
|
|
array_insert(stats, ARRAY_TAIL, sa);
|
|
array_sort(stats, sa_sort, NULL);
|
|
|
|
child_sa->get_usestats(child_sa, TRUE, NULL, &usage.bytes.received,
|
|
&usage.packets.received);
|
|
child_sa->get_usestats(child_sa, FALSE, NULL, &usage.bytes.sent,
|
|
&usage.packets.sent);
|
|
sa->usage = usage;
|
|
if (total)
|
|
{
|
|
add_usage(total, usage);
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Cleanup cached SAs
|
|
*/
|
|
static void cleanup_sas(private_eap_radius_accounting_t *this, ike_sa_t *ike_sa,
|
|
entry_t *entry)
|
|
{
|
|
enumerator_t *enumerator;
|
|
child_sa_t *child_sa;
|
|
sa_entry_t *sa, *found;
|
|
array_t *sas;
|
|
|
|
sas = array_create(0, 0);
|
|
enumerator = ike_sa->create_child_sa_enumerator(ike_sa);
|
|
while (enumerator->enumerate(enumerator, &child_sa))
|
|
{
|
|
INIT(sa,
|
|
.id = child_sa->get_unique_id(child_sa),
|
|
);
|
|
array_insert(sas, ARRAY_TAIL, sa);
|
|
array_sort(sas, sa_sort, NULL);
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
enumerator = array_create_enumerator(entry->cached);
|
|
while (enumerator->enumerate(enumerator, &sa))
|
|
{
|
|
if (array_bsearch(sas, sa, sa_find, &found) == -1)
|
|
{
|
|
/* SA is gone, add its latest stats to the total for this IKE_SA
|
|
* and remove the cache entry */
|
|
add_usage(&entry->usage, sa->usage);
|
|
array_remove_at(entry->cached, enumerator);
|
|
free(sa);
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
enumerator = array_create_enumerator(entry->migrated);
|
|
while (enumerator->enumerate(enumerator, &sa))
|
|
{
|
|
if (array_bsearch(sas, sa, sa_find, &found) == -1)
|
|
{
|
|
/* SA is gone, subtract stats from the total for this IKE_SA */
|
|
sub_usage(&entry->usage, sa->usage);
|
|
array_remove_at(entry->migrated, enumerator);
|
|
free(sa);
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
array_destroy_function(sas, (void*)free, NULL);
|
|
}
|
|
|
|
/**
|
|
* Send a RADIUS message, wait for response
|
|
*/
|
|
static bool send_message(private_eap_radius_accounting_t *this,
|
|
radius_message_t *request)
|
|
{
|
|
radius_message_t *response;
|
|
radius_client_t *client;
|
|
bool ack = FALSE;
|
|
|
|
client = eap_radius_create_client();
|
|
if (client)
|
|
{
|
|
response = client->request(client, request);
|
|
if (response)
|
|
{
|
|
ack = response->get_code(response) == RMC_ACCOUNTING_RESPONSE;
|
|
response->destroy(response);
|
|
}
|
|
client->destroy(client);
|
|
}
|
|
return ack;
|
|
}
|
|
|
|
/**
|
|
* Add common IKE_SA parameters to RADIUS account message
|
|
*/
|
|
static void add_ike_sa_parameters(private_eap_radius_accounting_t *this,
|
|
radius_message_t *message, ike_sa_t *ike_sa)
|
|
{
|
|
enumerator_t *enumerator;
|
|
host_t *vip, *host;
|
|
char buf[MAX_RADIUS_ATTRIBUTE_SIZE + 1];
|
|
chunk_t data;
|
|
uint32_t value;
|
|
|
|
/* virtual NAS-Port-Type */
|
|
value = htonl(5);
|
|
message->add(message, RAT_NAS_PORT_TYPE, chunk_from_thing(value));
|
|
/* framed ServiceType */
|
|
value = htonl(2);
|
|
message->add(message, RAT_SERVICE_TYPE, chunk_from_thing(value));
|
|
|
|
value = htonl(ike_sa->get_unique_id(ike_sa));
|
|
message->add(message, RAT_NAS_PORT, chunk_from_thing(value));
|
|
message->add(message, RAT_NAS_PORT_ID,
|
|
chunk_from_str(ike_sa->get_name(ike_sa)));
|
|
|
|
host = ike_sa->get_my_host(ike_sa);
|
|
data = host->get_address(host);
|
|
switch (host->get_family(host))
|
|
{
|
|
case AF_INET:
|
|
message->add(message, RAT_NAS_IP_ADDRESS, data);
|
|
break;
|
|
case AF_INET6:
|
|
message->add(message, RAT_NAS_IPV6_ADDRESS, data);
|
|
default:
|
|
break;
|
|
}
|
|
snprintf(buf, sizeof(buf), this->station_id_fmt, host);
|
|
message->add(message, RAT_CALLED_STATION_ID, chunk_from_str(buf));
|
|
host = ike_sa->get_other_host(ike_sa);
|
|
snprintf(buf, sizeof(buf), this->station_id_fmt, host);
|
|
message->add(message, RAT_CALLING_STATION_ID, chunk_from_str(buf));
|
|
|
|
snprintf(buf, sizeof(buf), "%Y", ike_sa->get_other_eap_id(ike_sa));
|
|
message->add(message, RAT_USER_NAME, chunk_from_str(buf));
|
|
|
|
enumerator = ike_sa->create_virtual_ip_enumerator(ike_sa, FALSE);
|
|
while (enumerator->enumerate(enumerator, &vip))
|
|
{
|
|
switch (vip->get_family(vip))
|
|
{
|
|
case AF_INET:
|
|
message->add(message, RAT_FRAMED_IP_ADDRESS,
|
|
vip->get_address(vip));
|
|
break;
|
|
case AF_INET6:
|
|
message->add(message, RAT_FRAMED_IPV6_ADDRESS,
|
|
vip->get_address(vip));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
}
|
|
|
|
/**
|
|
* Get an existing or create a new entry from the locked session table
|
|
*/
|
|
static entry_t* get_or_create_entry(private_eap_radius_accounting_t *this,
|
|
ike_sa_id_t *id, uint32_t unique)
|
|
{
|
|
entry_t *entry;
|
|
time_t now;
|
|
|
|
entry = this->sessions->get(this->sessions, id);
|
|
if (!entry)
|
|
{
|
|
now = time_monotonic(NULL);
|
|
|
|
INIT(entry,
|
|
.id = id->clone(id),
|
|
.created = now,
|
|
.interim = {
|
|
.last = now,
|
|
},
|
|
/* default terminate cause, if none other caught */
|
|
.cause = ACCT_CAUSE_USER_REQUEST,
|
|
);
|
|
snprintf(entry->sid, sizeof(entry->sid), "%u-%u", this->prefix, unique);
|
|
this->sessions->put(this->sessions, entry->id, entry);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
/* forward declaration */
|
|
static void schedule_interim(private_eap_radius_accounting_t *this,
|
|
entry_t *entry);
|
|
|
|
/**
|
|
* Data passed to send_interim() using callback job
|
|
*/
|
|
typedef struct {
|
|
/** reference to radius accounting */
|
|
private_eap_radius_accounting_t *this;
|
|
/** IKE_SA identifier to send interim update to */
|
|
ike_sa_id_t *id;
|
|
} interim_data_t;
|
|
|
|
/**
|
|
* Clean up interim data
|
|
*/
|
|
void destroy_interim_data(interim_data_t *this)
|
|
{
|
|
this->id->destroy(this->id);
|
|
free(this);
|
|
}
|
|
|
|
/**
|
|
* Send an interim update for entry of given IKE_SA identifier
|
|
*/
|
|
static job_requeue_t send_interim(interim_data_t *data)
|
|
{
|
|
private_eap_radius_accounting_t *this = data->this;
|
|
usage_t usage;
|
|
radius_message_t *message = NULL;
|
|
enumerator_t *enumerator;
|
|
ike_sa_t *ike_sa;
|
|
entry_t *entry;
|
|
uint32_t value;
|
|
array_t *stats;
|
|
sa_entry_t *sa, *found;
|
|
|
|
ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager, data->id);
|
|
if (!ike_sa)
|
|
{
|
|
return JOB_REQUEUE_NONE;
|
|
}
|
|
stats = collect_stats(ike_sa, &usage);
|
|
charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
|
|
|
|
/* avoid any races by returning IKE_SA before acquiring lock */
|
|
|
|
this->mutex->lock(this->mutex);
|
|
entry = this->sessions->get(this->sessions, data->id);
|
|
if (entry)
|
|
{
|
|
entry->interim.last = time_monotonic(NULL);
|
|
|
|
enumerator = array_create_enumerator(entry->cached);
|
|
while (enumerator->enumerate(enumerator, &sa))
|
|
{
|
|
if (array_bsearch(stats, sa, sa_find, &found) != -1)
|
|
{
|
|
/* SA is still around, update stats (e.g. for IKEv1 where
|
|
* SA might get used even after rekeying) */
|
|
sa->usage = found->usage;
|
|
}
|
|
else
|
|
{
|
|
/* SA is gone, add its last stats to the total for this IKE_SA
|
|
* and remove the cache entry */
|
|
add_usage(&entry->usage, sa->usage);
|
|
array_remove_at(entry->cached, enumerator);
|
|
free(sa);
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
enumerator = array_create_enumerator(entry->migrated);
|
|
while (enumerator->enumerate(enumerator, &sa))
|
|
{
|
|
if (array_bsearch(stats, sa, sa_find, &found) != -1)
|
|
{
|
|
/* SA is still around, but we have to compensate */
|
|
sub_usage(&usage, sa->usage);
|
|
}
|
|
else
|
|
{
|
|
/* SA is gone, subtract stats from the total for this IKE_SA */
|
|
sub_usage(&entry->usage, sa->usage);
|
|
array_remove_at(entry->migrated, enumerator);
|
|
free(sa);
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
add_usage(&usage, entry->usage);
|
|
|
|
message = radius_message_create(RMC_ACCOUNTING_REQUEST);
|
|
value = htonl(ACCT_STATUS_INTERIM_UPDATE);
|
|
message->add(message, RAT_ACCT_STATUS_TYPE, chunk_from_thing(value));
|
|
message->add(message, RAT_ACCT_SESSION_ID,
|
|
chunk_create(entry->sid, strlen(entry->sid)));
|
|
add_ike_sa_parameters(this, message, ike_sa);
|
|
|
|
value = htonl(usage.bytes.sent);
|
|
message->add(message, RAT_ACCT_OUTPUT_OCTETS, chunk_from_thing(value));
|
|
value = htonl(usage.bytes.sent >> 32);
|
|
if (value)
|
|
{
|
|
message->add(message, RAT_ACCT_OUTPUT_GIGAWORDS,
|
|
chunk_from_thing(value));
|
|
}
|
|
value = htonl(usage.packets.sent);
|
|
message->add(message, RAT_ACCT_OUTPUT_PACKETS, chunk_from_thing(value));
|
|
|
|
value = htonl(usage.bytes.received);
|
|
message->add(message, RAT_ACCT_INPUT_OCTETS, chunk_from_thing(value));
|
|
value = htonl(usage.bytes.received >> 32);
|
|
if (value)
|
|
{
|
|
message->add(message, RAT_ACCT_INPUT_GIGAWORDS,
|
|
chunk_from_thing(value));
|
|
}
|
|
value = htonl(usage.packets.received);
|
|
message->add(message, RAT_ACCT_INPUT_PACKETS, chunk_from_thing(value));
|
|
|
|
value = htonl(entry->interim.last - entry->created);
|
|
message->add(message, RAT_ACCT_SESSION_TIME, chunk_from_thing(value));
|
|
|
|
schedule_interim(this, entry);
|
|
}
|
|
this->mutex->unlock(this->mutex);
|
|
array_destroy_function(stats, (void*)free, NULL);
|
|
|
|
if (message)
|
|
{
|
|
if (!send_message(this, message))
|
|
{
|
|
if (lib->settings->get_bool(lib->settings,
|
|
"%s.plugins.eap-radius.accounting_close_on_timeout",
|
|
TRUE, lib->ns))
|
|
{
|
|
eap_radius_handle_timeout(data->id);
|
|
}
|
|
}
|
|
message->destroy(message);
|
|
}
|
|
return JOB_REQUEUE_NONE;
|
|
}
|
|
|
|
/**
|
|
* Schedule interim update for given entry
|
|
*/
|
|
static void schedule_interim(private_eap_radius_accounting_t *this,
|
|
entry_t *entry)
|
|
{
|
|
if (entry->interim.interval)
|
|
{
|
|
interim_data_t *data;
|
|
timeval_t tv = {
|
|
.tv_sec = entry->interim.last + entry->interim.interval,
|
|
};
|
|
|
|
INIT(data,
|
|
.this = this,
|
|
.id = entry->id->clone(entry->id),
|
|
);
|
|
lib->scheduler->schedule_job_tv(lib->scheduler,
|
|
(job_t*)callback_job_create_with_prio(
|
|
(callback_job_cb_t)send_interim,
|
|
data, (void*)destroy_interim_data,
|
|
(callback_job_cancel_t)return_false, JOB_PRIO_CRITICAL), tv);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an IKE_SA has assigned a virtual IP (to peer)
|
|
*/
|
|
static bool has_vip(ike_sa_t *ike_sa)
|
|
{
|
|
enumerator_t *enumerator;
|
|
host_t *host;
|
|
bool found;
|
|
|
|
enumerator = ike_sa->create_virtual_ip_enumerator(ike_sa, FALSE);
|
|
found = enumerator->enumerate(enumerator, &host);
|
|
enumerator->destroy(enumerator);
|
|
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* Send an accounting start message
|
|
*/
|
|
static void send_start(private_eap_radius_accounting_t *this, ike_sa_t *ike_sa)
|
|
{
|
|
radius_message_t *message;
|
|
entry_t *entry;
|
|
uint32_t value;
|
|
|
|
if (this->acct_req_vip && !has_vip(ike_sa))
|
|
{
|
|
return;
|
|
}
|
|
|
|
this->mutex->lock(this->mutex);
|
|
|
|
entry = get_or_create_entry(this, ike_sa->get_id(ike_sa),
|
|
ike_sa->get_unique_id(ike_sa));
|
|
if (entry->start_sent)
|
|
{
|
|
this->mutex->unlock(this->mutex);
|
|
return;
|
|
}
|
|
entry->start_sent = TRUE;
|
|
|
|
message = radius_message_create(RMC_ACCOUNTING_REQUEST);
|
|
value = htonl(ACCT_STATUS_START);
|
|
message->add(message, RAT_ACCT_STATUS_TYPE, chunk_from_thing(value));
|
|
message->add(message, RAT_ACCT_SESSION_ID,
|
|
chunk_create(entry->sid, strlen(entry->sid)));
|
|
|
|
if (!entry->interim.interval)
|
|
{
|
|
entry->interim.interval = lib->settings->get_time(lib->settings,
|
|
"%s.plugins.eap-radius.accounting_interval", 0, lib->ns);
|
|
if (entry->interim.interval)
|
|
{
|
|
DBG1(DBG_CFG, "scheduling RADIUS Interim-Updates every %us",
|
|
entry->interim.interval);
|
|
}
|
|
}
|
|
schedule_interim(this, entry);
|
|
this->mutex->unlock(this->mutex);
|
|
|
|
add_ike_sa_parameters(this, message, ike_sa);
|
|
if (!send_message(this, message))
|
|
{
|
|
eap_radius_handle_timeout(ike_sa->get_id(ike_sa));
|
|
}
|
|
message->destroy(message);
|
|
}
|
|
|
|
/**
|
|
* Send an account stop message
|
|
*/
|
|
static void send_stop(private_eap_radius_accounting_t *this, ike_sa_t *ike_sa)
|
|
{
|
|
radius_message_t *message;
|
|
enumerator_t *enumerator;
|
|
entry_t *entry;
|
|
sa_entry_t *sa;
|
|
uint32_t value;
|
|
|
|
this->mutex->lock(this->mutex);
|
|
entry = this->sessions->remove(this->sessions, ike_sa->get_id(ike_sa));
|
|
this->mutex->unlock(this->mutex);
|
|
if (entry)
|
|
{
|
|
if (!entry->start_sent)
|
|
{ /* we tried to authenticate this peer, but never sent a start */
|
|
destroy_entry(entry);
|
|
return;
|
|
}
|
|
enumerator = array_create_enumerator(entry->cached);
|
|
while (enumerator->enumerate(enumerator, &sa))
|
|
{
|
|
add_usage(&entry->usage, sa->usage);
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
enumerator = array_create_enumerator(entry->migrated);
|
|
while (enumerator->enumerate(enumerator, &sa))
|
|
{
|
|
sub_usage(&entry->usage, sa->usage);
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
message = radius_message_create(RMC_ACCOUNTING_REQUEST);
|
|
value = htonl(ACCT_STATUS_STOP);
|
|
message->add(message, RAT_ACCT_STATUS_TYPE, chunk_from_thing(value));
|
|
message->add(message, RAT_ACCT_SESSION_ID,
|
|
chunk_create(entry->sid, strlen(entry->sid)));
|
|
add_ike_sa_parameters(this, message, ike_sa);
|
|
|
|
value = htonl(entry->usage.bytes.sent);
|
|
message->add(message, RAT_ACCT_OUTPUT_OCTETS, chunk_from_thing(value));
|
|
value = htonl(entry->usage.bytes.sent >> 32);
|
|
if (value)
|
|
{
|
|
message->add(message, RAT_ACCT_OUTPUT_GIGAWORDS,
|
|
chunk_from_thing(value));
|
|
}
|
|
value = htonl(entry->usage.packets.sent);
|
|
message->add(message, RAT_ACCT_OUTPUT_PACKETS, chunk_from_thing(value));
|
|
|
|
value = htonl(entry->usage.bytes.received);
|
|
message->add(message, RAT_ACCT_INPUT_OCTETS, chunk_from_thing(value));
|
|
value = htonl(entry->usage.bytes.received >> 32);
|
|
if (value)
|
|
{
|
|
message->add(message, RAT_ACCT_INPUT_GIGAWORDS,
|
|
chunk_from_thing(value));
|
|
}
|
|
value = htonl(entry->usage.packets.received);
|
|
message->add(message, RAT_ACCT_INPUT_PACKETS, chunk_from_thing(value));
|
|
|
|
value = htonl(time_monotonic(NULL) - entry->created);
|
|
message->add(message, RAT_ACCT_SESSION_TIME, chunk_from_thing(value));
|
|
|
|
|
|
value = htonl(entry->cause);
|
|
message->add(message, RAT_ACCT_TERMINATE_CAUSE, chunk_from_thing(value));
|
|
|
|
if (!send_message(this, message))
|
|
{
|
|
eap_radius_handle_timeout(NULL);
|
|
}
|
|
message->destroy(message);
|
|
destroy_entry(entry);
|
|
}
|
|
}
|
|
|
|
METHOD(listener_t, alert, bool,
|
|
private_eap_radius_accounting_t *this, ike_sa_t *ike_sa, alert_t alert,
|
|
va_list args)
|
|
{
|
|
radius_acct_terminate_cause_t cause;
|
|
entry_t *entry;
|
|
|
|
switch (alert)
|
|
{
|
|
case ALERT_IKE_SA_EXPIRED:
|
|
cause = ACCT_CAUSE_SESSION_TIMEOUT;
|
|
break;
|
|
case ALERT_RETRANSMIT_SEND_TIMEOUT:
|
|
cause = ACCT_CAUSE_LOST_SERVICE;
|
|
break;
|
|
default:
|
|
return TRUE;
|
|
}
|
|
this->mutex->lock(this->mutex);
|
|
entry = this->sessions->get(this->sessions, ike_sa->get_id(ike_sa));
|
|
if (entry)
|
|
{
|
|
entry->cause = cause;
|
|
}
|
|
this->mutex->unlock(this->mutex);
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(listener_t, ike_updown, bool,
|
|
private_eap_radius_accounting_t *this, ike_sa_t *ike_sa, bool up)
|
|
{
|
|
if (!up)
|
|
{
|
|
enumerator_t *enumerator;
|
|
child_sa_t *child_sa;
|
|
|
|
/* update usage for all children just before sending stop */
|
|
enumerator = ike_sa->create_child_sa_enumerator(ike_sa);
|
|
while (enumerator->enumerate(enumerator, &child_sa))
|
|
{
|
|
update_usage(this, ike_sa, child_sa);
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
send_stop(this, ike_sa);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(listener_t, message_hook, bool,
|
|
private_eap_radius_accounting_t *this, ike_sa_t *ike_sa,
|
|
message_t *message, bool incoming, bool plain)
|
|
{
|
|
/* start accounting here, virtual IP now is set */
|
|
if (plain && ike_sa->get_state(ike_sa) == IKE_ESTABLISHED &&
|
|
!incoming && !message->get_request(message))
|
|
{
|
|
if (ike_sa->get_version(ike_sa) == IKEV2 &&
|
|
message->get_exchange_type(message) == IKE_AUTH)
|
|
{
|
|
send_start(this, ike_sa);
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(listener_t, assign_vips, bool,
|
|
private_eap_radius_accounting_t *this, ike_sa_t *ike_sa, bool assign)
|
|
{
|
|
/* start accounting as soon as the virtual IP is set */
|
|
if (assign && ike_sa->get_version(ike_sa) == IKEV1)
|
|
{
|
|
send_start(this, ike_sa);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(listener_t, ike_rekey, bool,
|
|
private_eap_radius_accounting_t *this, ike_sa_t *old, ike_sa_t *new)
|
|
{
|
|
entry_t *entry;
|
|
|
|
this->mutex->lock(this->mutex);
|
|
entry = this->sessions->remove(this->sessions, old->get_id(old));
|
|
if (entry)
|
|
{
|
|
/* update IKE_SA identifier */
|
|
entry->id->destroy(entry->id);
|
|
entry->id = new->get_id(new);
|
|
entry->id = entry->id->clone(entry->id);
|
|
/* fire new interim update job, old gets invalid */
|
|
schedule_interim(this, entry);
|
|
|
|
cleanup_sas(this, new, entry);
|
|
|
|
entry = this->sessions->put(this->sessions, entry->id, entry);
|
|
if (entry)
|
|
{
|
|
destroy_entry(entry);
|
|
}
|
|
}
|
|
this->mutex->unlock(this->mutex);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(listener_t, child_rekey, bool,
|
|
private_eap_radius_accounting_t *this, ike_sa_t *ike_sa,
|
|
child_sa_t *old, child_sa_t *new)
|
|
{
|
|
entry_t *entry;
|
|
|
|
update_usage(this, ike_sa, old);
|
|
this->mutex->lock(this->mutex);
|
|
entry = this->sessions->get(this->sessions, ike_sa->get_id(ike_sa));
|
|
if (entry)
|
|
{
|
|
cleanup_sas(this, ike_sa, entry);
|
|
}
|
|
this->mutex->unlock(this->mutex);
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(listener_t, children_migrate, bool,
|
|
private_eap_radius_accounting_t *this, ike_sa_t *ike_sa, ike_sa_id_t *new,
|
|
uint32_t unique)
|
|
{
|
|
enumerator_t *enumerator;
|
|
sa_entry_t *sa, *sa_new, *cached;
|
|
entry_t *entry_old, *entry_new;
|
|
array_t *stats;
|
|
|
|
if (!new)
|
|
{
|
|
return TRUE;
|
|
}
|
|
stats = collect_stats(ike_sa, NULL);
|
|
this->mutex->lock(this->mutex);
|
|
entry_old = this->sessions->get(this->sessions, ike_sa->get_id(ike_sa));
|
|
if (entry_old)
|
|
{
|
|
entry_new = get_or_create_entry(this, new, unique);
|
|
enumerator = array_create_enumerator(stats);
|
|
while (enumerator->enumerate(enumerator, &sa))
|
|
{
|
|
/* if the SA was already rekeyed/cached we cache it too on the new
|
|
* SA to track it properly until it's finally gone */
|
|
if (array_bsearch(entry_old->cached, sa, sa_find, &cached) != -1)
|
|
{
|
|
sa_new = clone_sa(sa);
|
|
array_insert_create(&entry_new->cached, ARRAY_TAIL, sa_new);
|
|
array_sort(entry_new->cached, sa_sort, NULL);
|
|
}
|
|
/* if the SA was used, we store it to compensate on the new SA */
|
|
if (sa->usage.bytes.sent || sa->usage.bytes.received ||
|
|
sa->usage.packets.sent || sa->usage.packets.received)
|
|
{
|
|
sa_new = clone_sa(sa);
|
|
array_insert_create(&entry_new->migrated, ARRAY_TAIL, sa_new);
|
|
array_sort(entry_new->migrated, sa_sort, NULL);
|
|
/* store/update latest stats on old SA to report in Stop */
|
|
update_sa(entry_old, sa->id, sa->usage);
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
}
|
|
this->mutex->unlock(this->mutex);
|
|
array_destroy_function(stats, (void*)free, NULL);
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(listener_t, child_updown, bool,
|
|
private_eap_radius_accounting_t *this, ike_sa_t *ike_sa,
|
|
child_sa_t *child_sa, bool up)
|
|
{
|
|
if (!up && ike_sa->get_state(ike_sa) == IKE_ESTABLISHED)
|
|
{
|
|
update_usage(this, ike_sa, child_sa);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(eap_radius_accounting_t, destroy, void,
|
|
private_eap_radius_accounting_t *this)
|
|
{
|
|
charon->bus->remove_listener(charon->bus, &this->public.listener);
|
|
singleton = NULL;
|
|
this->mutex->destroy(this->mutex);
|
|
this->sessions->destroy(this->sessions);
|
|
free(this);
|
|
}
|
|
|
|
/**
|
|
* See header
|
|
*/
|
|
eap_radius_accounting_t *eap_radius_accounting_create()
|
|
{
|
|
private_eap_radius_accounting_t *this;
|
|
|
|
INIT(this,
|
|
.public = {
|
|
.listener = {
|
|
.alert = _alert,
|
|
.ike_updown = _ike_updown,
|
|
.ike_rekey = _ike_rekey,
|
|
.message = _message_hook,
|
|
.assign_vips = _assign_vips,
|
|
.child_updown = _child_updown,
|
|
.child_rekey = _child_rekey,
|
|
.children_migrate = _children_migrate,
|
|
},
|
|
.destroy = _destroy,
|
|
},
|
|
/* use system time as Session ID prefix */
|
|
.prefix = (uint32_t)time(NULL),
|
|
.sessions = hashtable_create((hashtable_hash_t)hash,
|
|
(hashtable_equals_t)equals, 32),
|
|
.mutex = mutex_create(MUTEX_TYPE_DEFAULT),
|
|
);
|
|
if (lib->settings->get_bool(lib->settings,
|
|
"%s.plugins.eap-radius.station_id_with_port", TRUE, lib->ns))
|
|
{
|
|
this->station_id_fmt = "%#H";
|
|
}
|
|
else
|
|
{
|
|
this->station_id_fmt = "%H";
|
|
}
|
|
if (lib->settings->get_bool(lib->settings,
|
|
"%s.plugins.eap-radius.accounting", FALSE, lib->ns))
|
|
{
|
|
singleton = this;
|
|
charon->bus->add_listener(charon->bus, &this->public.listener);
|
|
}
|
|
this->acct_req_vip = lib->settings->get_bool(lib->settings,
|
|
"%s.plugins.eap-radius.accounting_requires_vip",
|
|
FALSE, lib->ns);
|
|
|
|
return &this->public;
|
|
}
|
|
|
|
/**
|
|
* See header
|
|
*/
|
|
void eap_radius_accounting_start_interim(ike_sa_t *ike_sa, uint32_t interval)
|
|
{
|
|
if (singleton)
|
|
{
|
|
entry_t *entry;
|
|
|
|
DBG1(DBG_CFG, "scheduling RADIUS Interim-Updates every %us", interval);
|
|
singleton->mutex->lock(singleton->mutex);
|
|
entry = get_or_create_entry(singleton, ike_sa->get_id(ike_sa),
|
|
ike_sa->get_unique_id(ike_sa));
|
|
entry->interim.interval = interval;
|
|
singleton->mutex->unlock(singleton->mutex);
|
|
}
|
|
}
|