diff --git a/src/libcharon/sa/child_sa.c b/src/libcharon/sa/child_sa.c index 97ee88acbc..1f50c49520 100644 --- a/src/libcharon/sa/child_sa.c +++ b/src/libcharon/sa/child_sa.c @@ -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, diff --git a/src/libcharon/sa/child_sa.h b/src/libcharon/sa/child_sa.h index 0b7d111142..7c3763b0a9 100644 --- a/src/libcharon/sa/child_sa.h +++ b/src/libcharon/sa/child_sa.h @@ -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 diff --git a/src/libcharon/sa/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c index c5cc34f0e7..30dba22db3 100644 --- a/src/libcharon/sa/ikev2/task_manager_v2.c +++ b/src/libcharon/sa/ikev2/task_manager_v2.c @@ -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); diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index 132c3de469..c3a12780f6 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -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, diff --git a/src/libcharon/sa/ikev2/tasks/child_create.h b/src/libcharon/sa/ikev2/tasks/child_create.h index 2e8c8bed74..eae39e61ca 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.h +++ b/src/libcharon/sa/ikev2/tasks/child_create.h @@ -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. * diff --git a/src/libcharon/sa/ikev2/tasks/child_delete.c b/src/libcharon/sa/ikev2/tasks/child_delete.c index eff0e6cc60..e2e198b285 100644 --- a/src/libcharon/sa/ikev2/tasks/child_delete.c +++ b/src/libcharon/sa/ikev2/tasks/child_delete.c @@ -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(), diff --git a/src/libcharon/sa/ikev2/tasks/child_delete.h b/src/libcharon/sa/ikev2/tasks/child_delete.h index ca57ae9cfa..6dc2141cb0 100644 --- a/src/libcharon/sa/ikev2/tasks/child_delete.h +++ b/src/libcharon/sa/ikev2/tasks/child_delete.h @@ -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 -#include #include +#include #include /** @@ -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_ @}*/ diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 6ffeb52c50..6c73d0671a 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -19,6 +19,7 @@ #include "child_rekey.h" #include +#include #include #include #include @@ -79,10 +80,16 @@ struct private_child_rekey_t { child_sa_t *child_sa; /** - * colliding task, may be delete or rekey + * Colliding passive rekey task */ task_t *collision; + /** + * SPIs of SAs the peer deleted and we haven't found while this task was + * active + */ + array_t *deleted_spis; + /** * State flags */ @@ -95,19 +102,33 @@ struct private_child_rekey_t { CHILD_REKEY_FOLLOWUP_KE = (1<<0), /** - * Set if we adopted a completed passive task, otherwise (i.e. for - * multi-KE rekeyings) we just reference it. + * Set if the passive rekey task is completed and we adopted it, + * otherwise (i.e. for multi-KE rekeyings) we just reference it. */ - CHILD_REKEY_ADOPTED_PASSIVE = (1<<1), + CHILD_REKEY_PASSIVE_INSTALLED = (1<<1), /** - * Indicate that the peer destroyed the redundant child from a - * collision. This happens if a peer's delete notification for the - * redundant child gets processed before the active rekey job is - * complete. If so, we must not touch the child created in the collision - * since it points to memory already freed. + * Indicates that the peer sent a DELETE for its own CHILD_SA of a + * collision. In regular rekeyings, this happens if a peer lost and + * the delete for the redundant SA gets processed before the active + * rekey job is complete. It could also mean the peer deleted its + * winning SA. */ - CHILD_REKEY_OTHER_DESTROYED = (1<<2), + CHILD_REKEY_OTHER_DELETED = (1<<2), + + /** + * Indicates that the peer sent a DELETE for the rekeyed/old CHILD_SA. + * This happens if the peer has won the rekey collision, but it might + * also happen if it incorrectly sent one after it replied to our + * CREATE_CHILD_SA request and the DELETE arrived before that response. + */ + CHILD_REKEY_OLD_SA_DELETED = (1<<3), + + /** + * After handling the collision, this indicates whether the peer deleted + * the winning replacement SA (either ours or its own). + */ + CHILD_REKEY_REPLACEMENT_DELETED = (1<<4), } flags; }; @@ -130,58 +151,90 @@ static void schedule_delayed_rekey(private_child_rekey_t *this) lib->scheduler->schedule_job(lib->scheduler, job, retry); } -/** - * Implementation of task_t.build for initiator, after rekeying - */ -static status_t build_i_delete(private_child_rekey_t *this, message_t *message) +METHOD(task_t, build_i_delete, status_t, + private_child_rekey_t *this, message_t *message) { /* update exchange type to INFORMATIONAL for the delete */ message->set_exchange_type(message, INFORMATIONAL); - return this->child_delete->task.build(&this->child_delete->task, message); } -/** - * Implementation of task_t.process for initiator, after rekeying - */ -static status_t process_i_delete(private_child_rekey_t *this, message_t *message) +METHOD(task_t, process_i_delete, status_t, + private_child_rekey_t *this, message_t *message) { return this->child_delete->task.process(&this->child_delete->task, message); } /** - * find a child using the REKEY_SA notify + * In failure cases, we don't use a child_delete task, but handle the deletes + * ourselves for more flexibility (in particular, adding multiple DELETE + * payloads to a single message). */ -static void find_child(private_child_rekey_t *this, message_t *message) +static void build_delete_old_sa(private_child_rekey_t *this, message_t *message) { - notify_payload_t *notify; + delete_payload_t *del; protocol_id_t protocol; uint32_t spi; - child_sa_t *child_sa; - notify = message->get_notify(message, REKEY_SA); - if (notify) + message->set_exchange_type(message, INFORMATIONAL); + + protocol = this->child_sa->get_protocol(this->child_sa); + spi = this->child_sa->get_spi(this->child_sa, TRUE); + + del = delete_payload_create(PLV2_DELETE, protocol); + del->add_spi(del, spi); + message->add_payload(message, (payload_t*)del); + + DBG1(DBG_IKE, "sending DELETE for %N CHILD_SA with SPI %.8x", + protocol_id_names, protocol, ntohl(spi)); +} + +METHOD(task_t, build_i_delete_replacement, status_t, + private_child_rekey_t *this, message_t *message) +{ + /* add the delete for the replacement we failed to create locally but the + * peer probably already has installed */ + this->child_create->task.build(&this->child_create->task, message); + return SUCCESS; +} + +METHOD(task_t, build_i_delete_old_destroy, status_t, + private_child_rekey_t *this, message_t *message) +{ + /* send the delete but then immediately destroy and possibly recreate the + * CHILD_SA as the peer deleted its replacement. treat this like the peer + * sent a delete for the original SA */ + build_delete_old_sa(this, message); + child_delete_destroy_and_reestablish(this->ike_sa, this->child_sa); + return SUCCESS; +} + +/** + * Delete either both or only the replacement SA and then destroy and recreate + * the old SA. + */ +static status_t build_delete_recreate(private_child_rekey_t *this, + message_t *message, bool delete_old) +{ + if (delete_old) { - protocol = notify->get_protocol_id(notify); - spi = notify->get_spi(notify); - - if (protocol == PROTO_ESP || protocol == PROTO_AH) - { - child_sa = this->ike_sa->get_child_sa(this->ike_sa, protocol, - spi, FALSE); - /* ignore rekeyed/deleted CHILD_SAs we keep around */ - if (child_sa && - child_sa->get_state(child_sa) != CHILD_DELETED) - { - this->child_sa = child_sa; - } - } - if (!this->child_sa) - { - this->protocol = protocol; - this->spi_data = chunk_clone(notify->get_spi_data(notify)); - } + build_delete_old_sa(this, message); } + this->child_create->task.build(&this->child_create->task, message); + child_delete_destroy_and_force_reestablish(this->ike_sa, this->child_sa); + return SUCCESS; +} + +METHOD(task_t, build_i_delete_replacement_recreate, status_t, + private_child_rekey_t *this, message_t *message) +{ + return build_delete_recreate(this, message, FALSE); +} + +METHOD(task_t, build_i_delete_both_recreate, status_t, + private_child_rekey_t *this, message_t *message) +{ + return build_delete_recreate(this, message, TRUE); } METHOD(task_t, build_i, status_t, @@ -266,6 +319,41 @@ METHOD(task_t, build_i, status_t, return NEED_MORE; } +/** + * Find a CHILD_SA using the REKEY_SA notify + */ +static void find_child(private_child_rekey_t *this, message_t *message) +{ + notify_payload_t *notify; + protocol_id_t protocol; + uint32_t spi; + child_sa_t *child_sa; + + notify = message->get_notify(message, REKEY_SA); + if (notify) + { + protocol = notify->get_protocol_id(notify); + spi = notify->get_spi(notify); + + if (protocol == PROTO_ESP || protocol == PROTO_AH) + { + child_sa = this->ike_sa->get_child_sa(this->ike_sa, protocol, + spi, FALSE); + /* ignore rekeyed/deleted CHILD_SAs we keep around */ + if (child_sa && + child_sa->get_state(child_sa) != CHILD_DELETED) + { + this->child_sa = child_sa; + } + } + if (!this->child_sa) + { + this->protocol = protocol; + this->spi_data = chunk_clone(notify->get_spi_data(notify)); + } + } +} + METHOD(task_t, process_r, status_t, private_child_rekey_t *this, message_t *message) { @@ -318,7 +406,7 @@ METHOD(task_t, build_r, status_t, child_sa_t *child_sa; child_sa_state_t state = CHILD_INSTALLED; uint32_t reqid; - bool followup_sent; + bool followup_sent = FALSE; if (!this->child_sa) { @@ -332,7 +420,9 @@ METHOD(task_t, build_r, status_t, } if (this->child_sa->get_state(this->child_sa) == CHILD_DELETING) { - DBG1(DBG_IKE, "unable to rekey, we are deleting the CHILD_SA"); + DBG1(DBG_IKE, "unable to rekey CHILD_SA %s{%u}, we are deleting it", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); return SUCCESS; } @@ -379,11 +469,11 @@ METHOD(task_t, build_r, status_t, if (child_sa && child_sa->get_state(child_sa) == CHILD_INSTALLED) { this->child_sa->set_state(this->child_sa, CHILD_REKEYED); - this->child_sa->set_rekey_spi(this->child_sa, - child_sa->get_spi(child_sa, FALSE)); - - /* FIXME: this might trigger twice if there was a collision */ - charon->bus->child_rekey(charon->bus, this->child_sa, child_sa); + /* link the SAs to handle possible collisions */ + this->child_sa->set_rekey_sa(this->child_sa, child_sa); + child_sa->set_rekey_sa(child_sa, this->child_sa); + /* like installing the outbound SA, we only trigger the child-rekey + * event once the old SA is deleted */ } else if (this->child_sa->get_state(this->child_sa) == CHILD_REKEYING) { /* rekeying failed, reuse old child */ @@ -392,9 +482,40 @@ METHOD(task_t, build_r, status_t, return SUCCESS; } +/** + * Check if the peer deleted the replacement SA we created while we waited for + * its completion. + */ +static bool is_our_replacement_deleted(private_child_rekey_t *this) +{ + uint32_t spi, peer_spi; + int i; + + if (!this->deleted_spis) + { + return FALSE; + } + + peer_spi = this->child_create->get_other_spi(this->child_create); + if (!peer_spi) + { + return FALSE; + } + + for (i = 0; i < array_count(this->deleted_spis); i++) + { + array_get(this->deleted_spis, i, &spi); + if (spi == peer_spi) + { + return TRUE; + } + } + return FALSE; +} + /** * Remove the passive rekey task that's waiting for IKE_FOLLOWUP_KE requests - * that will never come. + * that will never come if we won the collision. */ static void remove_passive_rekey_task(private_child_rekey_t *this) { @@ -416,42 +537,42 @@ static void remove_passive_rekey_task(private_child_rekey_t *this) } /** - * Handle a rekey collision + * Compare the nonces to determine if we lost the rekey collision. + * The SA with the lowest nonce should be deleted (if already complete), this + * checks if we or the peer created it */ -static child_sa_t *handle_collision(private_child_rekey_t *this, - child_sa_t **to_install, bool multi_ke) +static bool lost_collision(private_child_rekey_t *this) { private_child_rekey_t *other = (private_child_rekey_t*)this->collision; chunk_t this_nonce, other_nonce; - child_sa_t *to_delete, *child_sa; - if (this->collision->get_type(this->collision) == TASK_CHILD_DELETE) - { /* CHILD_DELETE, which we only adopt if it is for the CHILD_SA we are - * ourselves rekeying */ - to_delete = this->child_create->get_child(this->child_create); - if (multi_ke) - { - DBG1(DBG_IKE, "CHILD_SA rekey/delete collision, abort incomplete " - "multi-KE rekeying"); - } - else - { - DBG1(DBG_IKE, "CHILD_SA rekey/delete collision, deleting redundant " - "child %s{%d}", to_delete->get_name(to_delete), - to_delete->get_unique_id(to_delete)); - } - return to_delete; + if (!other) + { + return FALSE; } this_nonce = this->child_create->get_lower_nonce(this->child_create); other_nonce = other->child_create->get_lower_nonce(other->child_create); - /* the SA with the lowest nonce should be deleted (if already complete), - * check if we or the peer created it */ - if (memcmp(this_nonce.ptr, other_nonce.ptr, - min(this_nonce.len, other_nonce.len)) < 0) + return memcmp(this_nonce.ptr, other_nonce.ptr, + min(this_nonce.len, other_nonce.len)) < 0; +} + +/** + * Handle a rekey collision. Returns TRUE if we won the collision or there + * wasn't one. Also returns the SA that should be deleted and the winning SA + * of the collision, if any. + */ +static bool handle_collision(private_child_rekey_t *this, + child_sa_t **to_delete, child_sa_t **winning_sa, + bool multi_ke) +{ + private_child_rekey_t *other = (private_child_rekey_t*)this->collision; + child_sa_t *other_sa; + + if (lost_collision(this)) { - to_delete = this->child_create->get_child(this->child_create); + *to_delete = this->child_create->get_child(this->child_create); if (multi_ke) { DBG1(DBG_IKE, "CHILD_SA rekey collision lost, abort incomplete " @@ -460,38 +581,68 @@ static child_sa_t *handle_collision(private_child_rekey_t *this, else { DBG1(DBG_IKE, "CHILD_SA rekey collision lost, deleting " - "redundant child %s{%d}", to_delete->get_name(to_delete), - to_delete->get_unique_id(to_delete)); + "redundant child %s{%u}", (*to_delete)->get_name(*to_delete), + (*to_delete)->get_unique_id(*to_delete)); } - return to_delete; + /* check if the passive rekeying is completed */ + if (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) + { + *winning_sa = other->child_create->get_child(other->child_create); + + if (this->flags & CHILD_REKEY_OTHER_DELETED) + { + /* the peer deleted its own replacement SA while we waited + * for a response, set a flag to destroy the SA accordingly */ + this->flags |= CHILD_REKEY_REPLACEMENT_DELETED; + /* if the peer has not triggered a rekey event yet by deleting + * its own SA before deleting the old SA (if it did so at all), + * we trigger that now so listeners can track this properly */ + if (!(this->flags & CHILD_REKEY_OLD_SA_DELETED) || + (*winning_sa)->get_outbound_state(*winning_sa) != CHILD_OUTBOUND_INSTALLED) + { + charon->bus->child_rekey(charon->bus, this->child_sa, + *winning_sa); + } + } + /* check if the peer already sent a delete for the old SA */ + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + } + else if (this->flags & CHILD_REKEY_OTHER_DELETED) + { + /* make sure the old SA is in the correct state if the peer + * deleted its own SA but not yet the old one (weird, but who + * knows...) */ + this->child_sa->set_state(this->child_sa, CHILD_REKEYED); + } + } + return FALSE; } - *to_install = this->child_create->get_child(this->child_create); - to_delete = this->child_sa; + *winning_sa = this->child_create->get_child(this->child_create); + *to_delete = this->child_sa; + + /* regular rekeying without collision (or we already concluded it for a + * multi-KE rekeying), check if the peer deleted the new SA already */ + if (!this->collision) + { + if (is_our_replacement_deleted(this)) + { + this->flags |= CHILD_REKEY_REPLACEMENT_DELETED; + /* since we will destroy the winning SA, we have to trigger a rekey + * event before so listeners can track this properly */ + charon->bus->child_rekey(charon->bus, this->child_sa, *winning_sa); + } + return TRUE; + } /* the passive rekeying is complete only if it was single-KE. otherwise, * the peer would either have stopped before sending IKE_FOLLOWUP_KE when * it noticed it lost, or it responded with TEMPORARY_FAILURE to our * CREATE_CHILD_SA request if it already started sending them. */ - if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + if (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) { - /* we don't want to install the peer's redundant outbound SA */ - this->child_sa->set_rekey_spi(this->child_sa, 0); - /* don't touch child other created if it has already been deleted */ - if (!(this->flags & CHILD_REKEY_OTHER_DESTROYED)) - { - /* disable close action and updown event for redundant child the - * other is expected to delete */ - child_sa = other->child_create->get_child(other->child_create); - if (child_sa) - { - child_sa->set_close_action(child_sa, ACTION_NONE); - if (child_sa->get_state(child_sa) != CHILD_REKEYED) - { - child_sa->set_state(child_sa, CHILD_REKEYED); - } - } - } if (multi_ke) { DBG1(DBG_IKE, "CHILD_SA rekey collision won, continue with " @@ -502,8 +653,51 @@ static child_sa_t *handle_collision(private_child_rekey_t *this, else { DBG1(DBG_IKE, "CHILD_SA rekey collision won, deleting old child " - "%s{%d}", to_delete->get_name(to_delete), - to_delete->get_unique_id(to_delete)); + "%s{%u}", (*to_delete)->get_name(*to_delete), + (*to_delete)->get_unique_id(*to_delete)); + } + + other_sa = other->child_create->get_child(other->child_create); + + /* check if the peer already sent a delete for our winning SA */ + if (is_our_replacement_deleted(this)) + { + this->flags |= CHILD_REKEY_REPLACEMENT_DELETED; + /* similar to the case above, but here the peer might already have + * deleted its redundant SA, and it might have sent an incorrect + * delete for the old SA. if it did the latter first, then we will + * have concluded the rekeying and there was a rekey event from the + * old SA to the redundant one that we have to consider here */ + if (this->flags & CHILD_REKEY_OLD_SA_DELETED && other_sa && + other_sa->get_outbound_state(other_sa) == CHILD_OUTBOUND_INSTALLED) + { + charon->bus->child_rekey(charon->bus, other_sa, *winning_sa); + } + else + { + charon->bus->child_rekey(charon->bus, this->child_sa, + *winning_sa); + } + } + + /* check if the peer already sent a delete for its redundant SA */ + if (!(this->flags & CHILD_REKEY_OTHER_DELETED)) + { + /* unlink the redundant SA the peer is expected to delete, disable + * events and make sure the outbound SA isn't installed/registered */ + this->child_sa->set_rekey_sa(this->child_sa, NULL); + if (other_sa) + { + other_sa->set_rekey_sa(other_sa, NULL); + other_sa->set_state(other_sa, CHILD_REKEYED); + other_sa->remove_outbound(other_sa); + } + } + else if (other_sa) + { + /* the peer already deleted its redundant SA, but we have not yet + * destroyed it, do so now */ + child_delete_destroy_rekeyed(this->ike_sa, other_sa); } this->collision->destroy(this->collision); } @@ -525,7 +719,181 @@ static child_sa_t *handle_collision(private_child_rekey_t *this, remove_passive_rekey_task(this); } this->collision = NULL; - return to_delete; + return TRUE; +} + +/** + * Check if we can ignore a CHILD_SA_NOT_FOUND notify and log appropriate + * messages. + */ +static bool ignore_child_sa_not_found(private_child_rekey_t *this) +{ + private_child_rekey_t *other; + child_sa_t *other_sa; + + /* if the peer hasn't explicitly sent a delete for the CHILD_SA it wasn't + * able to find now, it might have lost the state, we can't ignore that and + * create a replacement */ + if (!(this->flags & CHILD_REKEY_OLD_SA_DELETED)) + { + DBG1(DBG_IKE, "peer didn't find CHILD_SA %s{%u} we tried to rekey, " + "create a replacement", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + return FALSE; + } + + /* if the peer explicitly deleted the original CHILD_SA before our request + * arrived, we adhere to that wish and close the SA. + * this is the case where the peer received the DELETE response before + * our rekey request, see below for the case where it hasn't received the + * response yet and responded with TEMPORARY_FAILURE */ + if (!this->collision) + { + DBG1(DBG_IKE, "closing CHILD_SA %s{%u} we tried to rekey because " + "the peer deleted it before it received our request", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + child_delete_destroy_and_reestablish(this->ike_sa, this->child_sa); + return TRUE; + } + + /* if there was a rekey collision and the peer deleted the original CHILD_SA + * before our request arrived and it has not deleted the new SA, we just + * abort our own rekeying and use the peer's replacement */ + if (!(this->flags & CHILD_REKEY_OTHER_DELETED)) + { + DBG1(DBG_IKE, "abort active rekeying for CHILD_SA %s{%u} because " + "it was successfully rekeyed by the peer before it received " + "our request", this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + return TRUE; + } + + /* the peer successfully rekeyed the same SA, deleted it, but then also + * deleted the CHILD_SA it created as replacement. adhere to that wish and + * close the replacement */ + other = (private_child_rekey_t*)this->collision; + other_sa = other->child_create->get_child(other->child_create); + + DBG1(DBG_IKE, "abort active rekeying for CHILD_SA %s{%u} because the other " + "peer already deleted its replacement CHILD_SA %s{%u} before " + "it received our request", this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa), + other_sa->get_name(other_sa), other_sa->get_unique_id(other_sa)); + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + child_delete_destroy_and_reestablish(this->ike_sa, other_sa); + return TRUE; +} + +/** + * Check if we can ignore failures to create the new CHILD_SA e.g. due to an + * error notify like TEMPORARY_FAILURE and log appropriate messages. + */ +static bool ignore_child_sa_failure(private_child_rekey_t *this) +{ + /* we are fine if there was a successful passive rekeying. the peer might + * not have detected the collision and responded with a TEMPORARY_FAILURE + * notify while deleting the old SA, which conflicted with our request */ + if (this->collision && (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) && + !(this->flags & CHILD_REKEY_OTHER_DELETED)) + { + DBG1(DBG_IKE, "abort active rekeying for CHILD_SA %s{%u} because " + "the peer successfully rekeyed it before receiving our request%s", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa), + this->flags & CHILD_REKEY_OLD_SA_DELETED ? "" + : ", waiting for delete"); + + /* if the peer already deleted the rekeyed SA, destroy it, otherwise + * just wait for the delete */ + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + } + return TRUE; + } + + /* if the peer initiated a delete for the old SA before our rekey request + * reached it, the expected response is TEMPORARY_FAILURE. adhere to that + * wish and abort the rekeying. + * this is the case where the peer has not yet received the DELETE response + * when our rekey request arrived, see above for the case where it has + * already received the response and responded with CHILD_SA_NOT_FOUND */ + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + DBG1(DBG_IKE, "closing CHILD_SA %s{%u} we tried to rekey because " + "the peer started to delete it before receiving our request", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + child_delete_destroy_and_reestablish(this->ike_sa, this->child_sa); + return TRUE; + } + return FALSE; +} + +/** + * Check if we can ignore local failures to create the new CHILD_SA e.g. due to + * a KE or kernel problem and log an appropriate message. + */ +static status_t handle_local_failure(private_child_rekey_t *this) +{ + /* if we lost the collision, we are expected to delete the failed SA + * anyway, so just do that and rely on the passive rekeying, which + * deletes the old SA (or has already done so, in which case we destroy the + * SA now) */ + if (this->collision && lost_collision(this)) + { + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + } + this->public.task.build = _build_i_delete_replacement; + return NEED_MORE; + } + + /* the peer sent a delete for our winning replacement SA, no need to send a + * delete for it again and adhere to this wish to delete the SA. + * however, we are expected to send a delete for the original SA, unless, + * it was already deleted by the peer as well (which would be incorrect) */ + if (is_our_replacement_deleted(this)) + { + DBG1(DBG_IKE, "closing CHILD_SA %s{%u} we tried to rekey because " + "the peer meanwhile sent a delete for its replacement", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + child_delete_destroy_and_reestablish(this->ike_sa, this->child_sa); + return SUCCESS; + } + this->public.task.build = _build_i_delete_old_destroy; + return NEED_MORE; + } + + /* as the winner of the collision or if there wasn't one, we're expected to + * delete the original SA, but we also want to recreate it because we + * failed to install the replacement. because the peer already has the + * replacement partially installed, we also need to send a delete for the + * failed one */ + this->public.task.build = _build_i_delete_both_recreate; + + if (this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + /* the peer already sent an incorrect delete for the original SA that + * arrived before the response to the rekeying, delete only the failed + * replacement and recreate the SA */ + DBG1(DBG_IKE, "peer sent an incorrect delete for CHILD_SA %s{%u} after " + "responding to our rekeying", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + this->public.task.build = _build_i_delete_replacement_recreate; + } + DBG1(DBG_IKE, "closing and recreating CHILD_SA %s{%u} after failing to " + "install replacement", this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + return NEED_MORE; } METHOD(task_t, process_i, status_t, @@ -533,7 +901,8 @@ METHOD(task_t, process_i, status_t, { protocol_id_t protocol; uint32_t spi; - child_sa_t *child_sa, *to_delete = NULL, *to_install = NULL; + child_sa_t *child_sa, *to_delete = NULL, *winning_sa = NULL; + bool collision_won; if (message->get_notify(message, NO_ADDITIONAL_SAS)) { @@ -547,74 +916,52 @@ METHOD(task_t, process_i, status_t, } if (message->get_notify(message, CHILD_SA_NOT_FOUND)) { - child_cfg_t *child_cfg; - child_init_args_t args = {}; - status_t status; - - if (this->collision && - this->collision->get_type(this->collision) == TASK_CHILD_DELETE) - { /* ignore this error if we already deleted the CHILD_SA on the - * peer's behalf (could happen if the other peer does not detect - * the collision and did not respond with TEMPORARY_FAILURE) */ + /* ignore CHILD_SA_NOT_FOUND error notify in some cases, otherwise + * create a replacement SA */ + if (ignore_child_sa_not_found(this)) + { return SUCCESS; } - DBG1(DBG_IKE, "peer didn't find the CHILD_SA we tried to rekey"); - /* FIXME: according to RFC 7296 we should only create a new CHILD_SA if - * it does not exist yet, we currently have no good way of checking for - * that (we could go by name, but that might be tricky e.g. due to - * narrowing) */ - spi = this->child_sa->get_spi(this->child_sa, TRUE); - protocol = this->child_sa->get_protocol(this->child_sa); - child_cfg = this->child_sa->get_config(this->child_sa); - child_cfg->get_ref(child_cfg); - args.reqid = this->child_sa->get_reqid_ref(this->child_sa); - args.label = this->child_sa->get_label(this->child_sa); - if (args.label) - { - args.label = args.label->clone(args.label); - } - charon->bus->child_updown(charon->bus, this->child_sa, FALSE); - this->ike_sa->destroy_child_sa(this->ike_sa, protocol, spi); - status = this->ike_sa->initiate(this->ike_sa, - child_cfg->get_ref(child_cfg), &args); - if (args.reqid) - { - charon->kernel->release_reqid(charon->kernel, args.reqid); - } - DESTROY_IF(args.label); - return status; + return child_delete_destroy_and_force_reestablish(this->ike_sa, + this->child_sa); } if (this->child_create->task.process(&this->child_create->task, message) == NEED_MORE) { - if (message->get_notify(message, INVALID_KE_PAYLOAD) || - !this->child_create->get_child(this->child_create)) - { /* bad key exchange mechanism, retry, or failure requiring delete */ - return NEED_MORE; + if (message->get_notify(message, INVALID_KE_PAYLOAD)) + { + /* invalid KE method => retry, unless we can ignore it */ + return ignore_child_sa_failure(this) ? SUCCESS : NEED_MORE; } + else if (!this->child_create->get_child(this->child_create)) + { + /* local failure requiring a delete, check what we have to do */ + return handle_local_failure(this); + } + /* multiple key exchanges */ this->flags |= CHILD_REKEY_FOLLOWUP_KE; /* there will only be a collision while we process a CREATE_CHILD_SA - * response, later we just respond with TEMPORARY_FAILURE and ignore - * the passive task - if we lost, the returned SA is the one we created - * in this task, since it's not complete yet, we abort the task */ - if (this->collision) + * response, later we just respond with TEMPORARY_FAILURE, so handle + * it now */ + if (!handle_collision(this, &to_delete, &winning_sa, TRUE)) { - to_delete = handle_collision(this, &to_install, TRUE); + /* we lost the collision. since the SA is not complete yet, we just + * abort the task */ + return SUCCESS; } - return (to_delete && to_delete != this->child_sa) ? SUCCESS : NEED_MORE; + return NEED_MORE; } child_sa = this->child_create->get_child(this->child_create); if (!child_sa || child_sa->get_state(child_sa) != CHILD_INSTALLED) { - /* establishing new child failed, reuse old and try again. but not when - * we received a delete in the meantime or passively rekeyed the SA */ - if (!this->collision || - (this->collision->get_type(this->collision) != TASK_CHILD_DELETE && - !(this->flags & CHILD_REKEY_ADOPTED_PASSIVE))) + /* check if we can ignore remote errors like TEMPORARY_FAILURE */ + if (!ignore_child_sa_failure(this)) { + /* otherwise (e.g. for an IKE/CHILD rekey collision), reuse the old + * CHILD_SA and try again */ schedule_delayed_rekey(this); } return SUCCESS; @@ -622,143 +969,195 @@ METHOD(task_t, process_i, status_t, /* there won't be a collision if this task is for a multi-KE rekeying, as a * collision during CREATE_CHILD_SA was cleaned up above */ - if (this->collision) - { - to_delete = handle_collision(this, &to_install, FALSE); - } - else - { - to_install = this->child_create->get_child(this->child_create); - to_delete = this->child_sa; - } - if (to_install) - { - if (to_install->install_outbound(to_install) != SUCCESS) - { - DBG1(DBG_IKE, "unable to install outbound IPsec SA (SAD) in kernel"); - charon->bus->alert(charon->bus, ALERT_INSTALL_CHILD_SA_FAILED, - to_install); - /* FIXME: delete the child_sa? fail the task? */ - } - else - { - linked_list_t *my_ts, *other_ts; + collision_won = handle_collision(this, &to_delete, &winning_sa, FALSE); - my_ts = linked_list_create_from_enumerator( - to_install->create_ts_enumerator(to_install, TRUE)); - other_ts = linked_list_create_from_enumerator( - to_install->create_ts_enumerator(to_install, FALSE)); + if (this->flags & CHILD_REKEY_REPLACEMENT_DELETED) + { + DBG1(DBG_IKE, "peer meanwhile sent a delete for CHILD_SA %s{%u} with " + "SPIs %.8x_i %.8x_o, abort rekeying", + winning_sa->get_name(winning_sa), + winning_sa->get_unique_id(winning_sa), + ntohl(winning_sa->get_spi(winning_sa, TRUE)), + ntohl(winning_sa->get_spi(winning_sa, FALSE))); + child_delete_destroy_and_reestablish(this->ike_sa, winning_sa); + } + else if (collision_won) + { + /* only conclude the rekeying here if we won, otherwise, we either + * already concluded the rekeying or we will do so when the peer deletes + * the old SA */ + child_rekey_conclude_rekeying(this->child_sa, winning_sa); + } - DBG0(DBG_IKE, "outbound CHILD_SA %s{%d} established " - "with SPIs %.8x_i %.8x_o and TS %#R === %#R", - to_install->get_name(to_install), - to_install->get_unique_id(to_install), - ntohl(to_install->get_spi(to_install, TRUE)), - ntohl(to_install->get_spi(to_install, FALSE)), - my_ts, other_ts); + if (collision_won && + this->flags & CHILD_REKEY_OLD_SA_DELETED) + { + /* the peer already deleted the rekeyed SA we were expected to delete + * with an incorrect delete to which we responded as usual but didn't + * destroy the SA yet */ + DBG1(DBG_IKE, "peer sent an incorrect delete for CHILD_SA %s{%u} after " + "responding to our rekeying", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + child_delete_destroy_rekeyed(this->ike_sa, this->child_sa); + return SUCCESS; + } - my_ts->destroy(my_ts); - other_ts->destroy(other_ts); - } - } - if (to_delete->get_state(to_delete) != CHILD_REKEYED) - { /* disable updown event for old/redundant CHILD_SA */ - to_delete->set_state(to_delete, CHILD_REKEYED); - } - if (to_delete == this->child_sa) - { /* invoke rekey hook if rekeying successful and remove the old - * outbound SA as we installed the new one already above, but might not - * be using it yet depending on how SAs/policies are handled */ - this->child_sa->remove_outbound(this->child_sa); - charon->bus->child_rekey(charon->bus, this->child_sa, - this->child_create->get_child(this->child_create)); + /* disable updown event for old/redundant CHILD_SA */ + to_delete->set_state(to_delete, CHILD_REKEYED); + /* and make sure the outbound SA is not registered, unless it is still fully + * installed, which happens if the rekeying is aborted. we keep it installed + * as we can't establish a replacement until the delete is done */ + if (to_delete->get_outbound_state(to_delete) != CHILD_OUTBOUND_INSTALLED) + { + to_delete->remove_outbound(to_delete); } + spi = to_delete->get_spi(to_delete, TRUE); protocol = to_delete->get_protocol(to_delete); /* rekeying done, delete the obsolete CHILD_SA using a subtask */ this->child_delete = child_delete_create(this->ike_sa, protocol, spi, FALSE); - this->public.task.build = (status_t(*)(task_t*,message_t*))build_i_delete; - this->public.task.process = (status_t(*)(task_t*,message_t*))process_i_delete; + this->public.task.build = _build_i_delete; + this->public.task.process = _process_i_delete; return NEED_MORE; } +/* + * Described in header + */ +bool child_rekey_conclude_rekeying(child_sa_t *old, child_sa_t *new) +{ + linked_list_t *my_ts, *other_ts; + + if (new->install_outbound(new) != SUCCESS) + { + /* shouldn't happen after we were able to install the inbound SA */ + DBG1(DBG_IKE, "unable to install outbound IPsec SA (SAD) in kernel"); + charon->bus->alert(charon->bus, ALERT_INSTALL_CHILD_SA_FAILED, + new); + return FALSE; + } + + my_ts = linked_list_create_from_enumerator( + new->create_ts_enumerator(new, TRUE)); + other_ts = linked_list_create_from_enumerator( + new->create_ts_enumerator(new, FALSE)); + + DBG0(DBG_IKE, "outbound CHILD_SA %s{%d} established " + "with SPIs %.8x_i %.8x_o and TS %#R === %#R", + new->get_name(new), new->get_unique_id(new), + ntohl(new->get_spi(new, TRUE)), ntohl(new->get_spi(new, FALSE)), + my_ts, other_ts); + + my_ts->destroy(my_ts); + other_ts->destroy(other_ts); + + /* remove the old outbound SA after we installed the new one. otherwise, it + * might not get used yet depending on how SAs/policies are handled in the + * kernel */ + old->remove_outbound(old); + + DBG0(DBG_IKE, "rekeyed CHILD_SA %s{%u} with SPIs %.8x_i %.8x_o with " + "%s{%u} with SPIs %.8x_i %.8x_o", + old->get_name(old), old->get_unique_id(old), + ntohl(old->get_spi(old, TRUE)), ntohl(old->get_spi(old, FALSE)), + new->get_name(new), new->get_unique_id(new), + ntohl(new->get_spi(new, TRUE)), ntohl(new->get_spi(new, FALSE))); + charon->bus->child_rekey(charon->bus, old, new); + return TRUE; +} + METHOD(task_t, get_type, task_type_t, private_child_rekey_t *this) { return TASK_CHILD_REKEY; } -METHOD(child_rekey_t, is_redundant, bool, - private_child_rekey_t *this, child_sa_t *child) +METHOD(child_rekey_t, handle_delete, child_rekey_collision_t, + private_child_rekey_t *this, child_sa_t *child, uint32_t spi) { - if (this->collision && - this->collision->get_type(this->collision) == TASK_CHILD_REKEY) + /* if we already completed our active rekeying and are deleting the + * old/redundant SA, there is no need to do anything special */ + if (this->child_delete) { - private_child_rekey_t *rekey = (private_child_rekey_t*)this->collision; - return child == rekey->child_create->get_child(rekey->child_create); + return CHILD_REKEY_COLLISION_NONE; } - return FALSE; + + if (!child) + { + /* check later if the SPI is the peer's of the SA we created (i.e. + * whether it deleted the new SA immediately after creation and we + * received that request before our active rekeying was complete) */ + array_insert_create_value(&this->deleted_spis, sizeof(uint32_t), + ARRAY_TAIL, &spi); + } + else if (child == this->child_sa) + { + /* the peer sent a delete for the old SA, might be because it won a + * collision, but could also be either because it initiated that before + * it received our CREATE_CHILD_SA request, or it incorrectly sent one + * as response to our request, we will check once we have the response + * to our rekeying */ + this->flags |= CHILD_REKEY_OLD_SA_DELETED; + return CHILD_REKEY_COLLISION_OLD; + } + else if (this->collision) + { + private_child_rekey_t *other = (private_child_rekey_t*)this->collision; + + if (child == other->child_create->get_child(other->child_create)) + { + /* the peer deleted the redundant (or in rare cases the winning) SA + * it created before our active rekeying was complete, how we handle + * this depends on the response to our rekeying */ + this->flags |= CHILD_REKEY_OTHER_DELETED; + return CHILD_REKEY_COLLISION_PEER; + } + } + return CHILD_REKEY_COLLISION_NONE; } METHOD(child_rekey_t, collide, bool, private_child_rekey_t *this, task_t *other) { - /* the task manager only detects exchange collision, but not if - * the collision is for the same child. we check it here. */ - if (other->get_type(other) == TASK_CHILD_REKEY) - { - private_child_rekey_t *rekey = (private_child_rekey_t*)other; - child_sa_t *other_child; + private_child_rekey_t *rekey = (private_child_rekey_t*)other; + child_sa_t *other_child; - if (rekey->child_sa != this->child_sa) - { /* not the same child => no collision */ - return FALSE; - } - /* ignore passive tasks that did not successfully create a CHILD_SA */ - other_child = rekey->child_create->get_child(rekey->child_create); - if (!other_child) - { - return FALSE; - } - if (other_child->get_state(other_child) != CHILD_INSTALLED) - { - DBG1(DBG_IKE, "colliding passive rekeying is not yet complete", - task_type_names, TASK_CHILD_REKEY); - /* we do reference the task to check its state later */ - this->collision = other; - return FALSE; - } - } - else if (other->get_type(other) == TASK_CHILD_DELETE) + if (rekey->child_sa != this->child_sa) { - child_delete_t *del = (child_delete_t*)other; - if (is_redundant(this, del->get_child(del))) - { - this->flags |= CHILD_REKEY_OTHER_DESTROYED; - return FALSE; - } - if (del->get_child(del) != this->child_sa) - { - /* not the same child => no collision */ - return FALSE; - } - } - else - { - /* shouldn't happen */ + /* not the same child => no collision */ return FALSE; } - DBG1(DBG_IKE, "detected %N collision with %N", task_type_names, - TASK_CHILD_REKEY, task_type_names, other->get_type(other)); - - if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + other_child = rekey->child_create->get_child(rekey->child_create); + if (!other_child) { - DESTROY_IF(this->collision); + /* ignore passive tasks that did not successfully create a CHILD_SA */ + return FALSE; } - this->flags |= CHILD_REKEY_ADOPTED_PASSIVE; + if (other_child->get_state(other_child) != CHILD_INSTALLED) + { + DBG1(DBG_IKE, "colliding passive rekeying for CHILD_SA %s{%u} is not " + "yet complete", this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + /* we do reference the task to check its state later */ + this->collision = other; + return FALSE; + } + if (this->collision && this->collision != other) + { + DBG1(DBG_IKE, "duplicate rekey collision for CHILD_SA %s{%u}???", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + return FALSE; + } + /* once the passive rekeying is complete, we adopt the task */ + DBG1(DBG_IKE, "detected rekey collision for CHILD_SA %s{%u}", + this->child_sa->get_name(this->child_sa), + this->child_sa->get_unique_id(this->child_sa)); + this->flags |= CHILD_REKEY_PASSIVE_INSTALLED; this->collision = other; return TRUE; } @@ -775,13 +1174,15 @@ METHOD(task_t, migrate, void, { this->child_create->task.migrate(&this->child_create->task, ike_sa); } - if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + if (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) { DESTROY_IF(this->collision); } + array_destroy(this->deleted_spis); this->ike_sa = ike_sa; this->collision = NULL; + this->flags = 0; } METHOD(task_t, destroy, void, @@ -795,10 +1196,11 @@ METHOD(task_t, destroy, void, { this->child_delete->task.destroy(&this->child_delete->task); } - if (this->flags & CHILD_REKEY_ADOPTED_PASSIVE) + if (this->flags & CHILD_REKEY_PASSIVE_INSTALLED) { DESTROY_IF(this->collision); } + array_destroy(this->deleted_spis); chunk_free(&this->spi_data); free(this); } @@ -818,7 +1220,7 @@ child_rekey_t *child_rekey_create(ike_sa_t *ike_sa, protocol_id_t protocol, .migrate = _migrate, .destroy = _destroy, }, - .is_redundant = _is_redundant, + .handle_delete = _handle_delete, .collide = _collide, }, .ike_sa = ike_sa, diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.h b/src/libcharon/sa/ikev2/tasks/child_rekey.h index 849df3a2b8..a8daed7436 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.h +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.h @@ -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 #include #include #include +/** + * 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_ @}*/ diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 8305fa2004..4b10e92053 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2017 Tobias Brunner + * Copyright (C) 2016-2022 Tobias Brunner * * Copyright (C) secunet Security Networks AG * @@ -17,6 +17,7 @@ #include "test_suite.h" #include +#include #include #include #include @@ -76,7 +77,7 @@ START_TEST(test_regular) assert_hook_not_called(child_updown); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, spi_b, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); @@ -85,7 +86,7 @@ START_TEST(test_regular) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, spi_a, 3); assert_no_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_NONE); @@ -94,7 +95,7 @@ START_TEST(test_regular) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_rekey(child_rekey, spi_b, 4); assert_jobs_scheduled(1); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -186,7 +187,7 @@ START_TEST(test_regular_multi_ke) assert_hook(); /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -196,7 +197,7 @@ START_TEST(test_regular_multi_ke) assert_hook(); /* <-- IKE_FOLLOWUP_KE { KEr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, spi_a, 3); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -206,7 +207,7 @@ START_TEST(test_regular_multi_ke) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_rekey(child_rekey, spi_b, 4); assert_jobs_scheduled(1); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -297,7 +298,7 @@ START_TEST(test_regular_ke_invalid) assert_hook(); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, spi_b, CHILD_REKEYED); @@ -306,7 +307,7 @@ START_TEST(test_regular_ke_invalid) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, spi_a, 4); assert_no_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_NONE); @@ -315,7 +316,7 @@ START_TEST(test_regular_ke_invalid) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_rekey(child_rekey, spi_b, 5); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, spi_b, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -351,7 +352,7 @@ START_TEST(test_regular_ke_invalid) assert_hook_not_called(child_updown); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 5, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); @@ -369,7 +370,7 @@ START_TEST(test_regular_ke_invalid) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_called(child_rekey); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 5, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -473,7 +474,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -483,7 +484,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); /* <-- IKE_FOLLOWUP_KE { KEr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, spi_a, 4); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -493,7 +494,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_rekey(child_rekey, spi_b, 5); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, spi_b, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -546,7 +547,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -566,7 +567,7 @@ START_TEST(test_regular_ke_invalid_multi_ke) assert_hook(); /* INFORMATIONAL { D } --> */ - assert_hook_not_called(child_rekey); + assert_hook_called(child_rekey); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 5, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -618,7 +619,7 @@ START_TEST(test_regular_responder_ignore_soft_expire) assert_hook_not_called(child_updown); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED); @@ -627,7 +628,7 @@ START_TEST(test_regular_responder_ignore_soft_expire) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, 1, 3); assert_no_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_NONE); @@ -635,14 +636,13 @@ START_TEST(test_regular_responder_ignore_soft_expire) assert_ipsec_sas_installed(a, 1, 3, 4); assert_hook(); - /* we don't expect this to get called anymore */ - assert_hook_not_called(child_rekey); /* this should not produce a message, if it does there won't be a delete * payload below */ call_ikesa(b, rekey_child_sa, PROTO_ESP, 2); assert_child_sa_state(b, 2, CHILD_REKEYED); /* INFORMATIONAL { D } --> */ + assert_hook_rekey(child_rekey, 2, 4); assert_jobs_scheduled(1); assert_single_payload(IN, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -651,6 +651,10 @@ START_TEST(test_regular_responder_ignore_soft_expire) assert_child_sa_count(b, 2); assert_ipsec_sas_installed(b, 2, 3, 4); assert_scheduler(); + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); assert_single_payload(IN, PLV2_DELETE); @@ -696,7 +700,7 @@ START_TEST(test_regular_responder_handle_hard_expire) assert_hook_not_called(child_updown); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_called(child_rekey); + assert_hook_not_called(child_rekey); assert_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED); @@ -705,7 +709,7 @@ START_TEST(test_regular_responder_handle_hard_expire) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ - assert_hook_called(child_rekey); + assert_hook_rekey(child_rekey, 1, 3); assert_no_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_NONE); @@ -713,17 +717,19 @@ START_TEST(test_regular_responder_handle_hard_expire) assert_ipsec_sas_installed(a, 1, 3, 4); assert_hook(); - /* we don't expect this to get called anymore */ - assert_hook_not_called(child_rekey); /* this is similar to a regular delete collision, but we don't actually - * want to send a delete back as that might conflict with a delayed - * CREATE_CHILD_SA response */ + * want to send a delete as that might conflict with a delayed + * CREATE_CHILD_SA response and the peer is expected to delete it anyway */ + assert_hook_rekey(child_rekey, 2, 4); call_ikesa(b, delete_child_sa, PROTO_ESP, 2, TRUE); assert_child_sa_count(b, 1); assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); /* the expire causes the outbound SA to get installed */ assert_ipsec_sas_installed(b, 3, 4); + assert_hook(); + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_no_jobs_scheduled(); assert_single_payload(IN, PLV2_DELETE); @@ -755,6 +761,305 @@ START_TEST(test_regular_responder_handle_hard_expire) } END_TEST +/** + * Check that the responder and initiator handle deletes for the new SA + * properly while waiting for the delete after a rekeying (e.g. if a script or + * user deletes the new, not fully installed, SA manually). + */ +START_TEST(test_regular_responder_delete) +{ + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + + /* this should not get called until the new SA is deleted */ + assert_hook_not_called(child_updown); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_not_called(child_rekey); + assert_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 4); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ + assert_hook_rekey(child_rekey, 1, 3); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 3, 4); + assert_hook(); + + assert_hook_rekey(child_rekey, 2, 4); + call_ikesa(b, delete_child_sa, PROTO_ESP, 4, FALSE); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + /* the delete causes the outbound SA to get installed/uninstalled */ + assert_ipsec_sas_installed(b, 2, 3, 4); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); + assert_jobs_scheduled(1); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 3, 4); + assert_scheduler(); + assert_hook(); + + /* child_updown */ + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(child_updown, FALSE); + assert_no_jobs_scheduled(); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 1); + assert_scheduler(); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_updown); + assert_jobs_scheduled(1); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 1); + assert_scheduler(); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + assert_no_jobs_scheduled(); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 2); + assert_scheduler(); + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_updown); + + /* simulate the execution of the scheduled job */ + destroy_rekeyed(a, 1); + assert_child_sa_count(a, 0); + assert_ipsec_sas_installed(a); + destroy_rekeyed(b, 2); + assert_child_sa_count(b, 0); + assert_ipsec_sas_installed(b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * This simulates what happens if the responder for some reason lost the + * CHILD_SA the initiator is trying to rekey. + */ +START_TEST(test_regular_responder_lost_sa) +{ + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + + /* destroy the CHILD_SA on the responder without notification */ + call_ikesa(b, destroy_child_sa, PROTO_ESP, 2); + assert_child_sa_count(b, 0); + + /* this should never get called as there is no successful rekeying on + * either side */ + assert_hook_not_called(child_rekey); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_notify(IN, REKEY_SA); + assert_single_notify(OUT, CHILD_SA_NOT_FOUND); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + + /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */ + assert_hook_updown(child_updown, FALSE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); + assert_ipsec_sas_installed(a); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_updown(child_updown, TRUE); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 4, 5); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Ni, [KEi,] TSi, TSr } */ + assert_hook_updown(child_updown, TRUE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 4, 5); + assert_hook(); + + /* child_rekey */ + assert_hook(); + + assert_sa_idle(a); + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * Helper to add DELETE payload. + */ +typedef struct { + listener_t listener; + uint32_t spi; +} incorrect_delete_listener_t; + +/** + * Add a DELETE payload to a message. + */ +static bool add_delete(incorrect_delete_listener_t *listener, ike_sa_t *ike_sa, + message_t *message, bool incoming, bool plain) +{ + delete_payload_t *payload; + + if (plain && !incoming && message->get_request(message)) + { + payload = delete_payload_create(PLV2_DELETE, PROTO_ESP); + payload->add_spi(payload, listener->spi); + message->add_payload(message, (payload_t*)payload); + return FALSE; + } + return TRUE; +} + +/** + * Send a DELETE for the given SPI from an SA. + */ +static void send_child_delete(ike_sa_t *sa, uint32_t spi) +{ + incorrect_delete_listener_t del = { + .listener = { .message = (void*)add_delete, }, + .spi = spi, + }; + + exchange_test_helper->add_listener(exchange_test_helper, &del.listener); + call_ikesa(sa, send_dpd); +} + +/** + * This simulates incorrect behavior by some IKEv2 responders, which send + * a delete for the old CHILD_SA even if there was no collision (don't know + * how they'd behave if there was a collision, maybe they'd send two). + * This is an issue if the DELETE arrives before the CREATE_CHILD_SA response. + */ +START_TEST(test_regular_responder_incorrect_delete) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + + /* this should never get called as this results in a successful rekeying */ + assert_hook_not_called(child_updown); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_not_called(child_rekey); + assert_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 4); + assert_hook(); + + /* delay the CREATE_CHILD_SA response */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* inject an incorrect delete for the old CHILD_SA by the responder, + * without messing with its internal state */ + send_child_delete(b, 2); + + /* <-- INFORMATIONAL { D } (incorrect behavior!) */ + assert_hook_not_called(child_rekey); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + assert_jobs_scheduled(1); + assert_hook_rekey(child_rekey, 1, 3); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 3, 4); + assert_hook(); + assert_scheduler(); + + /* INFORMATIONAL { D } (response to incorrect DELETE) --> + * this is ignored here because the DPD task doesn't handle the DELETE, so + * we simulate handling of the delete via expire, does not delay destroy */ + assert_no_jobs_scheduled(); + assert_hook_rekey(child_rekey, 2, 4); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + call_ikesa(b, delete_child_sa, PROTO_ESP, 2, TRUE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 3, 4); + assert_hook(); + assert_scheduler(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + + /* simulate the execution of the scheduled job */ + destroy_rekeyed(a, 1); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 3, 4); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 3, 4); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * Both peers initiate the CHILD_SA rekeying concurrently and should handle * the collision properly depending on the nonces. @@ -804,7 +1109,7 @@ START_TEST(test_collision) /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 2, 5); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -812,7 +1117,7 @@ START_TEST(test_collision) assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; - assert_hook_rekey(child_rekey, 1, 6); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -821,17 +1126,14 @@ START_TEST(test_collision) /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ if (data[_i].spi_del_a == 1) - { /* currently we call this again if we keep our own replacement as we - * already called it above */ + { assert_hook_rekey(child_rekey, 1, data[_i].spi_a); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_hook(); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_NONE); assert_ipsec_sas_installed(a, 1, 3, 5, 6); } else @@ -843,10 +1145,10 @@ START_TEST(test_collision) CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); assert_ipsec_sas_installed(a, 1, 2, 3, 6); } + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, + CHILD_OUTBOUND_NONE); /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ if (data[_i].spi_del_b == 2) { @@ -854,11 +1156,9 @@ START_TEST(test_collision) exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_hook(); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_NONE); assert_ipsec_sas_installed(b, 2, 4, 5, 6); } else @@ -870,19 +1170,28 @@ START_TEST(test_collision) CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); assert_ipsec_sas_installed(b, 1, 2, 4, 5); } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_NONE); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, b, NULL); + if (data[_i].spi_del_b == 2) + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } + else + { + assert_hook_rekey(child_rekey, 2, 5); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } + assert_scheduler(); assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - data[_i].spi_del_b == 2 ? CHILD_OUTBOUND_NONE - : CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, @@ -896,13 +1205,23 @@ START_TEST(test_collision) { assert_ipsec_sas_installed(b, 2, 3, 4, 5); } - assert_scheduler(); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); + if (data[_i].spi_del_a == 1) + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + } + else + { + assert_hook_rekey(child_rekey, 1, 6); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + } + assert_scheduler(); assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - data[_i].spi_del_a == 1 ? CHILD_OUTBOUND_NONE - : CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, @@ -916,7 +1235,10 @@ START_TEST(test_collision) { assert_ipsec_sas_installed(a, 1, 3, 4, 6); } - assert_scheduler(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -1051,7 +1373,7 @@ START_TEST(test_collision_multi_ke) if (data[_i].spi_del_a == 1) { /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ - assert_hook_rekey(child_rekey, 2, 5); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -1073,7 +1395,7 @@ START_TEST(test_collision_multi_ke) else { /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ - assert_hook_rekey(child_rekey, 1, 6); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -1093,12 +1415,10 @@ START_TEST(test_collision_multi_ke) assert_hook(); } - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - if (data[_i].spi_del_a == 1) { /* INFORMATIONAL { D } --> */ + assert_hook_rekey(child_rekey, 2, 5); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1106,8 +1426,10 @@ START_TEST(test_collision_multi_ke) assert_child_sa_count(b, 2); assert_ipsec_sas_installed(b, 2, 3, 5); assert_scheduler(); + assert_hook(); /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_rekey); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1115,14 +1437,12 @@ START_TEST(test_collision_multi_ke) assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 3, 5); assert_scheduler(); - - /* simulate the execution of the scheduled jobs */ - destroy_rekeyed(a, data[_i].spi_del_a); - destroy_rekeyed(b, data[_i].spi_del_a); + assert_hook(); } else { /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(child_rekey, 1, 6); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1130,8 +1450,10 @@ START_TEST(test_collision_multi_ke) assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 4, 6); assert_scheduler(); + assert_hook(); /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1139,12 +1461,18 @@ START_TEST(test_collision_multi_ke) assert_child_sa_count(b, 2); assert_ipsec_sas_installed(b, 2, 4, 6); assert_scheduler(); - - /* simulate the execution of the scheduled jobs */ - destroy_rekeyed(a, data[_i].spi_del_b); - destroy_rekeyed(b, data[_i].spi_del_b); + assert_hook(); } + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, data[_i].spi_del_a == 1 ? data[_i].spi_del_a + : data[_i].spi_del_b); + destroy_rekeyed(b, data[_i].spi_del_a == 1 ? data[_i].spi_del_a + : data[_i].spi_del_b); + assert_child_sa_count(a, 1); assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_b); assert_child_sa_count(b, 1); @@ -1231,7 +1559,7 @@ START_TEST(test_collision_mixed) /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, KEi, TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; - assert_hook_rekey(child_rekey, 1, 6); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1239,25 +1567,30 @@ START_TEST(test_collision_mixed) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } */ - assert_hook_not_called(child_rekey); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); - /* the single-KE passive task was completed and adopted above */ - assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); if (data[_i].spi_del_a == 1) - { + { /* a's multi-KE SA is the winner */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + /* the single-KE passive task was completed and adopted */ + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); assert_num_tasks(a, 1, TASK_QUEUE_ACTIVE); assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(a, 6, CHILD_REKEYED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_state(a, 6, CHILD_REKEYED, CHILD_OUTBOUND_NONE); + assert_hook(); } else - { + { /* b's single-KE SA is the winner */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + /* the single-KE passive task was completed and adopted */ + assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_hook(); } assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 2, 6); - assert_hook(); if (data[_i].spi_del_a == 1) { @@ -1267,24 +1600,25 @@ START_TEST(test_collision_mixed) assert_num_tasks(b, 1, TASK_QUEUE_PASSIVE); assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE); assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_NONE); assert_child_sa_count(b, 2); assert_ipsec_sas_installed(b, 1, 2, 4); assert_hook(); /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */ - assert_hook_rekey(child_rekey, 2, 5); + assert_hook_not_called(child_rekey); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_NONE); assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); assert_child_sa_count(b, 3); assert_ipsec_sas_installed(b, 1, 2, 4, 5); assert_hook(); /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_rekey); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); @@ -1292,10 +1626,9 @@ START_TEST(test_collision_mixed) assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 2, 6); assert_scheduler(); + assert_hook(); /* <-- IKE_FOLLOWUP_KE { KEr } */ - /* currently we call this again if we keep our own replacement as we - * already called it above */ assert_hook_rekey(child_rekey, 1, data[_i].spi_a); assert_payload(IN, PLV2_KEY_EXCHANGE); assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE); @@ -1320,12 +1653,10 @@ START_TEST(test_collision_mixed) assert_hook(); } - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - if (data[_i].spi_del_a == 1) { /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); @@ -1334,8 +1665,11 @@ START_TEST(test_collision_mixed) assert_child_sa_count(b, 3); assert_ipsec_sas_installed(b, 1, 2, 4, 5); assert_scheduler(); + assert_hook(); + /* INFORMATIONAL { D } --> */ + assert_hook_rekey(child_rekey, 2, 5); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1344,6 +1678,9 @@ START_TEST(test_collision_mixed) assert_child_sa_count(b, 3); assert_ipsec_sas_installed(b, 2, 4, 3, 5); assert_scheduler(); + assert_hook(); + + assert_hook_not_called(child_rekey); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); @@ -1360,10 +1697,13 @@ START_TEST(test_collision_mixed) destroy_rekeyed(a, data[_i].spi_del_b); destroy_rekeyed(b, data[_i].spi_del_a); destroy_rekeyed(b, data[_i].spi_del_b); + + assert_hook(); } else { /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(child_rekey, 1, 6); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -1371,6 +1711,9 @@ START_TEST(test_collision_mixed) assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 4, 6); assert_scheduler(); + assert_hook(); + + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); @@ -1384,8 +1727,13 @@ START_TEST(test_collision_mixed) /* simulate the execution of the scheduled jobs */ destroy_rekeyed(a, data[_i].spi_del_b); destroy_rekeyed(b, data[_i].spi_del_b); + + assert_hook(); } + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + assert_child_sa_count(a, 1); assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_b); assert_child_sa_count(b, 1); @@ -1458,7 +1806,7 @@ START_TEST(test_collision_delayed_response) /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 2, 5); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1466,7 +1814,7 @@ START_TEST(test_collision_delayed_response) assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; - assert_hook_rekey(child_rekey, 1, 6); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1481,56 +1829,59 @@ START_TEST(test_collision_delayed_response) { assert_hook_rekey(child_rekey, 2, data[_i].spi_b); exchange_test_helper->process_message(exchange_test_helper, b, NULL); - assert_hook(); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_NONE); assert_ipsec_sas_installed(b, 2, 4, 5, 6); + assert_hook(); } else { assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); - assert_hook(); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); assert_ipsec_sas_installed(b, 1, 2, 4, 5); + assert_hook(); } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_NONE); /* <-- INFORMATIONAL { D } */ - assert_hook_not_called(child_rekey); - assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_no_jobs_scheduled(); if (data[_i].spi_del_b == 2) { + assert_hook_rekey(child_rekey, 1, 6); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_ipsec_sas_installed(a, 1, 4, 6); + assert_hook(); } else { - assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, - CHILD_OUTBOUND_NONE); + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_REGISTERED); assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); } - assert_child_sa_count(a, 2); assert_scheduler(); + assert_child_sa_count(a, 2); /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); if (data[_i].spi_del_b == 2) { assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_ipsec_sas_installed(b, 2, 4, 5, 6); @@ -1550,35 +1901,45 @@ START_TEST(test_collision_delayed_response) assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + /* the second job here is for the retransmit of the delete */ + assert_jobs_scheduled(2); if (data[_i].spi_del_a == 1) { assert_hook_rekey(child_rekey, 1, data[_i].spi_a); exchange_test_helper->process_message(exchange_test_helper, a, msg); assert_hook(); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_NONE); - assert_ipsec_sas_installed(a, 1, 3, 5, 6); } else { assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, msg); assert_hook(); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); - assert_ipsec_sas_installed(a, 1, 3, 4, 6); } + assert_scheduler(); + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, + CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 3); + assert_ipsec_sas_installed(a, 1, 3, 6, + data[_i].spi_del_a == 1 ? 5 : 4); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, b, NULL); + if (data[_i].spi_del_b == 2) + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } + else + { + assert_hook_rekey(child_rekey, 2, 5); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } assert_child_sa_state(b, data[_i].spi_del_a, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETED, @@ -1589,6 +1950,9 @@ START_TEST(test_collision_delayed_response) data[_i].spi_del_b == 2 ? 6 : 3); assert_child_sa_count(b, 3); assert_scheduler(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -1598,9 +1962,9 @@ START_TEST(test_collision_delayed_response) CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_count(a, 3); assert_ipsec_sas_installed(a, 1, 3, 6, data[_i].spi_del_a == 1 ? 5 : 4); + assert_child_sa_count(a, 3); assert_scheduler(); /* simulate the execution of the scheduled jobs */ @@ -1622,6 +1986,284 @@ START_TEST(test_collision_delayed_response) } END_TEST +/** + * This is like the rekey collision above, but one peer deletes the + * redundant/old SA and then also the new one before the other peer receives + * the CREATE_CHILD_SA response: + * + * rekey ----\ /---- rekey + * \-----/----> detect collision + * detect collision <---------/ /---- + * ----\ / + * \----/-----> + * handle delete <--------/------- delete old SA + * --------/-------> + * handle delete <------/--------- delete new SA + * ------/---------> + * ignore rekey <----/ + */ +START_TEST(test_collision_delayed_response_delete) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* Four nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /----- ... + * ... -----\ + * We test this four times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[4]; + /* SPIs of the deleted CHILD_SA (either redundant or replaced) */ + uint32_t spi_del_a, spi_del_b; + /* SPIs of the kept CHILD_SA */ + uint32_t spi_a, spi_b; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 4, 3, 5 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 4, 3, 5 }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 2); + assert_ipsec_sas_installed(b, 1, 2); + + /* this should not get called until the replacement SA is deleted */ + assert_hook_not_called(child_updown); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 5); + assert_hook(); + /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); + + /* delay the CREATE_CHILD_SA response from b to a */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ + if (data[_i].spi_del_b == 2) + { + assert_hook_rekey(child_rekey, 2, data[_i].spi_b); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, + CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 4, 5, 6); + assert_hook(); + } + else + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, + CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 4, 5); + assert_hook(); + } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_NONE); + + /* <-- INFORMATIONAL { D } */ + if (data[_i].spi_del_b == 2) + { + assert_hook_rekey(child_rekey, 1, 6); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, + CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 4, 6); + assert_scheduler(); + assert_hook(); + } + else + { + assert_hook_not_called(child_rekey); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_scheduler(); + assert_hook(); + } + assert_child_sa_count(a, 2); + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + if (data[_i].spi_del_b == 2) + { + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, + CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 4, 5, 6); + } + else + { + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, + CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 4, 5); + } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 3); + assert_scheduler(); + assert_hook(); + + /* trigger a delete for the new CHILD_SA */ + call_ikesa(b, delete_child_sa, PROTO_ESP, data[_i].spi_b, FALSE); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_b, CHILD_DELETING, + CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 3); + assert_ipsec_sas_installed(b, 2, 4, 5, + data[_i].spi_del_b == 2 ? 6 : 3); + + /* child_updown */ + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_rekey); + assert_hook_not_called(child_updown); + if (data[_i].spi_del_b == 2) + { + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, data[_i].spi_a, CHILD_DELETING, + CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 4, 6); + assert_scheduler(); + } + else + { + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_scheduler(); + } + assert_child_sa_count(a, 2); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 2, data[_i].spi_del_b == 2 ? 5 : 4); + assert_scheduler(); + assert_hook(); + /* child_rekey */ + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + assert_hook_updown(child_updown, FALSE); + if (data[_i].spi_del_a == 1) + { + assert_hook_rekey(child_rekey, 1, 3); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, + CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); + } + else + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, + CHILD_OUTBOUND_NONE); + assert_ipsec_sas_installed(a, 1, 3); + assert_hook(); + } + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 2); + assert_hook(); + + /* we don't expect these hooks to get called anymore */ + assert_hook_not_called(child_updown); + assert_hook_not_called(child_rekey); + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_ipsec_sas_installed(b, 2, data[_i].spi_del_b == 2 ? 5 : 4); + assert_child_sa_count(b, 2); + assert_scheduler(); + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, + CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, data[_i].spi_del_a == 1 ? 6 : 3); + assert_scheduler(); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, data[_i].spi_del_a); + destroy_rekeyed(a, data[_i].spi_del_b); + assert_child_sa_count(a, 0); + assert_ipsec_sas_installed(a); + destroy_rekeyed(b, data[_i].spi_del_a); + destroy_rekeyed(b, data[_i].spi_del_b); + assert_child_sa_count(b, 0); + assert_ipsec_sas_installed(b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * This is like a regular rekey collision, but one CREATE_CHILD_SA response * is delayed: @@ -1717,7 +2359,7 @@ START_TEST(test_collision_delayed_response_multi_ke) assert_hook(); /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ - assert_hook_rekey(child_rekey, 1, 6); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1732,29 +2374,33 @@ START_TEST(test_collision_delayed_response_multi_ke) assert_ipsec_sas_installed(b, 2, 4, 6); assert_hook(); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - if (!after_delete) { /* a receives the response right after the IKE_FOLLOWUP_KE, the passive * rekeying is completed and the active aborted */ /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, msg); assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); } /* <-- INFORMATIONAL { D } */ - assert_jobs_scheduled(1); + assert_hook_rekey(child_rekey, 1, 6); + assert_jobs_scheduled(after_delete ? 0 : 1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_ipsec_sas_installed(a, 1, 4, 6); assert_child_sa_count(a, 2); assert_scheduler(); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); @@ -1768,12 +2414,14 @@ START_TEST(test_collision_delayed_response_multi_ke) if (after_delete) { /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, msg); assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_ipsec_sas_installed(a, 1, 4, 6); + assert_scheduler(); } /* simulate the execution of the scheduled jobs */ @@ -1803,14 +2451,27 @@ END_TEST * -------\--------> * \ /---- delete old SA * \-/----> detect collision - * detect collision <---------/ /---- TEMP_FAIL - * delete -----------/----> + * handle delete <---------/ /---- TEMP_FAIL + * -----------/----> * aborts rekeying <---------/ + * + * Besides the scenario depicted above, i.e. where the response arrives after + * handling B's delete request, we also test when it arrives before that: + * + * ... + * \ /---- delete old SA + * \-/----> detect collision + * aborts rekeying <---------/------ TEMP_FAIL + * handle delete <--------/ + * ----------------> */ START_TEST(test_collision_delayed_request) { ike_sa_t *a, *b; message_t *msg; + bool before_delete = _i >= 3; + + _i %= 3; exchange_test_helper->establish_sa(exchange_test_helper, &a, &b, NULL); @@ -1846,7 +2507,7 @@ START_TEST(test_collision_delayed_request) /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 1, 5); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1860,32 +2521,74 @@ START_TEST(test_collision_delayed_request) assert_ipsec_sas_installed(b, 2, 4, 5); assert_hook(); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> (delayed) */ + assert_hook_not_called(child_rekey); assert_single_notify(OUT, TEMPORARY_FAILURE); exchange_test_helper->process_message(exchange_test_helper, b, msg); assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_hook(); - /* <-- INFORMATIONAL { D } */ - assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_count(a, 2); - assert_ipsec_sas_installed(a, 1, 4, 5); - assert_scheduler(); + if (before_delete) + { + /* delay the DELETE request from b to a so TEMP_FAIL arrives before */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + } + else + { + /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(child_rekey, 1, 5); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + assert_hook(); + } /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ - assert_no_jobs_scheduled(); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_count(a, 2); - assert_ipsec_sas_installed(a, 1, 4, 5); - assert_scheduler(); + assert_hook_not_called(child_rekey); + if (before_delete) + { + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 2, 5); + assert_scheduler(); + } + else + { + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + } + assert_hook(); + + if (before_delete) + { + /* <-- INFORMATIONAL { D } (delayed) */ + assert_hook_rekey(child_rekey, 1, 5); + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + assert_hook(); + } + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); @@ -1969,7 +2672,7 @@ START_TEST(test_collision_delayed_request_more) /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 1, 5); + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); @@ -1983,17 +2686,20 @@ START_TEST(test_collision_delayed_request_more) assert_ipsec_sas_installed(b, 2, 4, 5); assert_hook(); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - /* <-- INFORMATIONAL { D } */ - assert_jobs_scheduled(1); + assert_hook_rekey(child_rekey, 1, 5); + assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 4, 5); assert_scheduler(); + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -2010,8 +2716,9 @@ START_TEST(test_collision_delayed_request_more) assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 2); assert_ipsec_sas_installed(b, 2, 4, 5); + /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */ - assert_no_jobs_scheduled(); + assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); @@ -2039,6 +2746,171 @@ START_TEST(test_collision_delayed_request_more) } END_TEST +/** + * Similar to above one peer fails to notice the collision but the + * CREATE_CHILD_SA request is even more delayed: + * + * rekey ----\ /---- rekey + * \ / + * detect collision <-----\---/ + * -------\--------> + * handle delete <-------\-------- delete old SA + * ---------\------> + * handle delete <---------\------ delete new SA + * -----------\----> + * \---> + * /---- CHILD_SA_NOT_FOUND + * aborts rekeying <----------/ + */ +START_TEST(test_collision_delayed_request_more_delete) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* Three nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * N3/5 <-----\--/ + * ... -----\ \-------> ... + * We test this three times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[3]; + } data[] = { + { { 0x00, 0xFF, 0xFF } }, + { { 0xFF, 0x00, 0xFF } }, + { { 0xFF, 0xFF, 0x00 } }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 2); + assert_ipsec_sas_installed(b, 1, 2); + + /* delay the CREATE_CHILD_SA request from a to b */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* this should not get called until the new SA is deleted */ + assert_hook_not_called(child_updown); + + /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 5); + assert_hook(); + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ + assert_hook_rekey(child_rekey, 2, 4); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, 2, 4, 5); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(child_rekey, 1, 5); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + assert_hook(); + + /* child_updown() */ + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + /* this is expected later */ + assert_hook_not_called(child_updown); + + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 2, 4, 5); + assert_scheduler(); + + /* trigger a delete for the new CHILD_SA */ + call_ikesa(b, delete_child_sa, PROTO_ESP, 5, FALSE); + + /* <-- INFORMATIONAL { D } */ + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 5, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 4, 5); + assert_scheduler(); + + /* child_updown() */ + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 2); + assert_scheduler(); + assert_hook(); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_not_called(child_updown); + assert_single_notify(OUT, CHILD_SA_NOT_FOUND); + exchange_test_helper->process_message(exchange_test_helper, b, msg); + assert_child_sa_state(b, 2, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */ + assert_hook_updown(child_updown, FALSE); + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 1); + assert_scheduler(); + assert_hook(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_updown); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, 1); + assert_child_sa_count(a, 0); + assert_ipsec_sas_installed(a); + destroy_rekeyed(b, 2); + assert_child_sa_count(b, 0); + assert_ipsec_sas_installed(b); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + assert_sa_idle(a); + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * In this scenario one of the peers does not notice that there is a * rekey collision: @@ -2056,7 +2928,7 @@ END_TEST * aborts rekeying <-------/ * * In a variation of this scenario, the TEMP_FAIL notify arrives before the - * delete does. + * delete does, similar to the non-multi-KE scenario above. */ START_TEST(test_collision_delayed_request_multi_ke) { @@ -2119,7 +2991,7 @@ START_TEST(test_collision_delayed_request_multi_ke) exchange_test_helper->process_message(exchange_test_helper, b, msg); /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */ - assert_hook_rekey(child_rekey, 1, 5); + assert_hook_not_called(child_rekey); assert_notify(IN, ADDITIONAL_KEY_EXCHANGE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE); @@ -2157,17 +3029,18 @@ START_TEST(test_collision_delayed_request_multi_ke) assert_ipsec_sas_installed(b, 2, 4, 5); assert_hook(); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - /* <-- INFORMATIONAL { D } */ - assert_jobs_scheduled(1); + assert_hook_rekey(child_rekey, 1, 5); + assert_jobs_scheduled(after_delete ? 0 : 1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 2); assert_ipsec_sas_installed(a, 1, 4, 5); assert_scheduler(); + assert_hook(); + + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); @@ -2181,7 +3054,7 @@ START_TEST(test_collision_delayed_request_multi_ke) if (after_delete) { /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ - assert_no_jobs_scheduled(); + assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, msg); assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); @@ -2268,127 +3141,141 @@ START_TEST(test_collision_ke_invalid) /* this should never get called as this results in a successful rekeying */ assert_hook_not_called(child_updown); + /* this should not be called until the active rekeyings are concluded */ + assert_hook_not_called(child_rekey); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 1); - assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ - assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 1); - assert_hook(); /* <-- CREATE_CHILD_SA { N(INVAL_KE) } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; - assert_hook_not_called(child_rekey); assert_single_notify(IN, INVALID_KE_PAYLOAD); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 1); - assert_hook(); /* CREATE_CHILD_SA { N(INVAL_KE) } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; - assert_hook_not_called(child_rekey); assert_single_notify(IN, INVALID_KE_PAYLOAD); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 1); - assert_hook(); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 2, 7); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, 7, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; - assert_hook_rekey(child_rekey, 1, 8); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 8, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + + /* child_rekey */ assert_hook(); /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ if (data[_i].spi_del_a == 1) - { /* currently we call this again if we keep our own replacement as we - * already called it above */ + { assert_hook_rekey(child_rekey, 1, data[_i].spi_a); exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_hook(); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_NONE); + assert_hook(); } else { + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); + assert_hook(); } + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, + CHILD_OUTBOUND_NONE); + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ if (data[_i].spi_del_b == 2) { assert_hook_rekey(child_rekey, 2, data[_i].spi_b); exchange_test_helper->process_message(exchange_test_helper, b, NULL); - assert_hook(); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, - CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_NONE); + assert_hook(); } else { + assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - CHILD_OUTBOUND_REGISTERED); + assert_hook(); } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_NONE); - - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, b, NULL); - assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, - data[_i].spi_del_b == 2 ? CHILD_OUTBOUND_NONE - : CHILD_OUTBOUND_REGISTERED); + if (data[_i].spi_del_b == 2) + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } + else + { + assert_hook_rekey(child_rekey, 2, data[_i].spi_b); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + } + assert_scheduler(); assert_child_sa_state(b, data[_i].spi_del_a, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING, + CHILD_OUTBOUND_NONE); assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 3); - assert_scheduler(); /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); + if (data[_i].spi_del_a == 1) + { + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + } + else + { + assert_hook_rekey(child_rekey, 1, data[_i].spi_a); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + } + assert_scheduler(); assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING, - data[_i].spi_del_a == 1 ? CHILD_OUTBOUND_NONE - : CHILD_OUTBOUND_REGISTERED); + CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_del_b, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 3); - assert_scheduler(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + /* <-- INFORMATIONAL { D } */ assert_jobs_scheduled(1); exchange_test_helper->process_message(exchange_test_helper, a, NULL); @@ -2487,47 +3374,43 @@ START_TEST(test_collision_ke_invalid_delayed_retry) /* this should never get called as this results in a successful rekeying */ assert_hook_not_called(child_updown); + /* this should not be called until b doesn't notice a collision */ + assert_hook_not_called(child_rekey); /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ - assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 1); - assert_hook(); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ - assert_hook_not_called(child_rekey); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 1); - assert_hook(); /* <-- CREATE_CHILD_SA { N(INVAL_KE) } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; - assert_hook_not_called(child_rekey); assert_single_notify(IN, INVALID_KE_PAYLOAD); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 1); - assert_hook(); /* CREATE_CHILD_SA { N(INVAL_KE) } --> */ exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; - assert_hook_not_called(child_rekey); assert_single_notify(IN, INVALID_KE_PAYLOAD); exchange_test_helper->process_message(exchange_test_helper, b, NULL); assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(b, 1); - assert_hook(); /* delay the CREATE_CHILD_SA request from a to b */ msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; - assert_hook_rekey(child_rekey, 1, 7); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + + /* child_rekey */ assert_hook(); + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ assert_hook_rekey(child_rekey, 2, 6); exchange_test_helper->process_message(exchange_test_helper, b, NULL); @@ -2535,30 +3418,36 @@ START_TEST(test_collision_ke_invalid_delayed_retry) assert_child_sa_state(b, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_hook(); - /* we don't expect this hook to get called anymore */ - assert_hook_not_called(child_rekey); - /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> (delayed) */ + assert_hook_not_called(child_rekey); assert_single_notify(OUT, TEMPORARY_FAILURE); exchange_test_helper->process_message(exchange_test_helper, b, msg); assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE); assert_child_sa_state(b, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_hook(); /* <-- INFORMATIONAL { D } */ - assert_jobs_scheduled(1); - exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); - assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); - assert_child_sa_count(a, 2); - assert_scheduler(); - - /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_hook_rekey(child_rekey, 1, 7); assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); assert_child_sa_count(a, 2); assert_scheduler(); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_hook_not_called(child_rekey); + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 7, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 2); + assert_scheduler(); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); /* INFORMATIONAL { D } --> */ assert_jobs_scheduled(1); @@ -2588,6 +3477,147 @@ START_TEST(test_collision_ke_invalid_delayed_retry) } END_TEST +/** + * This simulates incorrect behavior by a hypothetical IKEv2 responder, which + * might send a delete for the old CHILD_SA even if it lost the collision + * (compared to the incorrect delete without collision, see above, this hasn't + * been observed in the wild). + * This is an issue if the DELETE arrives before the CREATE_CHILD_SA response. + */ +START_TEST(test_collision_responder_incorrect_delete) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* make sure the responder looses the collision */ + exchange_test_helper->nonce_first_byte = 0xff; + initiate_rekey(a, 1); + assert_ipsec_sas_installed(a, 1, 2); + exchange_test_helper->nonce_first_byte = 0x00; + initiate_rekey(b, 2); + assert_ipsec_sas_installed(b, 1, 2); + + /* this should never get called as this results in a successful rekeying */ + assert_hook_not_called(child_updown); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + exchange_test_helper->nonce_first_byte = 0xff; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 5); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */ + exchange_test_helper->nonce_first_byte = 0xff; + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_hook(); + + /* delay the CREATE_CHILD_SA response */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, 1, 2, 4, 5); + + /* <-- INFORMATIONAL { D } */ + assert_no_jobs_scheduled(); + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_DELETING, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_scheduler(); + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_hook(); + assert_child_sa_state(b, 2, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(b, 3); + assert_ipsec_sas_installed(b, 1, 2, 4, 5); + assert_scheduler(); + + /* inject an incorrect delete for the old CHILD_SA by the responder, + * without messing with its internal state */ + send_child_delete(b, 2); + + /* <-- INFORMATIONAL { D } */ + assert_no_jobs_scheduled(); + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_hook(); + assert_child_sa_state(a, 1, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_DELETING, CHILD_OUTBOUND_REGISTERED); + assert_child_sa_count(a, 2); + assert_ipsec_sas_installed(a, 1, 2, 6); + assert_scheduler(); + + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + assert_jobs_scheduled(2); + assert_hook_rekey(child_rekey, 1, 3); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_hook(); + assert_child_sa_state(a, 1, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(a, 6, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 3); + assert_ipsec_sas_installed(a, 1, 3, 5, 6); + assert_scheduler(); + + /* INFORMATIONAL { D } (response to incorrect DELETE) --> */ + assert_no_jobs_scheduled(); + assert_hook_rekey(child_rekey, 2, 5); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + /* simulate handling of the delete via expire, does not delay destroy */ + call_ikesa(b, delete_child_sa, PROTO_ESP, 2, TRUE); + assert_child_sa_state(b, 4, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(b, 2); + assert_ipsec_sas_installed(b, 3, 4, 5); + assert_hook(); + assert_scheduler(); + + /* we don't expect this to get called anymore */ + assert_hook_not_called(child_rekey); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, 1); + destroy_rekeyed(a, 6); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, 3, 5); + destroy_rekeyed(b, 4); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, 3, 5); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + /** * One of the hosts initiates a DELETE of the CHILD_SA the other peer is * concurrently trying to rekey. @@ -2640,18 +3670,21 @@ START_TEST(test_collision_delete) */ /* <-- INFORMATIONAL { D } */ - assert_hook_updown(child_updown, FALSE); + assert_hook_not_called(child_updown); assert_single_payload(IN, PLV2_DELETE); assert_single_payload(OUT, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_count(a, 0); + /* the SA is not destroyed until we get the CREATE_CHILD_SA response */ + assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 1); assert_hook(); /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ - assert_hook_not_called(child_updown); + assert_hook_updown(child_updown, FALSE); /* we don't expect a job to retry the rekeying */ assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); assert_scheduler(); assert_hook(); @@ -2727,18 +3760,20 @@ START_TEST(test_collision_delete_multi_ke) assert_hook(); /* <-- INFORMATIONAL { D } */ - assert_hook_updown(child_updown, FALSE); + assert_hook_not_called(child_updown); assert_single_payload(IN, PLV2_DELETE); assert_single_payload(OUT, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_count(a, 0); + assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 1); assert_hook(); /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ - assert_hook_not_called(child_updown); + assert_hook_updown(child_updown, FALSE); /* we don't expect a job to retry the rekeying */ assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); assert_scheduler(); assert_hook(); @@ -2858,7 +3893,7 @@ END_TEST * /---- CHILD_SA_NOT_FOUND * aborts rekeying <----------/ */ - START_TEST(test_collision_delete_drop_rekey) +START_TEST(test_collision_delete_drop_rekey) { ike_sa_t *a, *b; message_t *msg; @@ -2891,11 +3926,12 @@ END_TEST */ /* <-- INFORMATIONAL { D } */ - assert_hook_updown(child_updown, FALSE); + assert_hook_not_called(child_updown); assert_single_payload(IN, PLV2_DELETE); assert_single_payload(OUT, PLV2_DELETE); exchange_test_helper->process_message(exchange_test_helper, a, NULL); - assert_child_sa_count(a, 0); + assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 1); assert_hook(); /* INFORMATIONAL { D } --> */ @@ -2916,10 +3952,11 @@ END_TEST assert_hook(); /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */ - assert_hook_not_called(child_updown); + assert_hook_updown(child_updown, FALSE); /* no jobs or tasks should get scheduled/queued */ assert_no_jobs_scheduled(); exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); assert_scheduler(); assert_hook(); @@ -2935,26 +3972,133 @@ END_TEST END_TEST /** - * FIXME: Not sure what we can do about the following: - * * One of the hosts initiates a rekeying of a CHILD_SA and after responding to * it the other peer deletes the new SA. However, the rekey response is * delayed or dropped, so the peer doing the rekeying receives a delete for an - * unknown CHILD_SA and then has a rekeyed CHILD_SA that should not exist. + * unknown CHILD_SA and has to consider this when processing the rekey response. * * rekey ----------------> * /---- rekey * unknown SA <----------/----- delete new SA * ----------/-----> - * <--------/ - * - * The peers' states are now out of sync. - * - * Perhaps the rekey initiator could keep track of deletes for non-existing SAs - * while rekeying and then check against the SPIs when handling the - * CREATE_CHILD_SA response. + * delete SA <--------/ */ +START_TEST(test_collision_delete_delayed_response) +{ + ike_sa_t *a, *b; + message_t *msg; + uint32_t spi_a = _i+1, spi_b = 2-_i; + if (_i) + { /* responder rekeys the CHILD_SA (SPI 2) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, NULL); + } + else + { /* initiator rekeys the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + } + initiate_rekey(a, spi_a); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_not_called(child_rekey); + assert_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, spi_b, CHILD_REKEYED, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED); + assert_ipsec_sas_installed(b, spi_a, spi_b, 4); + assert_hook(); + + + /* delay the CREATE_CHILD_SA response */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + assert_hook_rekey(child_rekey, spi_b, 4); + assert_hook_not_called(child_updown); + call_ikesa(b, delete_child_sa, PROTO_ESP, 4, FALSE); + assert_child_sa_state(b, spi_b, CHILD_REKEYED, CHILD_OUTBOUND_NONE); + assert_child_sa_state(b, 4, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_ipsec_sas_installed(b, spi_b, 3, 4); + assert_child_sa_count(b, 2); + assert_hook(); + assert_hook(); + + /* this is not expected to get called until the response is processed */ + assert_hook_not_called(child_rekey); + + /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_updown); + assert_single_payload(IN, PLV2_DELETE); + assert_message_empty(OUT); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, spi_a, spi_b); + assert_hook(); + + /* INFORMATIONAL { } --> */ + assert_hook_updown(child_updown, FALSE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, spi_b, CHILD_REKEYED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, spi_b); + assert_hook(); + + /* child_rekey */ + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Ni, [KEi,] TSi, TSr } (delayed) */ + assert_hook_rekey(child_rekey, spi_a, 3); + assert_hook_updown(child_updown, FALSE); + /* the job scheduled here is for the retransmit of the delete */ + assert_jobs_scheduled(1); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_state(a, spi_a, CHILD_DELETING, CHILD_OUTBOUND_INSTALLED); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, spi_a, spi_b); + assert_scheduler(); + assert_hook(); + assert_hook(); + + /* this is not expected to get called anymore */ + assert_hook_not_called(child_rekey); + + /* INFORMATIONAL { D } --> */ + assert_jobs_scheduled(1); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, spi_b, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(b, 1); + assert_ipsec_sas_installed(b, spi_b); + assert_scheduler(); + /* <-- INFORMATIONAL { D } */ + assert_jobs_scheduled(1); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, spi_a, CHILD_DELETED, CHILD_OUTBOUND_NONE); + assert_child_sa_count(a, 1); + assert_ipsec_sas_installed(a, spi_a); + assert_scheduler(); + + /* simulate the execution of the scheduled jobs */ + destroy_rekeyed(a, spi_a); + assert_child_sa_count(a, 0); + assert_ipsec_sas_installed(a); + destroy_rekeyed(b, spi_b); + assert_child_sa_count(b, 0); + assert_ipsec_sas_installed(a); + + /* child_rekey */ + assert_hook(); + + assert_sa_idle(a); + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST /** * One of the hosts initiates a rekey of the IKE_SA of the CHILD_SA the other @@ -3132,6 +4276,9 @@ Suite *child_rekey_suite_create() tcase_add_loop_test(tc, test_regular_ke_invalid_multi_ke, 0, 2); tcase_add_test(tc, test_regular_responder_ignore_soft_expire); tcase_add_test(tc, test_regular_responder_handle_hard_expire); + tcase_add_test(tc, test_regular_responder_delete); + tcase_add_test(tc, test_regular_responder_lost_sa); + tcase_add_test(tc, test_regular_responder_incorrect_delete); suite_add_tcase(s, tc); tc = tcase_create("collisions rekey"); @@ -3139,12 +4286,15 @@ Suite *child_rekey_suite_create() tcase_add_loop_test(tc, test_collision_multi_ke, 0, 4); tcase_add_loop_test(tc, test_collision_mixed, 0, 4); tcase_add_loop_test(tc, test_collision_delayed_response, 0, 4); + tcase_add_loop_test(tc, test_collision_delayed_response_delete, 0, 4); tcase_add_loop_test(tc, test_collision_delayed_response_multi_ke, 0, 4); - tcase_add_loop_test(tc, test_collision_delayed_request, 0, 3); + tcase_add_loop_test(tc, test_collision_delayed_request, 0, 6); tcase_add_loop_test(tc, test_collision_delayed_request_more, 0, 3); + tcase_add_loop_test(tc, test_collision_delayed_request_more_delete, 0, 3); tcase_add_loop_test(tc, test_collision_delayed_request_multi_ke, 0, 6); tcase_add_loop_test(tc, test_collision_ke_invalid, 0, 4); tcase_add_loop_test(tc, test_collision_ke_invalid_delayed_retry, 0, 3); + tcase_add_test(tc, test_collision_responder_incorrect_delete); suite_add_tcase(s, tc); tc = tcase_create("collisions delete"); @@ -3152,6 +4302,7 @@ Suite *child_rekey_suite_create() tcase_add_loop_test(tc, test_collision_delete_multi_ke, 0, 2); tcase_add_loop_test(tc, test_collision_delete_drop_delete, 0, 2); tcase_add_loop_test(tc, test_collision_delete_drop_rekey, 0, 2); + tcase_add_loop_test(tc, test_collision_delete_delayed_response, 0, 2); suite_add_tcase(s, tc); tc = tcase_create("collisions ike rekey");