]> git.ipfire.org Git - thirdparty/strongswan.git/commitdiff
ikev2: Make CHILD_SAs properly trackable during rekey collisions
authorTobias Brunner <tobias@strongswan.org>
Mon, 22 Aug 2022 13:43:16 +0000 (15:43 +0200)
committerTobias Brunner <tobias@strongswan.org>
Wed, 7 Aug 2024 14:20:19 +0000 (16:20 +0200)
As the winner of a rekey collision, we previously always triggered the
child_rekey() event once when creating the redundant SA on behalf of the
peer in the passive child-rekey task and then a second time when
creating the winning SA in the active task.  However, both calls passed
the replaced CHILD_SA as "old". This made tracking CHILD_SAs impossible
because there was no transition from the redundant, "new" SA of the
first event to the "new", winning SA of the second.  Of course, when the
second event was triggered, the redundant SA might not have existed
anymore because the peer is expected to delete it, which could happen
before the CREATE_CHILD_SA response arrives at the initiator.

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

Some corner cases are now also handled correctly, e.g. if a responder's
DELETE for the new CHILD_SA arrives before its CREATE_CHILD_SA response
that actually creates it on the initiator.  Also handled properly are
responders of rekeyings that incorrectly send a DELETE for the old
CHILD_SA (previously this caused both, the new and the old SA, to get
deleted).

src/libcharon/sa/child_sa.c
src/libcharon/sa/child_sa.h
src/libcharon/sa/ikev2/task_manager_v2.c
src/libcharon/sa/ikev2/tasks/child_create.c
src/libcharon/sa/ikev2/tasks/child_create.h
src/libcharon/sa/ikev2/tasks/child_delete.c
src/libcharon/sa/ikev2/tasks/child_delete.h
src/libcharon/sa/ikev2/tasks/child_rekey.c
src/libcharon/sa/ikev2/tasks/child_rekey.h
src/libcharon/tests/suites/test_child_rekey.c

