diff options
author | Tobias Brunner <tobias@strongswan.org> | 2016-06-17 18:53:51 +0200 |
---|---|---|
committer | Tobias Brunner <tobias@strongswan.org> | 2016-06-17 18:53:51 +0200 |
commit | 95a5806a8fb493c0e32608721f6bcda9846b651a (patch) | |
tree | c50c355b73ff9fb3679ad4beb7f9f0efd4d08e34 | |
parent | 408282196453b3f610c758d6f96236ac1e9da3c7 (diff) | |
parent | 5435a9a062453b460125c450c755d46c6c98684c (diff) | |
download | strongswan-95a5806a8fb493c0e32608721f6bcda9846b651a.tar.bz2 strongswan-95a5806a8fb493c0e32608721f6bcda9846b651a.tar.xz |
Merge branch 'exchange-collisions'
Improves the handling of IKEv2 exchange collisions in several corner
cases. TEMPORARY_FAILURE and CHILD_SA_NOT_FOUND notifies that were defined
with RFC 7296 are now handled and sent as appropriate.
The behavior in these situations is tested with new unit tests.
Fixes #379, #464, #876, #1293.
54 files changed, 6479 insertions, 329 deletions
diff --git a/conf/options/charon.opt b/conf/options/charon.opt index 86279ec83..3970012d2 100644 --- a/conf/options/charon.opt +++ b/conf/options/charon.opt @@ -253,6 +253,11 @@ charon.port_nat_t = 4500 allocated. Has to be different from **charon.port**, otherwise a random port will be allocated. +charon.prefer_configured_proposals = yes + Prefer locally configured proposals for IKE/IPsec over supplied ones as + responder (disabling this can avoid keying retries due to INVALID_KE_PAYLOAD + notifies). + charon.prefer_temporary_addrs = no By default public IPv6 addresses are preferred over temporary ones (RFC 4941), to make connections more stable. Enable this option to reverse this. diff --git a/src/libcharon/Makefile.am b/src/libcharon/Makefile.am index 77e2dca0d..550f6eb9c 100644 --- a/src/libcharon/Makefile.am +++ b/src/libcharon/Makefile.am @@ -154,6 +154,9 @@ AM_CPPFLAGS = \ -DIPSEC_DIR=\"${ipsecdir}\" \ -DIPSEC_PIDDIR=\"${piddir}\" +AM_CFLAGS = \ + @COVERAGE_CFLAGS@ + AM_LDFLAGS = \ -no-undefined diff --git a/src/libcharon/config/child_cfg.c b/src/libcharon/config/child_cfg.c index ce3a29d15..76d7f2c58 100644 --- a/src/libcharon/config/child_cfg.c +++ b/src/libcharon/config/child_cfg.c @@ -211,25 +211,40 @@ METHOD(child_cfg_t, get_proposals, linked_list_t*, METHOD(child_cfg_t, select_proposal, proposal_t*, private_child_cfg_t*this, linked_list_t *proposals, bool strip_dh, - bool private) + bool private, bool prefer_self) { - enumerator_t *stored_enum, *supplied_enum; - proposal_t *stored, *supplied, *selected = NULL; + enumerator_t *prefer_enum, *match_enum; + proposal_t *proposal, *match, *selected = NULL; - stored_enum = this->proposals->create_enumerator(this->proposals); - supplied_enum = proposals->create_enumerator(proposals); + if (prefer_self) + { + prefer_enum = this->proposals->create_enumerator(this->proposals); + match_enum = proposals->create_enumerator(proposals); + } + else + { + prefer_enum = proposals->create_enumerator(proposals); + match_enum = this->proposals->create_enumerator(this->proposals); + } - /* compare all stored proposals with all supplied. Stored ones are preferred. */ - while (stored_enum->enumerate(stored_enum, &stored)) + while (prefer_enum->enumerate(prefer_enum, &proposal)) { - stored = stored->clone(stored); - while (supplied_enum->enumerate(supplied_enum, &supplied)) + proposal = proposal->clone(proposal); + if (prefer_self) + { + proposals->reset_enumerator(proposals, match_enum); + } + else + { + this->proposals->reset_enumerator(this->proposals, match_enum); + } + while (match_enum->enumerate(match_enum, &match)) { if (strip_dh) { - stored->strip_dh(stored, MODP_NONE); + proposal->strip_dh(proposal, MODP_NONE); } - selected = stored->select(stored, supplied, private); + selected = proposal->select(proposal, match, private); if (selected) { DBG2(DBG_CFG, "received proposals: %#P", proposals); @@ -238,17 +253,15 @@ METHOD(child_cfg_t, select_proposal, proposal_t*, break; } } - stored->destroy(stored); + proposal->destroy(proposal); if (selected) { break; } - supplied_enum->destroy(supplied_enum); - supplied_enum = proposals->create_enumerator(proposals); } - stored_enum->destroy(stored_enum); - supplied_enum->destroy(supplied_enum); - if (selected == NULL) + prefer_enum->destroy(prefer_enum); + match_enum->destroy(match_enum); + if (!selected) { DBG1(DBG_CFG, "received proposals: %#P", proposals); DBG1(DBG_CFG, "configured proposals: %#P", this->proposals); diff --git a/src/libcharon/config/child_cfg.h b/src/libcharon/config/child_cfg.h index 6a1fa529c..e736b2737 100644 --- a/src/libcharon/config/child_cfg.h +++ b/src/libcharon/config/child_cfg.h @@ -100,10 +100,12 @@ struct child_cfg_t { * @param proposals list from which proposals are selected * @param strip_dh TRUE strip out diffie hellman groups * @param private accept algorithms from a private range + * @param prefer_self whether to prefer configured or supplied proposals * @return selected proposal, or NULL if nothing matches */ proposal_t* (*select_proposal)(child_cfg_t*this, linked_list_t *proposals, - bool strip_dh, bool private); + bool strip_dh, bool private, + bool prefer_self); /** * Add a traffic selector to the config. diff --git a/src/libcharon/config/ike_cfg.c b/src/libcharon/config/ike_cfg.c index a92622c1e..7d52ac88f 100644 --- a/src/libcharon/config/ike_cfg.c +++ b/src/libcharon/config/ike_cfg.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2015 Tobias Brunner + * Copyright (C) 2012-2016 Tobias Brunner * Copyright (C) 2005-2007 Martin Willi * Copyright (C) 2005 Jan Hutter * Hochschule fuer Technik Rapperswil @@ -310,42 +310,57 @@ METHOD(ike_cfg_t, get_proposals, linked_list_t*, } METHOD(ike_cfg_t, select_proposal, proposal_t*, - private_ike_cfg_t *this, linked_list_t *proposals, bool private) + private_ike_cfg_t *this, linked_list_t *proposals, bool private, + bool prefer_self) { - enumerator_t *stored_enum, *supplied_enum; - proposal_t *stored, *supplied, *selected; + enumerator_t *prefer_enum, *match_enum; + proposal_t *proposal, *match, *selected = NULL; - stored_enum = this->proposals->create_enumerator(this->proposals); - supplied_enum = proposals->create_enumerator(proposals); - - - /* compare all stored proposals with all supplied. Stored ones are preferred.*/ - while (stored_enum->enumerate(stored_enum, (void**)&stored)) + if (prefer_self) + { + prefer_enum = this->proposals->create_enumerator(this->proposals); + match_enum = proposals->create_enumerator(proposals); + } + else { - proposals->reset_enumerator(proposals, supplied_enum); + prefer_enum = proposals->create_enumerator(proposals); + match_enum = this->proposals->create_enumerator(this->proposals); + } - while (supplied_enum->enumerate(supplied_enum, (void**)&supplied)) + while (prefer_enum->enumerate(prefer_enum, (void**)&proposal)) + { + if (prefer_self) + { + proposals->reset_enumerator(proposals, match_enum); + } + else { - selected = stored->select(stored, supplied, private); + this->proposals->reset_enumerator(this->proposals, match_enum); + } + while (match_enum->enumerate(match_enum, (void**)&match)) + { + selected = proposal->select(proposal, match, private); if (selected) { - /* they match, return */ - stored_enum->destroy(stored_enum); - supplied_enum->destroy(supplied_enum); DBG2(DBG_CFG, "received proposals: %#P", proposals); DBG2(DBG_CFG, "configured proposals: %#P", this->proposals); DBG2(DBG_CFG, "selected proposal: %P", selected); - return selected; + break; } } + if (selected) + { + break; + } } - /* no proposal match :-(, will result in a NO_PROPOSAL_CHOSEN... */ - stored_enum->destroy(stored_enum); - supplied_enum->destroy(supplied_enum); - DBG1(DBG_CFG, "received proposals: %#P", proposals); - DBG1(DBG_CFG, "configured proposals: %#P", this->proposals); - - return NULL; + prefer_enum->destroy(prefer_enum); + match_enum->destroy(match_enum); + if (!selected) + { + DBG1(DBG_CFG, "received proposals: %#P", proposals); + DBG1(DBG_CFG, "configured proposals: %#P", this->proposals); + } + return selected; } METHOD(ike_cfg_t, get_dh_group, diffie_hellman_group_t, diff --git a/src/libcharon/config/ike_cfg.h b/src/libcharon/config/ike_cfg.h index c6d9554ce..5655a3497 100644 --- a/src/libcharon/config/ike_cfg.h +++ b/src/libcharon/config/ike_cfg.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2015 Tobias Brunner + * Copyright (C) 2012-2016 Tobias Brunner * Copyright (C) 2005-2007 Martin Willi * Copyright (C) 2005 Jan Hutter * Hochschule fuer Technik Rapperswil @@ -165,16 +165,17 @@ struct ike_cfg_t { linked_list_t* (*get_proposals) (ike_cfg_t *this); /** - * Select a proposed from suggested proposals. + * Select a proposal from a list of supplied proposals. * * Returned proposal must be destroyed after use. * * @param proposals list of proposals to select from * @param private accept algorithms from a private range + * @param prefer_self whether to prefer configured or supplied proposals * @return selected proposal, or NULL if none matches. */ proposal_t *(*select_proposal) (ike_cfg_t *this, linked_list_t *proposals, - bool private); + bool private, bool prefer_self); /** * Should we send a certificate request in IKE_SA_INIT? diff --git a/src/libcharon/config/proposal.c b/src/libcharon/config/proposal.c index 6675c1d6d..a83acec23 100644 --- a/src/libcharon/config/proposal.c +++ b/src/libcharon/config/proposal.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2014 Tobias Brunner + * Copyright (C) 2008-2016 Tobias Brunner * Copyright (C) 2006-2010 Martin Willi * Copyright (C) 2013-2015 Andreas Steffen * Hochschule fuer Technik Rapperswil @@ -210,7 +210,7 @@ static bool select_algo(private_proposal_t *this, proposal_t *other, e1 = create_enumerator(this, type); e2 = other->create_enumerator(other, type); - if (!e1->enumerate(e1, NULL, NULL)) + if (!e1->enumerate(e1, &alg1, NULL)) { if (!e2->enumerate(e2, &alg2, NULL)) { @@ -219,12 +219,23 @@ static bool select_algo(private_proposal_t *this, proposal_t *other, else if (optional) { do - { /* if the other peer proposes NONE, we accept the proposal */ + { /* if NONE is proposed, we accept the proposal */ found = !alg2; } while (!found && e2->enumerate(e2, &alg2, NULL)); } } + else if (!e2->enumerate(e2, NULL, NULL)) + { + if (optional) + { + do + { /* if NONE is proposed, we accept the proposal */ + found = !alg1; + } + while (!found && e1->enumerate(e1, &alg1, NULL)); + } + } e1->destroy(e1); e1 = create_enumerator(this, type); @@ -244,7 +255,6 @@ static bool select_algo(private_proposal_t *this, proposal_t *other, "but peer implementation is unknown, skipped"); continue; } - /* ok, we have an algorithm */ selected->add_algorithm(selected, type, alg1, ks1); found = TRUE; break; @@ -288,9 +298,7 @@ METHOD(proposal_t, select_proposal, proposal_t*, } DBG2(DBG_CFG, " proposal matches"); - selected->set_spi(selected, other->get_spi(other)); - return selected; } @@ -445,6 +453,16 @@ static void check_proposal(private_proposal_t *this) } } e->destroy(e); + /* remove MODP_NONE from IKE proposal */ + e = array_create_enumerator(this->transforms); + while (e->enumerate(e, &entry)) + { + if (entry->type == DIFFIE_HELLMAN_GROUP && !entry->alg) + { + array_remove_at(this->transforms, e); + } + } + e->destroy(e); } if (this->protocol == PROTO_ESP) diff --git a/src/libcharon/sa/ike_sa.c b/src/libcharon/sa/ike_sa.c index b7d71e4d6..009277ddd 100644 --- a/src/libcharon/sa/ike_sa.c +++ b/src/libcharon/sa/ike_sa.c @@ -1,9 +1,9 @@ /* - * Copyright (C) 2006-2015 Tobias Brunner + * Copyright (C) 2006-2016 Tobias Brunner * Copyright (C) 2006 Daniel Roethlisberger * Copyright (C) 2005-2009 Martin Willi * Copyright (C) 2005 Jan Hutter - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -71,6 +71,7 @@ ENUM(ike_sa_state_names, IKE_CREATED, IKE_DESTROYING, "ESTABLISHED", "PASSIVE", "REKEYING", + "REKEYED", "DELETING", "DESTROYING", ); @@ -2356,7 +2357,8 @@ METHOD(ike_sa_t, retransmit, status_t, reestablish(this); break; } - if (this->state != IKE_CONNECTING) + if (this->state != IKE_CONNECTING && + this->state != IKE_REKEYED) { charon->bus->ike_updown(charon->bus, &this->public, FALSE); } @@ -2508,6 +2510,7 @@ METHOD(ike_sa_t, roam, status_t, case IKE_DELETING: case IKE_DESTROYING: case IKE_PASSIVE: + case IKE_REKEYED: return SUCCESS; default: break; @@ -2617,6 +2620,12 @@ METHOD(ike_sa_t, queue_task, void, this->task_manager->queue_task(this->task_manager, task); } +METHOD(ike_sa_t, queue_task_delayed, void, + private_ike_sa_t *this, task_t *task, uint32_t delay) +{ + this->task_manager->queue_task_delayed(this->task_manager, task, delay); +} + METHOD(ike_sa_t, inherit_pre, void, private_ike_sa_t *this, ike_sa_t *other_public) { @@ -2935,6 +2944,7 @@ ike_sa_t * ike_sa_create(ike_sa_id_t *ike_sa_id, bool initiator, .create_task_enumerator = _create_task_enumerator, .flush_queue = _flush_queue, .queue_task = _queue_task, + .queue_task_delayed = _queue_task_delayed, #ifdef ME .act_as_mediation_server = _act_as_mediation_server, .get_server_reflexive_host = _get_server_reflexive_host, diff --git a/src/libcharon/sa/ike_sa.h b/src/libcharon/sa/ike_sa.h index 2122867de..6f5040d7c 100644 --- a/src/libcharon/sa/ike_sa.h +++ b/src/libcharon/sa/ike_sa.h @@ -1,9 +1,9 @@ /* - * Copyright (C) 2006-2015 Tobias Brunner + * Copyright (C) 2006-2016 Tobias Brunner * Copyright (C) 2006 Daniel Roethlisberger * Copyright (C) 2005-2009 Martin Willi * Copyright (C) 2005 Jan Hutter - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -58,12 +58,12 @@ typedef struct ike_sa_t ike_sa_t; /** * After which time rekeying should be retried if it failed, in seconds. */ -#define RETRY_INTERVAL 30 +#define RETRY_INTERVAL 15 /** * Jitter to subtract from RETRY_INTERVAL to randomize rekey retry. */ -#define RETRY_JITTER 20 +#define RETRY_JITTER 10 /** * Number of redirects allowed within REDIRECT_LOOP_DETECT_PERIOD. @@ -309,6 +309,11 @@ enum ike_sa_state_t { IKE_REKEYING, /** + * IKE_SA has been rekeyed (or is redundant) + */ + IKE_REKEYED, + + /** * IKE_SA is in progress of deletion */ IKE_DELETING, @@ -1119,6 +1124,15 @@ struct ike_sa_t { void (*queue_task)(ike_sa_t *this, task_t *task); /** + * Queue a task in the manager, but delay its initiation for at least the + * given number of seconds. + * + * @param task task to queue + * @param delay minimum delay in s before initiating the task + */ + void (*queue_task_delayed)(ike_sa_t *this, task_t *task, uint32_t delay); + + /** * Inherit required attributes to new SA before rekeying. * * Some properties of the SA must be applied before starting IKE_SA diff --git a/src/libcharon/sa/ike_sa_manager.c b/src/libcharon/sa/ike_sa_manager.c index 9b9ad93a2..ce44207c4 100644 --- a/src/libcharon/sa/ike_sa_manager.c +++ b/src/libcharon/sa/ike_sa_manager.c @@ -1415,7 +1415,8 @@ METHOD(ike_sa_manager_t, checkout_by_config, ike_sa_t*, { continue; } - if (entry->ike_sa->get_state(entry->ike_sa) == IKE_DELETING) + if (entry->ike_sa->get_state(entry->ike_sa) == IKE_DELETING || + entry->ike_sa->get_state(entry->ike_sa) == IKE_REKEYED) { /* skip IKE_SAs which are not usable, wake other waiting threads */ entry->condvar->signal(entry->condvar); continue; @@ -2188,7 +2189,7 @@ METHOD(ike_sa_manager_t, flush, void, unlock_all_segments(this); this->spi_lock->write_lock(this->spi_lock); - this->rng->destroy(this->rng); + DESTROY_IF(this->rng); this->rng = NULL; this->spi_cb.cb = NULL; this->spi_cb.data = NULL; diff --git a/src/libcharon/sa/ikev1/task_manager_v1.c b/src/libcharon/sa/ikev1/task_manager_v1.c index 126863a66..b0c4f5f84 100644 --- a/src/libcharon/sa/ikev1/task_manager_v1.c +++ b/src/libcharon/sa/ikev1/task_manager_v1.c @@ -1,7 +1,7 @@ /* * Copyright (C) 2007-2016 Tobias Brunner * Copyright (C) 2007-2011 Martin Willi - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -1513,8 +1513,8 @@ static bool has_queued(private_task_manager_t *this, task_type_t type) return found; } -METHOD(task_manager_t, queue_task, void, - private_task_manager_t *this, task_t *task) +METHOD(task_manager_t, queue_task_delayed, void, + private_task_manager_t *this, task_t *task, uint32_t delay) { task_type_t type = task->get_type(task); @@ -1535,6 +1535,12 @@ METHOD(task_manager_t, queue_task, void, this->queued_tasks->insert_last(this->queued_tasks, task); } +METHOD(task_manager_t, queue_task, void, + private_task_manager_t *this, task_t *task) +{ + queue_task_delayed(this, task, 0); +} + METHOD(task_manager_t, queue_ike, void, private_task_manager_t *this) { @@ -1975,6 +1981,7 @@ task_manager_v1_t *task_manager_v1_create(ike_sa_t *ike_sa) .task_manager = { .process_message = _process_message, .queue_task = _queue_task, + .queue_task_delayed = _queue_task_delayed, .queue_ike = _queue_ike, .queue_ike_rekey = _queue_ike_rekey, .queue_ike_reauth = _queue_ike_reauth, diff --git a/src/libcharon/sa/ikev1/tasks/aggressive_mode.c b/src/libcharon/sa/ikev1/tasks/aggressive_mode.c index af3a13924..9b5f676a3 100644 --- a/src/libcharon/sa/ikev1/tasks/aggressive_mode.c +++ b/src/libcharon/sa/ikev1/tasks/aggressive_mode.c @@ -378,6 +378,7 @@ METHOD(task_t, process_r, status_t, identification_t *id; linked_list_t *list; uint16_t group; + bool prefer_configured; this->ike_cfg = this->ike_sa->get_ike_cfg(this->ike_sa); DBG0(DBG_IKE, "%H is initiating a Aggressive Mode IKE_SA", @@ -401,8 +402,10 @@ METHOD(task_t, process_r, status_t, } list = sa_payload->get_proposals(sa_payload); + prefer_configured = lib->settings->get_bool(lib->settings, + "%s.prefer_configured_proposals", TRUE, lib->ns); this->proposal = this->ike_cfg->select_proposal(this->ike_cfg, - list, FALSE); + list, FALSE, prefer_configured); list->destroy_offset(list, offsetof(proposal_t, destroy)); if (!this->proposal) { @@ -640,7 +643,7 @@ METHOD(task_t, process_i, status_t, } list = sa_payload->get_proposals(sa_payload); this->proposal = this->ike_cfg->select_proposal(this->ike_cfg, - list, FALSE); + list, FALSE, TRUE); list->destroy_offset(list, offsetof(proposal_t, destroy)); if (!this->proposal) { diff --git a/src/libcharon/sa/ikev1/tasks/main_mode.c b/src/libcharon/sa/ikev1/tasks/main_mode.c index f0c145f7d..628ea0de8 100644 --- a/src/libcharon/sa/ikev1/tasks/main_mode.c +++ b/src/libcharon/sa/ikev1/tasks/main_mode.c @@ -367,7 +367,7 @@ METHOD(task_t, process_r, status_t, { linked_list_t *list; sa_payload_t *sa_payload; - bool private; + bool private, prefer_configured; this->ike_cfg = this->ike_sa->get_ike_cfg(this->ike_sa); DBG0(DBG_IKE, "%H is initiating a Main Mode IKE_SA", @@ -392,9 +392,11 @@ METHOD(task_t, process_r, status_t, list = sa_payload->get_proposals(sa_payload); private = this->ike_sa->supports_extension(this->ike_sa, - EXT_STRONGSWAN); + EXT_STRONGSWAN); + prefer_configured = lib->settings->get_bool(lib->settings, + "%s.prefer_configured_proposals", TRUE, lib->ns); this->proposal = this->ike_cfg->select_proposal(this->ike_cfg, - list, private); + list, private, prefer_configured); list->destroy_offset(list, offsetof(proposal_t, destroy)); if (!this->proposal) { @@ -641,7 +643,7 @@ METHOD(task_t, process_i, status_t, private = this->ike_sa->supports_extension(this->ike_sa, EXT_STRONGSWAN); this->proposal = this->ike_cfg->select_proposal(this->ike_cfg, - list, private); + list, private, TRUE); list->destroy_offset(list, offsetof(proposal_t, destroy)); if (!this->proposal) { diff --git a/src/libcharon/sa/ikev1/tasks/quick_mode.c b/src/libcharon/sa/ikev1/tasks/quick_mode.c index a6cc19390..bbd1cb09f 100644 --- a/src/libcharon/sa/ikev1/tasks/quick_mode.c +++ b/src/libcharon/sa/ikev1/tasks/quick_mode.c @@ -1051,7 +1051,7 @@ METHOD(task_t, process_r, status_t, linked_list_t *tsi, *tsr, *hostsi, *hostsr, *list = NULL; peer_cfg_t *peer_cfg; uint16_t group; - bool private; + bool private, prefer_configured; sa_payload = (sa_payload_t*)message->get_payload(message, PLV1_SECURITY_ASSOCIATION); @@ -1109,8 +1109,10 @@ METHOD(task_t, process_r, status_t, } private = this->ike_sa->supports_extension(this->ike_sa, EXT_STRONGSWAN); - this->proposal = this->config->select_proposal(this->config, - list, FALSE, private); + prefer_configured = lib->settings->get_bool(lib->settings, + "%s.prefer_configured_proposals", TRUE, lib->ns); + this->proposal = this->config->select_proposal(this->config, list, + FALSE, private, prefer_configured); list->destroy_offset(list, offsetof(proposal_t, destroy)); get_lifetimes(this); @@ -1323,8 +1325,8 @@ METHOD(task_t, process_i, status_t, } private = this->ike_sa->supports_extension(this->ike_sa, EXT_STRONGSWAN); - this->proposal = this->config->select_proposal(this->config, - list, FALSE, private); + this->proposal = this->config->select_proposal(this->config, list, + FALSE, private, TRUE); list->destroy_offset(list, offsetof(proposal_t, destroy)); if (!this->proposal) { diff --git a/src/libcharon/sa/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c index 702c383b1..41a4e1b75 100644 --- a/src/libcharon/sa/ikev2/task_manager_v2.c +++ b/src/libcharon/sa/ikev2/task_manager_v2.c @@ -1,7 +1,7 @@ /* - * Copyright (C) 2007-2015 Tobias Brunner + * Copyright (C) 2007-2016 Tobias Brunner * Copyright (C) 2007-2010 Martin Willi - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -43,30 +43,14 @@ #include <encoding/payloads/unknown_payload.h> #include <processing/jobs/retransmit_job.h> #include <processing/jobs/delete_ike_sa_job.h> +#include <processing/jobs/initiate_tasks_job.h> #ifdef ME #include <sa/ikev2/tasks/ike_me.h> #endif -typedef struct exchange_t exchange_t; - -/** - * An exchange in the air, used do detect and handle retransmission - */ -struct exchange_t { - - /** - * Message ID used for this transaction - */ - uint32_t mid; - - /** - * generated packet for retransmission - */ - packet_t *packet; -}; - typedef struct private_task_manager_t private_task_manager_t; +typedef struct queued_task_t queued_task_t; /** * private data of the task manager @@ -182,6 +166,22 @@ struct private_task_manager_t { }; /** + * Queued tasks + */ +struct queued_task_t { + + /** + * Queued task + */ + task_t *task; + + /** + * Time before which the task is not to be initiated + */ + timeval_t time; +}; + +/** * Reset retransmission packet list */ static void clear_packets(array_t *array) @@ -216,6 +216,12 @@ METHOD(task_manager_t, flush_queue, void, } while (array_remove(array, ARRAY_TAIL, &task)) { + if (queue == TASK_QUEUE_QUEUED) + { + queued_task_t *queued = (queued_task_t*)task; + task = queued->task; + free(queued); + } task->destroy(task); } } @@ -229,22 +235,28 @@ METHOD(task_manager_t, flush, void, } /** - * move a task of a specific type from the queue to the active list + * Move a task of a specific type from the queue to the active list, if it is + * not delayed. */ static bool activate_task(private_task_manager_t *this, task_type_t type) { enumerator_t *enumerator; - task_t *task; + queued_task_t *queued; + timeval_t now; bool found = FALSE; + time_monotonic(&now); + enumerator = array_create_enumerator(this->queued_tasks); - while (enumerator->enumerate(enumerator, (void**)&task)) + while (enumerator->enumerate(enumerator, (void**)&queued)) { - if (task->get_type(task) == type) + if (queued->task->get_type(queued->task) == type && + !timercmp(&now, &queued->time, <)) { DBG2(DBG_IKE, " activating %N task", task_type_names, type); array_remove_at(this->queued_tasks, enumerator); - array_insert(this->active_tasks, ARRAY_TAIL, task); + array_insert(this->active_tasks, ARRAY_TAIL, queued->task); + free(queued); found = TRUE; break; } @@ -535,6 +547,7 @@ METHOD(task_manager_t, initiate, status_t, break; } case IKE_REKEYING: + case IKE_REKEYED: if (activate_task(this, TASK_IKE_DELETE)) { exchange = INFORMATIONAL; @@ -611,7 +624,8 @@ METHOD(task_manager_t, initiate, status_t, case FAILED: default: this->initiating.type = EXCHANGE_TYPE_UNDEFINED; - if (this->ike_sa->get_state(this->ike_sa) != IKE_CONNECTING) + if (this->ike_sa->get_state(this->ike_sa) != IKE_CONNECTING && + this->ike_sa->get_state(this->ike_sa) != IKE_REKEYED) { charon->bus->ike_updown(charon->bus, this->ike_sa, FALSE); } @@ -759,8 +773,7 @@ static bool handle_collisions(private_task_manager_t *this, task_t *task) /* do we have to check */ if (type == TASK_IKE_REKEY || type == TASK_CHILD_REKEY || - type == TASK_CHILD_DELETE || type == TASK_IKE_DELETE || - type == TASK_IKE_REAUTH) + type == TASK_CHILD_DELETE || type == TASK_IKE_DELETE) { /* find an exchange collision, and notify these tasks */ enumerator = array_create_enumerator(this->active_tasks); @@ -769,8 +782,7 @@ static bool handle_collisions(private_task_manager_t *this, task_t *task) switch (active->get_type(active)) { case TASK_IKE_REKEY: - if (type == TASK_IKE_REKEY || type == TASK_IKE_DELETE || - type == TASK_IKE_REAUTH) + if (type == TASK_IKE_REKEY || type == TASK_IKE_DELETE) { ike_rekey_t *rekey = (ike_rekey_t*)active; rekey->collide(rekey, task); @@ -847,6 +859,10 @@ static status_t build_response(private_task_manager_t *this, message_t *request) /* FALL */ case DESTROY_ME: /* destroy IKE_SA, but SEND response first */ + if (handle_collisions(this, task)) + { + array_remove_at(this->passive_tasks, enumerator); + } delete = TRUE; break; } @@ -909,9 +925,11 @@ static status_t process_request(private_task_manager_t *this, payload_t *payload; notify_payload_t *notify; delete_payload_t *delete; + ike_sa_state_t state; if (array_count(this->passive_tasks) == 0) { /* create tasks depending on request type, if not already some queued */ + state = this->ike_sa->get_state(this->ike_sa); switch (message->get_exchange_type(message)) { case IKE_SA_INIT: @@ -947,8 +965,8 @@ static status_t process_request(private_task_manager_t *this, { /* FIXME: we should prevent this on mediation connections */ bool notify_found = FALSE, ts_found = FALSE; - if (this->ike_sa->get_state(this->ike_sa) == IKE_CREATED || - this->ike_sa->get_state(this->ike_sa) == IKE_CONNECTING) + if (state == IKE_CREATED || + state == IKE_CONNECTING) { DBG1(DBG_IKE, "received CREATE_CHILD_SA request for " "unestablished IKE_SA, rejected"); @@ -1013,6 +1031,14 @@ static status_t process_request(private_task_manager_t *this, case PLV2_NOTIFY: { notify = (notify_payload_t*)payload; + if (state == IKE_REKEYED) + { + DBG1(DBG_IKE, "received unexpected notify %N " + "for rekeyed IKE_SA, ignored", + notify_type_names, + notify->get_notify_type(notify)); + break; + } switch (notify->get_notify_type(notify)) { case ADDITIONAL_IP4_ADDRESS: @@ -1355,6 +1381,8 @@ METHOD(task_manager_t, process_message, status_t, status_t status; uint32_t mid; bool schedule_delete_job = FALSE; + ike_sa_state_t state; + exchange_type_t type; charon->bus->message(charon->bus, msg, TRUE, FALSE); status = parse_message(this, msg); @@ -1395,15 +1423,16 @@ METHOD(task_manager_t, process_message, status_t, { if (mid == this->responding.mid) { - /* reject initial messages if not received in specific states */ - if ((msg->get_exchange_type(msg) == IKE_SA_INIT && - this->ike_sa->get_state(this->ike_sa) != IKE_CREATED) || - (msg->get_exchange_type(msg) == IKE_AUTH && - this->ike_sa->get_state(this->ike_sa) != IKE_CONNECTING)) + /* reject initial messages if not received in specific states, + * after rekeying we only expect a DELETE in an INFORMATIONAL */ + type = msg->get_exchange_type(msg); + state = this->ike_sa->get_state(this->ike_sa); + if ((type == IKE_SA_INIT && state != IKE_CREATED) || + (type == IKE_AUTH && state != IKE_CONNECTING) || + (state == IKE_REKEYED && type != INFORMATIONAL)) { DBG1(DBG_IKE, "ignoring %N in IKE_SA state %N", - exchange_type_names, msg->get_exchange_type(msg), - ike_sa_state_names, this->ike_sa->get_state(this->ike_sa)); + exchange_type_names, type, ike_sa_state_names, state); return FAILED; } if (!this->ike_sa->supports_extension(this->ike_sa, EXT_MOBIKE)) @@ -1507,18 +1536,19 @@ METHOD(task_manager_t, process_message, status_t, return SUCCESS; } -METHOD(task_manager_t, queue_task, void, - private_task_manager_t *this, task_t *task) +METHOD(task_manager_t, queue_task_delayed, void, + private_task_manager_t *this, task_t *task, uint32_t delay) { + enumerator_t *enumerator; + queued_task_t *queued; + timeval_t time; + if (task->get_type(task) == TASK_IKE_MOBIKE) { /* there is no need to queue more than one mobike task */ - enumerator_t *enumerator; - task_t *current; - enumerator = array_create_enumerator(this->queued_tasks); - while (enumerator->enumerate(enumerator, ¤t)) + while (enumerator->enumerate(enumerator, &queued)) { - if (current->get_type(current) == TASK_IKE_MOBIKE) + if (queued->task->get_type(queued->task) == TASK_IKE_MOBIKE) { enumerator->destroy(enumerator); task->destroy(task); @@ -1527,8 +1557,35 @@ METHOD(task_manager_t, queue_task, void, } enumerator->destroy(enumerator); } - DBG2(DBG_IKE, "queueing %N task", task_type_names, task->get_type(task)); - array_insert(this->queued_tasks, ARRAY_TAIL, task); + time_monotonic(&time); + if (delay) + { + job_t *job; + + DBG2(DBG_IKE, "queueing %N task (delayed by %us)", task_type_names, + task->get_type(task), delay); + time.tv_sec += delay; + + job = (job_t*)initiate_tasks_job_create( + this->ike_sa->get_id(this->ike_sa)); + lib->scheduler->schedule_job_tv(lib->scheduler, job, time); + } + else + { + DBG2(DBG_IKE, "queueing %N task", task_type_names, + task->get_type(task)); + } + INIT(queued, + .task = task, + .time = time, + ); + array_insert(this->queued_tasks, ARRAY_TAIL, queued); +} + +METHOD(task_manager_t, queue_task, void, + private_task_manager_t *this, task_t *task) +{ + queue_task_delayed(this, task, 0); } /** @@ -1538,12 +1595,12 @@ static bool has_queued(private_task_manager_t *this, task_type_t type) { enumerator_t *enumerator; bool found = FALSE; - task_t *task; + queued_task_t *queued; enumerator = array_create_enumerator(this->queued_tasks); - while (enumerator->enumerate(enumerator, &task)) + while (enumerator->enumerate(enumerator, &queued)) { - if (task->get_type(task) == type) + if (queued->task->get_type(queued->task) == type) { found = TRUE; break; @@ -1622,7 +1679,7 @@ static void trigger_mbb_reauth(private_task_manager_t *this) child_cfg_t *cfg; ike_sa_t *new; host_t *host; - task_t *task; + queued_task_t *queued; new = charon->ike_sa_manager->checkout_new(charon->ike_sa_manager, this->ike_sa->get_version(this->ike_sa), TRUE); @@ -1653,13 +1710,14 @@ static void trigger_mbb_reauth(private_task_manager_t *this) enumerator->destroy(enumerator); enumerator = array_create_enumerator(this->queued_tasks); - while (enumerator->enumerate(enumerator, &task)) + while (enumerator->enumerate(enumerator, &queued)) { - if (task->get_type(task) == TASK_CHILD_CREATE) + if (queued->task->get_type(queued->task) == TASK_CHILD_CREATE) { - task->migrate(task, new); - new->queue_task(new, task); + queued->task->migrate(queued->task, new); + new->queue_task(new, queued->task); array_remove_at(this->queued_tasks, enumerator); + free(queued); } } enumerator->destroy(enumerator); @@ -1784,34 +1842,62 @@ METHOD(task_manager_t, adopt_tasks, void, private_task_manager_t *this, task_manager_t *other_public) { private_task_manager_t *other = (private_task_manager_t*)other_public; - task_t *task; + queued_task_t *queued; + timeval_t now; + + time_monotonic(&now); /* move queued tasks from other to this */ - while (array_remove(other->queued_tasks, ARRAY_TAIL, &task)) + while (array_remove(other->queued_tasks, ARRAY_TAIL, &queued)) { - DBG2(DBG_IKE, "migrating %N task", task_type_names, task->get_type(task)); - task->migrate(task, this->ike_sa); - array_insert(this->queued_tasks, ARRAY_HEAD, task); + DBG2(DBG_IKE, "migrating %N task", task_type_names, + queued->task->get_type(queued->task)); + queued->task->migrate(queued->task, this->ike_sa); + /* don't delay tasks on the new IKE_SA */ + queued->time = now; + array_insert(this->queued_tasks, ARRAY_HEAD, queued); } } /** - * Migrates child-creating tasks from src to dst + * Migrates child-creating tasks from other to this */ static void migrate_child_tasks(private_task_manager_t *this, - array_t *src, array_t *dst) + private_task_manager_t *other, + task_queue_t queue) { enumerator_t *enumerator; + array_t *array; task_t *task; - enumerator = array_create_enumerator(src); + switch (queue) + { + case TASK_QUEUE_ACTIVE: + array = other->active_tasks; + break; + case TASK_QUEUE_QUEUED: + array = other->queued_tasks; + break; + default: + return; + } + + enumerator = array_create_enumerator(array); while (enumerator->enumerate(enumerator, &task)) { + queued_task_t *queued = NULL; + + if (queue == TASK_QUEUE_QUEUED) + { + queued = (queued_task_t*)task; + task = queued->task; + } if (task->get_type(task) == TASK_CHILD_CREATE) { - array_remove_at(src, enumerator); + array_remove_at(array, enumerator); task->migrate(task, this->ike_sa); - array_insert(dst, ARRAY_TAIL, task); + queue_task(this, task); + free(queued); } } enumerator->destroy(enumerator); @@ -1823,9 +1909,9 @@ METHOD(task_manager_t, adopt_child_tasks, void, private_task_manager_t *other = (private_task_manager_t*)other_public; /* move active child tasks from other to this */ - migrate_child_tasks(this, other->active_tasks, this->queued_tasks); + migrate_child_tasks(this, other, TASK_QUEUE_ACTIVE); /* do the same for queued tasks */ - migrate_child_tasks(this, other->queued_tasks, this->queued_tasks); + migrate_child_tasks(this, other, TASK_QUEUE_QUEUED); } METHOD(task_manager_t, busy, bool, @@ -1838,7 +1924,9 @@ METHOD(task_manager_t, reset, void, private_task_manager_t *this, uint32_t initiate, uint32_t respond) { enumerator_t *enumerator; + queued_task_t *queued; task_t *task; + timeval_t now; /* reset message counters and retransmit packets */ clear_packets(this->responding.packets); @@ -1857,11 +1945,13 @@ METHOD(task_manager_t, reset, void, } this->initiating.type = EXCHANGE_TYPE_UNDEFINED; + time_monotonic(&now); /* reset queued tasks */ enumerator = array_create_enumerator(this->queued_tasks); - while (enumerator->enumerate(enumerator, &task)) + while (enumerator->enumerate(enumerator, &queued)) { - task->migrate(task, this->ike_sa); + queued->time = now; + queued->task->migrate(queued->task, this->ike_sa); } enumerator->destroy(enumerator); @@ -1869,12 +1959,25 @@ METHOD(task_manager_t, reset, void, while (array_remove(this->active_tasks, ARRAY_TAIL, &task)) { task->migrate(task, this->ike_sa); - array_insert(this->queued_tasks, ARRAY_HEAD, task); + INIT(queued, + .task = task, + .time = now, + ); + array_insert(this->queued_tasks, ARRAY_HEAD, queued); } this->reset = TRUE; } +/** + * Filter queued tasks + */ +static bool filter_queued(void *unused, queued_task_t **queued, task_t **task) +{ + *task = (*queued)->task; + return TRUE; +} + METHOD(task_manager_t, create_task_enumerator, enumerator_t*, private_task_manager_t *this, task_queue_t queue) { @@ -1885,7 +1988,9 @@ METHOD(task_manager_t, create_task_enumerator, enumerator_t*, case TASK_QUEUE_PASSIVE: return array_create_enumerator(this->passive_tasks); case TASK_QUEUE_QUEUED: - return array_create_enumerator(this->queued_tasks); + return enumerator_create_filter( + array_create_enumerator(this->queued_tasks), + (void*)filter_queued, NULL, NULL); default: return enumerator_create_empty(); } @@ -1921,6 +2026,7 @@ task_manager_v2_t *task_manager_v2_create(ike_sa_t *ike_sa) .task_manager = { .process_message = _process_message, .queue_task = _queue_task, + .queue_task_delayed = _queue_task_delayed, .queue_ike = _queue_ike, .queue_ike_rekey = _queue_ike_rekey, .queue_ike_reauth = _queue_ike_reauth, diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index 326993885..64a82850b 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -1,8 +1,8 @@ /* - * Copyright (C) 2008 Tobias Brunner + * Copyright (C) 2008-2016 Tobias Brunner * Copyright (C) 2005-2008 Martin Willi * Copyright (C) 2005 Jan Hutter - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -29,7 +29,7 @@ #include <encoding/payloads/delete_payload.h> #include <processing/jobs/delete_ike_sa_job.h> #include <processing/jobs/inactivity_job.h> - +#include <processing/jobs/initiate_tasks_job.h> typedef struct private_child_create_t private_child_create_t; @@ -205,6 +205,25 @@ struct private_child_create_t { }; /** + * Schedule a retry if creating the CHILD_SA temporary failed + */ +static void schedule_delayed_retry(private_child_create_t *this) +{ + child_create_t *task; + uint32_t retry; + + retry = RETRY_INTERVAL - (random() % RETRY_JITTER); + + task = child_create_create(this->ike_sa, + this->config->get_ref(this->config), FALSE, + this->packet_tsi, this->packet_tsr); + task->use_reqid(task, this->reqid); + DBG1(DBG_IKE, "creating CHILD_SA failed, trying again in %d seconds", + retry); + this->ike_sa->queue_task_delayed(this->ike_sa, (task_t*)task, retry); +} + +/** * get the nonce from a message */ static status_t get_nonce(message_t *message, chunk_t *nonce) @@ -464,7 +483,7 @@ static status_t select_and_install(private_child_create_t *this, chunk_t integ_i = chunk_empty, integ_r = chunk_empty; linked_list_t *my_ts, *other_ts; host_t *me, *other; - bool private; + bool private, prefer_configured; if (this->proposals == NULL) { @@ -481,8 +500,10 @@ static status_t select_and_install(private_child_create_t *this, other = this->ike_sa->get_other_host(this->ike_sa); private = this->ike_sa->supports_extension(this->ike_sa, EXT_STRONGSWAN); + prefer_configured = lib->settings->get_bool(lib->settings, + "%s.prefer_configured_proposals", TRUE, lib->ns); this->proposal = this->config->select_proposal(this->config, - this->proposals, no_dh, private); + this->proposals, no_dh, private, prefer_configured); if (this->proposal == NULL) { DBG1(DBG_IKE, "no acceptable proposal found"); @@ -1232,13 +1253,13 @@ METHOD(task_t, build_r, status_t, if (this->ike_sa->get_state(this->ike_sa) == IKE_REKEYING) { DBG1(DBG_IKE, "unable to create CHILD_SA while rekeying IKE_SA"); - message->add_notify(message, TRUE, NO_ADDITIONAL_SAS, chunk_empty); + message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); return SUCCESS; } if (this->ike_sa->get_state(this->ike_sa) == IKE_DELETING) { DBG1(DBG_IKE, "unable to create CHILD_SA while deleting IKE_SA"); - message->add_notify(message, TRUE, NO_ADDITIONAL_SAS, chunk_empty); + message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); return SUCCESS; } @@ -1441,6 +1462,17 @@ METHOD(task_t, process_i, status_t, /* an error in CHILD_SA creation is not critical */ return SUCCESS; } + case TEMPORARY_FAILURE: + { + DBG1(DBG_IKE, "received %N notify, will retry later", + notify_type_names, type); + enumerator->destroy(enumerator); + if (!this->rekey) + { /* the rekey task will retry itself if necessary */ + schedule_delayed_retry(this); + } + return SUCCESS; + } case INVALID_KE_PAYLOAD: { chunk_t data; diff --git a/src/libcharon/sa/ikev2/tasks/child_delete.c b/src/libcharon/sa/ikev2/tasks/child_delete.c index 053a5c51d..6fa8836ac 100644 --- a/src/libcharon/sa/ikev2/tasks/child_delete.c +++ b/src/libcharon/sa/ikev2/tasks/child_delete.c @@ -1,6 +1,7 @@ /* + * Copyright (C) 2009-2016 Tobias Brunner * Copyright (C) 2006-2007 Martin Willi - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -18,7 +19,7 @@ #include <daemon.h> #include <encoding/payloads/delete_payload.h> #include <sa/ikev2/tasks/child_create.h> - +#include <sa/ikev2/tasks/child_rekey.h> typedef struct private_child_delete_t private_child_delete_t; @@ -119,6 +120,33 @@ static void build_payloads(private_child_delete_t *this, message_t *message) } /** + * Check if the given CHILD_SA is the redundant SA created in a rekey collision. + */ +static bool is_redundant(private_child_delete_t *this, child_sa_t *child) +{ + 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; + + if (rekey->is_redundant(rekey, child)) + { + tasks->destroy(tasks); + return TRUE; + } + } + } + tasks->destroy(tasks); + return FALSE; +} + +/** * read in payloads and find the children to delete */ static void process_payloads(private_child_delete_t *this, message_t *message) @@ -157,24 +185,31 @@ static void process_payloads(private_child_delete_t *this, message_t *message) switch (child_sa->get_state(child_sa)) { - case CHILD_REKEYING: + case CHILD_REKEYED: this->rekeyed = TRUE; - /* we reply as usual, rekeying will fail */ break; case CHILD_DELETING: /* we don't send back a delete if we initiated ourself */ if (!this->initiator) { - this->ike_sa->destroy_child_sa(this->ike_sa, - protocol, spi); continue; } /* fall through */ + case CHILD_REKEYING: + /* we reply as usual, rekeying will fail */ case CHILD_INSTALLED: if (!this->initiator) - { /* reestablish installed children if required */ - this->check_delete_action = TRUE; + { + if (is_redundant(this, child_sa)) + { + this->rekeyed = TRUE; + } + else + { + this->check_delete_action = TRUE; + } } + break; default: break; } @@ -206,7 +241,7 @@ static status_t destroy_and_reestablish(private_child_delete_t *this) enumerator = this->child_sas->create_enumerator(this->child_sas); while (enumerator->enumerate(enumerator, (void**)&child_sa)) { - /* signal child down event if we are not rekeying */ + /* signal child down event if we weren't rekeying */ if (!this->rekeyed) { charon->bus->child_updown(charon->bus, child_sa, FALSE); @@ -308,7 +343,7 @@ METHOD(task_t, build_i, status_t, this->spi = child_sa->get_spi(child_sa, TRUE); } this->child_sas->insert_last(this->child_sas, child_sa); - if (child_sa->get_state(child_sa) == CHILD_REKEYING) + if (child_sa->get_state(child_sa) == CHILD_REKEYED) { this->rekeyed = TRUE; } @@ -347,11 +382,7 @@ METHOD(task_t, process_r, status_t, METHOD(task_t, build_r, status_t, private_child_delete_t *this, message_t *message) { - /* if we are rekeying, we send an empty informational */ - if (this->ike_sa->get_state(this->ike_sa) != IKE_REKEYING) - { - build_payloads(this, message); - } + build_payloads(this, message); DBG1(DBG_IKE, "CHILD_SA closed"); return destroy_and_reestablish(this); } diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 17b812e91..c04ec141f 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -1,7 +1,8 @@ /* + * Copyright (C) 2009-2016 Tobias Brunner * Copyright (C) 2005-2007 Martin Willi * Copyright (C) 2005 Jan Hutter - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -159,14 +160,21 @@ METHOD(task_t, build_i, status_t, { /* check if it is an outbound CHILD_SA */ this->child_sa = this->ike_sa->get_child_sa(this->ike_sa, this->protocol, this->spi, FALSE); - if (!this->child_sa) - { /* CHILD_SA is gone, unable to rekey. As an empty CREATE_CHILD_SA - * exchange is invalid, we fall back to an INFORMATIONAL exchange.*/ - message->set_exchange_type(message, INFORMATIONAL); - return SUCCESS; + if (this->child_sa) + { + /* we work only with the inbound SPI */ + this->spi = this->child_sa->get_spi(this->child_sa, TRUE); } - /* we work only with the inbound SPI */ - this->spi = this->child_sa->get_spi(this->child_sa, TRUE); + } + if (!this->child_sa || + (!this->child_create && + this->child_sa->get_state(this->child_sa) != CHILD_INSTALLED) || + (this->child_create && + this->child_sa->get_state(this->child_sa) != CHILD_REKEYING)) + { + /* CHILD_SA is gone or in the wrong state, unable to rekey */ + message->set_exchange_type(message, EXCHANGE_TYPE_UNDEFINED); + return SUCCESS; } config = this->child_sa->get_config(this->child_sa); @@ -218,12 +226,18 @@ METHOD(task_t, build_r, status_t, { child_cfg_t *config; uint32_t reqid; + child_sa_state_t state; - if (this->child_sa == NULL || - this->child_sa->get_state(this->child_sa) == CHILD_DELETING) + if (!this->child_sa) { DBG1(DBG_IKE, "unable to rekey, CHILD_SA not found"); - message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, chunk_empty); + message->add_notify(message, TRUE, CHILD_SA_NOT_FOUND, chunk_empty); + return SUCCESS; + } + if (this->child_sa->get_state(this->child_sa) == CHILD_DELETING) + { + DBG1(DBG_IKE, "unable to rekey, we are deleting the CHILD_SA"); + message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); return SUCCESS; } @@ -237,14 +251,16 @@ METHOD(task_t, build_r, status_t, this->child_create->set_config(this->child_create, config->get_ref(config)); this->child_create->task.build(&this->child_create->task, message); + state = this->child_sa->get_state(this->child_sa); + this->child_sa->set_state(this->child_sa, CHILD_REKEYING); + if (message->get_payload(message, PLV2_SECURITY_ASSOCIATION) == NULL) - { - /* rekeying failed, reuse old child */ - this->child_sa->set_state(this->child_sa, CHILD_INSTALLED); + { /* rekeying failed, reuse old child */ + this->child_sa->set_state(this->child_sa, state); return SUCCESS; } - this->child_sa->set_state(this->child_sa, CHILD_REKEYING); + this->child_sa->set_state(this->child_sa, CHILD_REKEYED); /* invoke rekey hook */ charon->bus->child_rekey(charon->bus, this->child_sa, @@ -284,9 +300,9 @@ static child_sa_t *handle_collision(private_child_rekey_t *this) if (child_sa) { child_sa->set_close_action(child_sa, ACTION_NONE); - if (child_sa->get_state(child_sa) != CHILD_REKEYING) + if (child_sa->get_state(child_sa) != CHILD_REKEYED) { - child_sa->set_state(child_sa, CHILD_REKEYING); + child_sa->set_state(child_sa, CHILD_REKEYED); } } } @@ -337,6 +353,34 @@ METHOD(task_t, process_i, status_t, this->ike_sa->get_id(this->ike_sa), TRUE)); return SUCCESS; } + if (message->get_notify(message, CHILD_SA_NOT_FOUND)) + { + child_cfg_t *child_cfg; + uint32_t reqid; + + 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); + reqid = this->child_sa->get_reqid(this->child_sa); + 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); + charon->bus->child_updown(charon->bus, this->child_sa, FALSE); + this->ike_sa->destroy_child_sa(this->ike_sa, protocol, spi); + return this->ike_sa->initiate(this->ike_sa, + child_cfg->get_ref(child_cfg), reqid, + NULL, NULL); + } if (this->child_create->task.process(&this->child_create->task, message) == NEED_MORE) @@ -346,10 +390,10 @@ METHOD(task_t, process_i, status_t, } if (message->get_payload(message, PLV2_SECURITY_ASSOCIATION) == NULL) { - /* establishing new child failed, reuse old. but not when we - * received a delete in the meantime */ - if (!(this->collision && - this->collision->get_type(this->collision) == TASK_CHILD_DELETE)) + /* establishing new child failed, reuse old and try again. but not when + * we received a delete in the meantime */ + if (!this->collision || + this->collision->get_type(this->collision) != TASK_CHILD_DELETE) { schedule_delayed_rekey(this); } @@ -377,9 +421,9 @@ METHOD(task_t, process_i, status_t, return SUCCESS; } /* disable updown event for redundant CHILD_SA */ - if (to_delete->get_state(to_delete) != CHILD_REKEYING) + if (to_delete->get_state(to_delete) != CHILD_REKEYED) { - to_delete->set_state(to_delete, CHILD_REKEYING); + to_delete->set_state(to_delete, CHILD_REKEYED); } spi = to_delete->get_spi(to_delete, TRUE); protocol = to_delete->get_protocol(to_delete); @@ -398,6 +442,18 @@ METHOD(task_t, get_type, task_type_t, return TASK_CHILD_REKEY; } +METHOD(child_rekey_t, is_redundant, bool, + private_child_rekey_t *this, child_sa_t *child) +{ + if (this->collision && + this->collision->get_type(this->collision) == TASK_CHILD_REKEY) + { + private_child_rekey_t *rekey = (private_child_rekey_t*)this->collision; + return child == rekey->child_create->get_child(rekey->child_create); + } + return FALSE; +} + METHOD(child_rekey_t, collide, void, private_child_rekey_t *this, task_t *other) { @@ -406,9 +462,18 @@ METHOD(child_rekey_t, collide, void, if (other->get_type(other) == TASK_CHILD_REKEY) { 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 */ + other->destroy(other); + return; + } + /* ignore passive tasks that did not successfully create a CHILD_SA */ + other_child = rekey->child_create->get_child(rekey->child_create); + if (!other_child || + other_child->get_state(other_child) != CHILD_INSTALLED) { - /* not the same child => no collision */ other->destroy(other); return; } @@ -416,19 +481,11 @@ METHOD(child_rekey_t, collide, void, else if (other->get_type(other) == TASK_CHILD_DELETE) { child_delete_t *del = (child_delete_t*)other; - if (this->collision && - this->collision->get_type(this->collision) == TASK_CHILD_REKEY) + if (is_redundant(this, del->get_child(del))) { - private_child_rekey_t *rekey; - - rekey = (private_child_rekey_t*)this->collision; - if (del->get_child(del) == rekey->child_create->get_child(rekey->child_create)) - { - /* peer deletes redundant child created in collision */ - this->other_child_destroyed = TRUE; - other->destroy(other); - return; - } + this->other_child_destroyed = TRUE; + other->destroy(other); + return; } if (del->get_child(del) != this->child_sa) { @@ -439,7 +496,7 @@ METHOD(child_rekey_t, collide, void, } else { - /* any other task is not critical for collisisions, ignore */ + /* any other task is not critical for collisions, ignore */ other->destroy(other); return; } @@ -496,6 +553,7 @@ child_rekey_t *child_rekey_create(ike_sa_t *ike_sa, protocol_id_t protocol, .migrate = _migrate, .destroy = _destroy, }, + .is_redundant = _is_redundant, .collide = _collide, }, .ike_sa = ike_sa, diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.h b/src/libcharon/sa/ikev2/tasks/child_rekey.h index cc041b953..0ad1a062d 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.h +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.h @@ -1,6 +1,7 @@ /* + * Copyright (C) 2016 Tobias Brunner * Copyright (C) 2007 Martin Willi - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -39,13 +40,25 @@ struct child_rekey_t { task_t task; /** - * Register a rekeying task which collides with this one + * Check if the given SA is the redundant CHILD_SA created during a rekey + * collision. + * + * This is called if the other peer deletes the redundant SA before we were + * able to handle the CREATE_CHILD_SA response. + * + * @param child CHILD_SA to check + * @return TRUE if the SA is the redundant CHILD_SA + */ + bool (*is_redundant)(child_rekey_t *this, child_sa_t *child); + + /** + * Register a rekeying/delete task which collides with this one * * If two peers initiate rekeying at the same time, the collision must * be handled gracefully. The task manager is aware of what exchanges - * are going on and notifies the outgoing task by passing the incoming. + * are going on and notifies the active task by passing the passive. * - * @param other incoming task + * @param other passive task (adopted) */ void (*collide)(child_rekey_t* this, task_t *other); }; diff --git a/src/libcharon/sa/ikev2/tasks/ike_delete.c b/src/libcharon/sa/ikev2/tasks/ike_delete.c index e972dba07..fd36b144a 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_delete.c +++ b/src/libcharon/sa/ikev2/tasks/ike_delete.c @@ -1,6 +1,7 @@ /* + * Copyright (C) 2016 Tobias Brunner * Copyright (C) 2006-2007 Martin Willi - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -17,7 +18,7 @@ #include <daemon.h> #include <encoding/payloads/delete_payload.h> - +#include <sa/ikev2/tasks/ike_rekey.h> typedef struct private_ike_delete_t private_ike_delete_t; @@ -45,11 +46,6 @@ struct private_ike_delete_t { * are we deleting a rekeyed SA? */ bool rekeyed; - - /** - * are we responding to a delete, but have initated our own? - */ - bool simultaneous; }; METHOD(task_t, build_i, status_t, @@ -68,7 +64,8 @@ METHOD(task_t, build_i, status_t, delete_payload = delete_payload_create(PLV2_DELETE, PROTO_IKE); message->add_payload(message, (payload_t*)delete_payload); - if (this->ike_sa->get_state(this->ike_sa) == IKE_REKEYING) + if (this->ike_sa->get_state(this->ike_sa) == IKE_REKEYING || + this->ike_sa->get_state(this->ike_sa) == IKE_REKEYED) { this->rekeyed = TRUE; } @@ -93,6 +90,33 @@ METHOD(task_t, process_i, status_t, return DESTROY_ME; } +/** + * Check if this delete happened after a rekey collsion + */ +static bool after_rekey_collision(private_ike_delete_t *this) +{ + 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_IKE_REKEY) + { + ike_rekey_t *rekey = (ike_rekey_t*)task; + + if (rekey->did_collide(rekey)) + { + tasks->destroy(tasks); + return TRUE; + } + } + } + tasks->destroy(tasks); + return FALSE; +} + METHOD(task_t, process_r, status_t, private_ike_delete_t *this, message_t *message) { @@ -119,16 +143,24 @@ METHOD(task_t, process_r, status_t, switch (this->ike_sa->get_state(this->ike_sa)) { + case IKE_REKEYING: + /* if the peer concurrently deleted the IKE_SA we treat this as + * regular delete. however, in case the peer did not detect a rekey + * collision it will delete the replaced IKE_SA if we are still in + * state IKE_REKEYING */ + if (after_rekey_collision(this)) + { + this->rekeyed = TRUE; + break; + } + /* fall-through */ case IKE_ESTABLISHED: this->ike_sa->set_state(this->ike_sa, IKE_DELETING); this->ike_sa->reestablish(this->ike_sa); return NEED_MORE; - case IKE_REKEYING: + case IKE_REKEYED: this->rekeyed = TRUE; break; - case IKE_DELETING: - this->simultaneous = TRUE; - break; default: break; } @@ -141,11 +173,6 @@ METHOD(task_t, build_r, status_t, { DBG0(DBG_IKE, "IKE_SA deleted"); - if (this->simultaneous) - { - /* wait for peer's response for our delete request */ - return SUCCESS; - } if (!this->rekeyed) { /* invoke ike_down() hook if SA has not been rekeyed */ charon->bus->ike_updown(charon->bus, this->ike_sa, FALSE); @@ -164,7 +191,6 @@ METHOD(task_t, migrate, void, private_ike_delete_t *this, ike_sa_t *ike_sa) { this->ike_sa = ike_sa; - this->simultaneous = FALSE; } METHOD(task_t, destroy, void, diff --git a/src/libcharon/sa/ikev2/tasks/ike_init.c b/src/libcharon/sa/ikev2/tasks/ike_init.c index b96969ebe..801b6d8f3 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_init.c +++ b/src/libcharon/sa/ikev2/tasks/ike_init.c @@ -373,13 +373,15 @@ static void process_payloads(private_ike_init_t *this, message_t *message) { sa_payload_t *sa_payload = (sa_payload_t*)payload; linked_list_t *proposal_list; - bool private; + bool private, prefer_configured; proposal_list = sa_payload->get_proposals(sa_payload); private = this->ike_sa->supports_extension(this->ike_sa, EXT_STRONGSWAN); + prefer_configured = lib->settings->get_bool(lib->settings, + "%s.prefer_configured_proposals", TRUE, lib->ns); this->proposal = this->config->select_proposal(this->config, - proposal_list, private); + proposal_list, private, prefer_configured); if (!this->proposal) { charon->bus->alert(charon->bus, ALERT_PROPOSAL_MISMATCH_IKE, diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index a85df114c..2f0552a33 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -1,7 +1,8 @@ /* + * Copyright (C) 2015-2016 Tobias Brunner * Copyright (C) 2005-2008 Martin Willi * Copyright (C) 2005 Jan Hutter - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -66,9 +67,30 @@ struct private_ike_rekey_t { * colliding task detected by the task manager */ task_t *collision; + + /** + * TRUE if rekeying can't be handled temporarily + */ + bool failed_temporarily; }; /** + * Schedule a retry if rekeying temporary failed + */ +static void schedule_delayed_rekey(private_ike_rekey_t *this) +{ + uint32_t retry; + job_t *job; + + retry = RETRY_INTERVAL - (random() % RETRY_JITTER); + job = (job_t*)rekey_ike_sa_job_create( + this->ike_sa->get_id(this->ike_sa), FALSE); + DBG1(DBG_IKE, "IKE_SA rekeying failed, trying again in %d seconds", retry); + this->ike_sa->set_state(this->ike_sa, IKE_ESTABLISHED); + lib->scheduler->schedule_job(lib->scheduler, job, retry); +} + +/** * Check if an IKE_SA has any queued tasks, return initiation job */ static job_t* check_queued_tasks(ike_sa_t *ike_sa) @@ -83,7 +105,6 @@ static job_t* check_queued_tasks(ike_sa_t *ike_sa) job = (job_t*)initiate_tasks_job_create(ike_sa->get_id(ike_sa)); } enumerator->destroy(enumerator); - return job; } @@ -117,20 +138,9 @@ static void establish_new(private_ike_rekey_t *this) } this->new_sa = NULL; charon->bus->set_sa(charon->bus, this->ike_sa); - } -} -METHOD(task_t, process_r_delete, status_t, - private_ike_rekey_t *this, message_t *message) -{ - establish_new(this); - return this->ike_delete->task.process(&this->ike_delete->task, message); -} - -METHOD(task_t, build_r_delete, status_t, - private_ike_rekey_t *this, message_t *message) -{ - return this->ike_delete->task.build(&this->ike_delete->task, message); + this->ike_sa->set_state(this->ike_sa, IKE_REKEYED); + } } METHOD(task_t, build_i_delete, status_t, @@ -172,36 +182,59 @@ METHOD(task_t, build_i, status_t, return NEED_MORE; } -METHOD(task_t, process_r, status_t, - private_ike_rekey_t *this, message_t *message) +/** + * Check if there are any half-open children + */ +static bool have_half_open_children(private_ike_rekey_t *this) { enumerator_t *enumerator; child_sa_t *child_sa; - - if (this->ike_sa->get_state(this->ike_sa) == IKE_DELETING) - { - DBG1(DBG_IKE, "peer initiated rekeying, but we are deleting"); - return NEED_MORE; - } + task_t *task; enumerator = this->ike_sa->create_child_sa_enumerator(this->ike_sa); while (enumerator->enumerate(enumerator, (void**)&child_sa)) { switch (child_sa->get_state(child_sa)) { - case CHILD_CREATED: case CHILD_REKEYING: case CHILD_RETRYING: case CHILD_DELETING: - /* we do not allow rekeying while we have children in-progress */ - DBG1(DBG_IKE, "peer initiated rekeying, but a child is half-open"); enumerator->destroy(enumerator); - return NEED_MORE; + return TRUE; default: break; } } enumerator->destroy(enumerator); + enumerator = this->ike_sa->create_task_enumerator(this->ike_sa, + TASK_QUEUE_ACTIVE); + while (enumerator->enumerate(enumerator, (void**)&task)) + { + if (task->get_type(task) == TASK_CHILD_CREATE) + { + enumerator->destroy(enumerator); + return TRUE; + } + } + enumerator->destroy(enumerator); + return FALSE; +} + +METHOD(task_t, process_r, status_t, + private_ike_rekey_t *this, message_t *message) +{ + if (this->ike_sa->get_state(this->ike_sa) == IKE_DELETING) + { + DBG1(DBG_IKE, "peer initiated rekeying, but we are deleting"); + this->failed_temporarily = TRUE; + return NEED_MORE; + } + if (have_half_open_children(this)) + { + DBG1(DBG_IKE, "peer initiated rekeying, but a child is half-open"); + this->failed_temporarily = TRUE; + return NEED_MORE; + } this->new_sa = charon->ike_sa_manager->checkout_new(charon->ike_sa_manager, this->ike_sa->get_version(this->ike_sa), FALSE); @@ -219,33 +252,57 @@ METHOD(task_t, process_r, status_t, METHOD(task_t, build_r, status_t, private_ike_rekey_t *this, message_t *message) { + if (this->failed_temporarily) + { + message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); + return SUCCESS; + } if (this->new_sa == NULL) { /* IKE_SA/a CHILD_SA is in an inacceptable state, deny rekeying */ message->add_notify(message, TRUE, NO_PROPOSAL_CHOSEN, chunk_empty); return SUCCESS; } - if (this->ike_init->task.build(&this->ike_init->task, message) == FAILED) { + this->ike_init->task.destroy(&this->ike_init->task); + this->ike_init = NULL; charon->bus->set_sa(charon->bus, this->ike_sa); return SUCCESS; } charon->bus->set_sa(charon->bus, this->ike_sa); - this->ike_sa->set_state(this->ike_sa, IKE_REKEYING); - /* rekeying successful, delete the IKE_SA using a subtask */ - this->ike_delete = ike_delete_create(this->ike_sa, FALSE); - this->public.task.build = _build_r_delete; - this->public.task.process = _process_r_delete; - - /* the peer does have to delete the IKE_SA. If it does not, we get a - * unusable IKE_SA in REKEYING state without a replacement. We consider - * this a timeout condition by the peer, and trigger a delete actively. */ - lib->scheduler->schedule_job(lib->scheduler, (job_t*) - delete_ike_sa_job_create(this->ike_sa->get_id(this->ike_sa), TRUE), 90); + if (this->ike_sa->get_state(this->ike_sa) != IKE_REKEYING) + { /* in case of a collision we let the initiating task handle this */ + establish_new(this); + /* make sure the IKE_SA is gone in case the peer fails to delete it */ + lib->scheduler->schedule_job(lib->scheduler, (job_t*) + delete_ike_sa_job_create(this->ike_sa->get_id(this->ike_sa), TRUE), + 90); + } + return SUCCESS; +} - return NEED_MORE; +/** + * Conclude any undetected rekey collision. + * + * If the peer does not detect the collision it will delete this IKE_SA. + * Depending on when our request reaches the peer and we receive the delete + * this may get called at different times. + * + * Returns TRUE if there was a collision, FALSE otherwise. + */ +static bool conclude_undetected_collision(private_ike_rekey_t *this) +{ + if (this->collision && + this->collision->get_type(this->collision) == TASK_IKE_REKEY) + { + DBG1(DBG_IKE, "peer did not notice IKE_SA rekey collision, abort " + "active rekeying"); + establish_new((private_ike_rekey_t*)this->collision); + return TRUE; + } + return FALSE; } METHOD(task_t, process_i, status_t, @@ -266,18 +323,9 @@ METHOD(task_t, process_i, status_t, { case FAILED: /* rekeying failed, fallback to old SA */ - if (!(this->collision && ( - this->collision->get_type(this->collision) == TASK_IKE_DELETE || - this->collision->get_type(this->collision) == TASK_IKE_REAUTH))) + if (!conclude_undetected_collision(this)) { - job_t *job; - uint32_t retry = RETRY_INTERVAL - (random() % RETRY_JITTER); - job = (job_t*)rekey_ike_sa_job_create( - this->ike_sa->get_id(this->ike_sa), FALSE); - DBG1(DBG_IKE, "IKE_SA rekeying failed, " - "trying again in %d seconds", retry); - this->ike_sa->set_state(this->ike_sa, IKE_ESTABLISHED); - lib->scheduler->schedule_job(lib->scheduler, job, retry); + schedule_delayed_rekey(this); } return SUCCESS; case NEED_MORE: @@ -293,55 +341,53 @@ METHOD(task_t, process_i, status_t, this->collision->get_type(this->collision) == TASK_IKE_REKEY) { private_ike_rekey_t *other = (private_ike_rekey_t*)this->collision; + host_t *host; + chunk_t this_nonce, other_nonce; - /* ike_init can be NULL, if child_sa is half-open */ - if (other->ike_init) - { - host_t *host; - chunk_t this_nonce, other_nonce; - - this_nonce = this->ike_init->get_lower_nonce(this->ike_init); - other_nonce = other->ike_init->get_lower_nonce(other->ike_init); + this_nonce = this->ike_init->get_lower_nonce(this->ike_init); + other_nonce = other->ike_init->get_lower_nonce(other->ike_init); - /* if we have the lower nonce, delete rekeyed SA. If not, delete - * the redundant. */ - if (memcmp(this_nonce.ptr, other_nonce.ptr, - min(this_nonce.len, other_nonce.len)) > 0) + /* if we have the lower nonce, delete rekeyed SA. If not, delete + * the redundant. */ + if (memcmp(this_nonce.ptr, other_nonce.ptr, + min(this_nonce.len, other_nonce.len)) < 0) + { + DBG1(DBG_IKE, "IKE_SA rekey collision lost, deleting redundant " + "IKE_SA %s[%d]", this->new_sa->get_name(this->new_sa), + this->new_sa->get_unique_id(this->new_sa)); + /* apply host for a proper delete */ + host = this->ike_sa->get_my_host(this->ike_sa); + this->new_sa->set_my_host(this->new_sa, host->clone(host)); + host = this->ike_sa->get_other_host(this->ike_sa); + this->new_sa->set_other_host(this->new_sa, host->clone(host)); + /* IKE_SAs in state IKE_REKEYED are silently deleted, so we use + * IKE_REKEYING */ + this->new_sa->set_state(this->new_sa, IKE_REKEYING); + if (this->new_sa->delete(this->new_sa) == DESTROY_ME) { - /* peer should delete this SA. Add a timeout just in case. */ - job_t *job = (job_t*)delete_ike_sa_job_create( - other->new_sa->get_id(other->new_sa), TRUE); - lib->scheduler->schedule_job(lib->scheduler, job, 10); - DBG1(DBG_IKE, "IKE_SA rekey collision won, waiting for delete"); - charon->ike_sa_manager->checkin(charon->ike_sa_manager, other->new_sa); - other->new_sa = NULL; + this->new_sa->destroy(this->new_sa); } else { - DBG1(DBG_IKE, "IKE_SA rekey collision lost, " - "deleting redundant IKE_SA"); - /* apply host for a proper delete */ - host = this->ike_sa->get_my_host(this->ike_sa); - this->new_sa->set_my_host(this->new_sa, host->clone(host)); - host = this->ike_sa->get_other_host(this->ike_sa); - this->new_sa->set_other_host(this->new_sa, host->clone(host)); - this->ike_sa->set_state(this->ike_sa, IKE_ESTABLISHED); - this->new_sa->set_state(this->new_sa, IKE_REKEYING); - if (this->new_sa->delete(this->new_sa) == DESTROY_ME) - { - this->new_sa->destroy(this->new_sa); - } - else - { - charon->ike_sa_manager->checkin( - charon->ike_sa_manager, this->new_sa); - } - charon->bus->set_sa(charon->bus, this->ike_sa); - this->new_sa = NULL; - establish_new(other); - return SUCCESS; + charon->ike_sa_manager->checkin(charon->ike_sa_manager, + this->new_sa); } + charon->bus->set_sa(charon->bus, this->ike_sa); + this->new_sa = NULL; + establish_new(other); + return SUCCESS; } + /* peer should delete this SA. Add a timeout just in case. */ + job_t *job = (job_t*)delete_ike_sa_job_create( + other->new_sa->get_id(other->new_sa), TRUE); + lib->scheduler->schedule_job(lib->scheduler, job, + HALF_OPEN_IKE_SA_TIMEOUT); + DBG1(DBG_IKE, "IKE_SA rekey collision won, waiting for delete for " + "redundant IKE_SA %s[%d]", other->new_sa->get_name(other->new_sa), + other->new_sa->get_unique_id(other->new_sa)); + other->new_sa->set_state(other->new_sa, IKE_REKEYED); + charon->ike_sa_manager->checkin(charon->ike_sa_manager, other->new_sa); + other->new_sa = NULL; charon->bus->set_sa(charon->bus, this->ike_sa); } @@ -361,11 +407,41 @@ METHOD(task_t, get_type, task_type_t, return TASK_IKE_REKEY; } +METHOD(ike_rekey_t, did_collide, bool, + private_ike_rekey_t *this) +{ + return this->collision && + this->collision->get_type(this->collision) == TASK_IKE_REKEY; +} + METHOD(ike_rekey_t, collide, void, private_ike_rekey_t* this, task_t *other) { DBG1(DBG_IKE, "detected %N collision with %N", task_type_names, TASK_IKE_REKEY, task_type_names, other->get_type(other)); + + switch (other->get_type(other)) + { + case TASK_IKE_DELETE: + conclude_undetected_collision(this); + other->destroy(other); + return; + case TASK_IKE_REKEY: + { + private_ike_rekey_t *rekey = (private_ike_rekey_t*)other; + + if (!rekey->ike_init) + { + DBG1(DBG_IKE, "colliding exchange did not result in an IKE_SA, " + "ignore"); + other->destroy(other); + return; + } + break; + } + default: + break; + } DESTROY_IF(this->collision); this->collision = other; } @@ -425,6 +501,7 @@ ike_rekey_t *ike_rekey_create(ike_sa_t *ike_sa, bool initiator) .migrate = _migrate, .destroy = _destroy, }, + .did_collide = _did_collide, .collide = _collide, }, .ike_sa = ike_sa, diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.h b/src/libcharon/sa/ikev2/tasks/ike_rekey.h index 6a12e9034..86b512c92 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.h +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.h @@ -1,6 +1,7 @@ /* + * Copyright (C) 2016 Tobias Brunner * Copyright (C) 2007 Martin Willi - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -38,6 +39,13 @@ struct ike_rekey_t { task_t task; /** + * Check if there was a rekey collision. + * + * @return TRUE if there was a rekey collision before + */ + bool (*did_collide)(ike_rekey_t *this); + + /** * Register a rekeying task which collides with this one. * * If two peers initiate rekeying at the same time, the collision must diff --git a/src/libcharon/sa/task_manager.h b/src/libcharon/sa/task_manager.h index b109c82b8..86077d373 100644 --- a/src/libcharon/sa/task_manager.h +++ b/src/libcharon/sa/task_manager.h @@ -1,6 +1,7 @@ /* + * Copyright (C) 2013-2016 Tobias Brunner * Copyright (C) 2006 Martin Willi - * Hochschule fuer Technik Rapperswil + * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the @@ -122,7 +123,17 @@ struct task_manager_t { * * @param task task to queue */ - void (*queue_task) (task_manager_t *this, task_t *task); + void (*queue_task)(task_manager_t *this, task_t *task); + + /** + * Queue a task in the manager, but delay its initiation for at least the + * given number of seconds. + * + * @param task task to queue + * @param delay minimum delay in s before initiating the task + */ + void (*queue_task_delayed)(task_manager_t *this, task_t *task, + uint32_t delay); /** * Queue IKE_SA establishing tasks. diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 0589269aa..b8670246b 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -1,8 +1,9 @@ -TESTS = libcharon_tests +TESTS = libcharon_tests exchange_tests check_PROGRAMS = $(TESTS) libcharon_tests_SOURCES = \ + suites/test_proposal.c \ suites/test_ike_cfg.c \ suites/test_mem_pool.c \ suites/test_message_chapoly.c \ @@ -21,3 +22,34 @@ libcharon_tests_LDADD = \ $(top_builddir)/src/libcharon/libcharon.la \ $(top_builddir)/src/libstrongswan/libstrongswan.la \ $(top_builddir)/src/libstrongswan/tests/libtest.la + + +exchange_tests_SOURCES = \ + suites/test_child_create.c \ + suites/test_child_delete.c \ + suites/test_child_rekey.c \ + suites/test_ike_delete.c \ + suites/test_ike_rekey.c \ + utils/exchange_test_asserts.h utils/exchange_test_asserts.c \ + utils/exchange_test_helper.h utils/exchange_test_helper.c \ + utils/job_asserts.h \ + utils/mock_dh.h utils/mock_dh.c \ + utils/mock_ipsec.h utils/mock_ipsec.c \ + utils/mock_nonce_gen.h utils/mock_nonce_gen.c \ + utils/mock_sender.h utils/mock_sender.c \ + utils/sa_asserts.h \ + exchange_tests.h exchange_tests.c + +exchange_tests_CFLAGS = \ + -I$(top_srcdir)/src/libcharon \ + -I$(top_srcdir)/src/libstrongswan \ + -I$(top_srcdir)/src/libstrongswan/tests \ + -DPLUGINDIR=\""$(abs_top_builddir)/src/libstrongswan/plugins\"" \ + -DPLUGINS=\""${s_plugins}\"" \ + @COVERAGE_CFLAGS@ + +exchange_tests_LDFLAGS = @COVERAGE_LDFLAGS@ +exchange_tests_LDADD = \ + $(top_builddir)/src/libcharon/libcharon.la \ + $(top_builddir)/src/libstrongswan/libstrongswan.la \ + $(top_builddir)/src/libstrongswan/tests/libtest.la diff --git a/src/libcharon/tests/exchange_tests.c b/src/libcharon/tests/exchange_tests.c new file mode 100644 index 000000000..eab50a875 --- /dev/null +++ b/src/libcharon/tests/exchange_tests.c @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include <test_runner.h> +#include <daemon.h> + +#include "utils/exchange_test_helper.h" + +/* declare test suite constructors */ +#define TEST_SUITE(x) test_suite_t* x(); +#define TEST_SUITE_DEPEND(x, ...) TEST_SUITE(x) +#include "exchange_tests.h" +#undef TEST_SUITE +#undef TEST_SUITE_DEPEND + +static test_configuration_t tests[] = { +#define TEST_SUITE(x) \ + { .suite = x, }, +#define TEST_SUITE_DEPEND(x, type, ...) \ + { .suite = x, .feature = PLUGIN_DEPENDS(type, __VA_ARGS__) }, +#include "exchange_tests.h" + { .suite = NULL, } +}; + +static bool test_runner_init(bool init) +{ + if (init) + { + char *plugins, *plugindir; + + libcharon_init(); + + plugins = getenv("TESTS_PLUGINS") ?: + lib->settings->get_str(lib->settings, + "tests.load", PLUGINS); + plugindir = lib->settings->get_str(lib->settings, + "tests.plugindir", PLUGINDIR); + plugin_loader_add_plugindirs(plugindir, plugins); + exchange_test_helper_init(plugins); + } + else + { + exchange_test_helper_deinit(); + libcharon_deinit(); + } + return TRUE; +} + +int main(int argc, char *argv[]) +{ + return test_runner_run("exchanges", tests, test_runner_init); +} diff --git a/src/libcharon/tests/exchange_tests.h b/src/libcharon/tests/exchange_tests.h new file mode 100644 index 000000000..30086721f --- /dev/null +++ b/src/libcharon/tests/exchange_tests.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +TEST_SUITE(ike_delete_suite_create) +TEST_SUITE(ike_rekey_suite_create) +TEST_SUITE(child_create_suite_create) +TEST_SUITE(child_delete_suite_create) +TEST_SUITE(child_rekey_suite_create) diff --git a/src/libcharon/tests/libcharon_tests.c b/src/libcharon/tests/libcharon_tests.c index 4692c3094..e25e5434f 100644 --- a/src/libcharon/tests/libcharon_tests.c +++ b/src/libcharon/tests/libcharon_tests.c @@ -53,9 +53,6 @@ static bool test_runner_init(bool init) } else { - lib->processor->set_threads(lib->processor, 0); - lib->processor->cancel(lib->processor); - lib->plugins->unload(lib->plugins); libcharon_deinit(); } return TRUE; diff --git a/src/libcharon/tests/libcharon_tests.h b/src/libcharon/tests/libcharon_tests.h index fb82baccb..f770f464d 100644 --- a/src/libcharon/tests/libcharon_tests.h +++ b/src/libcharon/tests/libcharon_tests.h @@ -1,4 +1,7 @@ /* + * Copyright (C) 2014-2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * * Copyright (C) 2014 Martin Willi * Copyright (C) 2014 revosec AG * @@ -13,6 +16,15 @@ * for more details. */ +/** + * @defgroup libcharon-tests tests + * @ingroup libcharon + * + * @defgroup test_utils_c test_utils + * @ingroup libcharon-tests + */ + +TEST_SUITE(proposal_suite_create) TEST_SUITE(ike_cfg_suite_create) TEST_SUITE(mem_pool_suite_create) TEST_SUITE_DEPEND(message_chapoly_suite_create, AEAD, ENCR_CHACHA20_POLY1305, 32) diff --git a/src/libcharon/tests/suites/test_child_create.c b/src/libcharon/tests/suites/test_child_create.c new file mode 100644 index 000000000..20a47f6bf --- /dev/null +++ b/src/libcharon/tests/suites/test_child_create.c @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "test_suite.h" + +#include <daemon.h> +#include <tests/utils/exchange_test_helper.h> +#include <tests/utils/exchange_test_asserts.h> +#include <tests/utils/job_asserts.h> +#include <tests/utils/sa_asserts.h> + +/** + * One of the peers tries to create a new CHILD_SA while the other concurrently + * started to rekey the IKE_SA. TEMPORARY_FAILURE should be returned on both + * sides and the peers should prepare to retry. + */ +START_TEST(test_collision_ike_rekey) +{ + child_cfg_t *child_cfg; + child_cfg_create_t child = { + .mode = MODE_TUNNEL, + }; + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + assert_hook_not_called(child_updown); + child_cfg = child_cfg_create("child", &child); + child_cfg->add_proposal(child_cfg, proposal_create_default(PROTO_ESP)); + child_cfg->add_traffic_selector(child_cfg, TRUE, + traffic_selector_create_dynamic(0, 0, 65535)); + child_cfg->add_traffic_selector(child_cfg, FALSE, + traffic_selector_create_dynamic(0, 0, 65535)); + call_ikesa(a, initiate, child_cfg, 0, NULL, NULL); + assert_child_sa_count(a, 1); + assert_hook(); + + call_ikesa(b, rekey); + + /* CREATE_CHILD_SA { SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_not_called(child_updown); + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_count(b, 1); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_hook_not_called(child_updown); + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 1); + assert_scheduler(); + assert_hook(); + + /* CREATE_CHILD_SA { N(TEMP_FAIL) } --> */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_ESTABLISHED); + assert_scheduler(); + + /* make sure no message was sent after handling the TEMPORARY_FAILURE and + * that the task to retry creating the CHILD_SA is queued and not active + * and it can't be initiated immediately */ + ck_assert(!exchange_test_helper->sender->dequeue(exchange_test_helper->sender)); + assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); + assert_num_tasks(a, 1, TASK_QUEUE_QUEUED); + call_ikesa(a, initiate, NULL, 0, NULL, NULL); + assert_num_tasks(a, 0, TASK_QUEUE_ACTIVE); + + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +Suite *child_create_suite_create() +{ + Suite *s; + TCase *tc; + + s = suite_create("child create"); + + tc = tcase_create("collisions ike rekey"); + tcase_add_test(tc, test_collision_ike_rekey); + suite_add_tcase(s, tc); + + return s; +} diff --git a/src/libcharon/tests/suites/test_child_delete.c b/src/libcharon/tests/suites/test_child_delete.c new file mode 100644 index 000000000..437e919c7 --- /dev/null +++ b/src/libcharon/tests/suites/test_child_delete.c @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "test_suite.h" + +#include <daemon.h> +#include <tests/utils/exchange_test_helper.h> +#include <tests/utils/exchange_test_asserts.h> +#include <tests/utils/job_asserts.h> +#include <tests/utils/sa_asserts.h> + +/** + * Regular CHILD_SA deletion either initiated by the original initiator or + * responder of the IKE_SA. + */ +START_TEST(test_regular) +{ + ike_sa_t *a, *b; + + if (_i) + { /* responder deletes the CHILD_SA (SPI 2) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, NULL); + } + else + { /* initiator deletes the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + } + assert_hook_not_called(child_updown); + call_ikesa(a, delete_child_sa, PROTO_ESP, _i+1, FALSE); + assert_child_sa_state(a, _i+1, CHILD_DELETING); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_count(b, 0); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(child_updown, FALSE); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * Both peers initiate the CHILD_SA deletion concurrently and should handle + * the collision properly. + */ +START_TEST(test_collision) +{ + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + /* both peers delete the CHILD_SA concurrently */ + assert_hook_not_called(child_updown); + call_ikesa(a, delete_child_sa, PROTO_ESP, 1, FALSE); + assert_child_sa_state(a, 1, CHILD_DELETING); + call_ikesa(b, delete_child_sa, PROTO_ESP, 2, FALSE); + assert_child_sa_state(b, 2, CHILD_DELETING); + assert_hook(); + + /* RFC 7296 says: + * + * Normally, the response in the INFORMATIONAL exchange will contain + * Delete payloads for the paired SAs going in the other direction. + * There is one exception. If, by chance, both ends of a set of SAs + * independently decide to close them, each may send a Delete payload + * and the two requests may cross in the network. If a node receives a + * delete request for SAs for which it has already issued a delete + * request, it MUST delete the outgoing SAs while processing the request + * and the incoming SAs while processing the response. In that case, + * the responses MUST NOT include Delete payloads for the deleted SAs, + * since that would result in duplicate deletion and could in theory + * delete the wrong SA. + * + * We don't handle SAs separately so we expect both are still installed, + * but the INFORMATIONAL response should not contain a DELETE payload. + */ + + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_updown); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETING); + /* <-- INFORMATIONAL { D } */ + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETING); + assert_hook(); + + /* <-- INFORMATIONAL { } */ + assert_hook_updown(child_updown, FALSE); + assert_message_empty(IN); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); + assert_hook(); + /* INFORMATIONAL { } --> */ + assert_hook_updown(child_updown, FALSE); + assert_message_empty(IN); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_count(b, 0); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * This is like the collision above but one of the DELETEs is dropped or delayed + * so the other peer is not aware that there is a collision. + */ +START_TEST(test_collision_drop) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + /* both peers delete the CHILD_SA concurrently */ + assert_hook_not_called(child_updown); + call_ikesa(a, delete_child_sa, PROTO_ESP, 1, FALSE); + assert_child_sa_state(a, 1, CHILD_DELETING); + call_ikesa(b, delete_child_sa, PROTO_ESP, 2, FALSE); + assert_child_sa_state(b, 2, CHILD_DELETING); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_updown); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETING); + assert_hook(); + + /* drop/delay the responder's message */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* <-- INFORMATIONAL { } */ + assert_hook_updown(child_updown, FALSE); + assert_message_empty(IN); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); + assert_hook(); + + /* <-- INFORMATIONAL { D } (delayed/retransmitted) */ + assert_hook_not_called(child_updown); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_hook(); + + /* INFORMATIONAL { } --> */ + assert_hook_updown(child_updown, FALSE); + assert_message_empty(IN); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_count(b, 0); + assert_hook(); + + 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 + * peer is concurrently trying to delete. + * + * delete ----\ /---- rekey IKE + * \-----/----> detect collision + * detect collision <---------/ /---- delete + * TEMP_FAIL ----\ / + * \----/-----> + * <--------/ + */ +START_TEST(test_collision_ike_rekey) +{ + ike_sa_t *a, *b; + uint32_t spi_a = _i+1; + + if (_i) + { /* responder deletes the CHILD_SA (SPI 2) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, NULL); + } + else + { /* initiator deletes the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + } + call_ikesa(a, delete_child_sa, PROTO_ESP, spi_a, FALSE); + assert_child_sa_state(a, spi_a, CHILD_DELETING); + call_ikesa(b, rekey); + assert_ike_sa_state(b, IKE_REKEYING); + + /* this should never get called as there is no successful rekeying */ + assert_hook_not_called(ike_rekey); + + /* RFC 7296, 2.25.2: If a peer receives a request to delete a Child SA when + * it is currently rekeying the IKE SA, it SHOULD reply as usual, with a + * Delete payload. + */ + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + assert_single_payload(OUT, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYING); + assert_child_sa_count(b, 0); + assert_hook(); + + /* RFC 7296, 2.25.1: If a peer receives a request to rekey the IKE SA, and + * it is currently, rekeying, or closing a Child SA of that IKE SA, it + * SHOULD reply with TEMPORARY_FAILURE. + */ + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, spi_a, CHILD_DELETING); + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(child_updown, FALSE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_count(a, 0); + assert_hook(); + + /* CREATE_CHILD_SA { N(TEMP_FAIL) } --> */ + /* we expect a job to retry the rekeying is scheduled */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_ESTABLISHED); + assert_scheduler(); + + /* ike_rekey */ + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * One of the hosts initiates a delete of the IKE_SA of the CHILD_SA the other + * peer is concurrently trying to delete. + * + * delete ----\ /---- delete IKE + * \-----/----> detect collision + * <---------/ /---- delete + * delete ----\ / + * \----/-----> + * sa already gone <--------/ + */ +START_TEST(test_collision_ike_delete) +{ + ike_sa_t *a, *b; + uint32_t spi_a = _i+1; + message_t *msg; + status_t s; + + 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); + } + call_ikesa(a, delete_child_sa, PROTO_ESP, spi_a, FALSE); + assert_child_sa_state(a, spi_a, CHILD_DELETING); + call_ikesa(b, delete); + assert_ike_sa_state(b, IKE_DELETING); + + /* RFC 7296, 2.25.2 does not explicitly state what the behavior SHOULD be if + * a peer receives a request to delete a CHILD_SA when it is currently + * closing the IKE SA. We expect a regular response. + */ + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + assert_single_payload(OUT, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + assert_child_sa_count(b, 0); + assert_hook(); + + /* RFC 7296, 2.25.1 does not explicitly state what the behavior SHOULD be if + * a peer receives a request to close the IKE SA if it is currently deleting + * a Child SA of that IKE SA. Let's just close the IKE_SA and forget the + * delete. + */ + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + assert_message_empty(OUT); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + assert_hook(); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + /* the SA is already gone */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + msg->destroy(msg); + + /* INFORMATIONAL { } --> */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_not_called(child_updown); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + assert_hook(); +} +END_TEST + +Suite *child_delete_suite_create() +{ + Suite *s; + TCase *tc; + + s = suite_create("child delete"); + + tc = tcase_create("regular"); + tcase_add_loop_test(tc, test_regular, 0, 2); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions"); + tcase_add_test(tc, test_collision); + tcase_add_test(tc, test_collision_drop); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions ike rekey"); + tcase_add_loop_test(tc, test_collision_ike_rekey, 0, 2); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions ike delete"); + tcase_add_loop_test(tc, test_collision_ike_delete, 0, 2); + suite_add_tcase(s, tc); + + return s; +} diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c new file mode 100644 index 000000000..fcac49388 --- /dev/null +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -0,0 +1,1569 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "test_suite.h" + +#include <daemon.h> +#include <tests/utils/exchange_test_helper.h> +#include <tests/utils/exchange_test_asserts.h> +#include <tests/utils/job_asserts.h> +#include <tests/utils/sa_asserts.h> + +/** + * Initiate rekeying the CHILD_SA with the given SPI on the given IKE_SA. + */ +#define initiate_rekey(sa, spi) ({ \ + assert_hook_not_called(child_updown); \ + assert_hook_not_called(child_rekey); \ + call_ikesa(sa, rekey_child_sa, PROTO_ESP, spi); \ + assert_child_sa_state(sa, spi, CHILD_REKEYING); \ + assert_hook(); \ + assert_hook(); \ +}) + +/** + * Regular CHILD_SA rekey either initiated by the original initiator or + * responder of the IKE_SA. + */ +START_TEST(test_regular) +{ + ike_sa_t *a, *b; + 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); + + /* 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_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); + assert_child_sa_state(b, 4, CHILD_INSTALLED); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ + assert_hook_called(child_rekey); + 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); + assert_child_sa_state(a, 3, CHILD_INSTALLED); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 4, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + assert_hook(); + /* <-- INFORMATIONAL { D } */ + 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, 3, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + assert_hook(); + + /* child_updown */ + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * CHILD_SA rekey where the responder does not agree with the DH group selected + * by the initiator, either initiated by the original initiator or responder of + * the IKE_SA. + */ +START_TEST(test_regular_ke_invalid) +{ + exchange_test_sa_conf_t conf = { + .initiator = { + .esp = "aes128-sha256-modp2048-modp3072", + }, + .responder = { + .esp = "aes128-sha256-modp3072-modp2048", + }, + }; + ike_sa_t *a, *b; + 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, &conf); + } + else + { /* initiator rekeys the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &conf); + } + initiate_rekey(a, spi_a); + + /* 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, spi_b, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(INVAL_KE) } */ + 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, spi_a, CHILD_REKEYING); + assert_child_sa_count(a, 1); + assert_hook(); + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_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); + assert_child_sa_state(b, 6, CHILD_INSTALLED); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ + assert_hook_called(child_rekey); + 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); + assert_child_sa_state(a, 5, CHILD_INSTALLED); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_not_called(child_rekey); + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 6, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + assert_hook(); + /* <-- INFORMATIONAL { D } */ + 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, 5, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + assert_hook(); + + /* child_updown */ + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * Check that the responder ignores soft expires while waiting for the delete + * after a rekeying. + */ +START_TEST(test_regular_responder_ignore_soft_expire) +{ + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + initiate_rekey(a, 1); + + /* 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_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); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ + assert_hook_called(child_rekey); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETING); + assert_child_sa_state(a, 3, CHILD_INSTALLED); + 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_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 4, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + /* <-- INFORMATIONAL { D } */ + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 3, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * Check that the responder handles hard expires properly while waiting for the + * delete after a rekeying (e.g. if the initiator of the rekeying fails to + * delete the CHILD_SA for some reason). + */ +START_TEST(test_regular_responder_handle_hard_expire) +{ + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + initiate_rekey(a, 1); + + /* 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_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); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } */ + assert_hook_called(child_rekey); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_DELETING); + assert_child_sa_state(a, 3, CHILD_INSTALLED); + 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 */ + assert_single_payload(OUT, PLV2_DELETE); + call_ikesa(b, delete_child_sa, PROTO_ESP, 2, TRUE); + assert_child_sa_state(b, 2, CHILD_DELETING); + + /* INFORMATIONAL { D } --> */ + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 4, CHILD_INSTALLED); + assert_child_sa_state(a, 2, CHILD_DELETING); + /* <-- INFORMATIONAL { D } */ + assert_single_payload(IN, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 3, CHILD_INSTALLED); + assert_child_sa_state(a, 1, CHILD_DELETING); + /* <-- INFORMATIONAL { } */ + assert_message_empty(IN); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 3, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + /* INFORMATIONAL { } --> */ + assert_message_empty(IN); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 4, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +END_TEST + +/** + * Both peers initiate the CHILD_SA reekying 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); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 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_rekey(child_rekey, 2, 5); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED); + assert_child_sa_state(b, 5, CHILD_INSTALLED); + 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); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED); + assert_child_sa_state(a, 6, CHILD_INSTALLED); + 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(); + } + 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_a, CHILD_DELETING); + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_REKEYED); + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED); + /* 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(); + } + 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_b, CHILD_DELETING); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + /* INFORMATIONAL { D } --> */ + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED); + assert_child_sa_count(b, 2); + /* <-- INFORMATIONAL { D } */ + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING); + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED); + assert_child_sa_count(a, 2); + /* <-- INFORMATIONAL { D } */ + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + /* INFORMATIONAL { D } --> */ + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + + /* child_rekey/child_updown */ + assert_hook(); + assert_hook(); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +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: + * + * rekey ----\ /---- rekey + * \-----/----> detect collision + * detect collision <---------/ /---- + * ----\ / + * \----/-----> + * handle delete <--------/------- delete SA + * --------/-------> + * handle rekey <------/ + * delete SA ----------------> + * <---------------- + */ +START_TEST(test_collision_delayed_response) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* Four nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /----- ... + * ... -----\ + * We test this four times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[4]; + /* SPIs of the deleted CHILD_SA (either redundant or replaced) */ + uint32_t spi_del_a, spi_del_b; + /* SPIs of the kept CHILD_SA */ + uint32_t spi_a, spi_b; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 4, 3, 5 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 2, 6, 4 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 4, 3, 5 }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 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_rekey(child_rekey, 2, 5); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED); + assert_child_sa_state(b, 5, CHILD_INSTALLED); + 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); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED); + assert_child_sa_state(a, 6, CHILD_INSTALLED); + assert_hook(); + + /* delay the CREATE_CHILD_SA response from b to a */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ + if (data[_i].spi_del_b == 2) + { + assert_hook_rekey(child_rekey, 2, data[_i].spi_b); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_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_b, CHILD_DELETING); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED); + + /* <-- INFORMATIONAL { D } */ + assert_hook_not_called(child_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + if (data[_i].spi_del_b == 2) + { + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + } + else + { + assert_child_sa_state(a, 1, CHILD_REKEYED); + assert_child_sa_count(a, 1); + } + /* INFORMATIONAL { D } --> */ + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED); + assert_child_sa_count(b, 2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */ + if (data[_i].spi_del_a == 1) + { + assert_hook_rekey(child_rekey, 1, data[_i].spi_a); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_hook(); + } + 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); + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED); + assert_child_sa_count(a, 2); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + /* INFORMATIONAL { D } --> */ + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + /* <-- INFORMATIONAL { D } */ + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + + /* 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 + * detect collision <---------/ /---- TEMP_FAIL + * delete -----------/----> + * aborts rekeying <---------/ + */ +START_TEST(test_collision_delayed_request) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* Three nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * N3/5 <-----\--/ + * ... -----\ \-------> ... + * We test this three times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[3]; + } data[] = { + { { 0x00, 0xFF, 0xFF } }, + { { 0xFF, 0x00, 0xFF } }, + { { 0xFF, 0xFF, 0x00 } }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 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_rekey(child_rekey, 1, 5); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED); + assert_child_sa_state(a, 5, CHILD_INSTALLED); + 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); + assert_child_sa_state(b, 4, CHILD_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_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, msg); + assert_child_sa_state(b, 2, CHILD_DELETING); + assert_child_sa_state(b, 4, CHILD_INSTALLED); + + /* <-- INFORMATIONAL { D } */ + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 5, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 5, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + assert_scheduler(); + + /* INFORMATIONAL { D } --> */ + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 4, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + + /* 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 + +/** + * Similar to above one peer fails to notice the collision but the + * CREATE_CHILD_SA request is even more delayed: + * + * rekey ----\ /---- rekey + * \ / + * detect collision <-----\---/ + * -------\--------> + * detect collision <-------\-------- delete old SA + * delete ---------\------> + * \-----> + * /---- CHILD_SA_NOT_FOUND + * aborts rekeying <----------/ + */ +START_TEST(test_collision_delayed_request_more) +{ + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* Three nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * N3/5 <-----\--/ + * ... -----\ \-------> ... + * We test this three times, each time a different nonce is the lowest. + */ + struct { + /* Nonces used at each point */ + u_char nonces[3]; + } data[] = { + { { 0x00, 0xFF, 0xFF } }, + { { 0xFF, 0x00, 0xFF } }, + { { 0xFF, 0xFF, 0x00 } }, + }; + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a, 1); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b, 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_rekey(child_rekey, 1, 5); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED); + assert_child_sa_state(a, 5, CHILD_INSTALLED); + 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); + assert_child_sa_state(b, 4, CHILD_INSTALLED); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + + /* <-- INFORMATIONAL { D } */ + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 5, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + /* INFORMATIONAL { D } --> */ + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 4, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + + /* 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, 4, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */ + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 5, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + assert_scheduler(); + + /* 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 + +/** + * Both peers initiate the CHILD_SA reekying concurrently but the proposed DH + * groups are not the same after handling the INVALID_KE_PAYLOAD they should + * still handle the collision properly depending on the nonces. + */ +START_TEST(test_collision_ke_invalid) +{ + exchange_test_sa_conf_t conf = { + .initiator = { + .esp = "aes128-sha256-modp2048-modp3072", + }, + .responder = { + .esp = "aes128-sha256-modp3072-modp2048", + }, + }; + ike_sa_t *a, *b; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &conf); + + /* Eight nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /---- INVAL_KE + * INVAL_KE -----\ / + * <-----\--/ + * N5/7 -----\ \-------> + * \ /---- N6/8 + * \--/----> N7/9 + * N8/10 <--------/ /---- ... + * ... ------\ + * + * 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 }, 7, 2,10, 8 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 8, 7, 9 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 7, 2,10, 8 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 8, 7, 9 }, + }; + + /* make sure the nonces of the first try don't affect the retries */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(a, 1); + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(b, 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); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYING); + 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); + 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); + 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); + 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, 9); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYED); + assert_child_sa_state(b, 9, CHILD_INSTALLED); + 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, 10); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED); + assert_child_sa_state(a,10, CHILD_INSTALLED); + 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(); + } + else + { + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + } + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING); + assert_child_sa_state(a, data[_i].spi_del_b, CHILD_REKEYED); + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED); + /* 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(); + } + else + { + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + } + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING); + assert_child_sa_state(b, data[_i].spi_del_a, CHILD_REKEYED); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(child_rekey); + /* INFORMATIONAL { D } --> */ + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_del_b, CHILD_DELETING); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED); + assert_child_sa_count(b, 2); + /* <-- INFORMATIONAL { D } */ + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, data[_i].spi_del_a, CHILD_DELETING); + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED); + assert_child_sa_count(a, 2); + /* <-- INFORMATIONAL { D } */ + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, data[_i].spi_a, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + /* INFORMATIONAL { D } --> */ + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, data[_i].spi_b, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + + /* 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 + +/** + * This is a variation of the above but with the retry by one peer delayed so + * that to the other peer it looks like there is no collision. + */ +START_TEST(test_collision_ke_invalid_delayed_retry) +{ + exchange_test_sa_conf_t conf = { + .initiator = { + .esp = "aes128-sha256-modp2048-modp3072", + }, + .responder = { + .esp = "aes128-sha256-modp3072-modp2048", + }, + }; + ike_sa_t *a, *b; + message_t *msg; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &conf); + + /* Seven nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * CHILD_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /---- INVAL_KE + * INVAL_KE -----\ / + * <-----\--/ + * N5/7 -----\ \-------> + * <-----\--------- N6/8 + * N7/9 -------\-------> + * <-------\------- DELETE + * ... ------\ \-----> + * /---- TEMP_FAIL + * + * 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 } }, + }; + + /* make sure the nonces of the first try don't affect the retries */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(a, 1); + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(b, 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); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_REKEYING); + 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); + 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); + 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); + 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, 9); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 1, CHILD_REKEYED); + assert_child_sa_state(a, 9, CHILD_INSTALLED); + assert_hook(); + /* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */ + assert_hook_rekey(child_rekey, 2, 8); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 2, CHILD_DELETING); + assert_child_sa_state(b, 8, CHILD_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_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, msg); + assert_child_sa_state(b, 2, CHILD_DELETING); + assert_child_sa_state(b, 8, CHILD_INSTALLED); + + /* <-- INFORMATIONAL { D } */ + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 9, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, 9, CHILD_INSTALLED); + assert_child_sa_count(a, 1); + assert_scheduler(); + + /* INFORMATIONAL { D } --> */ + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, 8, CHILD_INSTALLED); + assert_child_sa_count(b, 1); + + /* 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 + +/** + * One of the hosts initiates a DELETE of the CHILD_SA the other peer is + * concurrently trying to rekey. + * + * rekey ----\ /---- delete + * \-----/----> detect collision + * detect collision <---------/ /---- TEMP_FAIL + * delete ----\ / + * \----/-----> + * aborts rekeying <--------/ + */ +START_TEST(test_collision_delete) +{ + ike_sa_t *a, *b; + 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); + call_ikesa(b, delete_child_sa, PROTO_ESP, spi_b, FALSE); + assert_child_sa_state(b, spi_b, CHILD_DELETING); + + /* this should never get called as there is no successful rekeying on + * either side */ + assert_hook_not_called(child_rekey); + + /* RFC 7296, 2.25.1: If a peer receives a request to rekey a CHILD_SA that + * it is currently trying to close, it SHOULD reply with TEMPORARY_FAILURE. + */ + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_not_called(child_updown); + assert_notify(IN, REKEY_SA); + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, spi_b, CHILD_DELETING); + assert_hook(); + + /* RFC 7296, 2.25.1: If a peer receives a request to delete a CHILD_SA that + * it is currently trying to rekey, it SHOULD reply as usual, with a DELETE + * payload. + */ + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(child_updown, FALSE); + 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_hook(); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_hook_not_called(child_updown); + /* 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_scheduler(); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_count(b, 0); + assert_hook(); + + /* 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 DELETE of the CHILD_SA the other peer is + * concurrently trying to rekey. However, the delete request is delayed or + * dropped, so the peer doing the rekeying is unaware of the collision. + * + * rekey ----\ /---- delete + * \-----/----> detect collision + * reschedule <---------/------ TEMP_FAIL + * <--------/ + * delete ----------------> + * + * The job will not find the SA to retry rekeying. + */ +START_TEST(test_collision_delete_drop_delete) +{ + 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); + call_ikesa(b, delete_child_sa, PROTO_ESP, spi_b, FALSE); + assert_child_sa_state(b, spi_b, CHILD_DELETING); + + /* this should never get called as there is no successful rekeying on + * either side */ + assert_hook_not_called(child_rekey); + + /* RFC 7296, 2.25.1: If a peer receives a request to rekey a CHILD_SA that + * it is currently trying to close, it SHOULD reply with TEMPORARY_FAILURE. + */ + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_hook_not_called(child_updown); + assert_notify(IN, REKEY_SA); + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_state(b, spi_b, CHILD_DELETING); + assert_hook(); + + /* delay the DELETE request */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_hook_not_called(child_updown); + /* we expect a job to retry the rekeying is scheduled */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, spi_a, CHILD_INSTALLED); + assert_scheduler(); + assert_hook(); + + /* <-- INFORMATIONAL { D } (delayed) */ + assert_hook_updown(child_updown, FALSE); + assert_single_payload(IN, PLV2_DELETE); + assert_single_payload(OUT, PLV2_DELETE); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + assert_child_sa_count(a, 0); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_count(b, 0); + assert_hook(); + + /* 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 DELETE of the CHILD_SA the other peer is + * concurrently trying to rekey. However, the rekey request is delayed or + * dropped, so the peer doing the deleting is unaware of the collision. + * + * rekey ----\ /---- delete + * detect collision <----\-----/ + * delete ------\---------> + * \--------> + * /---- CHILD_SA_NOT_FOUND + * aborts rekeying <----------/ + */ + START_TEST(test_collision_delete_drop_rekey) +{ + 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); + call_ikesa(b, delete_child_sa, PROTO_ESP, spi_b, FALSE); + assert_child_sa_state(b, spi_b, CHILD_DELETING); + + /* this should never get called as there is no successful rekeying on + * either side */ + assert_hook_not_called(child_rekey); + + /* delay the CREATE_CHILD_SA request */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* RFC 7296, 2.25.1: If a peer receives a request to delete a CHILD_SA that + * it is currently trying to rekey, it SHOULD reply as usual, with a DELETE + * payload. + */ + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(child_updown, FALSE); + 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_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(child_updown, FALSE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_child_sa_count(b, 0); + assert_hook(); + + /* RFC 7296, 2.25.1: If a peer receives a to rekey a Child SA that does not + * exist, it SHOULD reply with CHILD_SA_NOT_FOUND. + */ + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> (delayed) */ + assert_hook_not_called(child_updown); + assert_notify(IN, REKEY_SA); + assert_single_notify(OUT, CHILD_SA_NOT_FOUND); + exchange_test_helper->process_message(exchange_test_helper, b, msg); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(NO_CHILD_SA) } */ + assert_hook_not_called(child_updown); + /* no jobs or tasks should get scheduled/queued */ + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_scheduler(); + assert_hook(); + + /* child_rekey */ + assert_hook(); + + assert_sa_idle(a); + assert_sa_idle(b); + + call_ikesa(a, destroy); + call_ikesa(b, destroy); +} +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. + * + * 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. + */ + + +/** + * One of the hosts initiates a rekey of the IKE_SA of the CHILD_SA the other + * peer is concurrently trying to rekey. + * + * rekey ----\ /---- rekey IKE + * \-----/----> detect collision + * detect collision <---------/ /---- TEMP_FAIL + * TEMP_FAIL ----\ / + * \----/-----> + * <--------/ + */ +START_TEST(test_collision_ike_rekey) +{ + ike_sa_t *a, *b; + uint32_t spi_a = _i+1; + + 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); + call_ikesa(b, rekey); + assert_ike_sa_state(b, IKE_REKEYING); + + /* these should never get called as there is no successful rekeying on + * either side */ + assert_hook_not_called(ike_rekey); + assert_hook_not_called(child_rekey); + + /* RFC 7296, 2.25.2: If a peer receives a request to rekey a CHILD_SA when + * it is currently rekeying the IKE SA, it SHOULD reply with + * TEMPORARY_FAILURE. + */ + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYING); + + /* RFC 7296, 2.25.1: If a peer receives a request to rekey the IKE SA, and + * it is currently, rekeying, or closing a Child SA of that IKE SA, it + * SHOULD reply with TEMPORARY_FAILURE. + */ + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, spi_a, CHILD_REKEYING); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + /* we expect a job to retry the rekeying is scheduled */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_child_sa_state(a, spi_a, CHILD_INSTALLED); + assert_scheduler(); + + /* CREATE_CHILD_SA { N(TEMP_FAIL) } --> */ + /* we expect a job to retry the rekeying is scheduled */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_ESTABLISHED); + assert_scheduler(); + + /* ike_rekey/child_rekey */ + assert_hook(); + 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 delete of the IKE_SA of the CHILD_SA the other + * peer is concurrently trying to rekey. + * + * rekey ----\ /---- delete IKE + * \-----/----> detect collision + * <---------/ /---- TEMP_FAIL + * delete ----\ / + * \----/-----> + * sa already gone <--------/ + */ +START_TEST(test_collision_ike_delete) +{ + ike_sa_t *a, *b; + uint32_t spi_a = _i+1; + message_t *msg; + status_t s; + + 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); + call_ikesa(b, delete); + assert_ike_sa_state(b, IKE_DELETING); + + /* this should never get called as there is no successful rekeying on + * either side */ + assert_hook_not_called(child_rekey); + + /* RFC 7296, 2.25.2 does not explicitly state what the behavior SHOULD be if + * a peer receives a request to rekey a CHILD_SA when it is currently + * closing the IKE SA. We expect a TEMPORARY_FAILURE notify. + */ + + /* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + + /* RFC 7296, 2.25.1 does not explicitly state what the behavior SHOULD be if + * a peer receives a request to close the IKE SA if it is currently rekeying + * a Child SA of that IKE SA. Let's just close the IKE_SA and forget the + * rekeying. + */ + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + assert_message_empty(OUT); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + assert_hook(); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + /* the SA is already gone */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + msg->destroy(msg); + + /* INFORMATIONAL { } --> */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + assert_hook(); + + /* child_rekey */ + assert_hook(); +} +END_TEST + +Suite *child_rekey_suite_create() +{ + Suite *s; + TCase *tc; + + s = suite_create("child rekey"); + + tc = tcase_create("regular"); + tcase_add_loop_test(tc, test_regular, 0, 2); + tcase_add_loop_test(tc, test_regular_ke_invalid, 0, 2); + tcase_add_test(tc, test_regular_responder_ignore_soft_expire); + tcase_add_test(tc, test_regular_responder_handle_hard_expire); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions rekey"); + tcase_add_loop_test(tc, test_collision, 0, 4); + tcase_add_loop_test(tc, test_collision_delayed_response, 0, 4); + tcase_add_loop_test(tc, test_collision_delayed_request, 0, 3); + tcase_add_loop_test(tc, test_collision_delayed_request_more, 0, 3); + tcase_add_loop_test(tc, test_collision_ke_invalid, 0, 4); + tcase_add_loop_test(tc, test_collision_ke_invalid_delayed_retry, 0, 3); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions delete"); + tcase_add_loop_test(tc, test_collision_delete, 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); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions ike rekey"); + tcase_add_loop_test(tc, test_collision_ike_rekey, 0, 2); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions ike delete"); + tcase_add_loop_test(tc, test_collision_ike_delete, 0, 2); + suite_add_tcase(s, tc); + + return s; +} diff --git a/src/libcharon/tests/suites/test_ike_delete.c b/src/libcharon/tests/suites/test_ike_delete.c new file mode 100644 index 000000000..d79f9bc50 --- /dev/null +++ b/src/libcharon/tests/suites/test_ike_delete.c @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "test_suite.h" + +#include <tests/utils/exchange_test_helper.h> +#include <tests/utils/exchange_test_asserts.h> +#include <tests/utils/sa_asserts.h> + +/** + * Regular IKE_SA delete either initiated by the original initiator or + * responder of the IKE_SA. + */ +START_TEST(test_regular) +{ + ike_sa_t *a, *b; + status_t s; + + if (_i) + { /* responder deletes the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, NULL); + } + else + { /* initiator deletes the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + } + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + call_ikesa(a, delete); + assert_ike_sa_state(a, IKE_DELETING); + assert_hook(); + assert_hook(); + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + assert_hook(); + + /* <-- INFORMATIONAL { } */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + assert_hook(); + assert_hook(); +} +END_TEST + +/** + * Both peers initiate the IKE_SA deletion concurrently and should handle the + * collision properly. + */ +START_TEST(test_collision) +{ + ike_sa_t *a, *b; + status_t s; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + call_ikesa(a, delete); + assert_ike_sa_state(a, IKE_DELETING); + call_ikesa(b, delete); + assert_ike_sa_state(b, IKE_DELETING); + assert_hook(); + assert_hook(); + + /* RFC 7296 says: If a peer receives a request to close an IKE SA that it + * is currently trying to close, it SHOULD reply as usual, and forget about + * its own close request. + * So we expect the SA to just get closed with an empty response still sent. + */ + + /* INFORMATIONAL { D } --> */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + assert_single_payload(IN, PLV2_DELETE); + assert_message_empty(OUT); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + assert_single_payload(IN, PLV2_DELETE); + assert_message_empty(OUT); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + assert_hook(); + assert_hook(); +} +END_TEST + +Suite *ike_delete_suite_create() +{ + Suite *s; + TCase *tc; + + s = suite_create("ike delete"); + + tc = tcase_create("regular"); + tcase_add_loop_test(tc, test_regular, 0, 2); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions"); + tcase_add_test(tc, test_collision); + suite_add_tcase(s, tc); + + return s; +} diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c new file mode 100644 index 000000000..ba39657a4 --- /dev/null +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -0,0 +1,1480 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "test_suite.h" + +#include <tests/utils/exchange_test_helper.h> +#include <tests/utils/exchange_test_asserts.h> +#include <tests/utils/job_asserts.h> +#include <tests/utils/sa_asserts.h> + +/** + * Initiate rekeying the given IKE_SA. + */ +#define initiate_rekey(sa) ({ \ + assert_hook_not_called(ike_rekey); \ + call_ikesa(sa, rekey); \ + assert_ike_sa_state(a, IKE_REKEYING); \ + assert_hook(); \ +}) + +/** + * Regular IKE_SA rekeying either initiated by the original initiator or + * responder of the IKE_SA. + */ +START_TEST(test_regular) +{ + ike_sa_t *a, *b, *new_sa; + status_t s; + + if (_i) + { /* responder rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, NULL); + } + else + { /* initiator rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + } + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + initiate_rekey(a); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + assert_hook_rekey(ike_rekey, 1, 3); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYED); + assert_child_sa_count(b, 0); + new_sa = assert_ike_sa_checkout(3, 4, FALSE); + assert_ike_sa_state(new_sa, IKE_ESTABLISHED); + assert_child_sa_count(new_sa, 1); + assert_ike_sa_count(1); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr } */ + assert_hook_rekey(ike_rekey, 1, 3); + assert_no_notify(IN, REKEY_SA); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_DELETING); + assert_child_sa_count(a, 0); + new_sa = assert_ike_sa_checkout(3, 4, TRUE); + assert_ike_sa_state(new_sa, IKE_ESTABLISHED); + assert_child_sa_count(new_sa, 1); + assert_ike_sa_count(2); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(ike_rekey); + + /* INFORMATIONAL { D } --> */ + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + /* <-- INFORMATIONAL { } */ + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + + /* ike_rekey/ike_updown/child_updown */ + assert_hook(); + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * IKE_SA rekeying where the responder does not agree with the DH group selected + * by the initiator, either initiated by the original initiator or responder of + * the IKE_SA. + */ +START_TEST(test_regular_ke_invalid) +{ + exchange_test_sa_conf_t conf = { + .initiator = { + .ike = "aes128-sha256-modp2048-modp3072", + }, + .responder = { + .ike = "aes128-sha256-modp3072-modp2048", + }, + }; + ike_sa_t *a, *b, *sa; + status_t s; + + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", + FALSE, lib->ns); + if (_i) + { /* responder rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, &conf); + } + else + { /* initiator rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &conf); + } + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", + TRUE, lib->ns); + + initiate_rekey(a); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_ESTABLISHED); + assert_child_sa_count(b, 1); + assert_ike_sa_count(0); + + /* <-- CREATE_CHILD_SA { N(INVAL_KE) } */ + assert_single_notify(IN, INVALID_KE_PAYLOAD); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + assert_hook_rekey(ike_rekey, 1, 3); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYED); + assert_child_sa_count(b, 0); + sa = assert_ike_sa_checkout(3, 5, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(1); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr } */ + assert_hook_rekey(ike_rekey, 1, 3); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_DELETING); + assert_child_sa_count(a, 0); + sa = assert_ike_sa_checkout(3, 5, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(2); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(ike_rekey); + + /* INFORMATIONAL { D } --> */ + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + /* <-- INFORMATIONAL { } */ + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + + /* ike_rekey/ike_updown/child_updown */ + assert_hook(); + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * Both peers initiate the IKE_SA rekeying concurrently and should handle the + * collision properly depending on the nonces. + */ +START_TEST(test_collision) +{ + ike_sa_t *a, *b, *sa; + status_t status; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* When rekeyings collide we get two IKE_SAs with a total of four nonces. + * The IKE_SA with the lowest nonce SHOULD be deleted by the peer that + * created that IKE_SA. The replaced IKE_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 + * IKE_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 IKE_SAs (either redundant or replaced) */ + uint32_t del_a_i, del_a_r; + uint32_t del_b_i, del_b_r; + /* SPIs of the kept IKE_SA */ + uint32_t spi_i, spi_r; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 2, 4, 6, 3, 5 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 2, 4, 6, 3, 5 }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYING); + assert_child_sa_count(b, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* simplify next steps by checking in original IKE_SAs */ + charon->ike_sa_manager->checkin(charon->ike_sa_manager, a); + charon->ike_sa_manager->checkin(charon->ike_sa_manager, b); + assert_ike_sa_count(2); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr } */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + /* as original initiator a is initiator of both SAs it could delete */ + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, TRUE); + assert_ike_sa_state(sa, IKE_DELETING); + assert_child_sa_count(sa, 0); + /* if b won it will delete the original SA a initiated */ + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, + data[_i].del_b_i == 1); + assert_ike_sa_state(sa, IKE_REKEYED); + assert_child_sa_count(sa, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, + data[_i].del_a_i == 1); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(4); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Nr, KEr } --> */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + /* if b wins it deletes the SA originally initiated by a */ + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, + data[_i].del_b_i != 1); + assert_ike_sa_state(sa, IKE_DELETING); + assert_child_sa_count(sa, 0); + /* a only deletes SAs for which b is responder */ + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, FALSE); + assert_ike_sa_state(sa, IKE_REKEYED); + assert_child_sa_count(sa, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, + data[_i].del_b_i == 1); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(6); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(ike_rekey); + + /* INFORMATIONAL { D } --> */ + assert_single_payload(IN, PLV2_DELETE); + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, FALSE); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(5); + /* <-- INFORMATIONAL { D } */ + assert_single_payload(IN, PLV2_DELETE); + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, + data[_i].del_b_i == 1); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(4); + /* <-- INFORMATIONAL { } */ + assert_message_empty(IN); + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, TRUE); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(3); + /* INFORMATIONAL { } --> */ + assert_message_empty(IN); + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, + data[_i].del_b_i != 1); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(2); + + /* ike_rekey/ike_updown/child_updown */ + assert_hook(); + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * Both peers initiate the IKE_SA rekeying concurrently but the proposed DH + * gropus are not the same. After handling the INVALID_KE_PAYLOAD they should + * still handle the collision properly depending on the nonces. + */ +START_TEST(test_collision_ke_invalid) +{ + exchange_test_sa_conf_t conf = { + .initiator = { + .ike = "aes128-sha256-modp2048-modp3072", + }, + .responder = { + .ike = "aes128-sha256-modp3072-modp2048", + }, + }; + ike_sa_t *a, *b, *sa; + status_t status; + + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", + FALSE, lib->ns); + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &conf); + + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", + TRUE, lib->ns); + + /* Six nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * IKE_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /---- INVAL_KE + * INVAL_KE -----\ / + * <-----\--/ + * N1/3 -----\ \-------> + * \ /---- N2/4 + * \--/----> N5/7 + * N6/8 <--------/ /---- ... + * ... ------\ + * 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 IKE_SAs (either redundant or replaced) */ + uint32_t del_a_i, del_a_r; + uint32_t del_b_i, del_b_r; + /* SPIs of the kept IKE_SA */ + uint32_t spi_i, spi_r; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 7, 1, 2, 4, 8 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 2, 4, 8, 3, 7 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 7, 1, 2, 4, 8 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 2, 4, 8, 3, 7 }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYING); + assert_child_sa_count(b, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(INVAL_KE) } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + assert_hook_not_called(ike_rekey); + assert_single_notify(IN, INVALID_KE_PAYLOAD); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + 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_ike_sa_state(b, IKE_REKEYING); + assert_child_sa_count(b, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYING); + assert_child_sa_count(b, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* simplify next steps by checking in original IKE_SAs */ + charon->ike_sa_manager->checkin(charon->ike_sa_manager, a); + charon->ike_sa_manager->checkin(charon->ike_sa_manager, b); + assert_ike_sa_count(2); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr } */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + /* as original initiator a is initiator of both SAs it could delete */ + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, TRUE); + assert_ike_sa_state(sa, IKE_DELETING); + assert_child_sa_count(sa, 0); + /* if b won it will delete the original SA a initiated */ + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, + data[_i].del_b_i == 1); + assert_ike_sa_state(sa, IKE_REKEYED); + assert_child_sa_count(sa, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, + data[_i].del_a_i == 1); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(4); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Nr, KEr } --> */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + /* if b wins it deletes the SA originally initiated by a */ + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, + data[_i].del_b_i != 1); + assert_ike_sa_state(sa, IKE_DELETING); + assert_child_sa_count(sa, 0); + /* a only deletes SAs for which b is responder */ + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, FALSE); + assert_ike_sa_state(sa, IKE_REKEYED); + assert_child_sa_count(sa, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, + data[_i].del_b_i == 1); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(6); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(ike_rekey); + + /* INFORMATIONAL { D } --> */ + assert_single_payload(IN, PLV2_DELETE); + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, FALSE); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(5); + /* <-- INFORMATIONAL { D } */ + assert_single_payload(IN, PLV2_DELETE); + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, + data[_i].del_b_i == 1); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(4); + /* <-- INFORMATIONAL { } */ + assert_message_empty(IN); + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, TRUE); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(3); + /* INFORMATIONAL { } --> */ + assert_message_empty(IN); + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, + data[_i].del_b_i != 1); + status = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, status); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(2); + + /* ike_rekey/ike_updown/child_updown */ + assert_hook(); + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * This is like the collision above but one of the retries is delayed. + */ +START_TEST(test_collision_ke_invalid_delayed_retry) +{ + exchange_test_sa_conf_t conf = { + .initiator = { + .ike = "aes128-sha256-modp2048-modp3072", + }, + .responder = { + .ike = "aes128-sha256-modp3072-modp2048", + }, + }; + ike_sa_t *a, *b, *sa; + message_t *msg; + status_t s; + + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", + FALSE, lib->ns); + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, &conf); + + lib->settings->set_bool(lib->settings, "%s.prefer_configured_proposals", + TRUE, lib->ns); + + /* Five nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * IKE_SA): + * N1/3 -----\ /----- N2/4 + * \--/-----> N3/5 + * N4/6 <-------/ /---- INVAL_KE + * INVAL_KE -----\ / + * <-----\--/ + * N1/3 -----\ \-------> + * <-----\--------- N2/4 + * N5/7 -------\-------> + * <-------\------- DELETE + * ... ------\ \-----> + * /---- TEMP_FAIL + * + * 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 } }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYING); + assert_child_sa_count(b, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(INVAL_KE) } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + assert_hook_not_called(ike_rekey); + assert_single_notify(IN, INVALID_KE_PAYLOAD); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + 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_ike_sa_state(b, IKE_REKEYING); + assert_child_sa_count(b, 1); + assert_ike_sa_count(0); + 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 { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Nr, KEr } --> */ + assert_hook_rekey(ike_rekey, 1, 4); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + assert_child_sa_count(b, 0); + sa = assert_ike_sa_checkout(4, 7, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(1); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> (delayed) */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, msg); + assert_ike_sa_state(b, IKE_DELETING); + + /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(ike_rekey, 1, 4); + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + sa = assert_ike_sa_checkout(4, 7, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + /* the SA is already gone */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + msg->destroy(msg); + + /* INFORMATIONAL { } --> */ + assert_hook_not_called(ike_rekey); + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + + /* ike_updown/child_updown */ + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +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: + * Peer A Peer B + * rekey ----\ /---- rekey + * \-----/----> detect collision + * detect collision <---------/ /---- + * -----------/----> + * handle delete <---------/------ delete redundant/old SA + * ---------/------> + * handle rekey <-------/ + * delete SA ----------------> + * <---------------- + * + * If peer B won the collision it deletes the old IKE_SA, in which case + * this situation is handled as if peer B was not aware of the collision (see + * below). That is, peer A finalizes the rekeying initiated by the peer and + * deletes the IKE_SA (it has no way of knowing whether the peer was aware of + * the collision or not). Peer B will expect the redundant IKE_SA to get + * deleted, but that will never happen if the response arrives after the SA is + * already gone. So a job should be queued that deletes it after a while. + * + * If peer B lost it will switch to the new IKE_SA and delete the redundant + * IKE_SA and expect a delete for the old IKE_SA. In this case peer A will + * simply retransmit until it receives a response to the rekey request, all the + * while ignoring the delete requests for the unknown IKE_SA. Afterwards, + * everything works as in a regular collision (however, until peer A receives + * the response it will not be able to receive any messages on the new IKE_SA). + */ +START_TEST(test_collision_delayed_response) +{ + ike_sa_t *a, *b, *sa; + message_t *msg, *d; + status_t s; + + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + + /* Four nonces and SPIs are needed (SPI 1 and 2 are used for the initial + * IKE_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 IKE_SAs (either redundant or replaced) */ + uint32_t del_a_i, del_a_r; + uint32_t del_b_i, del_b_r; + /* SPIs of the kept IKE_SA */ + uint32_t spi_i, spi_r; + } data[] = { + { { 0x00, 0xFF, 0xFF, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0x00, 0xFF, 0xFF }, 1, 2, 4, 6, 3, 5 }, + { { 0xFF, 0xFF, 0x00, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0xFF, 0xFF, 0x00 }, 1, 2, 4, 6, 3, 5 }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_REKEYING); + assert_child_sa_count(b, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[3]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* delay the CREATE_CHILD_SA response from b to a */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* simplify next steps by checking in original IKE_SAs */ + charon->ike_sa_manager->checkin(charon->ike_sa_manager, a); + charon->ike_sa_manager->checkin(charon->ike_sa_manager, b); + assert_ike_sa_count(2); + + /* CREATE_CHILD_SA { SA, Nr, KEr } --> */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + /* besides the job that retransmits the delete, we expect a job that + * deletes the redundant IKE_SA if we expect the other to delete it */ + assert_jobs_scheduled(data[_i].del_b_i == 1 ? 2 : 1); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + /* if b wins it deletes the SA originally initiated by a */ + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, + data[_i].del_b_i != 1); + assert_ike_sa_state(sa, IKE_DELETING); + assert_child_sa_count(sa, 0); + /* a only deletes SAs for which b is responder */ + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, FALSE); + assert_ike_sa_state(sa, IKE_REKEYED); + assert_child_sa_count(sa, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, + data[_i].del_b_i == 1); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(4); + assert_scheduler(); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + if (data[_i].del_b_i == 1) + { /* b won, it deletes the replaced IKE_SA */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, a, + NULL); + ck_assert_int_eq(DESTROY_ME, s); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, a); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(4); + assert_hook(); + + /* INFORMATIONAL { } --> */ + assert_hook_not_called(ike_rekey); + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, b, + NULL); + ck_assert_int_eq(DESTROY_ME, s); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, b); + assert_ike_sa_count(3); + assert_hook(); + /* the job will later remove this redundant IKE_SA on b */ + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, FALSE); + assert_ike_sa_state(sa, IKE_REKEYED); + assert_sa_idle(sa); + /* <-- CREATE_CHILD_SA { SA, Nr, KEr } (delayed) */ + /* the IKE_SA (a) does not exist anymore */ + msg->destroy(msg); + } + else + { /* b lost, the delete is for the non-existing redundant IKE_SA */ + d = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* <-- CREATE_CHILD_SA { SA, Nr, KEr } (delayed) */ + assert_hook_rekey(ike_rekey, 1, data[_i].spi_i); + exchange_test_helper->process_message(exchange_test_helper, a, msg); + /* as original initiator a is initiator of both SAs it could delete */ + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, TRUE); + assert_ike_sa_state(sa, IKE_DELETING); + assert_child_sa_count(sa, 0); + /* this is the redundant SA b is trying to delete */ + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, FALSE); + assert_ike_sa_state(sa, IKE_REKEYED); + assert_child_sa_count(sa, 0); + sa = assert_ike_sa_checkout(data[_i].spi_i, data[_i].spi_r, + data[_i].del_a_i == 1); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(6); + assert_hook(); + + /* we don't expect this hook to get called anymore */ + assert_hook_not_called(ike_rekey); + + /* INFORMATIONAL { D } --> */ + assert_single_payload(IN, PLV2_DELETE); + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, FALSE); + s = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, s); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(5); + /* <-- INFORMATIONAL { } */ + assert_message_empty(IN); + sa = assert_ike_sa_checkout(data[_i].del_a_i, data[_i].del_a_r, TRUE); + s = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, s); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(4); + + /* <-- INFORMATIONAL { D } (retransmit/delayed) */ + assert_single_payload(IN, PLV2_DELETE); + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, FALSE); + s = exchange_test_helper->process_message(exchange_test_helper, sa, d); + ck_assert_int_eq(DESTROY_ME, s); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(3); + /* INFORMATIONAL { } --> */ + assert_message_empty(IN); + sa = assert_ike_sa_checkout(data[_i].del_b_i, data[_i].del_b_r, TRUE); + s = exchange_test_helper->process_message(exchange_test_helper, sa, + NULL); + ck_assert_int_eq(DESTROY_ME, s); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, sa); + assert_ike_sa_count(2); + /* ike_rekey */ + assert_hook(); + } + + /* ike_updown/child_updown */ + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * In this scenario one of the peers does not notice that there is a rekey + * collision because the other request is dropped: + * + * rekey ----\ /---- rekey + * \ / + * detect collision <-----\---/ + * -------\--------> + * detect collision <-------\-------- delete old SA + * delete ---------\------> + * rekey done \-----> SA not found (or it never arrives) + */ +START_TEST(test_collision_dropped_request) +{ + ike_sa_t *a, *b, *sa; + message_t *msg; + status_t s; + + 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]; + /* SPIs of the deleted IKE_SAs (either redundant or replaced) */ + uint32_t del_a_i, del_a_r; + uint32_t del_b_i, del_b_r; + /* SPIs of the kept IKE_SA */ + uint32_t spi_i, spi_r; + } data[] = { + { { 0x00, 0xFF, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0x00, 0xFF }, 1, 2, 4, 6, 3, 5 }, + { { 0xFF, 0xFF, 0x00 }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0xFF, 0xFF }, 1, 2, 4, 6, 3, 5 }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + /* drop the CREATE_CHILD_SA request from a to b */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + msg->destroy(msg); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + assert_hook_rekey(ike_rekey, 1, 4); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + assert_child_sa_count(b, 0); + sa = assert_ike_sa_checkout(4, 5, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(1); + assert_hook(); + + /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(ike_rekey, 1, 4); + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + sa = assert_ike_sa_checkout(4, 5, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(2); + assert_hook(); + + /* INFORMATIONAL { } --> */ + assert_hook_not_called(ike_rekey); + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + + /* ike_updown/child_updown */ + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * In this scenario one of the peers does not notice that there is a rekey + * collision because the other request is delayed: + * + * rekey ----\ /---- rekey + * \ / + * detect collision <-----\---/ + * -------\--------> + * \ /---- delete old SA + * \-/----> detect collision + * detect collision <---------/ /---- TEMP_FAIL + * delete -----------/----> + * rekey done / + * sa already gone <--------/ + */ +START_TEST(test_collision_delayed_request) +{ + ike_sa_t *a, *b, *sa; + message_t *msg; + status_t s; + + 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]; + /* SPIs of the deleted IKE_SAs (either redundant or replaced) */ + uint32_t del_a_i, del_a_r; + uint32_t del_b_i, del_b_r; + /* SPIs of the kept IKE_SA */ + uint32_t spi_i, spi_r; + } data[] = { + { { 0x00, 0xFF, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0x00, 0xFF }, 1, 2, 4, 6, 3, 5 }, + { { 0xFF, 0xFF, 0x00 }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0xFF, 0xFF }, 1, 2, 4, 6, 3, 5 }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* delay the CREATE_CHILD_SA request from a to b */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + assert_hook_rekey(ike_rekey, 1, 4); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + assert_child_sa_count(b, 0); + sa = assert_ike_sa_checkout(4, 5, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(1); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> (delayed) */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, msg); + assert_ike_sa_state(b, IKE_DELETING); + + /* <-- INFORMATIONAL { D } */ + assert_hook_rekey(ike_rekey, 1, 4); + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + sa = assert_ike_sa_checkout(4, 5, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(2); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + /* the SA is already gone */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + msg->destroy(msg); + + /* INFORMATIONAL { } --> */ + assert_hook_not_called(ike_rekey); + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + + /* ike_updown/child_updown */ + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * In this scenario one of the peers does not notice that there is a rekey + * collision and the delete arrives after the TEMPORARY_FAILURE notify: + * + * rekey ----\ /---- rekey + * \ / + * detect collision <-----\---/ + * -------\--------> + * \ /---- delete old SA + * \-/----> detect collision + * no reschedule <---------/------ TEMP_FAIL + * detect collision <--------/ + * delete ----------------> + * rekey done + */ +START_TEST(test_collision_delayed_request_and_delete) +{ + ike_sa_t *a, *b, *sa; + message_t *msg; + status_t s; + + 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]; + /* SPIs of the deleted IKE_SAs (either redundant or replaced) */ + uint32_t del_a_i, del_a_r; + uint32_t del_b_i, del_b_r; + /* SPIs of the kept IKE_SA */ + uint32_t spi_i, spi_r; + } data[] = { + { { 0x00, 0xFF, 0xFF }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0x00, 0xFF }, 1, 2, 4, 6, 3, 5 }, + { { 0xFF, 0xFF, 0x00 }, 3, 5, 1, 2, 4, 6 }, + { { 0xFF, 0xFF, 0xFF }, 1, 2, 4, 6, 3, 5 }, + }; + /* these should never get called as this results in a successful rekeying */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + + exchange_test_helper->nonce_first_byte = data[_i].nonces[0]; + initiate_rekey(a); + exchange_test_helper->nonce_first_byte = data[_i].nonces[1]; + initiate_rekey(b); + + /* delay the CREATE_CHILD_SA request from a to b */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* <-- CREATE_CHILD_SA { SA, Ni, KEi } */ + exchange_test_helper->nonce_first_byte = data[_i].nonces[2]; + assert_hook_not_called(ike_rekey); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYING); + assert_child_sa_count(a, 1); + assert_ike_sa_count(0); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + assert_hook_rekey(ike_rekey, 1, 4); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + assert_child_sa_count(b, 0); + sa = assert_ike_sa_checkout(4, 5, TRUE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(1); + assert_hook(); + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> (delayed) */ + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, msg); + assert_ike_sa_state(b, IKE_DELETING); + + /* delay the INFORMATIONAL request from b to a */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_hook_rekey(ike_rekey, 1, 4); + assert_no_jobs_scheduled(); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_REKEYED); + assert_child_sa_count(a, 0); + sa = assert_ike_sa_checkout(4, 5, FALSE); + assert_ike_sa_state(sa, IKE_ESTABLISHED); + assert_child_sa_count(sa, 1); + assert_ike_sa_count(2); + assert_scheduler(); + assert_hook(); + + /* <-- INFORMATIONAL { D } (delayed) */ + assert_single_payload(IN, PLV2_DELETE); + s = exchange_test_helper->process_message(exchange_test_helper, a, msg); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + + /* INFORMATIONAL { } --> */ + assert_hook_not_called(ike_rekey); + assert_message_empty(IN); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + + /* ike_updown/child_updown */ + assert_hook(); + assert_hook(); + + charon->ike_sa_manager->flush(charon->ike_sa_manager); +} +END_TEST + +/** + * One of the hosts initiates a DELETE of the IKE_SA the other peer is + * concurrently trying to rekey. + * + * rekey ----\ /---- delete + * \-----/----> detect collision + * detect collision <---------/ /---- TEMP_FAIL + * delete ----\ / + * \----/-----> + * sa already gone <--------/ + */ +START_TEST(test_collision_delete) +{ + ike_sa_t *a, *b; + message_t *msg; + status_t s; + + if (_i) + { /* responder rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, NULL); + } + else + { /* initiator rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + } + /* this should never get called as this does not result in a successful + * rekeying on either side */ + assert_hook_not_called(ike_rekey); + + initiate_rekey(a); + call_ikesa(b, delete); + assert_ike_sa_state(b, IKE_DELETING); + + /* RFC 7296, 2.25.2: If a peer receives a request to rekey an IKE SA that + * it is currently trying to close, it SHOULD reply with TEMPORARY_FAILURE. + */ + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + assert_hook_not_called(ike_updown); + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + assert_ike_sa_count(0); + assert_hook(); + + /* RFC 7296, 2.25.2: If a peer receives a request to close an IKE SA that + * it is currently rekeying, it SHOULD reply as usual, and forget its own + * rekeying request. + */ + + /* <-- INFORMATIONAL { D } */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + assert_single_payload(IN, PLV2_DELETE); + assert_message_empty(OUT); + s = exchange_test_helper->process_message(exchange_test_helper, a, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + assert_hook(); + assert_hook(); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + /* the SA is already gone */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + msg->destroy(msg); + + /* INFORMATIONAL { } --> */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + assert_hook(); + + /* ike_rekey */ + assert_hook(); +} +END_TEST + +/** + * One of the hosts initiates a DELETE of the IKE_SA the other peer is + * concurrently trying to rekey. However, the delete request is delayed or + * dropped, so the peer doing the rekeying is unaware of the collision. + * + * rekey ----\ /---- delete + * \-----/----> detect collision + * reschedule <---------/------ TEMP_FAIL + * <--------/ + * delete ----------------> + */ +START_TEST(test_collision_delete_drop_delete) +{ + ike_sa_t *a, *b; + message_t *msg; + status_t s; + + if (_i) + { /* responder rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &b, &a, NULL); + } + else + { /* initiator rekeys the IKE_SA */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b, NULL); + } + /* this should never get called as this does not result in a successful + * rekeying on either side */ + assert_hook_not_called(ike_rekey); + + initiate_rekey(a); + call_ikesa(b, delete); + assert_ike_sa_state(b, IKE_DELETING); + + /* RFC 7296, 2.25.2: If a peer receives a request to rekey an IKE SA that + * it is currently trying to close, it SHOULD reply with TEMPORARY_FAILURE. + */ + + /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ + assert_hook_not_called(ike_updown); + assert_single_notify(OUT, TEMPORARY_FAILURE); + exchange_test_helper->process_message(exchange_test_helper, b, NULL); + assert_ike_sa_state(b, IKE_DELETING); + assert_ike_sa_count(0); + assert_hook(); + + /* delay the DELETE request */ + msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender); + + /* <-- CREATE_CHILD_SA { N(TEMP_FAIL) } */ + assert_hook_not_called(ike_updown); + assert_hook_not_called(child_updown); + /* we expect a job to retry the rekeying is scheduled */ + assert_jobs_scheduled(1); + exchange_test_helper->process_message(exchange_test_helper, a, NULL); + assert_ike_sa_state(a, IKE_ESTABLISHED); + assert_scheduler(); + assert_hook(); + assert_hook(); + + /* <-- INFORMATIONAL { D } (delayed) */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + assert_single_payload(IN, PLV2_DELETE); + assert_message_empty(OUT); + s = exchange_test_helper->process_message(exchange_test_helper, a, msg); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(a, destroy); + assert_hook(); + assert_hook(); + + /* INFORMATIONAL { } --> */ + assert_hook_updown(ike_updown, FALSE); + assert_hook_updown(child_updown, FALSE); + s = exchange_test_helper->process_message(exchange_test_helper, b, NULL); + ck_assert_int_eq(DESTROY_ME, s); + call_ikesa(b, destroy); + assert_hook(); + assert_hook(); + + /* ike_rekey */ + assert_hook(); +} +END_TEST + +Suite *ike_rekey_suite_create() +{ + Suite *s; + TCase *tc; + + s = suite_create("ike rekey"); + + tc = tcase_create("regular"); + tcase_add_loop_test(tc, test_regular, 0, 2); + tcase_add_loop_test(tc, test_regular_ke_invalid, 0, 2); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions rekey"); + tcase_add_loop_test(tc, test_collision, 0, 4); + 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_loop_test(tc, test_collision_delayed_response, 0, 4); + tcase_add_loop_test(tc, test_collision_dropped_request, 0, 3); + tcase_add_loop_test(tc, test_collision_delayed_request, 0, 3); + tcase_add_loop_test(tc, test_collision_delayed_request_and_delete, 0, 3); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions delete"); + tcase_add_loop_test(tc, test_collision_delete, 0, 2); + tcase_add_loop_test(tc, test_collision_delete_drop_delete, 0, 2); + suite_add_tcase(s, tc); + + return s; +} diff --git a/src/libcharon/tests/suites/test_proposal.c b/src/libcharon/tests/suites/test_proposal.c new file mode 100644 index 000000000..a6226f68f --- /dev/null +++ b/src/libcharon/tests/suites/test_proposal.c @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "test_suite.h" + +#include <config/proposal.h> + +static struct { + char *self; + char *other; + char *expected; +} select_data[] = { + { "aes128", "aes128", "aes128" }, + { "aes128", "aes256", NULL }, + { "aes128-aes256", "aes256-aes128", "aes128" }, + { "aes256-aes128", "aes128-aes256", "aes256" }, + { "aes128-aes256-sha1-sha256", "aes256-aes128-sha256-sha1", "aes128-sha1" }, + { "aes256-aes128-sha256-sha1", "aes128-aes256-sha1-sha256", "aes256-sha256" }, + { "aes128-sha256-modp3072", "aes128-sha256", NULL }, + { "aes128-sha256", "aes128-sha256-modp3072", NULL }, + { "aes128-sha256-modp3072", "aes128-sha256-modpnone", NULL }, + { "aes128-sha256-modpnone", "aes128-sha256-modp3072", NULL }, + { "aes128-sha256-modp3072-modpnone", "aes128-sha256", "aes128-sha256" }, + { "aes128-sha256", "aes128-sha256-modp3072-modpnone", "aes128-sha256" }, + { "aes128-sha256-modp3072-modpnone", "aes128-sha256-modpnone-modp3072", "aes128-sha256-modp3072" }, + { "aes128-sha256-modpnone-modp3072", "aes128-sha256-modp3072-modpnone", "aes128-sha256-modpnone" }, +}; + +START_TEST(test_select) +{ + proposal_t *self, *other, *selected, *expected; + + self = proposal_create_from_string(PROTO_ESP, + select_data[_i].self); + other = proposal_create_from_string(PROTO_ESP, + select_data[_i].other); + selected = self->select(self, other, FALSE); + if (select_data[_i].expected) + { + expected = proposal_create_from_string(PROTO_ESP, + select_data[_i].expected); + ck_assert(selected); + ck_assert_msg(expected->equals(expected, selected), "proposal %P does " + "not match expected %P", selected, expected); + expected->destroy(expected); + } + else + { + ck_assert(!selected); + } + DESTROY_IF(selected); + other->destroy(other); + self->destroy(self); +} +END_TEST + +Suite *proposal_suite_create() +{ + Suite *s; + TCase *tc; + + s = suite_create("proposal"); + + tc = tcase_create("select"); + tcase_add_loop_test(tc, test_select, 0, countof(select_data)); + suite_add_tcase(s, tc); + + return s; +} diff --git a/src/libcharon/tests/utils/exchange_test_asserts.c b/src/libcharon/tests/utils/exchange_test_asserts.c new file mode 100644 index 000000000..2602b97b7 --- /dev/null +++ b/src/libcharon/tests/utils/exchange_test_asserts.c @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include <inttypes.h> + +#include <test_suite.h> + +#include "exchange_test_asserts.h" + +/* + * Described in header + */ +bool exchange_test_asserts_hook(listener_t *listener) +{ + listener_hook_assert_t *this = (listener_hook_assert_t*)listener; + + this->count++; + return TRUE; +} + +/* + * Described in header + */ +bool exchange_test_asserts_ike_updown(listener_t *listener, ike_sa_t *ike_sa, + bool up) +{ + listener_hook_assert_t *this = (listener_hook_assert_t*)listener; + + this->count++; + assert_listener_msg(this->up == up, this, "IKE_SA not '%s'", + this->up ? "up" : "down"); + return TRUE; +} + +/* + * Described in header + */ +bool exchange_test_asserts_child_updown(listener_t *listener, ike_sa_t *ike_sa, + child_sa_t *child_sa, bool up) +{ + listener_hook_assert_t *this = (listener_hook_assert_t*)listener; + + this->count++; + assert_listener_msg(this->up == up, this, "CHILD_SA not '%s'", + this->up ? "up" : "down"); + return TRUE; +} + +/* + * Described in header + */ +bool exchange_test_asserts_ike_rekey(listener_t *listener, ike_sa_t *old, + ike_sa_t *new) +{ + listener_hook_assert_t *this = (listener_hook_assert_t*)listener; + ike_sa_id_t *id; + uint64_t spi; + + this->count++; + id = old->get_id(old); + spi = id->get_initiator_spi(id); + assert_listener_msg(this->spi_old == spi, this, "unexpected old IKE_SA " + "%.16"PRIx64"_i instead of %.16"PRIx64"_i", + be64toh(spi), be64toh(this->spi_old)); + id = new->get_id(new); + spi = id->get_initiator_spi(id); + assert_listener_msg(this->spi_new == spi, this, "unexpected new IKE_SA " + "%.16"PRIx64"_i instead of %.16"PRIx64"_i", + be64toh(spi), be64toh(this->spi_new)); + return TRUE; +} + +/* + * Described in header + */ +bool exchange_test_asserts_child_rekey(listener_t *listener, ike_sa_t *ike_sa, + child_sa_t *old, child_sa_t *new) +{ + listener_hook_assert_t *this = (listener_hook_assert_t*)listener; + uint32_t spi, expected; + + this->count++; + spi = old->get_spi(old, TRUE); + expected = this->spi_old; + assert_listener_msg(expected == spi, this, "unexpected old CHILD_SA %.8x " + "instead of %.8x", spi, expected); + spi = new->get_spi(new, TRUE); + expected = this->spi_new; + assert_listener_msg(expected == spi, this, "unexpected new CHILD_SA %.8x " + "instead of %.8x", spi, expected); + return TRUE; +} + +/** + * Assert a given message rule + */ +static void assert_message_rule(listener_message_assert_t *this, message_t *msg, + listener_message_rule_t *rule) +{ + if (rule->expected) + { + if (rule->payload) + { + assert_listener_msg(msg->get_payload(msg, rule->payload), + this, "expected payload (%N) not found", + payload_type_names, rule->payload); + + } + if (rule->notify) + { + assert_listener_msg(msg->get_notify(msg, rule->notify), + this, "expected notify payload (%N) not found", + notify_type_names, rule->notify); + } + } + else + { + if (rule->payload) + { + assert_listener_msg(!msg->get_payload(msg, rule->payload), + this, "unexpected payload (%N) found", + payload_type_names, rule->payload); + + } + if (rule->notify) + { + assert_listener_msg(!msg->get_notify(msg, rule->notify), + this, "unexpected notify payload (%N) found", + notify_type_names, rule->notify); + } + } +} + +/* + * Described in header + */ +bool exchange_test_asserts_message(listener_t *listener, ike_sa_t *ike_sa, + message_t *message, bool incoming, bool plain) +{ + listener_message_assert_t *this = (listener_message_assert_t*)listener; + + if (plain && this->incoming == incoming) + { + if (this->count >= 0) + { + enumerator_t *enumerator; + int count = 0; + enumerator = message->create_payload_enumerator(message); + while (enumerator->enumerate(enumerator, NULL)) + { + count++; + } + enumerator->destroy(enumerator); + assert_listener_msg(this->count == count, this, "unexpected payload " + "count in message (%d != %d)", this->count, + count); + } + if (this->num_rules) + { + int i; + + for (i = 0; i < this->num_rules; i++) + { + assert_message_rule(this, message, &this->rules[i]); + } + } + return FALSE; + } + return TRUE; +} diff --git a/src/libcharon/tests/utils/exchange_test_asserts.h b/src/libcharon/tests/utils/exchange_test_asserts.h new file mode 100644 index 000000000..32afcc2e4 --- /dev/null +++ b/src/libcharon/tests/utils/exchange_test_asserts.h @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +/** + * Special assertions using listener_t. + * + * @defgroup exchange_test_asserts exchange_test_asserts + * @{ @ingroup test_utils_c + */ + +#ifndef EXCHANGE_TEST_ASSERTS_H_ +#define EXCHANGE_TEST_ASSERTS_H_ + +#include <bus/listeners/listener.h> + +typedef struct listener_hook_assert_t listener_hook_assert_t; +typedef struct listener_message_assert_t listener_message_assert_t; +typedef struct listener_message_rule_t listener_message_rule_t; + +struct listener_hook_assert_t { + + /** + * Implemented interface + */ + listener_t listener; + + /** + * Original source file + */ + const char *file; + + /** + * Source line + */ + int line; + + /** + * Name of the hook + */ + const char *name; + + /** + * Expected number of calls (-1 to ignore) + */ + int expected; + + /** + * Number of times the hook was called + */ + int count; + + /** + * Expected updown result + */ + bool up; + + /** + * Initiator/Inbound SPIs to expect in rekey event + */ + uint64_t spi_old, spi_new; +}; + +/** + * Basic callback for methods on listener_t, counting the number of calls. + */ +bool exchange_test_asserts_hook(listener_t *this); + +/** + * Implementation of listener_t::ike_updown. + */ +bool exchange_test_asserts_ike_updown(listener_t *this, ike_sa_t *ike_sa, + bool up); + +/** + * Implementation of listener_t::child_updown. + */ +bool exchange_test_asserts_child_updown(listener_t *this, ike_sa_t *ike_sa, + child_sa_t *child_sa, bool up); + +/** + * Implementation of listener_t::ike_rekey. + */ +bool exchange_test_asserts_ike_rekey(listener_t *this, ike_sa_t *old, + ike_sa_t *new); + +/** + * Implementation of listener_t::child_rekey. + */ +bool exchange_test_asserts_child_rekey(listener_t *this, ike_sa_t *ike_sa, + child_sa_t *old, child_sa_t *new); + +/** + * Check if a statement evaluates to TRUE, use original source file and line + * in the error message if not. + * + * @param x statement to evaluate + * @param l listener providing original source file and line + * @param fmt printf format string + * @param ... arguments for fmt + */ +#define assert_listener_msg(x, l, fmt, ...) ({ \ + test_fail_if_worker_failed(); \ + if (!(x)) \ + { \ + test_fail_msg((l)->file, (l)->line, "%s: " fmt, #x, ##__VA_ARGS__); \ + } \ +}) + +/** + * Initialize an assertion that enforces that the given hook was called. + * Must be matched by a call to assert_hook(). + * + * @param name name of the hook + */ +#define assert_hook_called(name) \ + _assert_hook_init(name, exchange_test_asserts_hook, .expected = 1) + +/** + * Initialize an assertion that enforces that the given hook was not called. + * Must be matched by a call to assert_hook(). + * + * @param name name of the hook + */ +#define assert_hook_not_called(name) \ + _assert_hook_init(name, exchange_test_asserts_hook, .expected = 0) + +/** + * Initialize an assertion that enforces that the given updown hook was called + * with the expected result. + * Must be matched by a call to assert_hook(). + * + * @param name name of the hook + * @param e whether to expect up in the hook to be TRUE or not + */ +#define assert_hook_updown(name, e) \ + _assert_hook_init(name, \ + streq(#name, "ike_updown") ? (void*)exchange_test_asserts_ike_updown \ + : (void*)exchange_test_asserts_child_updown, \ + .expected = 1, \ + .up = e, \ + ) + +/** + * Initialize an assertion that enforces that the given rekey hook was called + * with the SAs with the matching initiator/inbound SPIs. + * Must be matched by a call to assert_hook(). + * + * @param name name of the hook + * @param old SPI of the old SA + * @param new SPI of the new SA + */ +#define assert_hook_rekey(name, old, new) \ + _assert_hook_init(name, \ + streq(#name, "ike_rekey") ? (void*)exchange_test_asserts_ike_rekey \ + : (void*)exchange_test_asserts_child_rekey, \ + .expected = 1, \ + .spi_old = old, \ + .spi_new = new, \ + ) + +/** + * Initialize assertions against invocations of listener_t hooks. Each call + * must be matched by a call to assert_hook(). + */ +#define _assert_hook_init(n, callback, ...) \ +do { \ + listener_hook_assert_t _hook_listener = { \ + .listener = { .n = (void*)callback, }, \ + .file = __FILE__, \ + .line = __LINE__, \ + .name = #n, \ + ##__VA_ARGS__ \ + }; \ + exchange_test_helper->add_listener(exchange_test_helper, &_hook_listener.listener) + +/** + * Enforce the most recently initialized hook assertion. + */ +#define assert_hook() \ + charon->bus->remove_listener(charon->bus, &_hook_listener.listener); \ + if (_hook_listener.expected > 0) { \ + if (_hook_listener.count > 0) { \ + assert_listener_msg(_hook_listener.expected == _hook_listener.count, \ + &_hook_listener, "hook '%s' was called %d times " \ + "instead of %d", _hook_listener.name, \ + _hook_listener.count, _hook_listener.expected); \ + } else { \ + assert_listener_msg(_hook_listener.count, &_hook_listener, \ + "hook '%s' was not called (expected %d)", _hook_listener.name, \ + _hook_listener.expected); \ + } \ + } else if (_hook_listener.expected == 0) { \ + assert_listener_msg(_hook_listener.count == 0, &_hook_listener, \ + "hook '%s' was called unexpectedly", _hook_listener.name); \ + } \ +} while(FALSE) + +/** + * Rules regarding payloads/notifies to expect/not expect in a message + */ +struct listener_message_rule_t { + + /** + * Whether the payload/notify is expected in the message, FALSE to fail if + * it is found + */ + bool expected; + + /** + * Payload type to expect/not expect + */ + payload_type_t payload; + + /** + * Notify type to expect/not expect (paylod type does not have to be + * specified) + */ + notify_type_t notify; +}; + +/** + * Data used to check plaintext messages via listener_t + */ +struct listener_message_assert_t { + + /** + * Implemented interface + */ + listener_t listener; + + /** + * Original source file + */ + const char *file; + + /** + * Source line + */ + int line; + + /** + * Whether to check the next inbound or outbound message + */ + bool incoming; + + /** + * Payload count to expect (-1 to ignore the count) + */ + int count; + + /** + * Payloads to expect or not expect in a message + */ + listener_message_rule_t *rules; + + /** + * Number of rules + */ + int num_rules; +}; + +/** + * Implementation of listener_t::message collecting data and asserting + * certain things. + */ +bool exchange_test_asserts_message(listener_t *this, ike_sa_t *ike_sa, + message_t *message, bool incoming, bool plain); + +/** + * Assert that the next in- or outbound plaintext message is empty. + * + * @param dir IN or OUT to check the next in- or outbound message + */ +#define assert_message_empty(dir) \ + _assert_payload(dir, 0) + +/** + * Assert that the next in- or outbound plaintext message contains exactly + * one payload of the given type. + * + * @param dir IN or OUT to check the next in- or outbound message + * @param expected expected payload type + */ +#define assert_single_payload(dir, expected) \ + _assert_payload(dir, 1, { TRUE, expected, 0 }) + +/** + * Assert that the next in- or outbound plaintext message contains exactly + * one notify of the given type. + * + * @param dir IN or OUT to check the next in- or outbound message + * @param expected expected notify type + */ +#define assert_single_notify(dir, expected) \ + _assert_payload(dir, 1, { TRUE, 0, expected }) + +/** + * Assert that the next in- or outbound plaintext message contains a notify + * of the given type. + * + * @param dir IN or OUT to check the next in- or outbound message + * @param expected expected notify type + */ +#define assert_notify(dir, expected) \ + _assert_payload(dir, -1, { TRUE, 0, expected }) + +/** + * Assert that the next in- or outbound plaintext message does not contain a + * notify of the given type. + * + * @param dir IN or OUT to check the next in- or outbound message + * @param unexpected not expected notify type + */ +#define assert_no_notify(dir, unexpected) \ + _assert_payload(dir, -1, { FALSE, 0, unexpected }) + +#define _assert_payload(dir, c, ...) ({ \ + listener_message_rule_t _rules[] = { __VA_ARGS__ }; \ + listener_message_assert_t _listener = { \ + .listener = { .message = exchange_test_asserts_message, }, \ + .file = __FILE__, \ + .line = __LINE__, \ + .incoming = streq(#dir, "IN") ? TRUE : FALSE, \ + .count = c, \ + .rules = _rules, \ + .num_rules = countof(_rules), \ + }; \ + exchange_test_helper->add_listener(exchange_test_helper, &_listener.listener); \ +}) + +#endif /** EXCHANGE_TEST_ASSERTS_H_ @}*/ diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c new file mode 100644 index 000000000..f32906d5d --- /dev/null +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "exchange_test_helper.h" +#include "mock_dh.h" +#include "mock_ipsec.h" +#include "mock_nonce_gen.h" + +#include <collections/array.h> +#include <credentials/sets/mem_cred.h> + +typedef struct private_exchange_test_helper_t private_exchange_test_helper_t; +typedef struct private_backend_t private_backend_t; + +/** + * Private data + */ +struct private_exchange_test_helper_t { + + /** + * Public interface + */ + exchange_test_helper_t public; + + /** + * Credentials + */ + mem_cred_t *creds; + + /** + * IKE_SA SPI counter + */ + refcount_t ike_spi; + + /** + * List of registered listeners + */ + array_t *listeners; +}; + +/** + * Custom backend_t implementation + */ +struct private_backend_t { + + /** + * Public interface + */ + backend_t public; + + /** + * Responder ike_cfg + */ + ike_cfg_t *ike_cfg; + + /** + * Responder peer_cfg/child_cfg + */ + peer_cfg_t *peer_cfg; +}; + +CALLBACK(get_ike_spi, uint64_t, + private_exchange_test_helper_t *this) +{ + return (uint64_t)ref_get(&this->ike_spi); +} + +/* + * Described in header + */ +exchange_test_helper_t *exchange_test_helper; + +static ike_cfg_t *create_ike_cfg(bool initiator, exchange_test_sa_conf_t *conf) +{ + ike_cfg_t *ike_cfg; + char *proposal = NULL; + + ike_cfg = ike_cfg_create(IKEV2, TRUE, FALSE, "127.0.0.1", IKEV2_UDP_PORT, + "127.0.0.1", IKEV2_UDP_PORT, FRAGMENTATION_NO, 0); + if (conf) + { + proposal = initiator ? conf->initiator.ike : conf->responder.ike; + } + if (proposal) + { + ike_cfg->add_proposal(ike_cfg, + proposal_create_from_string(PROTO_IKE, proposal)); + } + else + { + ike_cfg->add_proposal(ike_cfg, proposal_create_default(PROTO_IKE)); + } + return ike_cfg; +} + +static child_cfg_t *create_child_cfg(bool initiator, + exchange_test_sa_conf_t *conf) +{ + child_cfg_t *child_cfg; + child_cfg_create_t child = { + .mode = MODE_TUNNEL, + }; + char *proposal = NULL; + + child_cfg = child_cfg_create(initiator ? "init" : "resp", &child); + if (conf) + { + proposal = initiator ? conf->initiator.esp : conf->responder.esp; + } + if (proposal) + { + child_cfg->add_proposal(child_cfg, + proposal_create_from_string(PROTO_ESP, proposal)); + } + else + { + child_cfg->add_proposal(child_cfg, proposal_create_default(PROTO_ESP)); + } + child_cfg->add_traffic_selector(child_cfg, TRUE, + traffic_selector_create_dynamic(0, 0, 65535)); + child_cfg->add_traffic_selector(child_cfg, FALSE, + traffic_selector_create_dynamic(0, 0, 65535)); + return child_cfg; +} + +static void add_auth_cfg(peer_cfg_t *peer_cfg, bool initiator, bool local) +{ + auth_cfg_t *auth; + char *id = "init"; + + auth = auth_cfg_create(); + auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_PSK); + if (initiator ^ local) + { + id = "resp"; + } + auth->add(auth, AUTH_RULE_IDENTITY, identification_create_from_string(id)); + peer_cfg->add_auth_cfg(peer_cfg, auth, local); +} + +static peer_cfg_t *create_peer_cfg(bool initiator, + exchange_test_sa_conf_t *conf) +{ + peer_cfg_t *peer_cfg; + peer_cfg_create_t peer = { + .cert_policy = CERT_SEND_IF_ASKED, + .unique = UNIQUE_REPLACE, + .keyingtries = 1, + }; + + peer_cfg = peer_cfg_create(initiator ? "init" : "resp", + create_ike_cfg(initiator, conf), &peer); + add_auth_cfg(peer_cfg, initiator, TRUE); + add_auth_cfg(peer_cfg, initiator, FALSE); + return peer_cfg; +} + +METHOD(backend_t, create_ike_cfg_enumerator, enumerator_t*, + private_backend_t *this, host_t *me, host_t *other) +{ + return enumerator_create_single(this->ike_cfg, NULL); +} + +METHOD(backend_t, create_peer_cfg_enumerator, enumerator_t*, + private_backend_t *this, identification_t *me, identification_t *other) +{ + return enumerator_create_single(this->peer_cfg, NULL); +} + +METHOD(exchange_test_helper_t, process_message, status_t, + private_exchange_test_helper_t *this, ike_sa_t *ike_sa, message_t *message) +{ + status_t status = FAILED; + ike_sa_id_t *id; + + if (!message) + { + message = this->public.sender->dequeue(this->public.sender); + } + id = message->get_ike_sa_id(message); + id = id->clone(id); + id->switch_initiator(id); + if (!id->get_responder_spi(id) || id->equals(id, ike_sa->get_id(ike_sa))) + { + charon->bus->set_sa(charon->bus, ike_sa); + status = ike_sa->process_message(ike_sa, message); + charon->bus->set_sa(charon->bus, NULL); + } + message->destroy(message); + id->destroy(id); + return status; +} + +METHOD(exchange_test_helper_t, establish_sa, void, + private_exchange_test_helper_t *this, ike_sa_t **init, ike_sa_t **resp, + exchange_test_sa_conf_t *conf) +{ + private_backend_t backend = { + .public = { + .create_ike_cfg_enumerator = _create_ike_cfg_enumerator, + .create_peer_cfg_enumerator = _create_peer_cfg_enumerator, + .get_peer_cfg_by_name = (void*)return_null, + }, + }; + ike_sa_id_t *id_i, *id_r; + ike_sa_t *sa_i, *sa_r; + peer_cfg_t *peer_cfg; + child_cfg_t *child_cfg; + + sa_i = *init = charon->ike_sa_manager->checkout_new(charon->ike_sa_manager, + IKEV2, TRUE); + id_i = sa_i->get_id(sa_i); + + sa_r = *resp = charon->ike_sa_manager->checkout_new(charon->ike_sa_manager, + IKEV2, FALSE); + id_r = sa_r->get_id(sa_r); + + peer_cfg = create_peer_cfg(TRUE, conf); + child_cfg = create_child_cfg(TRUE, conf); + peer_cfg->add_child_cfg(peer_cfg, child_cfg->get_ref(child_cfg)); + sa_i->set_peer_cfg(sa_i, peer_cfg); + peer_cfg->destroy(peer_cfg); + call_ikesa(sa_i, initiate, child_cfg, 0, NULL, NULL); + + backend.ike_cfg = create_ike_cfg(FALSE, conf); + peer_cfg = backend.peer_cfg = create_peer_cfg(FALSE, conf); + child_cfg = create_child_cfg(FALSE, conf); + peer_cfg->add_child_cfg(peer_cfg, child_cfg->get_ref(child_cfg)); + child_cfg->destroy(child_cfg); + charon->backends->add_backend(charon->backends, &backend.public); + + /* IKE_SA_INIT --> */ + id_r->set_initiator_spi(id_r, id_i->get_initiator_spi(id_i)); + process_message(this, sa_r, NULL); + /* <-- IKE_SA_INIT */ + id_i->set_responder_spi(id_i, id_r->get_responder_spi(id_r)); + process_message(this, sa_i, NULL); + /* IKE_AUTH --> */ + process_message(this, sa_r, NULL); + /* <-- IKE_AUTH */ + process_message(this, sa_i, NULL); + + charon->backends->remove_backend(charon->backends, &backend.public); + DESTROY_IF(backend.peer_cfg); + DESTROY_IF(backend.ike_cfg); +} + +METHOD(exchange_test_helper_t, add_listener, void, + private_exchange_test_helper_t *this, listener_t *listener) +{ + array_insert_create(&this->listeners, ARRAY_TAIL, listener); + charon->bus->add_listener(charon->bus, listener); +} + +/** + * Enable logging in charon as requested + */ +static void initialize_logging() +{ + int level = LEVEL_SILENT; + char *verbosity; + + verbosity = getenv("TESTS_VERBOSITY"); + if (verbosity) + { + level = atoi(verbosity); + } + lib->settings->set_int(lib->settings, "%s.filelog.stderr.default", + lib->settings->get_int(lib->settings, "%s.filelog.stderr.default", + level, lib->ns), lib->ns); + lib->settings->set_bool(lib->settings, "%s.filelog.stderr.ike_name", TRUE, + lib->ns); + charon->load_loggers(charon, NULL, TRUE); +} + +/** + * Create a nonce generator with the first byte + */ +static nonce_gen_t *create_nonce_gen() +{ + return mock_nonce_gen_create(exchange_test_helper->nonce_first_byte); +} + +/* + * Described in header + */ +void exchange_test_helper_init(char *plugins) +{ + private_exchange_test_helper_t *this; + plugin_feature_t features[] = { + PLUGIN_REGISTER(DH, mock_dh_create), + /* we only need to support a limited number of DH groups */ + PLUGIN_PROVIDE(DH, MODP_2048_BIT), + PLUGIN_PROVIDE(DH, MODP_3072_BIT), + PLUGIN_PROVIDE(DH, ECP_256_BIT), + PLUGIN_REGISTER(NONCE_GEN, create_nonce_gen), + PLUGIN_PROVIDE(NONCE_GEN), + PLUGIN_DEPENDS(RNG, RNG_WEAK), + }; + + INIT(this, + .public = { + .sender = mock_sender_create(), + .establish_sa = _establish_sa, + .process_message = _process_message, + .add_listener = _add_listener, + }, + .creds = mem_cred_create(), + ); + + initialize_logging(); + lib->plugins->add_static_features(lib->plugins, "exchange-test-helper", + features, countof(features), TRUE, NULL, NULL); + /* the libcharon unit tests only load the libstrongswan plugins, unless + * TESTS_PLUGINS is defined */ + charon->initialize(charon, plugins); + lib->plugins->status(lib->plugins, LEVEL_CTRL); + + /* the original sender is not initialized because there is no socket */ + charon->sender = (sender_t*)this->public.sender; + /* and there is no kernel plugin loaded + * TODO: we'd have more control if we'd implement kernel_interface_t */ + charon->kernel->add_ipsec_interface(charon->kernel, mock_ipsec_create); + /* like SPIs for IPsec SAs, make IKE SPIs predictable */ + charon->ike_sa_manager->set_spi_cb(charon->ike_sa_manager, get_ike_spi, + this); + + lib->credmgr->add_set(lib->credmgr, &this->creds->set); + + this->creds->add_shared(this->creds, + shared_key_create(SHARED_IKE, chunk_clone(chunk_from_str("test"))), + identification_create_from_string("%any"), NULL); + + exchange_test_helper = &this->public; +} + +/* + * Described in header + */ +void exchange_test_helper_deinit() +{ + private_exchange_test_helper_t *this; + listener_t *listener; + + this = (private_exchange_test_helper_t*)exchange_test_helper; + + while (array_remove(this->listeners, ARRAY_HEAD, &listener)) + { + charon->bus->remove_listener(charon->bus, listener); + } + lib->credmgr->remove_set(lib->credmgr, &this->creds->set); + this->creds->destroy(this->creds); + /* flush SAs before destroying the sender (in case of test failures) */ + charon->ike_sa_manager->flush(charon->ike_sa_manager); + /* charon won't destroy this as it didn't initialize the original sender */ + charon->sender->destroy(charon->sender); + charon->sender = NULL; + array_destroy(this->listeners); + free(this); +} diff --git a/src/libcharon/tests/utils/exchange_test_helper.h b/src/libcharon/tests/utils/exchange_test_helper.h new file mode 100644 index 000000000..e1fdb012a --- /dev/null +++ b/src/libcharon/tests/utils/exchange_test_helper.h @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +/** + * This class and singleton object initializes charon and provides helper + * methods to create unit tests for IKEv2 exchanges. + * + * It also registers special implementations for the kernel_ipsec_t interface, + * the sender and provides dummy configs and credentials. + * + * @defgroup exchange_test_helper exchange_test_helper + * @{ @ingroup test_utils_c + */ + +#ifndef EXCHANGE_TEST_HELPER_H_ +#define EXCHANGE_TEST_HELPER_H_ + +#include <daemon.h> + +#include "mock_sender.h" + +typedef struct exchange_test_helper_t exchange_test_helper_t; +typedef struct exchange_test_sa_conf_t exchange_test_sa_conf_t; + +struct exchange_test_helper_t { + + /** + * Sender instance used during tests + */ + mock_sender_t *sender; + + /** + * Set the initial byte of all nonces generated by future nonce + * generators (already instatiated nonce generators are not affected). + */ + u_char nonce_first_byte; + + /** + * Creates an established IKE_SA/CHILD_SA + * + * @param[out] init IKE_SA of the initiator + * @param[out] resp IKE_SA of the responder + * @param conf configuration for SAs + */ + void (*establish_sa)(exchange_test_helper_t *this, ike_sa_t **init, + ike_sa_t **resp, exchange_test_sa_conf_t *conf); + + /** + * Pass a message to the given IKE_SA for processing, setting the IKE_SA on + * the bus while processing the message. + * + * @param ike_sa the IKE_SA receiving the message + * @param message the message, or NULL to pass the next message in the + * send queue (adopted) + * @return return value from ike_sa_t::process_message() + */ + status_t (*process_message)(exchange_test_helper_t *this, ike_sa_t *sa, + message_t *message); + + /** + * Register a listener with the bus. + * + * Don't use bus_t::add_listener() directly for listeners on the stack + * as that could lead to invalid listeners registered when hooks are + * triggered during cleanup if a test case fails. All of the listeners + * added this way are unregistered with the bus before cleaning up. + * + * @param listener listener to add to the bus + */ + void (*add_listener)(exchange_test_helper_t *this, listener_t *listener); +}; + +struct exchange_test_sa_conf_t { + + /** + * Configuration for initiator and responder + */ + struct { + /** IKE proposal */ + char *ike; + /** ESP proposal */ + char *esp; + } initiator, responder; +}; + +/** + * Since we don't use the IKE_SA manager to checkout SAs use this to call a + * method on the given IKE_SA in its context. + */ +#define call_ikesa(sa, method, ...) ({ \ + charon->bus->set_sa(charon->bus, sa); \ + sa->method(sa, ##__VA_ARGS__); \ + charon->bus->set_sa(charon->bus, NULL); \ +}) + +/** + * The one and only instance of the helper object. + * + * Set between exchange_test_helper_setup() and exchange_test_helper_teardown() + * calls. + */ +extern exchange_test_helper_t *exchange_test_helper; + +/** + * Initialize charon and the helper object. + * + * @param plugins plugins to load + */ +void exchange_test_helper_init(char *plugins); + +/** + * Deinitialize the helper object. + */ +void exchange_test_helper_deinit(); + +#endif /** EXCHANGE_TEST_HELPER_H_ @} */ diff --git a/src/libcharon/tests/utils/job_asserts.h b/src/libcharon/tests/utils/job_asserts.h new file mode 100644 index 000000000..3491f08c3 --- /dev/null +++ b/src/libcharon/tests/utils/job_asserts.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +/** + * Special assertions against job handling. + * + * @defgroup job_asserts job_asserts + * @{ @ingroup test_utils_c + */ + +#ifndef JOB_ASSERTS_H_ +#define JOB_ASSERTS_H_ + +/** + * Initialize an assertion that enforces that no jobs were scheduled. + * Must be matched by a call to assert_scheduler(). + */ +#define assert_no_jobs_scheduled() _assert_jobs_scheduled(0) + +/** + * Initialize an assertion that enforces that a specific number of jobs was + * scheduled. + * Must be matched by a call to assert_scheduler(). + * + * @param count expected number of jobs getting scheduled + */ +#define assert_jobs_scheduled(count) _assert_jobs_scheduled(count) + +/** + * Initialize assertions against job scheduling. + * Must be matched by a call to assert_scheduler(). + */ +#define _assert_jobs_scheduled(count) \ +do { \ + u_int _initial = lib->scheduler->get_job_load(lib->scheduler); \ + u_int _expected = count + +/** + * Enforce scheduler asserts. + */ +#define assert_scheduler() \ + u_int _actual = lib->scheduler->get_job_load(lib->scheduler) - _initial; \ + test_assert_msg(_expected == _actual, "unexpected number of jobs " \ + "scheduled (%u != %u)", _expected, _actual); \ +} while(FALSE) + +#endif /** JOB_ASSERTS_H_ @}*/ diff --git a/src/libcharon/tests/utils/mock_dh.c b/src/libcharon/tests/utils/mock_dh.c new file mode 100644 index 000000000..153bf1166 --- /dev/null +++ b/src/libcharon/tests/utils/mock_dh.c @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * Copyright (C) 2008 Martin Willi + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "mock_dh.h" + +typedef struct private_diffie_hellman_t private_diffie_hellman_t; + +/** + * Private data + */ +struct private_diffie_hellman_t { + + /** + * Public interface + */ + diffie_hellman_t public; + + /** + * Instantiated DH group + */ + diffie_hellman_group_t group; +}; + +METHOD(diffie_hellman_t, get_my_public_value, bool, + private_diffie_hellman_t *this, chunk_t *value) +{ + *value = chunk_empty; + return TRUE; +} + +METHOD(diffie_hellman_t, set_other_public_value, bool, + private_diffie_hellman_t *this, chunk_t value) +{ + return TRUE; +} + +METHOD(diffie_hellman_t, get_shared_secret, bool, + private_diffie_hellman_t *this, chunk_t *secret) +{ + *secret = chunk_empty; + return TRUE; +} + +METHOD(diffie_hellman_t, get_dh_group, diffie_hellman_group_t, + private_diffie_hellman_t *this) +{ + return this->group; +} + +METHOD(diffie_hellman_t, destroy, void, + private_diffie_hellman_t *this) +{ + free(this); +} + +/** + * See header + */ +diffie_hellman_t *mock_dh_create(diffie_hellman_group_t group) +{ + private_diffie_hellman_t *this; + + INIT(this, + .public = { + .get_shared_secret = _get_shared_secret, + .set_other_public_value = _set_other_public_value, + .get_my_public_value = _get_my_public_value, + .get_dh_group = _get_dh_group, + .destroy = _destroy, + }, + .group = group, + ); + return &this->public; +} diff --git a/src/libcharon/tests/utils/mock_dh.h b/src/libcharon/tests/utils/mock_dh.h new file mode 100644 index 000000000..332c65537 --- /dev/null +++ b/src/libcharon/tests/utils/mock_dh.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +/** + * Provides a DH implementation that does no real work to make the tests run + * faster. + * + * @defgroup mock_dh mock_dh + * @{ @ingroup test_utils_c + */ + +#ifndef MOCK_DH_H_ +#define MOCK_DH_H_ + +#include <crypto/diffie_hellman.h> + +/** + * Creates a diffie_hellman_t object. + * + * @param group Diffie Hellman group, supports MODP_NULL only + * @return created object + */ +diffie_hellman_t *mock_dh_create(diffie_hellman_group_t group); + +#endif /** MOCK_DH_H_ @}*/ diff --git a/src/libcharon/tests/utils/mock_ipsec.c b/src/libcharon/tests/utils/mock_ipsec.c new file mode 100644 index 000000000..d57a26a87 --- /dev/null +++ b/src/libcharon/tests/utils/mock_ipsec.c @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * Copyright (C) 2008 Martin Willi + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "mock_ipsec.h" + +typedef struct private_kernel_ipsec_t private_kernel_ipsec_t; + +/** + * Private data + */ +struct private_kernel_ipsec_t { + + /** + * Public interface + */ + kernel_ipsec_t public; + + /** + * Allocated SPI + */ + refcount_t spi; +}; + +METHOD(kernel_ipsec_t, get_spi, status_t, + private_kernel_ipsec_t *this, host_t *src, host_t *dst, uint8_t protocol, + uint32_t *spi) +{ + *spi = (uint32_t)ref_get(&this->spi); + return SUCCESS; +} + +METHOD(kernel_ipsec_t, get_cpi, status_t, + private_kernel_ipsec_t *this, host_t *src, host_t *dst, uint16_t *cpi) +{ + return FAILED; +} + +METHOD(kernel_ipsec_t, add_sa, status_t, + private_kernel_ipsec_t *this, kernel_ipsec_sa_id_t *id, + kernel_ipsec_add_sa_t *data) +{ + return SUCCESS; +} + +METHOD(kernel_ipsec_t, update_sa, status_t, + private_kernel_ipsec_t *this, kernel_ipsec_sa_id_t *id, + kernel_ipsec_update_sa_t *data) +{ + return SUCCESS; +} + +METHOD(kernel_ipsec_t, query_sa, status_t, + private_kernel_ipsec_t *this, kernel_ipsec_sa_id_t *id, + kernel_ipsec_query_sa_t *data, uint64_t *bytes, uint64_t *packets, + time_t *time) +{ + return NOT_SUPPORTED; +} + +METHOD(kernel_ipsec_t, del_sa, status_t, + private_kernel_ipsec_t *this, kernel_ipsec_sa_id_t *id, + kernel_ipsec_del_sa_t *data) +{ + return SUCCESS; +} + +METHOD(kernel_ipsec_t, add_policy, status_t, + private_kernel_ipsec_t *this, kernel_ipsec_policy_id_t *id, + kernel_ipsec_manage_policy_t *data) +{ + return SUCCESS; +} + +METHOD(kernel_ipsec_t, query_policy, status_t, + private_kernel_ipsec_t *this, kernel_ipsec_policy_id_t *id, + kernel_ipsec_query_policy_t *data, time_t *use_time) +{ + *use_time = 1; + return SUCCESS; +} + +METHOD(kernel_ipsec_t, del_policy, status_t, + private_kernel_ipsec_t *this, kernel_ipsec_policy_id_t *id, + kernel_ipsec_manage_policy_t *data) +{ + return SUCCESS; +} + +/* + * Described in header + */ +kernel_ipsec_t *mock_ipsec_create() +{ + private_kernel_ipsec_t *this; + + INIT(this, + .public = { + .get_spi = _get_spi, + .get_cpi = _get_cpi, + .add_sa = _add_sa, + .update_sa = _update_sa, + .query_sa = _query_sa, + .del_sa = _del_sa, + .flush_sas = (void*)return_failed, + .add_policy = _add_policy, + .query_policy = _query_policy, + .del_policy = _del_policy, + .flush_policies = (void*)return_failed, + .bypass_socket = (void*)return_true, + .enable_udp_decap = (void*)return_true, + .destroy = (void*)free, + }, + ); + return &this->public; +} diff --git a/src/libcharon/tests/utils/mock_ipsec.h b/src/libcharon/tests/utils/mock_ipsec.h new file mode 100644 index 000000000..cbf21524a --- /dev/null +++ b/src/libcharon/tests/utils/mock_ipsec.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +/** + * kernel_ipsec_t implementation used for exchange unit tests. Currently + * returns sequential SPIs, all other methods are noops. + * + * @defgroup mock_ipsec mock_ipsec + * @{ @ingroup test_utils_c + */ + +#ifndef MOCK_IPSEC_H_ +#define MOCK_IPSEC_H_ + +#include <kernel/kernel_ipsec.h> + +/** + * Create an instance of kernel_ipsec_t + * + * @return created object + */ +kernel_ipsec_t *mock_ipsec_create(); + +#endif /** MOCK_IPSEC_H_ @}*/ diff --git a/src/libcharon/tests/utils/mock_nonce_gen.c b/src/libcharon/tests/utils/mock_nonce_gen.c new file mode 100644 index 000000000..30910f991 --- /dev/null +++ b/src/libcharon/tests/utils/mock_nonce_gen.c @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "mock_nonce_gen.h" + +typedef struct private_nonce_gen_t private_nonce_gen_t; + +struct private_nonce_gen_t { + + /** + * Public interface + */ + nonce_gen_t public; + + /** + * Random number generator + */ + rng_t* rng; + + /** + * First byte to set to the nonces + */ + u_char first; +}; + +METHOD(nonce_gen_t, get_nonce, bool, + private_nonce_gen_t *this, size_t size, uint8_t *buffer) +{ + if (size > 0) + { + buffer[0] = this->first; + buffer++; + size--; + } + return this->rng->get_bytes(this->rng, size, buffer); +} + +METHOD(nonce_gen_t, allocate_nonce, bool, + private_nonce_gen_t *this, size_t size, chunk_t *chunk) +{ + *chunk = chunk_alloc(size); + if (!get_nonce(this, chunk->len, chunk->ptr)) + { + chunk_free(chunk); + return FALSE; + } + return TRUE; +} + +METHOD(nonce_gen_t, destroy, void, + private_nonce_gen_t *this) +{ + DESTROY_IF(this->rng); + free(this); +} + +/* + * Described in header + */ +nonce_gen_t *mock_nonce_gen_create(u_char first) +{ + private_nonce_gen_t *this; + + INIT(this, + .public = { + .get_nonce = _get_nonce, + .allocate_nonce = _allocate_nonce, + .destroy = _destroy, + }, + .rng = lib->crypto->create_rng(lib->crypto, RNG_WEAK), + .first = first, + ); + if (!this->rng) + { + destroy(this); + return NULL; + } + return &this->public; +} diff --git a/src/libcharon/tests/utils/mock_nonce_gen.h b/src/libcharon/tests/utils/mock_nonce_gen.h new file mode 100644 index 000000000..feeab8bc0 --- /dev/null +++ b/src/libcharon/tests/utils/mock_nonce_gen.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +/** + * Special nonce generator that sets the first byte of the generated nonces to + * a fixed specified value. + * + * @defgroup mock_nonce_gen mock_nonce_gen + * @{ @ingroup test_utils_c + */ + +#ifndef MOCK_NONCE_GEN_H_ +#define MOCK_NONCE_GEN_H_ + +#include <crypto/nonce_gen.h> + +/** + * Creates a nonce_gen_t instance. + * + * @param first first byte to set in generated nonces + * @return created object + */ +nonce_gen_t *mock_nonce_gen_create(u_char first); + +#endif /** MOCK_NONCE_GEN_H_ @} */ diff --git a/src/libcharon/tests/utils/mock_sender.c b/src/libcharon/tests/utils/mock_sender.c new file mode 100644 index 000000000..c090ff439 --- /dev/null +++ b/src/libcharon/tests/utils/mock_sender.c @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "mock_sender.h" + +#include <collections/linked_list.h> + +typedef struct private_mock_sender_t private_mock_sender_t; + +/** + * Private data + */ +struct private_mock_sender_t { + + /** + * Public interface + */ + mock_sender_t public; + + /** + * Packet queue, as message_t* + */ + linked_list_t *queue; +}; + + +METHOD(sender_t, send_, void, + private_mock_sender_t *this, packet_t *packet) +{ + message_t *message; + + message = message_create_from_packet(packet); + message->parse_header(message); + this->queue->insert_last(this->queue, message); +} + +METHOD(mock_sender_t, dequeue, message_t*, + private_mock_sender_t *this) +{ + message_t *message = NULL; + + this->queue->remove_first(this->queue, (void**)&message); + return message; +} + +METHOD(sender_t, destroy, void, + private_mock_sender_t *this) +{ + this->queue->destroy_offset(this->queue, offsetof(message_t, destroy)); + free(this); +} + +/* + * Described in header + */ +mock_sender_t *mock_sender_create() +{ + private_mock_sender_t *this; + + INIT(this, + .public = { + .interface = { + .send = _send_, + .send_no_marker = (void*)nop, + .flush = (void*)nop, + .destroy = _destroy, + }, + .dequeue = _dequeue, + }, + .queue = linked_list_create(), + ); + return &this->public; +} diff --git a/src/libcharon/tests/utils/mock_sender.h b/src/libcharon/tests/utils/mock_sender.h new file mode 100644 index 000000000..5eabddadc --- /dev/null +++ b/src/libcharon/tests/utils/mock_sender.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +/** + * sender_t implementation that does not pass the sent packet to a socket but + * instead provides it for immediate delivery to an ike_sa_t object. + * + * @defgroup mock_sender mock_sender + * @{ @ingroup test_utils_c + */ + +#ifndef MOCK_SENDER_H_ +#define MOCK_SENDER_H_ + +#include <encoding/message.h> +#include <network/sender.h> + +typedef struct mock_sender_t mock_sender_t; + +struct mock_sender_t { + + /** + * Implemented interface + */ + sender_t interface; + + /** + * Remove the next packet in the send queue as message_t object. The IKE + * header is already parsed (which is assumed does not fail) so it can + * directly be passed to ike_sa_t::process_message(). + * + * @return message or NULL if none is queued + */ + message_t *(*dequeue)(mock_sender_t *this); +}; + +/** + * Creates a mock_sender_t instance. + * + * @return created object + */ +mock_sender_t *mock_sender_create(); + +#endif /** MOCK_SENDER_H_ @} */ diff --git a/src/libcharon/tests/utils/sa_asserts.h b/src/libcharon/tests/utils/sa_asserts.h new file mode 100644 index 000000000..7afa3b55b --- /dev/null +++ b/src/libcharon/tests/utils/sa_asserts.h @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2016 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +/** + * Special assertions against IKE_SAs and CHILD_SAs (e.g. regarding their + * state). + * + * @defgroup sa_asserts sa_asserts + * @{ @ingroup test_utils_c + */ + +#ifndef SA_ASSERTS_H_ +#define SA_ASSERTS_H_ + +#include <inttypes.h> + +/** + * Check that there exists a specific number of IKE_SAs in the manager. + */ +#define assert_ike_sa_count(count) \ +({ \ + typeof(count) _count = count; \ + u_int _actual = charon->ike_sa_manager->get_count(charon->ike_sa_manager); \ + test_assert_msg(_count == _actual, "unexpected number of IKE_SAs in " \ + "manager (%d != %d)", _count, _actual); \ +}) + +/** + * Check that the IKE_SA with the given SPIs and initiator flag is in the + * manager and return it. Does not actually keep the SA checked out as + * that would block cleaning up if asserts against it fail (since we control + * access to SAs it's also not really necessary). + */ +#define assert_ike_sa_checkout(spi_i, spi_r, initiator) \ +({ \ + typeof(spi_i) _spi_i = spi_i; \ + typeof(spi_r) _spi_r = spi_r; \ + typeof(initiator) _init = initiator; \ + ike_sa_id_t *_id = ike_sa_id_create(IKEV2, _spi_i, _spi_r, _init); \ + ike_sa_t *_ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager, _id); \ + test_assert_msg(_ike_sa, "IKE_SA with SPIs %.16"PRIx64"_i %.16"PRIx64"_r " \ + "(%d) does not exist", be64toh(_spi_i), be64toh(_spi_r), _init); \ + _id->destroy(_id); \ + charon->ike_sa_manager->checkin(charon->ike_sa_manager, _ike_sa); \ + _ike_sa; \ +}) + +/** + * Check if the given IKE_SA is in the expected state. + */ +#define assert_ike_sa_state(ike_sa, state) \ +({ \ + typeof(ike_sa) _sa = ike_sa; \ + typeof(state) _state = state; \ + test_assert_msg(_state == _sa->get_state(_sa), "%N != %N", \ + ike_sa_state_names, _state, \ + ike_sa_state_names, _sa->get_state(_sa)); \ +}) + +/** + * Check that there exists a specific number of CHILD_SAs. + */ +#define assert_child_sa_count(ike_sa, count) \ +({ \ + typeof(ike_sa) _sa = ike_sa; \ + typeof(count) _count = count; \ + test_assert_msg(_count == _sa->get_child_count(_sa), "unexpected number " \ + "of CHILD_SAs in IKE_SA %s (%d != %d)", #ike_sa, _count, \ + _sa->get_child_count(_sa)); \ +}) + +/** + * Check if the CHILD_SA with the given SPI is in the expected state. + */ +#define assert_child_sa_state(ike_sa, spi, state) \ +({ \ + typeof(ike_sa) _sa = ike_sa; \ + typeof(spi) _spi = spi; \ + typeof(state) _state = state; \ + child_sa_t *_child = _sa->get_child_sa(_sa, PROTO_ESP, _spi, TRUE) ?: \ + _sa->get_child_sa(_sa, PROTO_ESP, _spi, FALSE); \ + test_assert_msg(_child, "CHILD_SA with SPI %.8x does not exist", \ + ntohl(_spi)); \ + test_assert_msg(_state == _child->get_state(_child), "%N != %N", \ + child_sa_state_names, _state, \ + child_sa_state_names, _child->get_state(_child)); \ +}) + +/** + * Assert that the CHILD_SA with the given inbound SPI does not exist. + */ +#define assert_child_sa_not_exists(ike_sa, spi) \ +({ \ + typeof(ike_sa) _sa = ike_sa; \ + typeof(spi) _spi = spi; \ + child_sa_t *_child = _sa->get_child_sa(_sa, PROTO_ESP, _spi, TRUE) ?: \ + _sa->get_child_sa(_sa, PROTO_ESP, _spi, FALSE); \ + test_assert_msg(!_child, "CHILD_SA with SPI %.8x exists", ntohl(_spi)); \ +}) + +/** + * Assert that there is a specific number of tasks in a given queue + * + * @param ike_sa IKE_SA to check + * @param count number of expected tasks + * @param queue queue to check (task_queue_t) + */ +#define assert_num_tasks(ike_sa, count, queue) \ +({ \ + typeof(ike_sa) _sa = ike_sa; \ + typeof(count) _count = count; \ + int _c = 0; task_t *_task; \ + enumerator_t *_enumerator = _sa->create_task_enumerator(_sa, queue); \ + while (_enumerator->enumerate(_enumerator, &_task)) { _c++; } \ + _enumerator->destroy(_enumerator); \ + test_assert_msg(_count == _c, "unexpected number of tasks in " #queue " " \ + "of IKE_SA %s (%d != %d)", #ike_sa, _count, _c); \ +}) + +/** + * Assert that all task queues of the given IKE_SA are empty + * + * @param ike_sa IKE_SA to check + */ +#define assert_sa_idle(ike_sa) \ +({ \ + typeof(ike_sa) _ike_sa = ike_sa; \ + assert_num_tasks(_ike_sa, 0, TASK_QUEUE_QUEUED); \ + assert_num_tasks(_ike_sa, 0, TASK_QUEUE_ACTIVE); \ + assert_num_tasks(_ike_sa, 0, TASK_QUEUE_PASSIVE); \ +}) + +#endif /** SA_ASSERTS_H_ @}*/ diff --git a/src/libstrongswan/crypto/crypto_factory.c b/src/libstrongswan/crypto/crypto_factory.c index b0b86372c..35dcf25ac 100644 --- a/src/libstrongswan/crypto/crypto_factory.c +++ b/src/libstrongswan/crypto/crypto_factory.c @@ -347,6 +347,10 @@ METHOD(crypto_factory_t, create_nonce_gen, nonce_gen_t*, while (enumerator->enumerate(enumerator, &entry)) { nonce_gen = entry->create_nonce_gen(); + if (nonce_gen) + { + break; + } } enumerator->destroy(enumerator); this->lock->unlock(this->lock); diff --git a/src/libstrongswan/crypto/proposal/proposal_keywords_static.txt b/src/libstrongswan/crypto/proposal/proposal_keywords_static.txt index a3c5c0f3a..87602430d 100644 --- a/src/libstrongswan/crypto/proposal/proposal_keywords_static.txt +++ b/src/libstrongswan/crypto/proposal/proposal_keywords_static.txt @@ -141,6 +141,7 @@ prfmd5, PSEUDO_RANDOM_FUNCTION, PRF_HMAC_MD5, 0 prfaesxcbc, PSEUDO_RANDOM_FUNCTION, PRF_AES128_XCBC, 0 prfcamelliaxcbc, PSEUDO_RANDOM_FUNCTION, PRF_CAMELLIA128_XCBC, 0 prfaescmac, PSEUDO_RANDOM_FUNCTION, PRF_AES128_CMAC, 0 +modpnone, DIFFIE_HELLMAN_GROUP, MODP_NONE, 0 modpnull, DIFFIE_HELLMAN_GROUP, MODP_NULL, 0 modp768, DIFFIE_HELLMAN_GROUP, MODP_768_BIT, 0 modp1024, DIFFIE_HELLMAN_GROUP, MODP_1024_BIT, 0 diff --git a/src/libstrongswan/tests/test_runner.c b/src/libstrongswan/tests/test_runner.c index 66d0e612d..ed77b3c86 100644 --- a/src/libstrongswan/tests/test_runner.c +++ b/src/libstrongswan/tests/test_runner.c @@ -90,6 +90,28 @@ static void apply_filter(array_t *loaded, char *filter, bool exclude) } /** + * Check if the given string is contained in the filter string. + */ +static bool is_in_filter(const char *find, char *filter) +{ + enumerator_t *names; + bool found = FALSE; + char *name; + + names = enumerator_create_token(filter, ",", " "); + while (names->enumerate(names, &name)) + { + if (streq(name, find)) + { + found = TRUE; + break; + } + } + names->destroy(names); + return found; +} + +/** * Removes and destroys test suites that are not selected or * explicitly excluded. */ @@ -524,11 +546,17 @@ int test_runner_run(const char *name, test_configuration_t configs[], enumerator_t *enumerator; int passed = 0, result; level_t level = LEVEL_SILENT; - char *cfg, *verbosity; + char *cfg, *runners, *verbosity; /* redirect all output to stderr (to redirect make's stdout to /dev/null) */ dup2(2, 1); + runners = getenv("TESTS_RUNNERS"); + if (runners && !is_in_filter(name, runners)) + { + return EXIT_SUCCESS; + } + cfg = getenv("TESTS_STRONGSWAN_CONF"); suites = load_suites(configs, init, cfg); diff --git a/src/libstrongswan/tests/test_runner.h b/src/libstrongswan/tests/test_runner.h index 5c3057096..e0fe767c5 100644 --- a/src/libstrongswan/tests/test_runner.h +++ b/src/libstrongswan/tests/test_runner.h @@ -70,6 +70,7 @@ struct test_configuration_t { * - TESTS_VERBOSITY: Numerical loglevel for debug log * - TESTS_STRONGSWAN_CONF: Specify a path to a custom strongswan.conf * - TESTS_PLUGINS: Specify an explicit list of plugins to load + * - TESTS_RUNNERS: Run specific test runners only * - TESTS_SUITES: Run specific test suites only * - TESTS_SUITES_EXCLUDE: Don't run specific test suites * - TESTS_REDUCED_KEYLENGTHS: Test minimal keylengths for public key tests only @@ -77,6 +78,9 @@ struct test_configuration_t { * Please note that TESTS_PLUGINS actually must be implemented by the init * callback function, as plugin loading is delegated. * + * EXIT_SUCCESS is returned right away if TESTS_RUNNERS is defined but the name + * passed to this function is not contained in it. + * * @param name name of test runner * @param config test suite constructors with dependencies * @param init_cb init/deinit callback diff --git a/src/starter/Makefile.am b/src/starter/Makefile.am index 787cec41c..873c20ace 100644 --- a/src/starter/Makefile.am +++ b/src/starter/Makefile.am @@ -28,6 +28,9 @@ AM_CPPFLAGS = \ -DPLUGINS=\""${starter_plugins}\"" \ -DDEBUG +AM_CFLAGS = \ + @COVERAGE_CFLAGS@ + AM_YFLAGS = -v -d starter_LDADD = \ |