ikev2: Make CHILD_SAs properly trackable during rekey collisions

As the winner of a rekey collision, we previously always triggered the
child_rekey() event once when creating the redundant SA on behalf of the
peer in the passive child-rekey task and then a second time when
creating the winning SA in the active task.  However, both calls passed
the replaced CHILD_SA as "old". This made tracking CHILD_SAs impossible
because there was no transition from the redundant, "new" SA of the
first event to the "new", winning SA of the second.  Of course, when the
second event was triggered, the redundant SA might not have existed
anymore because the peer is expected to delete it, which could happen
before the CREATE_CHILD_SA response arrives at the initiator.

This refactoring ensures that the child_rekey() event is triggered in
a way that makes the CHILD_SAs trackable in all reasonable (and even
some unreasonable) scenarios.  The event is generally only triggered
once after installing the outbound SA for the new/winning CHILD_SA.
This can be when processing the CREATE_CHILD_SA in the active child-rekey
task, or when processing the DELETE for the old SA in a passive
child-delete task.  There are some cases where the event is still
triggered twice, but it is now ensured that listeners can properly
transition to the winning SA.

Some corner cases are now also handled correctly, e.g. if a responder's
DELETE for the new CHILD_SA arrives before its CREATE_CHILD_SA response
that actually creates it on the initiator.  Also handled properly are
responders of rekeyings that incorrectly send a DELETE for the old
CHILD_SA (previously this caused both, the new and the old SA, to get
deleted).
This commit is contained in:
Tobias Brunner 2022-08-22 15:43:16 +02:00
parent 7ad610a140
commit d2b2e1b3fa
10 changed files with 2641 additions and 856 deletions

View File