index 97ee88acbc12bb4e76e45c7fb06d41068f4013a7..1f50c49520717310de4b7c7c606f150b1277e34a 100644 (file)
@@ -131,9 +131,10 @@ struct private_child_sa_t {
        bool tfcv3;
 
        /**
-        * The outbound SPI of the CHILD_SA that replaced this one during a rekeying
+        * The "other" CHILD_SA involved in a passive rekeying (either replacing
+        * this one, or being replaced by it)
         */
-       uint32_t rekey_spi;
+       child_sa_t *rekey_sa;
 
        /**
         * Protocol used to protect this SA, ESP|AH
@@ -1588,16 +1589,16 @@ METHOD(child_sa_t, remove_outbound, void,
        this->outbound_state = CHILD_OUTBOUND_NONE;
 }
 
-METHOD(child_sa_t, set_rekey_spi, void,
-       private_child_sa_t *this, uint32_t spi)
+METHOD(child_sa_t, set_rekey_sa, void,
+       private_child_sa_t *this, child_sa_t *sa)
 {
-       this->rekey_spi = spi;
+       this->rekey_sa = sa;
 }
 
-METHOD(child_sa_t, get_rekey_spi, uint32_t,
+METHOD(child_sa_t, get_rekey_sa, child_sa_t*,
        private_child_sa_t *this)
 {
-       return this->rekey_spi;
+       return this->rekey_sa;
 }
 
 CALLBACK(reinstall_vip, void,
@@ -2077,8 +2078,8 @@ child_sa_t *child_sa_create(host_t *me, host_t *other, child_cfg_t *config,
                        .register_outbound = _register_outbound,
                        .install_outbound = _install_outbound,
                        .remove_outbound = _remove_outbound,
-                       .set_rekey_spi = _set_rekey_spi,
-                       .get_rekey_spi = _get_rekey_spi,
+                       .set_rekey_sa = _set_rekey_sa,
+                       .get_rekey_sa = _get_rekey_sa,
                        .update = _update,
                        .set_policies = _set_policies,
                        .install_policies = _install_policies,
index 0b7d1111429f6c60948d60e05744f93487132ab2..7c3763b0a980cc56c222f92a4848318817a465b8 100644 (file)
@@ -504,23 +504,24 @@ struct child_sa_t {
        status_t (*install_policies)(child_sa_t *this);
 
        /**
-        * Set the outbound SPI of the CHILD_SA that replaced this CHILD_SA during
-        * a rekeying.
+        * Set the CHILD_SA that either replaced this one or the CHILD_SA that is
+        * being replaced by this one during a passive rekeying (i.e. it links the
+        * two SAs bidirectionally).
         *
-        * @param spi           outbound SPI of the CHILD_SA that replaced this CHILD_SA
+        * @param sa            other CHILD_SA involved in a passive rekeying
         */
-       void (*set_rekey_spi)(child_sa_t *this, uint32_t spi);
+       void (*set_rekey_sa)(child_sa_t *this, child_sa_t *sa);
 
        /**
-        * Get the outbound SPI of the CHILD_SA that replaced this CHILD_SA during
-        * a rekeying.
+        * Get the CHILD_SA that's linked to this in a passive rekeying (either
+        * replacing this one, or being replaced by it).
         *
-        * @return                      outbound SPI of the CHILD_SA that replaced this CHILD_SA
+        * @return                      other CHILD_SA involved in a passive rekeying
         */
-       uint32_t (*get_rekey_spi)(child_sa_t *this);
+       child_sa_t *(*get_rekey_sa)(child_sa_t *this);
 
        /**
-        * Update hosts and ecapsulation mode in the kernel SAs and policies.
+        * Update hosts and encapsulation mode in the kernel SAs and policies.
         *
         * @param me            the new local host
         * @param other         the new remote host
index c5cc34f0e7d8b6839494c70ccfce30cedd29fac7..30dba22db3fff09c940cd305a6fd1bc0626b9e50 100644 (file)
@@ -935,9 +935,10 @@ static bool handle_collisions(private_task_manager_t *this, task_t *task)
 
        type = task->get_type(task);
 
-       /* do we have to check  */
-       if (type == TASK_IKE_REKEY || type == TASK_CHILD_REKEY ||
-               type == TASK_CHILD_DELETE || type == TASK_IKE_DELETE)
+       /* collisions between a child-rekey and child-delete task are handled
+        * directly by the latter */
+       if (type == TASK_IKE_REKEY || type == TASK_IKE_DELETE ||
+               type == TASK_CHILD_REKEY)
        {
                /* find an exchange collision, and notify these tasks */
                enumerator = array_create_enumerator(this->active_tasks);
@@ -954,7 +955,7 @@ static bool handle_collisions(private_task_manager_t *this, task_t *task)
                                        }
                                        continue;
                                case TASK_CHILD_REKEY:
-                                       if (type == TASK_CHILD_REKEY || type == TASK_CHILD_DELETE)
+                                       if (type == TASK_CHILD_REKEY)
                                        {
                                                child_rekey_t *rekey = (child_rekey_t*)active;
                                                adopted = rekey->collide(rekey, task);
index 132c3de469d87b560b8ac81743cb0125a14c4811..c3a12780f68d3ceffa23df6d240e1f2a624520b9 100644 (file)
@@ -2184,7 +2184,7 @@ METHOD(task_t, build_i_delete, status_t,
                DBG1(DBG_IKE, "sending DELETE for %N CHILD_SA with SPI %.8x",
                         protocol_id_names, this->proto, ntohl(this->my_spi));
        }
-       return NEED_MORE;
+       return SUCCESS;
 }
 
 /**
@@ -2195,7 +2195,6 @@ static status_t delete_failed_sa(private_child_create_t *this)
        if (this->my_spi && this->proto)
        {
                this->public.task.build = _build_i_delete;
-               this->public.task.process = (void*)return_success;
                /* destroying it here allows the rekey task to differentiate between
                 * this and the multi-KE case */
                this->child_sa->destroy(this->child_sa);
@@ -2232,6 +2231,17 @@ static status_t key_exchange_done_and_install_i(private_child_create_t *this,
 METHOD(task_t, process_i_multi_ke, status_t,
        private_child_create_t *this, message_t *message)
 {
+       if (message->get_notify(message, TEMPORARY_FAILURE))
+       {
+               DBG1(DBG_IKE, "received %N notify", notify_type_names,
+                        TEMPORARY_FAILURE);
+               if (!this->rekey)
+               {       /* the rekey task will retry itself if necessary */
+                       schedule_delayed_retry(this);
+               }
+               return SUCCESS;
+       }
+
        process_payloads_multi_ke(this, message);
 
        if (this->ke_failed)
@@ -2304,8 +2314,7 @@ METHOD(task_t, process_i, status_t,
                                }
                                case TEMPORARY_FAILURE:
                                {
-                                       DBG1(DBG_IKE, "received %N notify, will retry later",
-                                                notify_type_names, type);
+                                       DBG1(DBG_IKE, "received %N notify", notify_type_names, type);
                                        enumerator->destroy(enumerator);
                                        if (!this->rekey)
                                        {       /* the rekey task will retry itself if necessary */
@@ -2365,6 +2374,15 @@ METHOD(task_t, process_i, status_t,
 
        process_payloads(this, message);
 
+       if (!select_proposal(this, no_ke))
+       {
+               handle_child_sa_failure(this, message);
+               return delete_failed_sa(this);
+       }
+
+       this->other_spi = this->proposal->get_spi(this->proposal);
+       this->proposal->set_spi(this->proposal, this->my_spi);
+
        if (this->ipcomp == IPCOMP_NONE && this->ipcomp_received != IPCOMP_NONE)
        {
                DBG1(DBG_IKE, "received an IPCOMP_SUPPORTED notify without requesting"
@@ -2386,15 +2404,6 @@ METHOD(task_t, process_i, status_t,
                return delete_failed_sa(this);
        }
 
-       if (!select_proposal(this, no_ke))
-       {
-               handle_child_sa_failure(this, message);
-               return delete_failed_sa(this);
-       }
-
-       this->other_spi = this->proposal->get_spi(this->proposal);
-       this->proposal->set_spi(this->proposal, this->my_spi);
-
        if (!check_ke_method(this, NULL))
        {
                handle_child_sa_failure(this, message);
@@ -2485,6 +2494,12 @@ METHOD(child_create_t, get_child, child_sa_t*,
        return this->child_sa;
 }
 
+METHOD(child_create_t, get_other_spi, uint32_t,
+       private_child_create_t *this)
+{
+       return this->other_spi;
+}
+
 METHOD(child_create_t, set_config, void,
        private_child_create_t *this, child_cfg_t *cfg)
 {
@@ -2623,6 +2638,7 @@ child_create_t *child_create_create(ike_sa_t *ike_sa,
        INIT(this,
                .public = {
                        .get_child = _get_child,
+                       .get_other_spi = _get_other_spi,
                        .set_config = _set_config,
                        .get_lower_nonce = _get_lower_nonce,
                        .use_reqid = _use_reqid,
index 2e8c8bed7410e368f835eccbb678cddbe89d04a6..eae39e61cad57215aa82a63eff9a7ba4f5fe5fc4 100644 (file)
@@ -103,6 +103,13 @@ struct child_create_t {
         */
        child_sa_t* (*get_child) (child_create_t *this);
 
+       /**
+        * Get the SPI of the other peer's selected proposal, if available.
+        *
+        * @return                      other's SPI, 0 if unknown
+        */
+       uint32_t (*get_other_spi)(child_create_t *this);
+
        /**
         * Enforce a specific CHILD_SA config as responder.
         *
index eff0e6cc601182aede6846c15b52646b7f318bdc..e2e198b285c1d1c349959a2f5a300f76329e4ceb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009-2016 Tobias Brunner
+ * Copyright (C) 2009-2022 Tobias Brunner
  * Copyright (C) 2006-2007 Martin Willi
  *
  * Copyright (C) secunet Security Networks AG
@@ -76,10 +76,10 @@ struct private_child_delete_t {
 typedef struct {
        /** Deleted CHILD_SA */
        child_sa_t *child_sa;
-       /** Whether the CHILD_SA was rekeyed */
-       bool rekeyed;
-       /** Whether to enforce any delete action policy */
-       bool check_delete_action;
+       /** The original state of the CHILD_SA */
+       child_sa_state_t orig_state;
+       /** How this CHILD_SA collides with an active rekeying */
+       child_rekey_collision_t collision;
 } entry_t;
 
 CALLBACK(match_child, bool,
@@ -133,267 +133,294 @@ 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)
 {
-       enumerator_t *tasks;
-       task_t *task;
-
-       tasks = this->ike_sa->create_task_enumerator(this->ike_sa,
-                                                                                                TASK_QUEUE_ACTIVE);
-       while (tasks->enumerate(tasks, &task))
-       {
-               if (task->get_type(task) == TASK_CHILD_REKEY)
-               {
-                       child_rekey_t *rekey = (child_rekey_t*)task;
+       child_sa_t *child_sa;
 
-                       if (rekey->is_redundant(rekey, child))
-                       {
-                               tasks->destroy(tasks);
-                               return TRUE;
-                       }
-               }
-       }
-       tasks->destroy(tasks);
-       return FALSE;
+       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);
 }
 
 /**
- * Install the outbound CHILD_SA with the given SPI
+ * Destroy and optionally reestablish the given CHILD_SA according to config.
  */
-static void install_outbound(private_child_delete_t *this,
-                                                        protocol_id_t protocol, uint32_t spi)
+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_sa_t *child_sa;
-       linked_list_t *my_ts, *other_ts;
-       status_t status;
+       child_init_args_t args = {};
+       child_cfg_t *child_cfg;
+       protocol_id_t protocol;
+       uint32_t spi;
+       action_t action;
+       status_t status = SUCCESS;
 
-       if (!spi)
+       child_sa->set_state(child_sa, CHILD_DELETED);
+       if (trigger_updown)
        {
-               return;
+               charon->bus->child_updown(charon->bus, child_sa, FALSE);
        }
 
-       child_sa = this->ike_sa->get_child_sa(this->ike_sa, protocol,
-                                                                                 spi, FALSE);
-       if (!child_sa)
+       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)
        {
-               DBG1(DBG_IKE, "CHILD_SA not found after rekeying");
-               return;
+               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));
 
-       status = child_sa->install_outbound(child_sa);
-       if (status != SUCCESS)
+       ike_sa->destroy_child_sa(ike_sa, protocol, spi);
+
+       if (delete_action)
        {
-               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;
+               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;
+}
 
-       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));
+/*
+ * 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);
 
-       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);
+       now = time_monotonic(NULL);
+       delay = lib->settings->get_int(lib->settings, "%s.delete_rekeyed_delay",
+                                                                  DELETE_REKEYED_DELAY, lib->ns);
 
-       my_ts->destroy(my_ts);
-       other_ts->destroy(other_ts);
+       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);
 }
 
 /**
- * read in payloads and find the children to delete
+ * 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 void process_payloads(private_child_delete_t *this, message_t *message)
+static bool keep_while_rekeying(entry_t *entry)
 {
-       enumerator_t *payloads, *spis;
-       payload_t *payload;
-       delete_payload_t *delete_payload;
-       uint32_t spi;
-       protocol_id_t protocol;
-       child_sa_t *child_sa;
-       entry_t *entry;
-
-       payloads = message->create_payload_enumerator(message);
-       while (payloads->enumerate(payloads, &payload))
+       switch (entry->collision)
        {
-               if (payload->get_type(payload) == PLV2_DELETE)
-               {
-                       delete_payload = (delete_payload_t*)payload;
-                       protocol = delete_payload->get_protocol_id(delete_payload);
-                       if (protocol != PROTO_ESP && protocol != PROTO_AH)
-                       {
-                               continue;
-                       }
-                       spis = delete_payload->create_spi_enumerator(delete_payload);
-                       while (spis->enumerate(spis, &spi))
+               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)
                        {
-                               child_sa = this->ike_sa->get_child_sa(this->ike_sa, protocol,
-                                                                                                         spi, FALSE);
-                               if (!child_sa)
-                               {
-                                       DBG1(DBG_IKE, "received DELETE for unknown %N CHILD_SA with"
-                                                " SPI %.8x", protocol_id_names, protocol, ntohl(spi));
-                                       continue;
-                               }
-                               DBG1(DBG_IKE, "received DELETE for %N CHILD_SA with SPI %.8x",
-                                        protocol_id_names, protocol, ntohl(spi));
-
-                               if (this->child_sas->find_first(this->child_sas, match_child,
-                                                                                               NULL, child_sa))
-                               {
-                                       continue;
-                               }
-                               INIT(entry,
-                                       .child_sa = child_sa
-                               );
-                               switch (child_sa->get_state(child_sa))
-                               {
-                                       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;
-                               }
-                               this->child_sas->insert_last(this->child_sas, entry);
+                               return TRUE;
                        }
-                       spis->destroy(spis);
-               }
+                       /* 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;
        }
-       payloads->destroy(payloads);
+       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
+ * 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;
+       child_sa_t *child_sa, *other;
        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)
+               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))
                {
-                       charon->bus->child_updown(charon->bus, child_sa, FALSE);
+                       /* 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;
                }
-               else
+
+               child_sa->set_state(child_sa, CHILD_DELETED);
+
+               if (entry->orig_state == CHILD_REKEYED)
                {
-                       /* 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)))
+                       /* 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)
                        {
-                               lib->scheduler->schedule_job(lib->scheduler,
-                                       (job_t*)delete_child_sa_job_create_id(
-                                                                       child_sa->get_unique_id(child_sa)), delay);
-                               continue;
+                               conclude_rekeying(this, child_sa);
                        }
-                       else if (now < expire)
-                       {       /* let it expire naturally */
-                               continue;
+                       else
+                       {
+                               child_sa->remove_outbound(child_sa);
                        }
-                       /* 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)
+                       /* 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)
                        {
-                               charon->traps->install(charon->traps,
-                                                                          this->ike_sa->get_peer_cfg(this->ike_sa),
-                                                                          child_cfg);
+                               log_kept_sa(entry);
                        }
-                       if (action & ACTION_START)
+                       else
                        {
-                               child_cfg->get_ref(child_cfg);
-                               status = this->ike_sa->initiate(this->ike_sa, child_cfg, &args);
+                               child_delete_destroy_rekeyed(this->ike_sa, child_sa);
                        }
                }
-               child_cfg->destroy(child_cfg);
-               if (args.reqid)
-               {
-                       charon->kernel->release_reqid(charon->kernel, args.reqid);
-               }
-               DESTROY_IF(args.label);
-               if (status != SUCCESS)
+               else
                {
-                       break;
+                       /* 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);
@@ -401,7 +428,7 @@ static status_t destroy_and_reestablish(private_child_delete_t *this)
 }
 
 /**
- * send closing signals for all CHILD_SAs over the bus
+ * Print a log message for every closed CHILD_SA
  */
 static void log_children(private_child_delete_t *this)
 {
@@ -421,7 +448,7 @@ static void log_children(private_child_delete_t *this)
                                                        child_sa->create_ts_enumerator(child_sa, FALSE));
                if (this->expired)
                {
-                       DBG0(DBG_IKE, "closing expired CHILD_SA %s{%d} "
+                       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)),
@@ -432,7 +459,7 @@ static void log_children(private_child_delete_t *this)
                        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 "
+                       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,
@@ -448,51 +475,80 @@ static void log_children(private_child_delete_t *this)
 METHOD(task_t, build_i, status_t,
        private_child_delete_t *this, message_t *message)
 {
-       child_sa_t *child_sa;
+       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 */
+       {
+               /* 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 */
+               {
+                       /* 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);
        }
 
-       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));
+       /* 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 */
-               this->ike_sa->destroy_child_sa(this->ike_sa, this->protocol, this->spi);
+       {
+               /* 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,
-               .rekeyed = child_sa->get_state(child_sa) == CHILD_REKEYED,
+               .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 (!entry->rekeyed && this->expired)
+       if (this->expired)
        {
                child_cfg_t *child_cfg;
 
@@ -505,11 +561,120 @@ METHOD(task_t, build_i, status_t,
        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);
+       while (tasks->enumerate(tasks, &task))
+       {
+               if (task->get_type(task) == TASK_CHILD_REKEY)
+               {
+                       rekey = (child_rekey_t*)task;
+                       collision = rekey->handle_delete(rekey, child, spi);
+                       break;
+               }
+       }
+       tasks->destroy(tasks);
+       return collision;
+}
+
+/**
+ * Read payloads and find the children to delete.
+ */
+static void process_payloads(private_child_delete_t *this, message_t *message)
+{
+       enumerator_t *payloads, *spis;
+       payload_t *payload;
+       delete_payload_t *delete_payload;
+       uint32_t spi;
+       protocol_id_t protocol;
+       child_sa_t *child_sa;
+       entry_t *entry;
+
+       payloads = message->create_payload_enumerator(message);
+       while (payloads->enumerate(payloads, &payload))
+       {
+               if (payload->get_type(payload) == PLV2_DELETE)
+               {
+                       delete_payload = (delete_payload_t*)payload;
+                       protocol = delete_payload->get_protocol_id(delete_payload);
+                       if (protocol != PROTO_ESP && protocol != PROTO_AH)
+                       {
+                               continue;
+                       }
+                       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"
+                                                " SPI %.8x", protocol_id_names, protocol, ntohl(spi));
+                                       continue;
+                               }
+                               DBG1(DBG_IKE, "received DELETE for %N CHILD_SA with SPI %.8x",
+                                        protocol_id_names, protocol, ntohl(spi));
+
+                               if (this->child_sas->find_first(this->child_sas, match_child,
+                                                                                               NULL, child_sa))
+                               {
+                                       continue;
+                               }
+                               else if (this->initiator)
+                               {
+                                       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);
+               }
+       }
+       payloads->destroy(payloads);
+}
+
 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(),
index ca57ae9cfab306a5f724487cbd0c94881fc00dde..6dc2141cb0990a647755f47269a024a5941e977c 100644 (file)
@@ -1,4 +1,5 @@
 /*
+ * Copyright (C) 2022 Tobias Brunner
  * Copyright (C) 2007 Martin Willi
  *
  * Copyright (C) secunet Security Networks AG
@@ -25,8 +26,8 @@
 typedef struct child_delete_t child_delete_t;
 
 #include <library.h>
-#include <sa/ike_sa.h>
 #include <sa/task.h>
+#include <sa/ike_sa.h>
 #include <sa/child_sa.h>
 
 /**
@@ -38,13 +39,6 @@ struct child_delete_t {
         * Implements the task_t interface
         */
        task_t task;
-
-       /**
-        * Get the CHILD_SA to delete by this task.
-        *
-        * @return                      child_sa
-        */
-       child_sa_t* (*get_child) (child_delete_t *this);
 };
 
 /**
@@ -59,4 +53,35 @@ struct child_delete_t {
 child_delete_t *child_delete_create(ike_sa_t *ike_sa, protocol_id_t protocol,
                                                                        uint32_t spi, bool expired);
 
+/**
+ * Destroy the given CHILD_SA and trigger events and configured actions.
+ *
+ * @param ike_sa               IKE_SA the child_sa belongs to
+ * @param child_sa             CHILD_SA to destroy and potentially reestablish
+ * @return                             status of reestablishment
+ */
+status_t child_delete_destroy_and_reestablish(ike_sa_t *ike_sa,
+                                                                                         child_sa_t *child_sa);
+
+/**
+ * Destroy the given CHILD_SA and trigger events and force a recreation.
+ *
+ * @param ike_sa               IKE_SA the child_sa belongs to
+ * @param child_sa             CHILD_SA to destroy and reestablish
+ * @return                             status of reestablishment
+ */
+status_t child_delete_destroy_and_force_reestablish(ike_sa_t *ike_sa,
+                                                                                                       child_sa_t *child_sa);
+
+/**
+ * Destroy the given CHILD_SA with a configured delay, so delayed inbound
+ * packets can still be processed.
+ *
+ * @note The outbound SA should already be uninstalled when calling this.
+ *
+ * @param ike_sa               IKE_SA the child_sa belongs to
+ * @param child_sa             CHILD_SA to destroy and potentially reestablish
+ */
+void child_delete_destroy_rekeyed(ike_sa_t *ike_sa, child_sa_t *child_sa);
+
 #endif /** CHILD_DELETE_H_ @}*/
index 6ffeb52c504d97e691cca2d520e1183b5aa6fdc6..6c73d0671aa2d23d5cb6bab8a5fc80db01235444 100644 (file)
@@ -19,6 +19,7 @@
 #include "child_rekey.h"
 
 #include <daemon.h>
+#include <encoding/payloads/delete_payload.h>
 #include <encoding/payloads/notify_payload.h>
 #include <sa/ikev2/tasks/child_create.h>
 #include <sa/ikev2/tasks/child_delete.h>
@@ -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_PASSIVE_INSTALLED = (1<<1),
+
+               /**
+                * 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_ADOPTED_PASSIVE = (1<<1),
+               CHILD_REKEY_OTHER_DELETED = (1<<2),
 
                /**
-                * 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 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_OTHER_DESTROYED = (1<<2),
+               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)
-       {
-               protocol = notify->get_protocol_id(notify);
-               spi = notify->get_spi(notify);
+       message->set_exchange_type(message, INFORMATIONAL);
 
-               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));
-               }
+       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)
+       {
+               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));
+               }
+               /* 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 to_delete;
+               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) */
-                       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)
+               /* ignore CHILD_SA_NOT_FOUND error notify in some cases, otherwise
+                * create a replacement SA */
+               if (ignore_child_sa_not_found(this))
                {
-                       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);
+                       return SUCCESS;
                }
-               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)
+       collision_won = handle_collision(this, &to_delete, &winning_sa, FALSE);
+
+       if (this->flags & CHILD_REKEY_REPLACEMENT_DELETED)
        {
-               to_delete = handle_collision(this, &to_install, FALSE);
+               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
+       else if (collision_won)
        {
-               to_install = this->child_create->get_child(this->child_create);
-               to_delete = this->child_sa;
+               /* 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);
        }
-       if (to_install)
+
+       if (collision_won &&
+               this->flags & CHILD_REKEY_OLD_SA_DELETED)
        {
-               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;
-
-                       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));
-
-                       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);
-
-                       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);
+               /* 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;
        }
-       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;
-               }
+       if (rekey->child_sa != this->child_sa)
+       {
+               /* not the same child => no collision */
+               return FALSE;
        }
-       else if (other->get_type(other) == TASK_CHILD_DELETE)
+
+       other_child = rekey->child_create->get_child(rekey->child_create);
+       if (!other_child)
        {
-               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;
-               }
+               /* ignore passive tasks that did not successfully create a CHILD_SA */
+               return FALSE;
        }
-       else
+       if (other_child->get_state(other_child) != CHILD_INSTALLED)
        {
-               /* shouldn't happen */
+               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;
        }
-
-       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)
+       if (this->collision && this->collision != other)
        {
-               DESTROY_IF(this->collision);
+               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;
        }
-       this->flags |= CHILD_REKEY_ADOPTED_PASSIVE;
+       /* 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,
index 849df3a2b86753eb90b3d66764d471594ced940b..a8daed743687ca9cbc4b14ec6c082319012307b8 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016-2020 Tobias Brunner
+ * Copyright (C) 2016-2022 Tobias Brunner
  * Copyright (C) 2007 Martin Willi
  *
  * Copyright (C) secunet Security Networks AG
 #define CHILD_REKEY_H_
 
 typedef struct child_rekey_t child_rekey_t;
+typedef enum child_rekey_collision_t child_rekey_collision_t;
 
 #include <library.h>
 #include <sa/ike_sa.h>
 #include <sa/child_sa.h>
 #include <sa/task.h>
 
+/**
+ * Type of collision an active rekey task may have with an inbound DELETE.
+ */
+enum child_rekey_collision_t {
+       /** Unrelated SA or unknown SPI (might be for the SA this task creates) */
+       CHILD_REKEY_COLLISION_NONE = 0,
+       /** Deleted SA is the one created by the peer in a collision */
+       CHILD_REKEY_COLLISION_PEER,
+       /** Deleted SA is the SA the active task is rekeying */
+       CHILD_REKEY_COLLISION_OLD,
+};
+
 /**
  * Task of type TASK_CHILD_REKEY, rekey an established CHILD_SA.
  */
@@ -41,21 +54,27 @@ struct child_rekey_t {
        task_t task;
 
        /**
-        * Check if the given SA is the redundant CHILD_SA created during a rekey
-        * collision.
+        * Handle a DELETE for the given CHILD_SA/SPI that might be related to
+        * this rekeiyng if the CREATE_CHILD_SA response is delayed.
+        *
+        * This 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.
         *
-        * This is called if the other peer deletes the redundant SA before we were
-        * able to handle the CREATE_CHILD_SA response.
+        * 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
-        * @return                      TRUE if the SA is the redundant CHILD_SA
+        * @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_ @}*/
index 8305fa2004e78b7d7f1ec8801c8cd1aade73e2ed..4b10e92053f4550c1db7cb939c9069fc31037e58 100644 (file)
@@ -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 <daemon.h>
+#include <encoding/payloads/delete_payload.h>
 #include <tests/utils/exchange_test_helper.h>
 #include <tests/utils/exchange_test_asserts.h>
 #include <tests/utils/job_asserts.h>
@@ -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);
@@ -756,203 +762,109 @@ START_TEST(test_regular_responder_handle_hard_expire)
 END_TEST
 
 /**
- * Both peers initiate the CHILD_SA rekeying concurrently and should handle
- * the collision properly depending on the nonces.
+ * 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_collision)
+START_TEST(test_regular_responder_delete)
 {
        ike_sa_t *a, *b;
 
        exchange_test_helper->establish_sa(exchange_test_helper,
                                                                           &a, &b, NULL);
-
-       /* When rekeyings collide we get two CHILD_SAs with a total of four nonces.
-        * The CHILD_SA with the lowest nonce SHOULD be deleted by the peer that
-        * created that CHILD_SA.  The replaced CHILD_SA is deleted by the peer that
-        * initiated the surviving SA.
-        * 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 never get called as this results in a successful rekeying */
+       /* 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_rekey(child_rekey, 2, 5);
+       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, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED);
-       assert_ipsec_sas_installed(b, 1, 2, 5);
+       assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED);
+       assert_ipsec_sas_installed(b, 1, 2, 4);
        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);
+
+       /* <-- 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_REKEYED, CHILD_OUTBOUND_INSTALLED);
-       assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED);
-       assert_ipsec_sas_installed(a, 1, 2, 6);
+       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();
 
-       /* <-- 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);
-               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
-       {
-               assert_hook_not_called(child_rekey);
-               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_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);
-       }
-       /* 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);
-               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
-       {
-               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_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();
 
-       /* we don't expect this hook to get called anymore */
-       assert_hook_not_called(child_rekey);
        /* 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, data[_i].spi_del_b, CHILD_DELETING,
-                                                 data[_i].spi_del_b == 2 ? CHILD_OUTBOUND_NONE
-                                                                                                 : CHILD_OUTBOUND_REGISTERED);
-       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,
-                                                 CHILD_OUTBOUND_INSTALLED);
-       assert_child_sa_count(b, 3);
-       if (data[_i].spi_del_b == 2)
-       {
-               assert_ipsec_sas_installed(b, 2, 4, 5, 6);
-       }
-       else
-       {
-               assert_ipsec_sas_installed(b, 2, 3, 4, 5);
-       }
+       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_jobs_scheduled(1);
+       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, data[_i].spi_del_a, CHILD_DELETING,
-                                                 data[_i].spi_del_a == 1 ? CHILD_OUTBOUND_NONE
-                                                                                                 : CHILD_OUTBOUND_REGISTERED);
-       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);
-       if (data[_i].spi_del_a == 1)
-       {
-               assert_ipsec_sas_installed(a, 1, 3, 5, 6);
-       }
-       else
-       {
-               assert_ipsec_sas_installed(a, 1, 3, 4, 6);
-       }
+       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, 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_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_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_jobs_scheduled(1);
+       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, data[_i].spi_del_b, CHILD_DELETED,
-                                                 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,
-                                                 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);
+       assert_child_sa_state(b, 2, CHILD_DELETED);
+       assert_child_sa_count(b, 1);
+       assert_ipsec_sas_installed(b, 2);
        assert_scheduler();
+       assert_hook();
 
-       /* 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, 1);
-       assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_b);
-       destroy_rekeyed(b, data[_i].spi_del_a);
-       destroy_rekeyed(b, data[_i].spi_del_b);
-       assert_child_sa_count(b, 1);
-       assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b);
+       /* 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();
@@ -964,15 +876,640 @@ START_TEST(test_collision)
 END_TEST
 
 /**
- * Both peers initiate a multi-KE CHILD_SA rekeying concurrently and should
- * handle the collision properly depending on the nonces.
+ * This simulates what happens if the responder for some reason lost the
+ * CHILD_SA the initiator is trying to rekey.
  */
-START_TEST(test_collision_multi_ke)
+START_TEST(test_regular_responder_lost_sa)
 {
        ike_sa_t *a, *b;
 
        exchange_test_helper->establish_sa(exchange_test_helper,
-                                                                          &a, &b, &multi_ke_conf);
+                                                                          &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.
+ */
+START_TEST(test_collision)
+{
+       ike_sa_t *a, *b;
+
+       exchange_test_helper->establish_sa(exchange_test_helper,
+                                                                          &a, &b, NULL);
+
+       /* When rekeyings collide we get two CHILD_SAs with a total of four nonces.
+        * The CHILD_SA with the lowest nonce SHOULD be deleted by the peer that
+        * created that CHILD_SA.  The replaced CHILD_SA is deleted by the peer that
+        * initiated the surviving SA.
+        * 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 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 = 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();
+
+       /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */
+       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, NULL);
+               assert_hook();
+               assert_child_sa_state(a, data[_i].spi_del_b, CHILD_REKEYED,
+                                                         CHILD_OUTBOUND_NONE);
+               assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED,
+                                                         CHILD_OUTBOUND_INSTALLED);
+               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, NULL);
+               assert_hook();
+               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_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)
+       {
+               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_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_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_ipsec_sas_installed(b, 1, 2, 4, 5);
+       }
+       assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING,
+                                                 CHILD_OUTBOUND_NONE);
+
+       /* INFORMATIONAL { D } --> */
+       assert_jobs_scheduled(1);
+       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,
+                                                 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,
+                                                 CHILD_OUTBOUND_INSTALLED);
+       assert_child_sa_count(b, 3);
+       if (data[_i].spi_del_b == 2)
+       {
+               assert_ipsec_sas_installed(b, 2, 4, 5, 6);
+       }
+       else
+       {
+               assert_ipsec_sas_installed(b, 2, 3, 4, 5);
+       }
+       /* <-- INFORMATIONAL { D } */
+       assert_jobs_scheduled(1);
+       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,
+                                                 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);
+       if (data[_i].spi_del_a == 1)
+       {
+               assert_ipsec_sas_installed(a, 1, 3, 5, 6);
+       }
+       else
+       {
+               assert_ipsec_sas_installed(a, 1, 3, 4, 6);
+       }
+
+       /* 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);
+       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_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_scheduler();
+       /* 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_DELETED,
+                                                 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,
+                                                 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);
+       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, 1);
+       assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_b);
+       destroy_rekeyed(b, data[_i].spi_del_a);
+       destroy_rekeyed(b, data[_i].spi_del_b);
+       assert_child_sa_count(b, 1);
+       assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b);
+
+       /* child_rekey/child_updown */
+       assert_hook();
+       assert_hook();
+
+       call_ikesa(a, destroy);
+       call_ikesa(b, destroy);
+}
+END_TEST
+
+/**
+ * Both peers initiate a multi-KE CHILD_SA rekeying concurrently and should
+ * handle the collision properly depending on the nonces.
+ */
+START_TEST(test_collision_multi_ke)
+{
+       ike_sa_t *a, *b;
+
+       exchange_test_helper->establish_sa(exchange_test_helper,
+                                                                          &a, &b, &multi_ke_conf);
+
+       /* When rekeyings collide we get two CHILD_SAs with a total of four nonces.
+        * The CHILD_SA with the lowest nonce SHOULD be deleted by the peer that
+        * created that CHILD_SA.  However, with multiple key exchanges, no CHILD_SA
+        * has yet been established, so the losing peer just doesn't continue with
+        * IKE_FOLLOWUP_KE exchanges (i.e. that SA is not explicitly deleted later).
+        * The replaced CHILD_SA is deleted by the peer that initiated the
+        * surviving SA.  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 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 = 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_REKEYING, CHILD_OUTBOUND_INSTALLED);
+       assert_ipsec_sas_installed(b, 1, 2);
+       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_REKEYING, CHILD_OUTBOUND_INSTALLED);
+       assert_ipsec_sas_installed(a, 1, 2);
+       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);
+       /* if a won, it must remove the passive task, otherwise the active task,
+        * no new SA is yet created */
+       assert_num_tasks(a, data[_i].spi_del_a == 1 ? 0 : 1, TASK_QUEUE_PASSIVE);
+       assert_num_tasks(a, data[_i].spi_del_a == 1 ? 1 : 0, TASK_QUEUE_ACTIVE);
+       assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED);
+       assert_ipsec_sas_installed(a, 1, 2);
+       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, b, NULL);
+       assert_num_tasks(b, data[_i].spi_del_b == 2 ? 0 : 1, TASK_QUEUE_PASSIVE);
+       assert_num_tasks(b, data[_i].spi_del_b == 2 ? 1 : 0, TASK_QUEUE_ACTIVE);
+       assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED);
+       assert_ipsec_sas_installed(b, 1, 2);
+       assert_hook();
+
+       if (data[_i].spi_del_a == 1)
+       {
+               /* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */
+               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, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED);
+               assert_ipsec_sas_installed(b, 1, 2, 5);
+               assert_hook();
+
+               /* <-- IKE_FOLLOWUP_KE { KEr } */
+               assert_hook_rekey(child_rekey, 1, data[_i].spi_a);
+               assert_payload(IN, PLV2_KEY_EXCHANGE);
+               assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE);
+               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, 5);
+               assert_hook();
+       }
+       else
+       {
+               /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */
+               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);
+               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();
+
+               /* IKE_FOLLOWUP_KE { KEr } --> */
+               assert_hook_rekey(child_rekey, 2, data[_i].spi_b);
+               assert_payload(IN, PLV2_KEY_EXCHANGE);
+               assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE);
+               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, 6);
+               assert_hook();
+       }
+
+       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);
+               assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
+               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);
+               assert_child_sa_state(a, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
+               assert_child_sa_count(a, 2);
+               assert_ipsec_sas_installed(a, 1, 3, 5);
+               assert_scheduler();
+               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);
+               assert_child_sa_state(a, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
+               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);
+               assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
+               assert_child_sa_count(b, 2);
+               assert_ipsec_sas_installed(b, 2, 4, 6);
+               assert_scheduler();
+               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);
+       assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b);
+
+       /* child_rekey/child_updown */
+       assert_hook();
+       assert_hook();
+
+       call_ikesa(a, destroy);
+       call_ikesa(b, destroy);
+}
+END_TEST
+
+/**
+ * Both peers initiate a CHILD_SA rekeying concurrently, but only one of them
+ * proposes multiple key exchanges, they should still handle the collision
+ * properly.
+ */
+START_TEST(test_collision_mixed)
+{
+       exchange_test_sa_conf_t conf = {
+               .initiator = {
+                       .esp = "aes256-sha256-modp3072-ke1_ecp256,aes256-sha256-modp3072",
+               },
+               .responder = {
+                       .esp = "aes256-sha256-modp3072,aes256-sha256-modp3072-ke1_ecp256",
+               },
+       };
+       ike_sa_t *a, *b;
+
+       /* let's accept what the peer proposes first */
+       lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals",
+                                                       FALSE, lib->ns);
+
+       exchange_test_helper->establish_sa(exchange_test_helper,
+                                                                          &a, &b, &conf);
 
        /* When rekeyings collide we get two CHILD_SAs with a total of four nonces.
         * The CHILD_SA with the lowest nonce SHOULD be deleted by the peer that
@@ -1024,40 +1561,71 @@ START_TEST(test_collision_multi_ke)
        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_REKEYING, CHILD_OUTBOUND_INSTALLED);
-       assert_ipsec_sas_installed(a, 1, 2);
+       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();
 
        /* <-- 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);
-       /* if a won, it must remove the passive task, otherwise the active task,
-        * no new SA is yet created */
-       assert_num_tasks(a, data[_i].spi_del_a == 1 ? 0 : 1, TASK_QUEUE_PASSIVE);
-       assert_num_tasks(a, data[_i].spi_del_a == 1 ? 1 : 0, TASK_QUEUE_ACTIVE);
-       assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED);
-       assert_ipsec_sas_installed(a, 1, 2);
-       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, b, NULL);
-       assert_num_tasks(b, data[_i].spi_del_b == 2 ? 0 : 1, TASK_QUEUE_PASSIVE);
-       assert_num_tasks(b, data[_i].spi_del_b == 2 ? 1 : 0, TASK_QUEUE_ACTIVE);
-       assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED);
-       assert_ipsec_sas_installed(b, 1, 2);
-       assert_hook();
+       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_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);
 
        if (data[_i].spi_del_a == 1)
        {
+               /* 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, b, NULL);
+               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_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_NONE);
                assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_REGISTERED);
-               assert_ipsec_sas_installed(b, 1, 2, 5);
+               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);
+               assert_child_sa_state(a, 6, CHILD_DELETED, CHILD_OUTBOUND_NONE);
+               assert_child_sa_count(a, 2);
+               assert_ipsec_sas_installed(a, 1, 2, 6);
+               assert_scheduler();
                assert_hook();
 
                /* <-- IKE_FOLLOWUP_KE { KEr } */
@@ -1067,62 +1635,75 @@ START_TEST(test_collision_multi_ke)
                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, 5);
+               assert_child_sa_state(a, 6, CHILD_DELETED, CHILD_OUTBOUND_NONE);
+               assert_ipsec_sas_installed(a, 1, 3, 5, 6);
                assert_hook();
        }
        else
        {
-               /* <-- IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } */
-               assert_hook_rekey(child_rekey, 1, 6);
-               assert_payload(IN, PLV2_KEY_EXCHANGE);
-               assert_notify(IN, ADDITIONAL_KEY_EXCHANGE);
-               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();
-
-               /* IKE_FOLLOWUP_KE { KEr } --> */
-               assert_hook_rekey(child_rekey, 2, data[_i].spi_b);
-               assert_payload(IN, PLV2_KEY_EXCHANGE);
-               assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE);
+               /* CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } --> */
+               assert_hook_rekey(child_rekey, 2, 4);
                exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+               assert_num_tasks(b, 0, TASK_QUEUE_PASSIVE);
+               assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE);
                assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE);
                assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
+               assert_child_sa_count(a, 2);
                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 (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);
+               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();
+               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);
+               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, 2, 3, 5);
+               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);
                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, 3, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
-               assert_child_sa_count(a, 2);
-               assert_ipsec_sas_installed(a, 1, 3, 5);
+               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();
 
                /* simulate the execution of the scheduled jobs */
                destroy_rekeyed(a, data[_i].spi_del_a);