@ -131,9 +131,10 @@ struct private_child_sa_t {
bool tfcv3;
/**
* The outbound SPI of the CHILD_SA that replaced this one during a rekeying
* The "other" CHILD_SA involved in a passive rekeying (either replacing
* this one, or being replaced by it)
*/
uint32_t rekey_spi;
child_sa_t *rekey_sa;
/**
* Protocol used to protect this SA, ESP|AH
@ -1588,16 +1589,16 @@ METHOD(child_sa_t, remove_outbound, void,
this->outbound_state = CHILD_OUTBOUND_NONE;
}
METHOD(child_sa_t, set_rekey_spi, void,
private_child_sa_t *this, uint32_t spi)
METHOD(child_sa_t, set_rekey_sa, void,
private_child_sa_t *this, child_sa_t *sa)
{
this->rekey_spi = spi;
this->rekey_sa = sa;
}
METHOD(child_sa_t, get_rekey_spi, uint32_t,
METHOD(child_sa_t, get_rekey_sa, child_sa_t*,
private_child_sa_t *this)
{
return this->rekey_spi;
return this->rekey_sa;
}
CALLBACK(reinstall_vip, void,
@ -2077,8 +2078,8 @@ child_sa_t *child_sa_create(host_t *me, host_t *other, child_cfg_t *config,
.register_outbound = _register_outbound,
.install_outbound = _install_outbound,
.remove_outbound = _remove_outbound,
.set_rekey_spi = _set_rekey_spi,
.get_rekey_spi = _get_rekey_spi,
.set_rekey_sa = _set_rekey_sa,
.get_rekey_sa = _get_rekey_sa,
.update = _update,
.set_policies = _set_policies,
.install_policies = _install_policies,

View File

@ -504,23 +504,24 @@ struct child_sa_t {
status_t (*install_policies)(child_sa_t *this);
/**
* Set the outbound SPI of the CHILD_SA that replaced this CHILD_SA during
* a rekeying.
* Set the CHILD_SA that either replaced this one or the CHILD_SA that is
* being replaced by this one during a passive rekeying (i.e. it links the
* two SAs bidirectionally).
*
* @param spi outbound SPI of the CHILD_SA that replaced this CHILD_SA
* @param sa other CHILD_SA involved in a passive rekeying
*/
void (*set_rekey_spi)(child_sa_t *this, uint32_t spi);
void (*set_rekey_sa)(child_sa_t *this, child_sa_t *sa);
/**
* Get the outbound SPI of the CHILD_SA that replaced this CHILD_SA during
* a rekeying.
* Get the CHILD_SA that's linked to this in a passive rekeying (either
* replacing this one, or being replaced by it).
*
* @return outbound SPI of the CHILD_SA that replaced this CHILD_SA
* @return other CHILD_SA involved in a passive rekeying
*/
uint32_t (*get_rekey_spi)(child_sa_t *this);
child_sa_t *(*get_rekey_sa)(child_sa_t *this);
/**
* Update hosts and ecapsulation mode in the kernel SAs and policies.
* Update hosts and encapsulation mode in the kernel SAs and policies.
*
* @param me the new local host
* @param other the new remote host

View File

@ -935,9 +935,10 @@ static bool handle_collisions(private_task_manager_t *this, task_t *task)
type = task->get_type(task);
/* do we have to check */
if (type == TASK_IKE_REKEY || type == TASK_CHILD_REKEY ||
type == TASK_CHILD_DELETE || type == TASK_IKE_DELETE)
/* collisions between a child-rekey and child-delete task are handled
* directly by the latter */
if (type == TASK_IKE_REKEY || type == TASK_IKE_DELETE ||
type == TASK_CHILD_REKEY)
{
/* find an exchange collision, and notify these tasks */
enumerator = array_create_enumerator(this->active_tasks);
@ -954,7 +955,7 @@ static bool handle_collisions(private_task_manager_t *this, task_t *task)
}
continue;
case TASK_CHILD_REKEY:
if (type == TASK_CHILD_REKEY || type == TASK_CHILD_DELETE)
if (type == TASK_CHILD_REKEY)
{
child_rekey_t *rekey = (child_rekey_t*)active;
adopted = rekey->collide(rekey, task);

View File

@ -2184,7 +2184,7 @@ METHOD(task_t, build_i_delete, status_t,
DBG1(DBG_IKE, "sending DELETE for %N CHILD_SA with SPI %.8x",
protocol_id_names, this->proto, ntohl(this->my_spi));
}
return NEED_MORE;
return SUCCESS;
}
/**
@ -2195,7 +2195,6 @@ static status_t delete_failed_sa(private_child_create_t *this)
if (this->my_spi && this->proto)
{
this->public.task.build = _build_i_delete;
this->public.task.process = (void*)return_success;
/* destroying it here allows the rekey task to differentiate between
* this and the multi-KE case */
this->child_sa->destroy(this->child_sa);
@ -2232,6 +2231,17 @@ static status_t key_exchange_done_and_install_i(private_child_create_t *this,
METHOD(task_t, process_i_multi_ke, status_t,
private_child_create_t *this, message_t *message)
{
if (message->get_notify(message, TEMPORARY_FAILURE))
{
DBG1(DBG_IKE, "received %N notify", notify_type_names,
TEMPORARY_FAILURE);
if (!this->rekey)
{ /* the rekey task will retry itself if necessary */
schedule_delayed_retry(this);
}
return SUCCESS;
}
process_payloads_multi_ke(this, message);
if (this->ke_failed)
@ -2304,8 +2314,7 @@ METHOD(task_t, process_i, status_t,
}
case TEMPORARY_FAILURE:
{
DBG1(DBG_IKE, "received %N notify, will retry later",
notify_type_names, type);
DBG1(DBG_IKE, "received %N notify", notify_type_names, type);
enumerator->destroy(enumerator);
if (!this->rekey)
{ /* the rekey task will retry itself if necessary */
@ -2365,6 +2374,15 @@ METHOD(task_t, process_i, status_t,
process_payloads(this, message);
if (!select_proposal(this, no_ke))
{
handle_child_sa_failure(this, message);
return delete_failed_sa(this);
}
this->other_spi = this->proposal->get_spi(this->proposal);
this->proposal->set_spi(this->proposal, this->my_spi);
if (this->ipcomp == IPCOMP_NONE && this->ipcomp_received != IPCOMP_NONE)
{
DBG1(DBG_IKE, "received an IPCOMP_SUPPORTED notify without requesting"
@ -2386,15 +2404,6 @@ METHOD(task_t, process_i, status_t,
return delete_failed_sa(this);
}
if (!select_proposal(this, no_ke))
{
handle_child_sa_failure(this, message);
return delete_failed_sa(this);
}
this->other_spi = this->proposal->get_spi(this->proposal);
this->proposal->set_spi(this->proposal, this->my_spi);
if (!check_ke_method(this, NULL))
{
handle_child_sa_failure(this, message);
@ -2485,6 +2494,12 @@ METHOD(child_create_t, get_child, child_sa_t*,
return this->child_sa;
}
METHOD(child_create_t, get_other_spi, uint32_t,
private_child_create_t *this)
{
return this->other_spi;
}
METHOD(child_create_t, set_config, void,
private_child_create_t *this, child_cfg_t *cfg)
{
@ -2623,6 +2638,7 @@ child_create_t *child_create_create(ike_sa_t *ike_sa,
INIT(this,
.public = {
.get_child = _get_child,
.get_other_spi = _get_other_spi,
.set_config = _set_config,
.get_lower_nonce = _get_lower_nonce,
.use_reqid = _use_reqid,

View File

@ -103,6 +103,13 @@ struct child_create_t {
*/
child_sa_t* (*get_child) (child_create_t *this);
/**
* Get the SPI of the other peer's selected proposal, if available.
*
* @return other's SPI, 0 if unknown
*/
uint32_t (*get_other_spi)(child_create_t *this);
/**
* Enforce a specific CHILD_SA config as responder.
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2009-2016 Tobias Brunner
* Copyright (C) 2009-2022 Tobias Brunner
* Copyright (C) 2006-2007 Martin Willi
*
* Copyright (C) secunet Security Networks AG
@ -76,10 +76,10 @@ struct private_child_delete_t {
typedef struct {
/** Deleted CHILD_SA */
child_sa_t *child_sa;
/** Whether the CHILD_SA was rekeyed */
bool rekeyed;
/** Whether to enforce any delete action policy */
bool check_delete_action;
/** The original state of the CHILD_SA */
child_sa_state_t orig_state;
/** How this CHILD_SA collides with an active rekeying */
child_rekey_collision_t collision;
} entry_t;
CALLBACK(match_child, bool,
@ -133,18 +133,448 @@ static void build_payloads(private_child_delete_t *this, message_t *message)
default:
break;
}
entry->child_sa->set_state(entry->child_sa, CHILD_DELETING);
}
enumerator->destroy(enumerator);
}
/**
* Check if the given CHILD_SA is the redundant SA created in a rekey collision.
* Install the outbound SA of the CHILD_SA that replaced the given CHILD_SA
* in a rekeying.
*/
static bool is_redundant(private_child_delete_t *this, child_sa_t *child)
static void conclude_rekeying(private_child_delete_t *this, child_sa_t *old)
{
child_sa_t *child_sa;
child_sa = old->get_rekey_sa(old);
old->set_rekey_sa(old, NULL);
child_sa->set_rekey_sa(child_sa, NULL);
child_rekey_conclude_rekeying(old, child_sa);
}
/**
* Destroy and optionally reestablish the given CHILD_SA according to config.
*/
static status_t destroy_and_reestablish_internal(ike_sa_t *ike_sa,
child_sa_t *child_sa,
bool trigger_updown,
bool delete_action,
action_t forced_action)
{
child_init_args_t args = {};
child_cfg_t *child_cfg;
protocol_id_t protocol;
uint32_t spi;
action_t action;
status_t status = SUCCESS;
child_sa->set_state(child_sa, CHILD_DELETED);
if (trigger_updown)
{
charon->bus->child_updown(charon->bus, child_sa, FALSE);
}
protocol = child_sa->get_protocol(child_sa);
spi = child_sa->get_spi(child_sa, TRUE);
child_cfg = child_sa->get_config(child_sa);
child_cfg->get_ref(child_cfg);
args.reqid = child_sa->get_reqid_ref(child_sa);
args.label = child_sa->get_label(child_sa);
if (args.label)
{
args.label = args.label->clone(args.label);
}
action = forced_action ?: child_sa->get_close_action(child_sa);
DBG1(DBG_IKE, "CHILD_SA %s{%u} closed", child_sa->get_name(child_sa),
child_sa->get_unique_id(child_sa));
ike_sa->destroy_child_sa(ike_sa, protocol, spi);
if (delete_action)
{
if (action & ACTION_TRAP)
{
charon->traps->install(charon->traps,
ike_sa->get_peer_cfg(ike_sa),
child_cfg);
}
if (action & ACTION_START)
{
child_cfg->get_ref(child_cfg);
status = ike_sa->initiate(ike_sa, child_cfg, &args);
}
}
child_cfg->destroy(child_cfg);
if (args.reqid)
{
charon->kernel->release_reqid(charon->kernel, args.reqid);
}
DESTROY_IF(args.label);
return status;
}
/*
* Described in header
*/
status_t child_delete_destroy_and_reestablish(ike_sa_t *ike_sa,
child_sa_t *child_sa)
{
return destroy_and_reestablish_internal(ike_sa, child_sa, TRUE, TRUE, 0);
}
/*
* Described in header
*/
status_t child_delete_destroy_and_force_reestablish(ike_sa_t *ike_sa,
child_sa_t *child_sa)
{
return destroy_and_reestablish_internal(ike_sa, child_sa, TRUE, TRUE,
ACTION_START);
}
/*
* Described in header
*/
void child_delete_destroy_rekeyed(ike_sa_t *ike_sa, child_sa_t *child_sa)
{
time_t now, expire;
u_int delay;
/* make sure the SA is in the correct state and the outbound SA is not
* installed */
child_sa->remove_outbound(child_sa);
child_sa->set_state(child_sa, CHILD_DELETED);
now = time_monotonic(NULL);
delay = lib->settings->get_int(lib->settings, "%s.delete_rekeyed_delay",
DELETE_REKEYED_DELAY, lib->ns);
expire = child_sa->get_lifetime(child_sa, TRUE);
if (delay && (!expire || ((now + delay) < expire)))
{
DBG1(DBG_IKE, "delay closing of inbound CHILD_SA %s{%u} for %us",
child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa),
delay);
lib->scheduler->schedule_job(lib->scheduler,
(job_t*)delete_child_sa_job_create_id(
child_sa->get_unique_id(child_sa)), delay);
return;
}
else if (now < expire)
{
/* let it expire naturally */
DBG1(DBG_IKE, "let rekeyed inbound CHILD_SA %s{%u} expire naturally "
"in %us", child_sa->get_name(child_sa),
child_sa->get_unique_id(child_sa), expire-now);
return;
}
/* no delay and no lifetime, destroy it immediately. since we suppress
* actions, there is no need to check the return value */
destroy_and_reestablish_internal(ike_sa, child_sa, FALSE, FALSE, 0);
}
/**
* Check if the SA should be ignored and kept until a concurrent active rekeying
* is concluded (the rekey task is responsible for destroying the CHILD_SA).
*/
static bool keep_while_rekeying(entry_t *entry)
{
switch (entry->collision)
{
case CHILD_REKEY_COLLISION_NONE:
break;
case CHILD_REKEY_COLLISION_OLD:
/* if the peer deletes the SA we are trying to rekey and there
* hasn't been a collision, it might have sent the delete before our
* request arrived. but it could also be an incorrect delete sent
* after it processed our rekey request, which we'd have to ignore.
* the active rekey task will decide once it has the response */
if (entry->orig_state == CHILD_REKEYING)
{
return TRUE;
}
/* if there was a collision, the peer is expected to delete the old
* SA only if it won the collision, the SA is in state CHILD_REKEYED
* in this case. we don't completely ignore the SA and conclude the
* rekeying for it now to switch to the new outbound SA (the peer
* will remove the old inbound SA once it receives the DELETE
* response), but don't destroy the old SA yet even though we return
* FALSE here.
* the active rekey task will later decide if the delete was
* legitimate or an incorrect delete for the old SA */
break;
case CHILD_REKEY_COLLISION_PEER:
/* the peer deletes the SA it created itself before we received
* the rekey response, this is either the redundant SA, which
* would be fine, or the winning SA it already is deleting for
* some reason (presumably, after also sending a delete for the
* rekeyed SA). let the active rekey task decide once it receives
* the response and knows who won the collision */
return TRUE;
}
return FALSE;
}
/**
* Log an SA we are not yet closing completely.
*/
static void log_kept_sa(entry_t *entry)
{
DBG1(DBG_IKE, "keeping %s CHILD_SA %s{%u} until active rekeying is "
"concluded",
entry->collision == CHILD_REKEY_COLLISION_OLD ? "rekeyed"
: "peer's",
entry->child_sa->get_name(entry->child_sa),
entry->child_sa->get_unique_id(entry->child_sa));
}
/**
* Destroy the children listed in this->child_sas, reestablish by policy
*/
static status_t destroy_and_reestablish(private_child_delete_t *this)
{
enumerator_t *enumerator;
entry_t *entry;
child_sa_t *child_sa, *other;
status_t status = SUCCESS;
enumerator = this->child_sas->create_enumerator(this->child_sas);
while (enumerator->enumerate(enumerator, (void**)&entry))
{
child_sa = entry->child_sa;
other = child_sa->get_rekey_sa(child_sa);
/* check if we have to keep the SA during a collision with an active
* rekey task */
if (keep_while_rekeying(entry))
{
/* if the peer deleted its own SA, reset the link to the old SA,
* which might already be reset if the peer deleted the old SA
* first (the active rekey task will eventually destroy both) */
if (other && entry->collision == CHILD_REKEY_COLLISION_PEER)
{
child_sa->set_rekey_sa(child_sa, NULL);
other->set_rekey_sa(other, NULL);
/* reset the state of the old SA until the active rekey task is
* done, but only if it's not also getting deleted by the peer
* and is already in state DELETING. note that we won't end up
* here if the peer deleted the old SA first as the link between
* the two SAs would already be reset then. so this is only the
* case if the peer sends the deletes for both SAs in the same
* message and the payload for the old one comes after the one
* for its own SA */
if (other->get_state(other) == CHILD_REKEYED)
{
other->set_state(other, CHILD_REKEYING);
}
}
log_kept_sa(entry);
continue;
}
child_sa->set_state(child_sa, CHILD_DELETED);
if (entry->orig_state == CHILD_REKEYED)
{
/* conclude the rekeying as responder/loser. the initiator/winner
* already did this right after the rekeying was completed (or
* before a delete was initiated), but in some cases the outbound
* SA was not yet removed, make sure it is */
if (other)
{
conclude_rekeying(this, child_sa);
}
else
{
child_sa->remove_outbound(child_sa);
}
/* if this is a delete for the SA we are actively rekeying, let the
* rekey task handle the SA appropriately once the collision is
* resolved. otherwise, destroy the SA now, but usually delayed to
* process delayed packets */
if (entry->collision == CHILD_REKEY_COLLISION_OLD)
{
log_kept_sa(entry);
}
else
{
child_delete_destroy_rekeyed(this->ike_sa, child_sa);
}
}
else
{
/* regular CHILD_SA delete, with one special case after a lost
* collision. usually, the peer will delete the old SA and we
* conclude the rekeying above. however, if it deletes its winning
* SA first, we assume it wants to delete the CHILD_SA and we
* conclude the rekeying here to trigger the events correctly */
if (other && entry->orig_state == CHILD_INSTALLED)
{
conclude_rekeying(this, other);
}
status = destroy_and_reestablish_internal(this->ike_sa, child_sa,
TRUE, !this->initiator &&
entry->orig_state == CHILD_INSTALLED, 0);
if (status != SUCCESS)
{
break;
}
}
}
enumerator->destroy(enumerator);
return status;
}
/**
* Print a log message for every closed CHILD_SA
*/
static void log_children(private_child_delete_t *this)
{
linked_list_t *my_ts, *other_ts;
enumerator_t *enumerator;
entry_t *entry;
child_sa_t *child_sa;
uint64_t bytes_in, bytes_out;
enumerator = this->child_sas->create_enumerator(this->child_sas);
while (enumerator->enumerate(enumerator, (void**)&entry))
{
child_sa = entry->child_sa;
my_ts = linked_list_create_from_enumerator(
child_sa->create_ts_enumerator(child_sa, TRUE));
other_ts = linked_list_create_from_enumerator(
child_sa->create_ts_enumerator(child_sa, FALSE));
if (this->expired)
{
DBG0(DBG_IKE, "closing expired CHILD_SA %s{%u} "
"with SPIs %.8x_i %.8x_o and TS %#R === %#R",
child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa),
ntohl(child_sa->get_spi(child_sa, TRUE)),
ntohl(child_sa->get_spi(child_sa, FALSE)), my_ts, other_ts);
}
else
{
child_sa->get_usestats(child_sa, TRUE, NULL, &bytes_in, NULL);
child_sa->get_usestats(child_sa, FALSE, NULL, &bytes_out, NULL);
DBG0(DBG_IKE, "closing CHILD_SA %s{%u} with SPIs %.8x_i "
"(%llu bytes) %.8x_o (%llu bytes) and TS %#R === %#R",
child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa),
ntohl(child_sa->get_spi(child_sa, TRUE)), bytes_in,
ntohl(child_sa->get_spi(child_sa, FALSE)), bytes_out,
my_ts, other_ts);
}
my_ts->destroy(my_ts);
other_ts->destroy(other_ts);
}
enumerator->destroy(enumerator);
}
METHOD(task_t, build_i, status_t,
private_child_delete_t *this, message_t *message)
{
child_sa_t *child_sa, *other;
entry_t *entry;
child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol,
this->spi, TRUE);
if (!child_sa)
{
/* check if it is an outbound SA */
child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol,
this->spi, FALSE);
if (!child_sa)
{
/* child does not exist anymore, abort exchange */
message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED);
return SUCCESS;
}
/* we work only with the inbound SPI */
this->spi = child_sa->get_spi(child_sa, TRUE);
}
/* check if this SA is involved in a passive rekeying, either the old
* rekeyed one or the new one created by the peer */
other = child_sa->get_rekey_sa(child_sa);
if (other)
{
if (child_sa->get_state(child_sa) == CHILD_REKEYED)
{
/* the peer was expected to delete this rekeyed SA. we don't send a
* DELETE, in particular, if this is triggered by an expire, because
* that could cause a collision if the CREATE_CHILD_SA response is
* delayed (the peer might interpret that as a deletion of the SA by
* a user and might then ignore the CREATE_CHILD_SA response once it
* arrives - like old strongSwan versions did - although it
* shouldn't as we properly replied to that request so only a delete
* for the new CHILD_SA should result in a deletion) */
child_sa->set_state(child_sa, CHILD_DELETED);
conclude_rekeying(this, child_sa);
}
else
{
/* the rekeying for the new SA we are about to delete on the user's
* behalf has not yet been completed, that is, we are waiting for
* the delete for the old SA and have not yet fully installed this
* new one. we do that now so events are triggered properly when
* we delete it */
DBG2(DBG_IKE, "complete rekeying for %s{%u} before deleting "
"replacement CHILD_SA %s{%u}",
other->get_name(other), other->get_unique_id(other),
child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa));
conclude_rekeying(this, other);
}
}
if (child_sa->get_state(child_sa) == CHILD_DELETED)
{
/* DELETEs for this CHILD_SA were already exchanged, but it was not yet
* destroyed to allow delayed packets to get processed, or we suppress
* the DELETE explicitly (see above) */
destroy_and_reestablish_internal(this->ike_sa, child_sa, FALSE, FALSE, 0);
message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED);
return SUCCESS;
}
INIT(entry,
.child_sa = child_sa,
.orig_state = child_sa->get_state(child_sa),
);
child_sa->set_state(child_sa, CHILD_DELETING);
this->child_sas->insert_last(this->child_sas, entry);
log_children(this);
build_payloads(this, message);
if (this->expired)
{
child_cfg_t *child_cfg;
DBG1(DBG_IKE, "scheduling CHILD_SA recreate after hard expire");
child_cfg = child_sa->get_config(child_sa);
this->ike_sa->queue_task(this->ike_sa, (task_t*)
child_create_create(this->ike_sa, child_cfg->get_ref(child_cfg),
FALSE, NULL, NULL));
}
return NEED_MORE;
}
/**
* Check if the given CHILD_SA is the SA created by the peer in a rekey
* collision and allow the active rekey task to collect the SPI if it's not yet
* known, in which case it could be for the SA we created in an active rekeying
* that we haven't yet completed.
*/
static child_rekey_collision_t possible_rekey_collision(
private_child_delete_t *this,
child_sa_t *child, uint32_t spi)
{
enumerator_t *tasks;
task_t *task;
child_rekey_t *rekey;
child_rekey_collision_t collision = CHILD_REKEY_COLLISION_NONE;
tasks = this->ike_sa->create_task_enumerator(this->ike_sa,
TASK_QUEUE_ACTIVE);
@ -152,71 +582,17 @@ static bool is_redundant(private_child_delete_t *this, child_sa_t *child)
{
if (task->get_type(task) == TASK_CHILD_REKEY)
{
child_rekey_t *rekey = (child_rekey_t*)task;
if (rekey->is_redundant(rekey, child))
{
tasks->destroy(tasks);
return TRUE;
}
rekey = (child_rekey_t*)task;
collision = rekey->handle_delete(rekey, child, spi);
break;
}
}
tasks->destroy(tasks);
return FALSE;
return collision;
}
/**
* Install the outbound CHILD_SA with the given SPI
*/
static void install_outbound(private_child_delete_t *this,
protocol_id_t protocol, uint32_t spi)
{
child_sa_t *child_sa;
linked_list_t *my_ts, *other_ts;
status_t status;
if (!spi)
{
return;
}
child_sa = this->ike_sa->get_child_sa(this->ike_sa, protocol,
spi, FALSE);
if (!child_sa)
{
DBG1(DBG_IKE, "CHILD_SA not found after rekeying");
return;
}
status = child_sa->install_outbound(child_sa);
if (status != SUCCESS)
{
DBG1(DBG_IKE, "unable to install outbound IPsec SA (SAD) in kernel");
charon->bus->alert(charon->bus, ALERT_INSTALL_CHILD_SA_FAILED,
child_sa);
/* FIXME: delete the new child_sa? */
return;
}
my_ts = linked_list_create_from_enumerator(
child_sa->create_ts_enumerator(child_sa, TRUE));
other_ts = linked_list_create_from_enumerator(
child_sa->create_ts_enumerator(child_sa, FALSE));
DBG0(DBG_IKE, "outbound CHILD_SA %s{%d} established "
"with SPIs %.8x_i %.8x_o and TS %#R === %#R",
child_sa->get_name(child_sa),
child_sa->get_unique_id(child_sa),
ntohl(child_sa->get_spi(child_sa, TRUE)),
ntohl(child_sa->get_spi(child_sa, FALSE)),
my_ts, other_ts);
my_ts->destroy(my_ts);
other_ts->destroy(other_ts);
}
/**
* read in payloads and find the children to delete
* Read payloads and find the children to delete.
*/
static void process_payloads(private_child_delete_t *this, message_t *message)
{
@ -242,8 +618,14 @@ static void process_payloads(private_child_delete_t *this, message_t *message)
spis = delete_payload->create_spi_enumerator(delete_payload);
while (spis->enumerate(spis, &spi))
{
child_rekey_collision_t collision = CHILD_REKEY_COLLISION_NONE;
child_sa = this->ike_sa->get_child_sa(this->ike_sa, protocol,
spi, FALSE);
if (!this->initiator)
{
collision = possible_rekey_collision(this, child_sa, spi);
}
if (!child_sa)
{
DBG1(DBG_IKE, "received DELETE for unknown %N CHILD_SA with"
@ -258,43 +640,29 @@ static void process_payloads(private_child_delete_t *this, message_t *message)
{
continue;
}
INIT(entry,
.child_sa = child_sa
);
switch (child_sa->get_state(child_sa))
else if (this->initiator)
{
case CHILD_REKEYED:
entry->rekeyed = TRUE;
break;
case CHILD_DELETED:
/* already deleted but not yet destroyed, ignore */
case CHILD_DELETING:
/* we don't send back a delete if we already initiated
* a delete ourself */
if (!this->initiator)
{
free(entry);
continue;
}
break;
case CHILD_REKEYING:
/* we reply as usual, rekeying will fail */
case CHILD_INSTALLED:
if (!this->initiator)
{
if (is_redundant(this, child_sa))
{
entry->rekeyed = TRUE;
}
else
{
entry->check_delete_action = TRUE;
}
}
break;
default:
break;
DBG1(DBG_IKE, "ignore DELETE for %N CHILD_SA with SPI "
"%.8x in response, didn't request its deletion",
protocol_id_names, protocol, ntohl(spi));
continue;
}
INIT(entry,
.child_sa = child_sa,
.orig_state = child_sa->get_state(child_sa),
.collision = collision,
);
if (entry->orig_state == CHILD_DELETED ||
entry->orig_state == CHILD_DELETING)
{
/* we either already deleted but have not yet destroyed the
* SA, which we ignore; or we're actively deleting it, in
* which case we don't send back a DELETE either */
free(entry);
continue;
}
child_sa->set_state(child_sa, CHILD_DELETING);
this->child_sas->insert_last(this->child_sas, entry);
}
spis->destroy(spis);
@ -303,213 +671,10 @@ static void process_payloads(private_child_delete_t *this, message_t *message)
payloads->destroy(payloads);
}
/**
* destroy the children listed in this->child_sas, reestablish by policy
*/
static status_t destroy_and_reestablish(private_child_delete_t *this)
{
child_init_args_t args = {};
enumerator_t *enumerator;
entry_t *entry;
child_sa_t *child_sa;
child_cfg_t *child_cfg;
protocol_id_t protocol;
uint32_t spi;
action_t action;
status_t status = SUCCESS;
time_t now, expire;
u_int delay;
now = time_monotonic(NULL);
delay = lib->settings->get_int(lib->settings, "%s.delete_rekeyed_delay",
DELETE_REKEYED_DELAY, lib->ns);
enumerator = this->child_sas->create_enumerator(this->child_sas);
while (enumerator->enumerate(enumerator, (void**)&entry))
{
child_sa = entry->child_sa;
child_sa->set_state(child_sa, CHILD_DELETED);
/* signal child down event if we weren't rekeying */
protocol = child_sa->get_protocol(child_sa);
if (!entry->rekeyed)
{
charon->bus->child_updown(charon->bus, child_sa, FALSE);
}
else
{
/* the following two calls are only relevant as responder/loser of
* rekeyings as the initiator/winner already did this right after
* the rekeying was completed, either way, we delay destroying
* the CHILD_SA, by default, so we can process delayed packets */
install_outbound(this, protocol, child_sa->get_rekey_spi(child_sa));
child_sa->remove_outbound(child_sa);
expire = child_sa->get_lifetime(child_sa, TRUE);
if (delay && (!expire || ((now + delay) < expire)))
{
lib->scheduler->schedule_job(lib->scheduler,
(job_t*)delete_child_sa_job_create_id(
child_sa->get_unique_id(child_sa)), delay);
continue;
}
else if (now < expire)
{ /* let it expire naturally */
continue;
}
/* no delay and no lifetime, destroy it immediately */
}
spi = child_sa->get_spi(child_sa, TRUE);
child_cfg = child_sa->get_config(child_sa);
child_cfg->get_ref(child_cfg);
args.reqid = child_sa->get_reqid_ref(child_sa);
args.label = child_sa->get_label(child_sa);
if (args.label)
{
args.label = args.label->clone(args.label);
}
action = child_sa->get_close_action(child_sa);
this->ike_sa->destroy_child_sa(this->ike_sa, protocol, spi);
if (entry->check_delete_action)
{ /* enforce child_cfg policy if deleted passively */
if (action & ACTION_TRAP)
{
charon->traps->install(charon->traps,
this->ike_sa->get_peer_cfg(this->ike_sa),
child_cfg);
}
if (action & ACTION_START)
{
child_cfg->get_ref(child_cfg);
status = this->ike_sa->initiate(this->ike_sa, child_cfg, &args);
}
}
child_cfg->destroy(child_cfg);
if (args.reqid)
{
charon->kernel->release_reqid(charon->kernel, args.reqid);
}
DESTROY_IF(args.label);
if (status != SUCCESS)
{
break;
}
}
enumerator->destroy(enumerator);
return status;
}
/**
* send closing signals for all CHILD_SAs over the bus
*/
static void log_children(private_child_delete_t *this)
{
linked_list_t *my_ts, *other_ts;
enumerator_t *enumerator;
entry_t *entry;
child_sa_t *child_sa;
uint64_t bytes_in, bytes_out;
enumerator = this->child_sas->create_enumerator(this->child_sas);
while (enumerator->enumerate(enumerator, (void**)&entry))
{
child_sa = entry->child_sa;
my_ts = linked_list_create_from_enumerator(
child_sa->create_ts_enumerator(child_sa, TRUE));
other_ts = linked_list_create_from_enumerator(
child_sa->create_ts_enumerator(child_sa, FALSE));
if (this->expired)
{
DBG0(DBG_IKE, "closing expired CHILD_SA %s{%d} "
"with SPIs %.8x_i %.8x_o and TS %#R === %#R",
child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa),
ntohl(child_sa->get_spi(child_sa, TRUE)),
ntohl(child_sa->get_spi(child_sa, FALSE)), my_ts, other_ts);
}
else
{
child_sa->get_usestats(child_sa, TRUE, NULL, &bytes_in, NULL);
child_sa->get_usestats(child_sa, FALSE, NULL, &bytes_out, NULL);
DBG0(DBG_IKE, "closing CHILD_SA %s{%d} with SPIs %.8x_i "
"(%llu bytes) %.8x_o (%llu bytes) and TS %#R === %#R",
child_sa->get_name(child_sa), child_sa->get_unique_id(child_sa),
ntohl(child_sa->get_spi(child_sa, TRUE)), bytes_in,
ntohl(child_sa->get_spi(child_sa, FALSE)), bytes_out,
my_ts, other_ts);
}
my_ts->destroy(my_ts);
other_ts->destroy(other_ts);
}
enumerator->destroy(enumerator);
}
METHOD(task_t, build_i, status_t,
private_child_delete_t *this, message_t *message)
{
child_sa_t *child_sa;
entry_t *entry;
child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol,
this->spi, TRUE);
if (!child_sa)
{ /* check if it is an outbound sa */
child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol,
this->spi, FALSE);
if (!child_sa)
{ /* child does not exist anymore */
return SUCCESS;
}
/* we work only with the inbound SPI */
this->spi = child_sa->get_spi(child_sa, TRUE);
}
if (this->expired && child_sa->get_state(child_sa) == CHILD_REKEYED)
{ /* the peer was expected to delete this SA, but if we send a DELETE
* we might cause a collision there if the CREATE_CHILD_SA response
* is delayed (the peer wouldn't know if we deleted this SA due to an
* expire or because of a forced delete by the user and might then
* ignore the CREATE_CHILD_SA response once it arrives) */
child_sa->set_state(child_sa, CHILD_DELETED);
install_outbound(this, this->protocol,
child_sa->get_rekey_spi(child_sa));
}
if (child_sa->get_state(child_sa) == CHILD_DELETED)
{ /* DELETEs for this CHILD_SA were already exchanged, but it was not yet
* destroyed to allow delayed packets to get processed */
this->ike_sa->destroy_child_sa(this->ike_sa, this->protocol, this->spi);
message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED);
return SUCCESS;
}
INIT(entry,
.child_sa = child_sa,
.rekeyed = child_sa->get_state(child_sa) == CHILD_REKEYED,
);
this->child_sas->insert_last(this->child_sas, entry);
log_children(this);
build_payloads(this, message);
if (!entry->rekeyed && this->expired)
{
child_cfg_t *child_cfg;
DBG1(DBG_IKE, "scheduling CHILD_SA recreate after hard expire");
child_cfg = child_sa->get_config(child_sa);
this->ike_sa->queue_task(this->ike_sa, (task_t*)
child_create_create(this->ike_sa, child_cfg->get_ref(child_cfg),
FALSE, NULL, NULL));
}
return NEED_MORE;
}
METHOD(task_t, process_i, status_t,
private_child_delete_t *this, message_t *message)
{
process_payloads(this, message);
DBG1(DBG_IKE, "CHILD_SA closed");
return destroy_and_reestablish(this);
}
@ -525,7 +690,6 @@ METHOD(task_t, build_r, status_t,
private_child_delete_t *this, message_t *message)
{
build_payloads(this, message);
DBG1(DBG_IKE, "CHILD_SA closed");
return destroy_and_reestablish(this);
}
@ -535,19 +699,6 @@ METHOD(task_t, get_type, task_type_t,
return TASK_CHILD_DELETE;
}
METHOD(child_delete_t , get_child, child_sa_t*,
private_child_delete_t *this)
{
child_sa_t *child_sa = NULL;
entry_t *entry;
if (this->child_sas->get_first(this->child_sas, (void**)&entry) == SUCCESS)
{
child_sa = entry->child_sa;
}
return child_sa;
}
METHOD(task_t, migrate, void,
private_child_delete_t *this, ike_sa_t *ike_sa)
{
@ -579,7 +730,6 @@ child_delete_t *child_delete_create(ike_sa_t *ike_sa, protocol_id_t protocol,
.migrate = _migrate,
.destroy = _destroy,
},
.get_child = _get_child,
},
.ike_sa = ike_sa,
.child_sas = linked_list_create(),

View File

@ -1,4 +1,5 @@
/*
* Copyright (C) 2022 Tobias Brunner
* Copyright (C) 2007 Martin Willi
*
* Copyright (C) secunet Security Networks AG
@ -25,8 +26,8 @@
typedef struct child_delete_t child_delete_t;
#include <library.h>
#include <sa/ike_sa.h>
#include <sa/task.h>
#include <sa/ike_sa.h>
#include <sa/child_sa.h>
/**
@ -38,13 +39,6 @@ struct child_delete_t {
* Implements the task_t interface
*/
task_t task;
/**
* Get the CHILD_SA to delete by this task.
*
* @return child_sa
*/
child_sa_t* (*get_child) (child_delete_t *this);
};
/**
@ -59,4 +53,35 @@ struct child_delete_t {
child_delete_t *child_delete_create(ike_sa_t *ike_sa, protocol_id_t protocol,
uint32_t spi, bool expired);
/**
* Destroy the given CHILD_SA and trigger events and configured actions.
*
* @param ike_sa IKE_SA the child_sa belongs to
* @param child_sa CHILD_SA to destroy and potentially reestablish
* @return status of reestablishment
*/
status_t child_delete_destroy_and_reestablish(ike_sa_t *ike_sa,
child_sa_t *child_sa);
/**
* Destroy the given CHILD_SA and trigger events and force a recreation.
*
* @param ike_sa IKE_SA the child_sa belongs to
* @param child_sa CHILD_SA to destroy and reestablish
* @return status of reestablishment
*/
status_t child_delete_destroy_and_force_reestablish(ike_sa_t *ike_sa,
child_sa_t *child_sa);
/**
* Destroy the given CHILD_SA with a configured delay, so delayed inbound
* packets can still be processed.
*
* @note The outbound SA should already be uninstalled when calling this.
*
* @param ike_sa IKE_SA the child_sa belongs to
* @param child_sa CHILD_SA to destroy and potentially reestablish
*/
void child_delete_destroy_rekeyed(ike_sa_t *ike_sa, child_sa_t *child_sa);
#endif /** CHILD_DELETE_H_ @}*/

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2016-2020 Tobias Brunner
* Copyright (C) 2016-2022 Tobias Brunner
* Copyright (C) 2007 Martin Willi
*
* Copyright (C) secunet Security Networks AG
@ -24,12 +24,25 @@
#define CHILD_REKEY_H_
typedef struct child_rekey_t child_rekey_t;
typedef enum child_rekey_collision_t child_rekey_collision_t;
#include <library.h>
#include <sa/ike_sa.h>
#include <sa/child_sa.h>
#include <sa/task.h>
/**
* Type of collision an active rekey task may have with an inbound DELETE.
*/
enum child_rekey_collision_t {
/** Unrelated SA or unknown SPI (might be for the SA this task creates) */
CHILD_REKEY_COLLISION_NONE = 0,
/** Deleted SA is the one created by the peer in a collision */
CHILD_REKEY_COLLISION_PEER,
/** Deleted SA is the SA the active task is rekeying */
CHILD_REKEY_COLLISION_OLD,
};
/**
* Task of type TASK_CHILD_REKEY, rekey an established CHILD_SA.
*/
@ -41,21 +54,27 @@ struct child_rekey_t {
task_t task;
/**
* Check if the given SA is the redundant CHILD_SA created during a rekey
* collision.
* Handle a DELETE for the given CHILD_SA/SPI that might be related to
* this rekeiyng if the CREATE_CHILD_SA response is delayed.
*
* This is called if the other peer deletes the redundant SA before we were
* able to handle the CREATE_CHILD_SA response.
* This checks if the given SA is the CHILD_SA created by the peer during a
* rekey collision or if it's for the old SA.
*
* @param child CHILD_SA to check
* @return TRUE if the SA is the redundant CHILD_SA
* If child is NULL, the SPI is collected as it might be for the SA this
* task is actively creating (the peer sends the inbound SA we don't know
* yet).
*
* @param child CHILD_SA to check
* @param spi SPI in case child is not known
* @return type of collision
*/
bool (*is_redundant)(child_rekey_t *this, child_sa_t *child);
child_rekey_collision_t (*handle_delete)(child_rekey_t *this,
child_sa_t *child, uint32_t spi);
/**
* Register a rekeying/delete task which collides with this one
* Register a rekey task which collides with this one.
*
* If two peers initiate rekeying at the same time, the collision must
* If two peers initiate rekeyings at the same time, the collision must
* be handled gracefully. The task manager is aware of what exchanges
* are going on and notifies the active task by passing the passive.
*
@ -77,4 +96,16 @@ struct child_rekey_t {
child_rekey_t *child_rekey_create(ike_sa_t *ike_sa, protocol_id_t protocol,
uint32_t spi);
/**
* Conclude the rekeying for the given CHILD_SAs by installing the outbound
* SA for the new CHILD_SA, uninstalling the one for the old and triggering
* an appropriate log message and event.
*
* @param old the old CHILD_SA
* @param new the new CHILD_SA
* @return TRUE if new outbound SA installed successfully
*/
bool child_rekey_conclude_rekeying(child_sa_t *old, child_sa_t *new);
#endif /** CHILD_REKEY_H_ @}*/

File diff suppressed because it is too large Load Diff