+               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);
@@ -1130,6 +1711,9 @@ START_TEST(test_collision_multi_ke)
                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);
@@ -1143,8 +1727,13 @@ START_TEST(test_collision_multi_ke)
                /* 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);
@@ -1160,37 +1749,31 @@ START_TEST(test_collision_multi_ke)
 END_TEST
 
 /**
- * Both peers initiate a CHILD_SA rekeying concurrently, but only one of them
- * proposes multiple key exchanges, they should still handle the collision
- * properly.
+ * This is like the rekey collision above, but one peer deletes the
+ * redundant/old SA before the other peer receives the CREATE_CHILD_SA
+ * response:
+ *
+ *            rekey ----\       /---- rekey
+ *                       \-----/----> detect collision
+ * detect collision <---------/ /----
+ *                  ----\      /
+ *                       \----/----->
+ *    handle delete <--------/------- delete SA
+ *                  --------/------->
+ *     handle rekey <------/
+ *        delete SA ---------------->
+ *                  <----------------
  */
-START_TEST(test_collision_mixed)
+START_TEST(test_collision_delayed_response)
 {
-       exchange_test_sa_conf_t conf = {
-               .initiator = {
-                       .esp = "aes256-sha256-modp3072-ke1_ecp256,aes256-sha256-modp3072",
-               },
-               .responder = {
-                       .esp = "aes256-sha256-modp3072,aes256-sha256-modp3072-ke1_ecp256",
-               },
-       };
        ike_sa_t *a, *b;
-
-       /* let's accept what the peer proposes first */
-       lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals",
-                                                       FALSE, lib->ns);
+       message_t *msg;
 
        exchange_test_helper->establish_sa(exchange_test_helper,
-                                                                          &a, &b, &conf);
+                                                                          &a, &b, NULL);
 
-       /* When rekeyings collide we get two CHILD_SAs with a total of four nonces.
-        * The CHILD_SA with the lowest nonce SHOULD be deleted by the peer that
-        * created that CHILD_SA.  However, with multiple key exchanges, no CHILD_SA
-        * has yet been established, so the losing peer just doesn't continue with
-        * IKE_FOLLOWUP_KE exchanges (i.e. that SA is not explicitly deleted later).
-        * The replaced CHILD_SA is deleted by the peer that initiated the
-        * surviving SA.  Four nonces and SPIs are needed (SPI 1 and 2 are used for
-        * the initial CHILD_SA):
+       /* 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 <-------/ /----- ...
@@ -1221,173 +1804,176 @@ START_TEST(test_collision_mixed)
        /* 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 } --> */
+       /* 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_REKEYING, CHILD_OUTBOUND_INSTALLED);
-       assert_ipsec_sas_installed(b, 1, 2);
+       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 } */
+       /* <-- 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);
        assert_ipsec_sas_installed(a, 1, 2, 6);
        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)
+       /* 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 } */
+       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_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_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_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(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_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_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_count(a, 2);
-       assert_ipsec_sas_installed(a, 1, 2, 6);
+       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();
 
+       /* <-- 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)
        {
-               /* 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, b, NULL);
-               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_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_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, 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_jobs_scheduled(1);
-               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, 6, CHILD_DELETED, CHILD_OUTBOUND_NONE);
-               assert_child_sa_count(a, 2);
-               assert_ipsec_sas_installed(a, 1, 2, 6);
-               assert_scheduler();
-
-               /* <-- 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);
-               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_child_sa_state(a, 6, CHILD_DELETED, CHILD_OUTBOUND_NONE);
-               assert_ipsec_sas_installed(a, 1, 3, 5, 6);
+               exchange_test_helper->process_message(exchange_test_helper, a, msg);
                assert_hook();
        }
        else
        {
-               /* CREATE_CHILD_SA { SA, Nr, KEr, TSi, TSr, N(ADD_KE) } --> */
-               assert_hook_rekey(child_rekey, 2, 4);
-               exchange_test_helper->process_message(exchange_test_helper, b, NULL);
-               assert_num_tasks(b, 0, TASK_QUEUE_PASSIVE);
-               assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE);
-               assert_child_sa_state(b, 2, CHILD_DELETING, CHILD_OUTBOUND_NONE);
-               assert_child_sa_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
-               assert_child_sa_count(a, 2);
-               assert_ipsec_sas_installed(b, 2, 4, 6);
+               assert_hook_not_called(child_rekey);
+               exchange_test_helper->process_message(exchange_test_helper, a, msg);
                assert_hook();
        }
+       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);
-
-       if (data[_i].spi_del_a == 1)
+       /* INFORMATIONAL { D } --> */
+       assert_jobs_scheduled(1);
+       if (data[_i].spi_del_b == 2)
        {
-               /* INFORMATIONAL { D } --> */
-               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);
-               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();
-
-               /* INFORMATIONAL { D } --> */
-               assert_jobs_scheduled(1);
+               assert_hook_not_called(child_rekey);
                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_DELETED, CHILD_OUTBOUND_NONE);
-               assert_child_sa_state(b, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
-               assert_child_sa_count(b, 3);
-               assert_ipsec_sas_installed(b, 2, 4, 3, 5);
-               assert_scheduler();
-
-               /* <-- 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, 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();
-
-               /* simulate the execution of the scheduled jobs */
-               destroy_rekeyed(a, data[_i].spi_del_a);
-               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_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, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
-               assert_child_sa_count(a, 2);
-               assert_ipsec_sas_installed(a, 1, 4, 6);
-               assert_scheduler();
-
-               /* INFORMATIONAL { D } --> */
-               assert_jobs_scheduled(1);
+               assert_hook_rekey(child_rekey, 2, 5);
                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, 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();
        }
+       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_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED,
+                                                 CHILD_OUTBOUND_INSTALLED);
+       assert_ipsec_sas_installed(b, 2, 4, 5,
+                                                          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);
+       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_state(a, data[_i].spi_a, CHILD_INSTALLED,
+                                                 CHILD_OUTBOUND_INSTALLED);
+       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 */
+       destroy_rekeyed(a, data[_i].spi_del_a);
+       destroy_rekeyed(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);
+       destroy_rekeyed(b, data[_i].spi_del_a);
+       destroy_rekeyed(b, data[_i].spi_del_b);
        assert_child_sa_count(b, 1);
        assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b);
 
@@ -1402,21 +1988,21 @@ END_TEST
 
 /**
  * This is like the rekey collision above, but one peer deletes the
- * redundant/old SA before the other peer receives the CREATE_CHILD_SA
- * response:
+ * 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 SA
+ *    handle delete <--------/------- delete old SA
  *                  --------/------->
- *     handle rekey <------/
- *        delete SA ---------------->
- *                  <----------------
+ *    handle delete <------/--------- delete new SA
+ *                  ------/--------->
+ *     ignore rekey <----/
  */
-START_TEST(test_collision_delayed_response)
+START_TEST(test_collision_delayed_response_delete)
 {
        ike_sa_t *a, *b;
        message_t *msg;
@@ -1453,12 +2039,12 @@ START_TEST(test_collision_delayed_response)
        initiate_rekey(b, 2);
        assert_ipsec_sas_installed(b, 1, 2);
 
-       /* this should never get called as this results in a successful rekeying */
+       /* 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_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 +2052,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,100 +2067,162 @@ 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);
        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_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);
+               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);
-       assert_scheduler();
        /* 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);
-               assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED,
+               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(b, 2, 4, 5, 6);
+               assert_ipsec_sas_installed(a, 1, 4, 6);
+               assert_scheduler();
        }
        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,
+               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(b, 1, 2, 4, 5);
+               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, 3);
+       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, data[_i].spi_a);
+               assert_hook_rekey(child_rekey, 1, 3);
                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);
+                                                         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_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);
+                                                         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_state(a, data[_i].spi_a, CHILD_INSTALLED,
-                                                 CHILD_OUTBOUND_INSTALLED);
-       assert_child_sa_count(a, 3);
+       assert_child_sa_count(a, 2);
+       assert_hook();
 
-       /* we don't expect this hook to get called anymore */
+       /* 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);
@@ -1583,11 +2231,8 @@ START_TEST(test_collision_delayed_response)
                                                  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_INSTALLED,
-                                                 CHILD_OUTBOUND_INSTALLED);
-       assert_ipsec_sas_installed(b, 2, 4, 5,
-                                                          data[_i].spi_del_b == 2 ? 6 : 3);
-       assert_child_sa_count(b, 3);
+       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);
@@ -1596,22 +2241,19 @@ START_TEST(test_collision_delayed_response)
                                                  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);
+       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, 1);
-       assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_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, 1);
-       assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b);
+       assert_child_sa_count(b, 0);
+       assert_ipsec_sas_installed(b);
 
        /* child_rekey/child_updown */
        assert_hook();
@@ -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,82 +2374,266 @@ 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_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);
+       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_ipsec_sas_installed(b, 2, 4, 6);
+       assert_child_sa_count(b, 2);
+       assert_scheduler();
+
+       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 */
+       destroy_rekeyed(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);
+       destroy_rekeyed(b, data[_i].spi_del_b);
+       assert_child_sa_count(b, 1);
+       assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b);
+
+       /* child_rekey/child_updown */
+       assert_hook();
+       assert_hook();
+
+       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:
+ *
+ *            rekey ----\       /---- rekey
+ *                       \     /
+ * detect collision <-----\---/
+ *                  -------\-------->
+ *                          \   /---- delete old SA
+ *                           \-/----> detect collision
+ *    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);
+
+       /* 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 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 = 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();
+
+
+       /* 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();
+
+       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_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();
        }
 
-       /* <-- 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, 6, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
-       assert_ipsec_sas_installed(a, 1, 4, 6);
-       assert_child_sa_count(a, 2);
-       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, 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_ipsec_sas_installed(b, 2, 4, 6);
        assert_child_sa_count(b, 2);
+       assert_ipsec_sas_installed(b, 2, 4, 5);
        assert_scheduler();
 
-       if (after_delete)
-       {
-               /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */
-               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);
-       }
-
        /* simulate the execution of the scheduled jobs */
-       destroy_rekeyed(a, data[_i].spi_del_b);
+       destroy_rekeyed(a, 1);
        assert_child_sa_count(a, 1);
-       assert_ipsec_sas_installed(a, data[_i].spi_a, data[_i].spi_b);
-       destroy_rekeyed(b, data[_i].spi_del_b);
+       assert_ipsec_sas_installed(a, 4, 5);
+       destroy_rekeyed(b, 2);
        assert_child_sa_count(b, 1);
-       assert_ipsec_sas_installed(b, data[_i].spi_a, data[_i].spi_b);
+       assert_ipsec_sas_installed(b, 4, 5);
 
        /* 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:
+ * Similar to above one peer fails to notice the collision but the
+ * CREATE_CHILD_SA request is even more delayed:
  *
  *            rekey ----\       /---- rekey
  *                       \     /
  * detect collision <-----\---/
  *                  -------\-------->
- *                          \   /---- delete old SA
- *                           \-/----> detect collision
- * detect collision <---------/ /---- TEMP_FAIL
- *           delete -----------/---->
- *  aborts rekeying <---------/
+ * detect collision <-------\-------- delete old SA
+ *           delete ---------\------>
+ *                            \----->
+ *                              /---- CHILD_SA_NOT_FOUND
+ *  aborts rekeying <----------/
  */
-START_TEST(test_collision_delayed_request)
+START_TEST(test_collision_delayed_request_more)
 {
        ike_sa_t *a, *b;
        message_t *msg;
@@ -1846,7 +2672,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,25 +2686,8 @@ 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_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);
-
        /* <-- 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();
-
-       /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */
+       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);
@@ -1886,6 +2695,10 @@ START_TEST(test_collision_delayed_request)
        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);
@@ -1896,6 +2709,23 @@ START_TEST(test_collision_delayed_request)
        assert_ipsec_sas_installed(b, 2, 4, 5);
        assert_scheduler();
 
+       /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */
+       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_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_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();
+
        /* simulate the execution of the scheduled jobs */
        destroy_rekeyed(a, 1);
        assert_child_sa_count(a, 1);
@@ -1924,13 +2754,15 @@ END_TEST
  *                       \     /
  * detect collision <-----\---/
  *                  -------\-------->
- * detect collision <-------\-------- delete old SA
- *           delete ---------\------>
- *                            \----->
+ *    handle delete <-------\-------- delete old SA
+ *                  ---------\------>
+ *    handle delete <---------\------ delete new SA
+ *                  -----------\---->
+ *                              \--->
  *                              /---- CHILD_SA_NOT_FOUND
  *  aborts rekeying <----------/
  */
-START_TEST(test_collision_delayed_request_more)
+START_TEST(test_collision_delayed_request_more_delete)
 {
        ike_sa_t *a, *b;
        message_t *msg;
@@ -1964,12 +2796,12 @@ START_TEST(test_collision_delayed_request_more)
        /* delay the CREATE_CHILD_SA request from a to b */
        msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender);
 
-       /* this should never get called as this results in a successful rekeying */
+       /* 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_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 +2815,26 @@ 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();
+
+       /* 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);
@@ -2003,29 +2844,60 @@ START_TEST(test_collision_delayed_request_more)
        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_state(b, 4, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
-       assert_child_sa_count(b, 2);
-       assert_ipsec_sas_installed(b, 2, 4, 5);
+       assert_child_sa_count(b, 1);
+       assert_ipsec_sas_installed(b, 2);
+       assert_hook();
+
        /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */
-       assert_no_jobs_scheduled();
+       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_state(a, 5, CHILD_INSTALLED, CHILD_OUTBOUND_INSTALLED);
-       assert_child_sa_count(a, 2);
-       assert_ipsec_sas_installed(a, 1, 4, 5);
+       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, 1);
-       assert_ipsec_sas_installed(a, 4, 5);
+       assert_child_sa_count(a, 0);
+       assert_ipsec_sas_installed(a);
        destroy_rekeyed(b, 2);
-       assert_child_sa_count(b, 1);
-       assert_ipsec_sas_installed(b, 4, 5);
+       assert_child_sa_count(b, 0);
+       assert_ipsec_sas_installed(b);
 
        /* child_rekey/child_updown */
        assert_hook();
@@ -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);
+       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_no_jobs_scheduled();
+       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");