From 3b50e6fc3ea080e552b6a43b2e7e766a77ab5c0f Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 12 May 2016 13:49:11 +0200 Subject: [PATCH 01/84] unit-tests: Don't unload plugins before calling libcharon_deinit() libcharon_deinit() already calls all the functions we called manually. Unloading the plugins will not work if charon->initialize() is called as charon's static plugin features would already be unloaded before the destroyed members are accessed in destroy() to flush them. --- src/libcharon/tests/libcharon_tests.c | 3 --- 1 file changed, 3 deletions(-) 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; From b104b2a73793fede5b19fd545322095bf3711f84 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 12 May 2016 14:30:54 +0200 Subject: [PATCH 02/84] unit-tests: Defining TESTS_RUNNERS allows to only run specific test runners --- src/libstrongswan/tests/test_runner.c | 30 ++++++++++++++++++++++++++- src/libstrongswan/tests/test_runner.h | 4 ++++ 2 files changed, 33 insertions(+), 1 deletion(-) 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 @@ -89,6 +89,28 @@ static void apply_filter(array_t *loaded, char *filter, bool exclude) names->destroy(names); } +/** + * 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 From 87539617f1d35a0260ab55d26c19dd98a8f32302 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 12 May 2016 15:33:44 +0200 Subject: [PATCH 03/84] unit-tests: Add mock sender_t implementation for unit testing This allows to retrieve packets sent by an IKE_SA and pass it to another IKE_SA directly via process_message(). --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/libcharon_tests.h | 11 ++++ src/libcharon/tests/utils/mock_sender.c | 85 +++++++++++++++++++++++++ src/libcharon/tests/utils/mock_sender.h | 56 ++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 src/libcharon/tests/utils/mock_sender.c create mode 100644 src/libcharon/tests/utils/mock_sender.h diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 0589269aa..7c98c2119 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -6,6 +6,7 @@ libcharon_tests_SOURCES = \ suites/test_ike_cfg.c \ suites/test_mem_pool.c \ suites/test_message_chapoly.c \ + utils/mock_sender.h utils/mock_sender.c \ libcharon_tests.h libcharon_tests.c libcharon_tests_CFLAGS = \ diff --git a/src/libcharon/tests/libcharon_tests.h b/src/libcharon/tests/libcharon_tests.h index fb82baccb..d17ea041d 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,14 @@ * for more details. */ +/** + * @defgroup libcharon-tests tests + * @ingroup libcharon + * + * @defgroup test_utils_c test_utils + * @ingroup libcharon-tests + */ + 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/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 . + * + * 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 + +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 . + * + * 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 +#include + +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_ @} */ From 7a5dd544f6926ff4759949bd57aeee2f5d5f5754 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 12 May 2016 16:14:03 +0200 Subject: [PATCH 04/84] unit-tests: Add mock kernel_ipsec_t implementation for unit tests Provides predictable sequential SPIs. --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/utils/mock_ipsec.c | 128 +++++++++++++++++++++++++ src/libcharon/tests/utils/mock_ipsec.h | 36 +++++++ 3 files changed, 165 insertions(+) create mode 100644 src/libcharon/tests/utils/mock_ipsec.c create mode 100644 src/libcharon/tests/utils/mock_ipsec.h diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 7c98c2119..0d2dd4b00 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -6,6 +6,7 @@ libcharon_tests_SOURCES = \ suites/test_ike_cfg.c \ suites/test_mem_pool.c \ suites/test_message_chapoly.c \ + utils/mock_ipsec.h utils/mock_ipsec.c \ utils/mock_sender.h utils/mock_sender.c \ libcharon_tests.h libcharon_tests.c 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 . + * + * 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 . + * + * 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 + +/** + * Create an instance of kernel_ipsec_t + * + * @return created object + */ +kernel_ipsec_t *mock_ipsec_create(); + +#endif /** MOCK_IPSEC_H_ @}*/ From c7f5259cdeb8af6c4b1632d24343c6655dda941c Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 12 May 2016 17:03:44 +0200 Subject: [PATCH 05/84] unit-tests: Add helper class/object to test IKE exchanges --- src/libcharon/tests/Makefile.am | 1 + .../tests/utils/exchange_test_helper.c | 246 ++++++++++++++++++ .../tests/utils/exchange_test_helper.h | 84 ++++++ 3 files changed, 331 insertions(+) create mode 100644 src/libcharon/tests/utils/exchange_test_helper.c create mode 100644 src/libcharon/tests/utils/exchange_test_helper.h diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 0d2dd4b00..65d44898c 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -6,6 +6,7 @@ libcharon_tests_SOURCES = \ suites/test_ike_cfg.c \ suites/test_mem_pool.c \ suites/test_message_chapoly.c \ + utils/exchange_test_helper.h utils/exchange_test_helper.c \ utils/mock_ipsec.h utils/mock_ipsec.c \ utils/mock_sender.h utils/mock_sender.c \ libcharon_tests.h libcharon_tests.c 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..f3f6996a0 --- /dev/null +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -0,0 +1,246 @@ +/* + * 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 . + * + * 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_ipsec.h" + +#include + +typedef struct private_exchange_test_helper_t private_exchange_test_helper_t; + +/** + * Private data + */ +struct private_exchange_test_helper_t { + + /** + * Public interface + */ + exchange_test_helper_t public; + + /** + * Config backend + */ + backend_t backend; + + /** + * Credentials + */ + mem_cred_t *creds; +}; + +/* + * Described in header + */ +exchange_test_helper_t *exchange_test_helper; + +static ike_cfg_t *create_ike_cfg() +{ + ike_cfg_t *ike_cfg; + + 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); + ike_cfg->add_proposal(ike_cfg, proposal_create_default(PROTO_IKE)); + return ike_cfg; +} + +static child_cfg_t *create_child_cfg(bool initiator) +{ + child_cfg_t *child_cfg; + child_cfg_create_t child = { + .mode = MODE_TUNNEL, + }; + + child_cfg = child_cfg_create(initiator ? "init" : "resp", &child); + child_cfg->add_proposal(child_cfg, proposal_create_default(PROTO_ESP)); + child_cfg->add_proposal(child_cfg, proposal_create_default_aead(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) +{ + 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(), + &peer); + add_auth_cfg(peer_cfg, initiator, TRUE); + add_auth_cfg(peer_cfg, initiator, FALSE); + peer_cfg->add_child_cfg(peer_cfg, create_child_cfg(initiator)); + return peer_cfg; +} + +METHOD(backend_t, create_ike_cfg_enumerator, enumerator_t*, + backend_t *this, host_t *me, host_t *other) +{ + ike_cfg_t *ike_cfg = create_ike_cfg(); + return enumerator_create_single(ike_cfg, (void*)ike_cfg->destroy); +} + +METHOD(backend_t, create_peer_cfg_enumerator, enumerator_t*, + backend_t *this, identification_t *me, identification_t *other) +{ + peer_cfg_t *peer_cfg = create_peer_cfg(FALSE); + return enumerator_create_single(peer_cfg, (void*)peer_cfg->destroy); +} + +METHOD(exchange_test_helper_t, process_message, void, + private_exchange_test_helper_t *this, ike_sa_t *ike_sa, message_t *message) +{ + if (!message) + { + message = this->public.sender->dequeue(this->public.sender); + } + charon->bus->set_sa(charon->bus, ike_sa); + ike_sa->process_message(ike_sa, message); + charon->bus->set_sa(charon->bus, NULL); + message->destroy(message); +} + +METHOD(exchange_test_helper_t, establish_sa, void, + private_exchange_test_helper_t *this, ike_sa_t **init, ike_sa_t **resp) +{ + ike_sa_id_t *id_i, *id_r; + ike_sa_t *sa_i, *sa_r; + peer_cfg_t *peer_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); + sa_i->set_peer_cfg(sa_i, peer_cfg); + peer_cfg->destroy(peer_cfg); + charon->bus->set_sa(charon->bus, sa_i); + sa_i->initiate(sa_i, create_child_cfg(TRUE), 0, NULL, NULL); + /* 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); +} + +/** + * 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); +} + +/* + * Described in header + */ +void exchange_test_helper_init(char *plugins) +{ + private_exchange_test_helper_t *this; + + INIT(this, + .public = { + .sender = mock_sender_create(), + .establish_sa = _establish_sa, + .process_message = _process_message, + }, + .backend = { + .create_ike_cfg_enumerator = _create_ike_cfg_enumerator, + .create_peer_cfg_enumerator = _create_peer_cfg_enumerator, + .get_peer_cfg_by_name = (void*)return_null, + }, + .creds = mem_cred_create(), + ); + + initialize_logging(); + /* 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); + + charon->backends->add_backend(charon->backends, &this->backend); + 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; + + this = (private_exchange_test_helper_t*)exchange_test_helper; + + charon->backends->remove_backend(charon->backends, &this->backend); + lib->credmgr->remove_set(lib->credmgr, &this->creds->set); + this->creds->destroy(this->creds); + /* can't let charon do it as it happens too late */ + charon->sender->destroy(charon->sender); + charon->sender = NULL; + 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..891ce2870 --- /dev/null +++ b/src/libcharon/tests/utils/exchange_test_helper.h @@ -0,0 +1,84 @@ +/* + * 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 . + * + * 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 + +#include "mock_sender.h" + +typedef struct exchange_test_helper_t exchange_test_helper_t; + +struct exchange_test_helper_t { + + /** + * Sender instance used during tests + */ + mock_sender_t *sender; + + /** + * Creates an established IKE_SA/CHILD_SA + * + * @param[out] init IKE_SA of the initiator + * @param[out] resp IKE_SA of the responder + */ + void (*establish_sa)(exchange_test_helper_t *this, ike_sa_t **init, + ike_sa_t **resp); + + /** + * 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) + */ + void (*process_message)(exchange_test_helper_t *this, ike_sa_t *sa, + message_t *message); +}; + +/** + * 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_ @} */ From d3c4d55391e89d773e51e0773de7dadff8f976ab Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 13 May 2016 11:57:11 +0200 Subject: [PATCH 06/84] unit-tests: Add separate test runner to test IKEv2 exchanges This allows proper initialization of the daemon and the helper object. --- src/libcharon/tests/Makefile.am | 26 +++++++++-- src/libcharon/tests/exchange_tests.c | 64 ++++++++++++++++++++++++++++ src/libcharon/tests/exchange_tests.h | 14 ++++++ 3 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 src/libcharon/tests/exchange_tests.c create mode 100644 src/libcharon/tests/exchange_tests.h diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 65d44898c..1c9d9d8f3 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -1,4 +1,4 @@ -TESTS = libcharon_tests +TESTS = libcharon_tests exchange_tests check_PROGRAMS = $(TESTS) @@ -6,9 +6,6 @@ libcharon_tests_SOURCES = \ suites/test_ike_cfg.c \ suites/test_mem_pool.c \ suites/test_message_chapoly.c \ - utils/exchange_test_helper.h utils/exchange_test_helper.c \ - utils/mock_ipsec.h utils/mock_ipsec.c \ - utils/mock_sender.h utils/mock_sender.c \ libcharon_tests.h libcharon_tests.c libcharon_tests_CFLAGS = \ @@ -24,3 +21,24 @@ 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 = \ + utils/exchange_test_helper.h utils/exchange_test_helper.c \ + utils/mock_ipsec.h utils/mock_ipsec.c \ + utils/mock_sender.h utils/mock_sender.c \ + 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 . + * + * 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 +#include + +#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..944861055 --- /dev/null +++ b/src/libcharon/tests/exchange_tests.h @@ -0,0 +1,14 @@ +/* + * 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 . + * + * 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. + */ From 735b0cdd01e7300a98e3dd103ba218806d27670b Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 12 May 2016 17:59:42 +0200 Subject: [PATCH 07/84] unit-tests: Add asserts against SAs (e.g. their states) --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/utils/sa_asserts.h | 55 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/libcharon/tests/utils/sa_asserts.h diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 1c9d9d8f3..e51b73571 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -27,6 +27,7 @@ exchange_tests_SOURCES = \ utils/exchange_test_helper.h utils/exchange_test_helper.c \ utils/mock_ipsec.h utils/mock_ipsec.c \ utils/mock_sender.h utils/mock_sender.c \ + utils/sa_asserts.h \ exchange_tests.h exchange_tests.c exchange_tests_CFLAGS = \ diff --git a/src/libcharon/tests/utils/sa_asserts.h b/src/libcharon/tests/utils/sa_asserts.h new file mode 100644 index 000000000..80e390ee3 --- /dev/null +++ b/src/libcharon/tests/utils/sa_asserts.h @@ -0,0 +1,55 @@ +/* + * 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 . + * + * 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_ + +/** + * 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 inbound 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); \ + 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)); \ +}) + +#endif /** SA_ASSERTS_H_ @}*/ From 632ba2a21e28d80fadd14ccc1d910d8469f67979 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 13 May 2016 20:48:44 +0200 Subject: [PATCH 08/84] unit-tests: Add asserts against hooks on listener_t and messages captured there --- src/libcharon/tests/Makefile.am | 1 + .../tests/utils/exchange_test_asserts.c | 98 +++++++ .../tests/utils/exchange_test_asserts.h | 265 ++++++++++++++++++ 3 files changed, 364 insertions(+) create mode 100644 src/libcharon/tests/utils/exchange_test_asserts.c create mode 100644 src/libcharon/tests/utils/exchange_test_asserts.h diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index e51b73571..451f95e6f 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -24,6 +24,7 @@ libcharon_tests_LDADD = \ exchange_tests_SOURCES = \ + utils/exchange_test_asserts.h utils/exchange_test_asserts.c \ utils/exchange_test_helper.h utils/exchange_test_helper.c \ utils/mock_ipsec.h utils/mock_ipsec.c \ utils/mock_sender.h utils/mock_sender.c \ 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..4fd5df52a --- /dev/null +++ b/src/libcharon/tests/utils/exchange_test_asserts.c @@ -0,0 +1,98 @@ +/* + * 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 . + * + * 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 + +#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_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->payload) + { + assert_listener_msg(message->get_payload(message, this->payload), + this, "expected payload (%N) not found", + payload_type_names, this->payload); + } + if (this->notify) + { + assert_listener_msg(message->get_notify(message, this->notify), + this, "expected notify payload (%N) not found", + notify_type_names, this->notify); + } + 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..cdee76b0a --- /dev/null +++ b/src/libcharon/tests/utils/exchange_test_asserts.h @@ -0,0 +1,265 @@ +/* + * 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 . + * + * 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 + +typedef struct listener_hook_assert_t listener_hook_assert_t; +typedef struct listener_message_assert_t listener_message_assert_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; +}; + +/** + * 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); + +/** + * 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 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__ \ + }; \ + charon->bus->add_listener(charon->bus, &_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) + +/** + * 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 + */ + int count; + + /** + * Payload type to look for + */ + payload_type_t payload; + + /** + * Notify type to look for + */ + notify_type_t notify; +}; + +/** + * 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, 0, 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, 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, 0, expected); \ +}) + +#define _assert_payload(dir, c, p, n) ({ \ + listener_message_assert_t _listener = { \ + .listener = { .message = exchange_test_asserts_message, }, \ + .file = __FILE__, \ + .line = __LINE__, \ + .incoming = streq(#dir, "IN") ? TRUE : FALSE, \ + .count = c, \ + .payload = p, \ + }; \ + charon->bus->add_listener(charon->bus, &_listener.listener); \ +}) + +#endif /** EXCHANGE_TEST_ASSERTS_H_ @}*/ From 94012e3f420bbd499e50ffd79613a1a611ea05b6 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 12 May 2016 12:22:35 +0200 Subject: [PATCH 09/84] child-delete: Remove unnecessary call to destroy_child_sa() Generally, we will not find the CHILD_SA by searching for it with the outbound SPI (the initiator of the DELETE sent its inbound SPI) - and if we found a CHILD_SA it would most likely be the wrong one (one in which we used the same inbound SPI as the peer used for the one it deletes). And we don't actually want to destroy the CHILD_SA at this point as we know we already initiated a DELETE ourselves, which means that task still has a reference to it and will destroy the CHILD_SA when it receives the response from the other peer. --- src/libcharon/sa/ikev2/tasks/child_delete.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_delete.c b/src/libcharon/sa/ikev2/tasks/child_delete.c index 053a5c51d..52661f954 100644 --- a/src/libcharon/sa/ikev2/tasks/child_delete.c +++ b/src/libcharon/sa/ikev2/tasks/child_delete.c @@ -165,8 +165,6 @@ static void process_payloads(private_child_delete_t *this, message_t *message) /* 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 */ From 33e2620b8c5cda421d1138e3be070b44aa105734 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 12 May 2016 17:25:42 +0200 Subject: [PATCH 10/84] unit-tests: Add a unit test for CHILD_SA DELETE collisions --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/exchange_tests.h | 2 + .../tests/suites/test_child_delete.c | 146 ++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 src/libcharon/tests/suites/test_child_delete.c diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 451f95e6f..824126ffe 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -24,6 +24,7 @@ libcharon_tests_LDADD = \ exchange_tests_SOURCES = \ + suites/test_child_delete.c \ utils/exchange_test_asserts.h utils/exchange_test_asserts.c \ utils/exchange_test_helper.h utils/exchange_test_helper.c \ utils/mock_ipsec.h utils/mock_ipsec.c \ diff --git a/src/libcharon/tests/exchange_tests.h b/src/libcharon/tests/exchange_tests.h index 944861055..d65b1df69 100644 --- a/src/libcharon/tests/exchange_tests.h +++ b/src/libcharon/tests/exchange_tests.h @@ -12,3 +12,5 @@ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ + +TEST_SUITE(child_delete_suite_create) 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..31473769f --- /dev/null +++ b/src/libcharon/tests/suites/test_child_delete.c @@ -0,0 +1,146 @@ +/* + * 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 . + * + * 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 +#include +#include +#include + +/** + * 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); + } + else + { /* initiator deletes the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b); + } + assert_hook_not_called(child_updown); + a->delete_child_sa(a, 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(); + + a->destroy(a); + b->destroy(b); +} +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); + /* both peers delete the CHILD_SA concurrently */ + assert_hook_not_called(child_updown); + a->delete_child_sa(a, PROTO_ESP, 1, FALSE); + assert_child_sa_state(a, 1, CHILD_DELETING); + b->delete_child_sa(b, 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(); + + a->destroy(a); + b->destroy(b); +} +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("collision"); + tcase_add_test(tc, test_collision); + suite_add_tcase(s, tc); + + return s; +} From 5d97e5c30f96de31037d45d9dcd3e12610112a9d Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 13 May 2016 08:44:13 +0200 Subject: [PATCH 11/84] unit-tests: Call methods on IKE_SAs in their context --- src/libcharon/tests/suites/test_child_delete.c | 14 +++++++------- src/libcharon/tests/utils/exchange_test_helper.c | 3 +-- src/libcharon/tests/utils/exchange_test_helper.h | 10 ++++++++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/libcharon/tests/suites/test_child_delete.c b/src/libcharon/tests/suites/test_child_delete.c index 31473769f..b357beec5 100644 --- a/src/libcharon/tests/suites/test_child_delete.c +++ b/src/libcharon/tests/suites/test_child_delete.c @@ -39,7 +39,7 @@ START_TEST(test_regular) &a, &b); } assert_hook_not_called(child_updown); - a->delete_child_sa(a, PROTO_ESP, _i+1, FALSE); + call_ikesa(a, delete_child_sa, PROTO_ESP, _i+1, FALSE); assert_child_sa_state(a, _i+1, CHILD_DELETING); assert_hook(); @@ -57,8 +57,8 @@ START_TEST(test_regular) assert_child_sa_count(a, 0); assert_hook(); - a->destroy(a); - b->destroy(b); + call_ikesa(a, destroy); + call_ikesa(b, destroy); } END_TEST @@ -74,9 +74,9 @@ START_TEST(test_collision) &a, &b); /* both peers delete the CHILD_SA concurrently */ assert_hook_not_called(child_updown); - a->delete_child_sa(a, PROTO_ESP, 1, FALSE); + call_ikesa(a, delete_child_sa, PROTO_ESP, 1, FALSE); assert_child_sa_state(a, 1, CHILD_DELETING); - b->delete_child_sa(b, PROTO_ESP, 2, FALSE); + call_ikesa(b, delete_child_sa, PROTO_ESP, 2, FALSE); assert_child_sa_state(b, 2, CHILD_DELETING); assert_hook(); @@ -122,8 +122,8 @@ START_TEST(test_collision) assert_child_sa_count(b, 0); assert_hook(); - a->destroy(a); - b->destroy(b); + call_ikesa(a, destroy); + call_ikesa(b, destroy); } END_TEST diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index f3f6996a0..488a5ee9b 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -150,8 +150,7 @@ METHOD(exchange_test_helper_t, establish_sa, void, peer_cfg = create_peer_cfg(TRUE); sa_i->set_peer_cfg(sa_i, peer_cfg); peer_cfg->destroy(peer_cfg); - charon->bus->set_sa(charon->bus, sa_i); - sa_i->initiate(sa_i, create_child_cfg(TRUE), 0, NULL, NULL); + call_ikesa(sa_i, initiate, create_child_cfg(TRUE), 0, NULL, NULL); /* IKE_SA_INIT --> */ id_r->set_initiator_spi(id_r, id_i->get_initiator_spi(id_i)); process_message(this, sa_r, NULL); diff --git a/src/libcharon/tests/utils/exchange_test_helper.h b/src/libcharon/tests/utils/exchange_test_helper.h index 891ce2870..9768d42d0 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.h +++ b/src/libcharon/tests/utils/exchange_test_helper.h @@ -61,6 +61,16 @@ struct exchange_test_helper_t { message_t *message); }; +/** + * 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. * From 29f1637b9a30da6933f0de5034073b62e112b8f1 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 13 May 2016 08:50:17 +0200 Subject: [PATCH 12/84] unit-tests: Make IKE SPIs predictable --- src/libcharon/tests/utils/exchange_test_helper.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index 488a5ee9b..17553bc81 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -39,8 +39,19 @@ struct private_exchange_test_helper_t { * Credentials */ mem_cred_t *creds; + + /** + * IKE_SA SPI counter + */ + refcount_t ike_spi; }; +CALLBACK(get_ike_spi, uint64_t, + private_exchange_test_helper_t *this) +{ + return (uint64_t)ref_get(&this->ike_spi); +} + /* * Described in header */ @@ -215,6 +226,9 @@ void exchange_test_helper_init(char *plugins) /* 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); charon->backends->add_backend(charon->backends, &this->backend); lib->credmgr->add_set(lib->credmgr, &this->creds->set); From 1f73a74be0c2398b6261710b0ca25494352f2a25 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 13 May 2016 12:12:51 +0200 Subject: [PATCH 13/84] unit-tests: Add mock DH implementation that's basically a noop If the openssl plugin is built DH isn't that much of an overhead as ecp256 is used, but the default MODP group is now modp3072. --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/utils/mock_dh.c | 87 +++++++++++++++++++++++++++++ src/libcharon/tests/utils/mock_dh.h | 37 ++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 src/libcharon/tests/utils/mock_dh.c create mode 100644 src/libcharon/tests/utils/mock_dh.h diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 824126ffe..68090c75f 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -27,6 +27,7 @@ exchange_tests_SOURCES = \ suites/test_child_delete.c \ utils/exchange_test_asserts.h utils/exchange_test_asserts.c \ utils/exchange_test_helper.h utils/exchange_test_helper.c \ + utils/mock_dh.h utils/mock_dh.c \ utils/mock_ipsec.h utils/mock_ipsec.c \ utils/mock_sender.h utils/mock_sender.c \ utils/sa_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 . + * + * 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 . + * + * 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 + +/** + * 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_ @}*/ From 7e5424868de9e73d0cf20c2973cbc983f6427542 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 13 May 2016 12:16:45 +0200 Subject: [PATCH 14/84] unit-tests: Register mock DH implementation as static plugin feature --- src/libcharon/tests/utils/exchange_test_helper.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index 17553bc81..47da9e49a 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -15,6 +15,7 @@ #include "exchange_test_helper.h" #include "mock_ipsec.h" +#include "mock_dh.h" #include @@ -201,6 +202,13 @@ static void initialize_logging() 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), + }; INIT(this, .public = { @@ -217,10 +225,13 @@ void exchange_test_helper_init(char *plugins) ); 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 From 32cfe1e00ca38b2e3159fa17dbd85b3fd78a3b99 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 16 May 2016 19:35:14 +0200 Subject: [PATCH 15/84] unit-tests: Add another CHILD_SA delete collision --- .../tests/suites/test_child_delete.c | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/libcharon/tests/suites/test_child_delete.c b/src/libcharon/tests/suites/test_child_delete.c index b357beec5..1b44798c8 100644 --- a/src/libcharon/tests/suites/test_child_delete.c +++ b/src/libcharon/tests/suites/test_child_delete.c @@ -127,6 +127,60 @@ START_TEST(test_collision) } 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); + /* 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 + Suite *child_delete_suite_create() { Suite *s; @@ -138,8 +192,9 @@ Suite *child_delete_suite_create() tcase_add_loop_test(tc, test_regular, 0, 2); suite_add_tcase(s, tc); - tc = tcase_create("collision"); + tc = tcase_create("collisions"); tcase_add_test(tc, test_collision); + tcase_add_test(tc, test_collision_drop); suite_add_tcase(s, tc); return s; From fbb0b3cb0ace2bf96ea9d46deae123fe2ac27ed7 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 17 May 2016 13:49:58 +0200 Subject: [PATCH 16/84] unit-tests: Make message asserts more flexible --- .../tests/utils/exchange_test_asserts.c | 57 +++++++++++--- .../tests/utils/exchange_test_asserts.h | 75 +++++++++++++++---- 2 files changed, 106 insertions(+), 26 deletions(-) diff --git a/src/libcharon/tests/utils/exchange_test_asserts.c b/src/libcharon/tests/utils/exchange_test_asserts.c index 4fd5df52a..d95af5993 100644 --- a/src/libcharon/tests/utils/exchange_test_asserts.c +++ b/src/libcharon/tests/utils/exchange_test_asserts.c @@ -56,6 +56,46 @@ bool exchange_test_asserts_child_updown(listener_t *listener, ike_sa_t *ike_sa, 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 */ @@ -80,17 +120,14 @@ bool exchange_test_asserts_message(listener_t *listener, ike_sa_t *ike_sa, "count in message (%d != %d)", this->count, count); } - if (this->payload) + if (this->num_rules) { - assert_listener_msg(message->get_payload(message, this->payload), - this, "expected payload (%N) not found", - payload_type_names, this->payload); - } - if (this->notify) - { - assert_listener_msg(message->get_notify(message, this->notify), - this, "expected notify payload (%N) not found", - notify_type_names, this->notify); + int i; + + for (i = 0; i < this->num_rules; i++) + { + assert_message_rule(this, message, &this->rules[i]); + } } return FALSE; } diff --git a/src/libcharon/tests/utils/exchange_test_asserts.h b/src/libcharon/tests/utils/exchange_test_asserts.h index cdee76b0a..5c91ae16b 100644 --- a/src/libcharon/tests/utils/exchange_test_asserts.h +++ b/src/libcharon/tests/utils/exchange_test_asserts.h @@ -27,6 +27,7 @@ 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 { @@ -171,6 +172,29 @@ do { \ } \ } 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 */ @@ -197,19 +221,19 @@ struct listener_message_assert_t { bool incoming; /** - * Payload count to expect + * Payload count to expect (-1 to ignore the count) */ int count; /** - * Payload type to look for + * Payloads to expect or not expect in a message */ - payload_type_t payload; + listener_message_rule_t *rules; /** - * Notify type to look for + * Number of rules */ - notify_type_t notify; + int num_rules; }; /** @@ -224,9 +248,8 @@ bool exchange_test_asserts_message(listener_t *this, ike_sa_t *ike_sa, * * @param dir IN or OUT to check the next in- or outbound message */ -#define assert_message_empty(dir) ({ \ - _assert_payload(dir, 0, 0, 0); \ -}) +#define assert_message_empty(dir) \ + _assert_payload(dir, 0) /** * Assert that the next in- or outbound plaintext message contains exactly @@ -235,9 +258,8 @@ bool exchange_test_asserts_message(listener_t *this, ike_sa_t *ike_sa, * @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, expected, 0); \ -}) +#define assert_single_payload(dir, expected) \ + _assert_payload(dir, 1, { TRUE, expected, 0 }) /** * Assert that the next in- or outbound plaintext message contains exactly @@ -246,18 +268,39 @@ bool exchange_test_asserts_message(listener_t *this, ike_sa_t *ike_sa, * @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, 0, expected); \ -}) +#define assert_single_notify(dir, expected) \ + _assert_payload(dir, 1, { TRUE, 0, expected }) -#define _assert_payload(dir, c, p, n) ({ \ +/** + * 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, \ - .payload = p, \ + .rules = _rules, \ + .num_rules = countof(_rules), \ }; \ charon->bus->add_listener(charon->bus, &_listener.listener); \ }) From c1289eb5784851d5b0a60dd694d1df094c360016 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 17 May 2016 20:03:59 +0200 Subject: [PATCH 17/84] unit-tests: Add mock nonce generator We don't make the full nonces configurable but only the first byte, which should be enough to force a nonce to be smaller than others. --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/utils/mock_nonce_gen.c | 91 ++++++++++++++++++++++ src/libcharon/tests/utils/mock_nonce_gen.h | 37 +++++++++ 3 files changed, 129 insertions(+) create mode 100644 src/libcharon/tests/utils/mock_nonce_gen.c create mode 100644 src/libcharon/tests/utils/mock_nonce_gen.h diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 68090c75f..4c37ef97d 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -29,6 +29,7 @@ exchange_tests_SOURCES = \ utils/exchange_test_helper.h utils/exchange_test_helper.c \ 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 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 . + * + * 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 . + * + * 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 + +/** + * 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_ @} */ From 990c62cdcccea52f145e53e9f27acd1a66e7c6da Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 17 May 2016 20:05:06 +0200 Subject: [PATCH 18/84] crypto-factory: Stop after successfully creating one nonce generator Fixes: e2fc09c186c3 ("Add nonce generator interface") --- src/libstrongswan/crypto/crypto_factory.c | 4 ++++ 1 file changed, 4 insertions(+) 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); From dda5aab0f491d4be64fb04166fc4f4dcb68e4880 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 17 May 2016 20:06:24 +0200 Subject: [PATCH 19/84] unit-tests: Register nonce generator and make first nonce byte configurable --- src/libcharon/tests/utils/exchange_test_helper.c | 14 +++++++++++++- src/libcharon/tests/utils/exchange_test_helper.h | 6 ++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index 47da9e49a..018804e6c 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -14,8 +14,9 @@ */ #include "exchange_test_helper.h" -#include "mock_ipsec.h" #include "mock_dh.h" +#include "mock_ipsec.h" +#include "mock_nonce_gen.h" #include @@ -196,6 +197,14 @@ static void initialize_logging() 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 */ @@ -208,6 +217,9 @@ void exchange_test_helper_init(char *plugins) 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, diff --git a/src/libcharon/tests/utils/exchange_test_helper.h b/src/libcharon/tests/utils/exchange_test_helper.h index 9768d42d0..861ce0d47 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.h +++ b/src/libcharon/tests/utils/exchange_test_helper.h @@ -40,6 +40,12 @@ struct exchange_test_helper_t { */ 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 * From 5d7f03dcaae3bdb8587a9d4a8f86eb91ce2438b2 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 18 May 2016 17:15:12 +0200 Subject: [PATCH 20/84] unit-tests: Match in and outbound SPIs in SA asserts Since we use unique sequential SPIs that should be OK. --- src/libcharon/tests/utils/sa_asserts.h | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/libcharon/tests/utils/sa_asserts.h b/src/libcharon/tests/utils/sa_asserts.h index 80e390ee3..9faa2ff08 100644 --- a/src/libcharon/tests/utils/sa_asserts.h +++ b/src/libcharon/tests/utils/sa_asserts.h @@ -37,14 +37,15 @@ }) /** - * Check if the CHILD_SA with the given inbound SPI is in the expected state. + * 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); \ + 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", \ @@ -52,4 +53,16 @@ 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)); \ +}) + #endif /** SA_ASSERTS_H_ @}*/ From 14588d99a42ab0d0a037b73991c675c97f2e7865 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 18 May 2016 17:16:29 +0200 Subject: [PATCH 21/84] unit-tests: Add asserts against ike|child_rekey hooks --- .../tests/utils/exchange_test_asserts.c | 47 +++++++++++++++++++ .../tests/utils/exchange_test_asserts.h | 35 ++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/libcharon/tests/utils/exchange_test_asserts.c b/src/libcharon/tests/utils/exchange_test_asserts.c index d95af5993..2602b97b7 100644 --- a/src/libcharon/tests/utils/exchange_test_asserts.c +++ b/src/libcharon/tests/utils/exchange_test_asserts.c @@ -13,6 +13,8 @@ * for more details. */ +#include + #include #include "exchange_test_asserts.h" @@ -56,6 +58,51 @@ bool exchange_test_asserts_child_updown(listener_t *listener, ike_sa_t *ike_sa, 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 */ diff --git a/src/libcharon/tests/utils/exchange_test_asserts.h b/src/libcharon/tests/utils/exchange_test_asserts.h index 5c91ae16b..906760c8b 100644 --- a/src/libcharon/tests/utils/exchange_test_asserts.h +++ b/src/libcharon/tests/utils/exchange_test_asserts.h @@ -65,6 +65,11 @@ struct listener_hook_assert_t { * Expected updown result */ bool up; + + /** + * Initiator/Inbound SPIs to expect in rekey event + */ + uint64_t spi_old, spi_new; }; /** @@ -84,6 +89,18 @@ bool exchange_test_asserts_ike_updown(listener_t *this, ike_sa_t *ike_sa, 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. @@ -135,6 +152,24 @@ bool exchange_test_asserts_child_updown(listener_t *this, ike_sa_t *ike_sa, .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(). From fa098aa97c444e6d1631155a94bf04fecbdd082e Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 17 May 2016 20:07:09 +0200 Subject: [PATCH 22/84] unit-tests: Add unit tests for basic CHILD_SA rekeying --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/exchange_tests.h | 1 + src/libcharon/tests/suites/test_child_rekey.c | 235 ++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 src/libcharon/tests/suites/test_child_rekey.c diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 4c37ef97d..74042fe23 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -25,6 +25,7 @@ libcharon_tests_LDADD = \ exchange_tests_SOURCES = \ suites/test_child_delete.c \ + suites/test_child_rekey.c \ utils/exchange_test_asserts.h utils/exchange_test_asserts.c \ utils/exchange_test_helper.h utils/exchange_test_helper.c \ utils/mock_dh.h utils/mock_dh.c \ diff --git a/src/libcharon/tests/exchange_tests.h b/src/libcharon/tests/exchange_tests.h index d65b1df69..8be5f542d 100644 --- a/src/libcharon/tests/exchange_tests.h +++ b/src/libcharon/tests/exchange_tests.h @@ -14,3 +14,4 @@ */ TEST_SUITE(child_delete_suite_create) +TEST_SUITE(child_rekey_suite_create) 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..2cf2b3fd0 --- /dev/null +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -0,0 +1,235 @@ +/* + * 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 . + * + * 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 +#include +#include +#include + +/** + * 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); + } + else + { /* initiator rekeys the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b); + } + 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); + /* FIXME: keeping this in CHILD_REKEYING is not ideal */ + assert_child_sa_state(b, spi_b, CHILD_REKEYING); + 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 + +/** + * 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); + + /* 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_REKEYING); + 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_REKEYING); + 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 + { + 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_REKEYING); + 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_REKEYING); + 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 + +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); + suite_add_tcase(s, tc); + + tc = tcase_create("collisions"); + tcase_add_loop_test(tc, test_collision, 0, 4); + suite_add_tcase(s, tc); + + return s; +} From b7b414463d7448f7fbac5537694b298baedf9b47 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 19 May 2016 15:06:27 +0200 Subject: [PATCH 23/84] child-create: Handle TEMPORARY_FAILURE notify as failure We will later add code to retry creating the CHILD_SA if we are not rekeying. Rekeying is already rescheduled as with any other errors. --- src/libcharon/sa/ikev2/tasks/child_create.c | 1 + src/libcharon/sa/ikev2/tasks/child_rekey.c | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index 326993885..ea56e161e 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -1432,6 +1432,7 @@ METHOD(task_t, process_i, status_t, case FAILED_CP_REQUIRED: case TS_UNACCEPTABLE: case INVALID_SELECTORS: + case TEMPORARY_FAILURE: { DBG1(DBG_IKE, "received %N notify, no CHILD_SA built", notify_type_names, type); diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 17b812e91..66bd3b4be 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -346,10 +346,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); } From 334b0bc84a754d021d0d7fecded1e7f0fae4eea7 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 19 May 2016 15:31:02 +0200 Subject: [PATCH 24/84] child-rekey: Recreate the CHILD_SA if we receive a CHILD_SA_NOT_FOUND notify --- src/libcharon/sa/ikev2/tasks/child_rekey.c | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 66bd3b4be..bd94ce698 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -337,6 +337,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) From 5bbb9d703c861b3c080d7c625421e4cedd5df8b6 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 19 May 2016 17:23:32 +0200 Subject: [PATCH 25/84] child-rekey: Use more appropriate error notifies if CHILD_SA is not found or getting deleted These are the notifies we should return according to RFC 7296. --- src/libcharon/sa/ikev2/tasks/child_rekey.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index bd94ce698..b32d2d725 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -219,11 +219,16 @@ METHOD(task_t, build_r, status_t, child_cfg_t *config; uint32_t reqid; - 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; } From b79beba5182f5e05d495a7660695dc8f24ba70d0 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 19 May 2016 19:25:12 +0200 Subject: [PATCH 26/84] unit-tests: Add asserts against task queues of IKE_SAs --- src/libcharon/tests/utils/sa_asserts.h | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/libcharon/tests/utils/sa_asserts.h b/src/libcharon/tests/utils/sa_asserts.h index 9faa2ff08..0d1b9faed 100644 --- a/src/libcharon/tests/utils/sa_asserts.h +++ b/src/libcharon/tests/utils/sa_asserts.h @@ -65,4 +65,36 @@ 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_ @}*/ From cdbf9428890a324b4cbd8bdbe0f1cfe669870a15 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 20 May 2016 10:49:21 +0200 Subject: [PATCH 27/84] ikev2: Use CHILD_REKEYED for replaced CHILD_SAs after rekeying This allows handling collisions better, in particular with deletions. --- src/libcharon/sa/ikev2/tasks/child_delete.c | 10 ++++++---- src/libcharon/sa/ikev2/tasks/child_rekey.c | 11 ++++++----- src/libcharon/tests/suites/test_child_rekey.c | 11 +++++------ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_delete.c b/src/libcharon/sa/ikev2/tasks/child_delete.c index 52661f954..dd77665f7 100644 --- a/src/libcharon/sa/ikev2/tasks/child_delete.c +++ b/src/libcharon/sa/ikev2/tasks/child_delete.c @@ -157,9 +157,8 @@ 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 */ @@ -168,11 +167,14 @@ static void process_payloads(private_child_delete_t *this, message_t *message) 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; } + break; default: break; } @@ -204,7 +206,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); @@ -306,7 +308,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; } diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index b32d2d725..57085e8cd 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -241,6 +241,7 @@ METHOD(task_t, build_r, status_t, config = this->child_sa->get_config(this->child_sa); this->child_create->set_config(this->child_create, config->get_ref(config)); this->child_create->task.build(&this->child_create->task, message); + this->child_sa->set_state(this->child_sa, CHILD_REKEYING); if (message->get_payload(message, PLV2_SECURITY_ASSOCIATION) == NULL) { @@ -249,7 +250,7 @@ METHOD(task_t, build_r, status_t, 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, @@ -289,9 +290,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); } } } @@ -410,9 +411,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); diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 2cf2b3fd0..2ed1f89d5 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -60,8 +60,7 @@ START_TEST(test_regular) assert_hook_called(child_rekey); assert_notify(IN, REKEY_SA); exchange_test_helper->process_message(exchange_test_helper, b, NULL); - /* FIXME: keeping this in CHILD_REKEYING is not ideal */ - assert_child_sa_state(b, spi_b, CHILD_REKEYING); + assert_child_sa_state(b, spi_b, CHILD_REKEYED); assert_child_sa_state(b, 4, CHILD_INSTALLED); assert_hook(); @@ -145,14 +144,14 @@ START_TEST(test_collision) 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_REKEYING); + 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_REKEYING); + assert_child_sa_state(a, 1, CHILD_REKEYED); assert_child_sa_state(a, 6, CHILD_INSTALLED); assert_hook(); @@ -169,7 +168,7 @@ START_TEST(test_collision) 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_REKEYING); + 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) @@ -183,7 +182,7 @@ START_TEST(test_collision) 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_REKEYING); + 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 */ From 62a2567b060b57b74d26a3f4a14e6468ec8ab777 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Sat, 21 May 2016 11:12:16 +0200 Subject: [PATCH 28/84] unit-tests: Add asserts against job scheduling --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/utils/job_asserts.h | 59 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/libcharon/tests/utils/job_asserts.h diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 74042fe23..c83514546 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -28,6 +28,7 @@ exchange_tests_SOURCES = \ suites/test_child_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 \ 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 . + * + * 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_ @}*/ From 42af3b4cceafbc27fe1176e41be68c866c2eae19 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Sat, 21 May 2016 12:22:06 +0200 Subject: [PATCH 29/84] unit-tests: Add tests for CHILD_SA rekeying/deletion collisions --- src/libcharon/tests/suites/test_child_rekey.c | 289 +++++++++++++++++- 1 file changed, 288 insertions(+), 1 deletion(-) diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 2ed1f89d5..4fd480756 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -18,6 +18,7 @@ #include #include #include +#include #include /** @@ -215,6 +216,286 @@ START_TEST(test_collision) } 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); + } + else + { /* initiator rekeys the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b); + } + 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); + } + else + { /* initiator rekeys the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b); + } + 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); + } + else + { /* initiator rekeys the CHILD_SA (SPI 1) */ + exchange_test_helper->establish_sa(exchange_test_helper, + &a, &b); + } + 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 CREAE_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. + */ + + Suite *child_rekey_suite_create() { Suite *s; @@ -226,9 +507,15 @@ Suite *child_rekey_suite_create() tcase_add_loop_test(tc, test_regular, 0, 2); suite_add_tcase(s, tc); - tc = tcase_create("collisions"); + tc = tcase_create("collisions rekey"); tcase_add_loop_test(tc, test_collision, 0, 4); 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); + return s; } From 557e262f048400dbfeee500e5b067fafb2e173bd Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 24 May 2016 14:14:05 +0200 Subject: [PATCH 30/84] unit-tests: Make IKE and ESP proposals configurable --- .../tests/suites/test_child_delete.c | 8 +- src/libcharon/tests/suites/test_child_rekey.c | 18 +-- .../tests/utils/exchange_test_helper.c | 117 +++++++++++++----- .../tests/utils/exchange_test_helper.h | 17 ++- 4 files changed, 116 insertions(+), 44 deletions(-) diff --git a/src/libcharon/tests/suites/test_child_delete.c b/src/libcharon/tests/suites/test_child_delete.c index 1b44798c8..51b3f059d 100644 --- a/src/libcharon/tests/suites/test_child_delete.c +++ b/src/libcharon/tests/suites/test_child_delete.c @@ -31,12 +31,12 @@ START_TEST(test_regular) if (_i) { /* responder deletes the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, - &b, &a); + &b, &a, NULL); } else { /* initiator deletes the CHILD_SA (SPI 1) */ exchange_test_helper->establish_sa(exchange_test_helper, - &a, &b); + &a, &b, NULL); } assert_hook_not_called(child_updown); call_ikesa(a, delete_child_sa, PROTO_ESP, _i+1, FALSE); @@ -71,7 +71,7 @@ START_TEST(test_collision) ike_sa_t *a, *b; exchange_test_helper->establish_sa(exchange_test_helper, - &a, &b); + &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); @@ -137,7 +137,7 @@ START_TEST(test_collision_drop) message_t *msg; exchange_test_helper->establish_sa(exchange_test_helper, - &a, &b); + &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); diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 4fd480756..7355a8c2a 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -45,12 +45,12 @@ START_TEST(test_regular) if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, - &b, &a); + &b, &a, NULL); } else { /* initiator rekeys the CHILD_SA (SPI 1) */ exchange_test_helper->establish_sa(exchange_test_helper, - &a, &b); + &a, &b, NULL); } initiate_rekey(a, spi_a); @@ -105,7 +105,7 @@ START_TEST(test_collision) ike_sa_t *a, *b; exchange_test_helper->establish_sa(exchange_test_helper, - &a, &b); + &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 @@ -235,12 +235,12 @@ START_TEST(test_collision_delete) if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, - &b, &a); + &b, &a, NULL); } else { /* initiator rekeys the CHILD_SA (SPI 1) */ exchange_test_helper->establish_sa(exchange_test_helper, - &a, &b); + &a, &b, NULL); } initiate_rekey(a, spi_a); call_ikesa(b, delete_child_sa, PROTO_ESP, spi_b, FALSE); @@ -322,12 +322,12 @@ START_TEST(test_collision_delete_drop_delete) if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, - &b, &a); + &b, &a, NULL); } else { /* initiator rekeys the CHILD_SA (SPI 1) */ exchange_test_helper->establish_sa(exchange_test_helper, - &a, &b); + &a, &b, NULL); } initiate_rekey(a, spi_a); call_ikesa(b, delete_child_sa, PROTO_ESP, spi_b, FALSE); @@ -407,12 +407,12 @@ END_TEST if (_i) { /* responder rekeys the CHILD_SA (SPI 2) */ exchange_test_helper->establish_sa(exchange_test_helper, - &b, &a); + &b, &a, NULL); } else { /* initiator rekeys the CHILD_SA (SPI 1) */ exchange_test_helper->establish_sa(exchange_test_helper, - &a, &b); + &a, &b, NULL); } initiate_rekey(a, spi_a); call_ikesa(b, delete_child_sa, PROTO_ESP, spi_b, FALSE); diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index 018804e6c..038cca07a 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -21,6 +21,7 @@ #include typedef struct private_exchange_test_helper_t private_exchange_test_helper_t; +typedef struct private_backend_t private_backend_t; /** * Private data @@ -32,11 +33,6 @@ struct private_exchange_test_helper_t { */ exchange_test_helper_t public; - /** - * Config backend - */ - backend_t backend; - /** * Credentials */ @@ -48,6 +44,27 @@ struct private_exchange_test_helper_t { refcount_t ike_spi; }; +/** + * 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) { @@ -59,26 +76,52 @@ CALLBACK(get_ike_spi, uint64_t, */ exchange_test_helper_t *exchange_test_helper; -static ike_cfg_t *create_ike_cfg() +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); - ike_cfg->add_proposal(ike_cfg, proposal_create_default(PROTO_IKE)); + 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) +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); - child_cfg->add_proposal(child_cfg, proposal_create_default(PROTO_ESP)); - child_cfg->add_proposal(child_cfg, proposal_create_default_aead(PROTO_ESP)); + 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, @@ -101,7 +144,8 @@ static void add_auth_cfg(peer_cfg_t *peer_cfg, bool initiator, bool local) peer_cfg->add_auth_cfg(peer_cfg, auth, local); } -static peer_cfg_t *create_peer_cfg(bool initiator) +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 = { @@ -110,26 +154,23 @@ static peer_cfg_t *create_peer_cfg(bool initiator) .keyingtries = 1, }; - peer_cfg = peer_cfg_create(initiator ? "init" : "resp", create_ike_cfg(), - &peer); + 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); - peer_cfg->add_child_cfg(peer_cfg, create_child_cfg(initiator)); return peer_cfg; } METHOD(backend_t, create_ike_cfg_enumerator, enumerator_t*, - backend_t *this, host_t *me, host_t *other) + private_backend_t *this, host_t *me, host_t *other) { - ike_cfg_t *ike_cfg = create_ike_cfg(); - return enumerator_create_single(ike_cfg, (void*)ike_cfg->destroy); + return enumerator_create_single(this->ike_cfg, NULL); } METHOD(backend_t, create_peer_cfg_enumerator, enumerator_t*, - backend_t *this, identification_t *me, identification_t *other) + private_backend_t *this, identification_t *me, identification_t *other) { - peer_cfg_t *peer_cfg = create_peer_cfg(FALSE); - return enumerator_create_single(peer_cfg, (void*)peer_cfg->destroy); + return enumerator_create_single(this->peer_cfg, NULL); } METHOD(exchange_test_helper_t, process_message, void, @@ -146,11 +187,20 @@ METHOD(exchange_test_helper_t, process_message, void, } METHOD(exchange_test_helper_t, establish_sa, void, - private_exchange_test_helper_t *this, ike_sa_t **init, ike_sa_t **resp) + 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); @@ -160,10 +210,20 @@ METHOD(exchange_test_helper_t, establish_sa, void, IKEV2, FALSE); id_r = sa_r->get_id(sa_r); - peer_cfg = create_peer_cfg(TRUE); + 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, create_child_cfg(TRUE), 0, NULL, NULL); + 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); @@ -174,6 +234,10 @@ METHOD(exchange_test_helper_t, establish_sa, void, 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); } /** @@ -228,11 +292,6 @@ void exchange_test_helper_init(char *plugins) .establish_sa = _establish_sa, .process_message = _process_message, }, - .backend = { - .create_ike_cfg_enumerator = _create_ike_cfg_enumerator, - .create_peer_cfg_enumerator = _create_peer_cfg_enumerator, - .get_peer_cfg_by_name = (void*)return_null, - }, .creds = mem_cred_create(), ); @@ -253,7 +312,6 @@ void exchange_test_helper_init(char *plugins) charon->ike_sa_manager->set_spi_cb(charon->ike_sa_manager, get_ike_spi, this); - charon->backends->add_backend(charon->backends, &this->backend); lib->credmgr->add_set(lib->credmgr, &this->creds->set); this->creds->add_shared(this->creds, @@ -272,7 +330,6 @@ void exchange_test_helper_deinit() this = (private_exchange_test_helper_t*)exchange_test_helper; - charon->backends->remove_backend(charon->backends, &this->backend); lib->credmgr->remove_set(lib->credmgr, &this->creds->set); this->creds->destroy(this->creds); /* can't let charon do it as it happens too late */ diff --git a/src/libcharon/tests/utils/exchange_test_helper.h b/src/libcharon/tests/utils/exchange_test_helper.h index 861ce0d47..6530f7564 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.h +++ b/src/libcharon/tests/utils/exchange_test_helper.h @@ -32,6 +32,7 @@ #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 { @@ -51,9 +52,10 @@ struct exchange_test_helper_t { * * @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); + 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 @@ -67,6 +69,19 @@ struct exchange_test_helper_t { message_t *message); }; +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. From 5ef033c53b99498f20e44676c65e284e3dfff15b Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 25 May 2016 14:55:16 +0200 Subject: [PATCH 31/84] child-rekey: Don't change state to INSTALLED if it was already REKEYING This happens if there is a rekey collision and the peers disagree on the DH group. --- src/libcharon/sa/ikev2/tasks/child_rekey.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 57085e8cd..3ce683440 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -218,6 +218,7 @@ METHOD(task_t, build_r, status_t, { child_cfg_t *config; uint32_t reqid; + child_sa_state_t state; if (!this->child_sa) { @@ -241,12 +242,13 @@ METHOD(task_t, build_r, status_t, config = this->child_sa->get_config(this->child_sa); 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; } From f97bac55941d761f4261c5e2c2f6621cfa822fbd Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 25 May 2016 15:15:51 +0200 Subject: [PATCH 32/84] unit-tests: Test for rekeying if INVALID_KE_PAYLOAD notifies are received --- src/libcharon/tests/suites/test_child_rekey.c | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 7355a8c2a..bec3dee7b 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -96,6 +96,94 @@ START_TEST(test_regular) } 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 + /** * Both peers initiate the CHILD_SA reekying concurrently and should handle * the collision properly depending on the nonces. @@ -216,6 +304,169 @@ START_TEST(test_collision) } 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 }, + }; + + initiate_rekey(a, 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 } --> */ + 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 + /** * One of the hosts initiates a DELETE of the CHILD_SA the other peer is * concurrently trying to rekey. @@ -505,10 +756,12 @@ Suite *child_rekey_suite_create() 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); suite_add_tcase(s, tc); tc = tcase_create("collisions delete"); From 4f1012b3e11b939ccf5c388209372d57c4ecf191 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 25 May 2016 19:11:51 +0200 Subject: [PATCH 33/84] child-rekey: Add method to check for the redundant SA created in a collision --- src/libcharon/sa/ikev2/tasks/child_rekey.c | 34 +++++++++++++--------- src/libcharon/sa/ikev2/tasks/child_rekey.h | 21 ++++++++++--- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 3ce683440..5873a84af 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 @@ -434,6 +435,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) { @@ -452,19 +465,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) { @@ -475,7 +480,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; } @@ -532,6 +537,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); }; From 7a23226c328101630a2756e0c33e30bf65242b85 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 25 May 2016 19:12:53 +0200 Subject: [PATCH 34/84] child-delete: Check if the deleted CHILD_SA is the redundant SA of a collision This happens if the peer deletes the redundant SA before we are able to handle the response. The deleted SA will be in state CHILD_INSTALLED but we don't want to trigger the child_updown() event for it or recreate it. --- src/libcharon/sa/ikev2/tasks/child_delete.c | 43 +++++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_delete.c b/src/libcharon/sa/ikev2/tasks/child_delete.c index dd77665f7..b79ccf374 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 #include #include - +#include typedef struct private_child_delete_t private_child_delete_t; @@ -118,6 +119,33 @@ static void build_payloads(private_child_delete_t *this, message_t *message) enumerator->destroy(enumerator); } +/** + * 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 */ @@ -171,8 +199,15 @@ static void process_payloads(private_child_delete_t *this, message_t *message) /* 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: From b6a3c444e4a4600bd6ec48668e9b076597f87211 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 26 May 2016 11:21:45 +0200 Subject: [PATCH 35/84] unit-tests: Add tests where a peer is not aware of a CHILD_SA rekey collision --- src/libcharon/tests/suites/test_child_rekey.c | 355 +++++++++++++++++- 1 file changed, 354 insertions(+), 1 deletion(-) diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index bec3dee7b..eb01dfa1d 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -254,7 +254,9 @@ START_TEST(test_collision) } 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); @@ -268,7 +270,9 @@ START_TEST(test_collision) } 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); @@ -304,6 +308,352 @@ START_TEST(test_collision) } 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 @@ -673,7 +1023,7 @@ END_TEST * either side */ assert_hook_not_called(child_rekey); - /* delay the CREAE_CHILD_SA request */ + /* 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 @@ -761,6 +1111,9 @@ Suite *child_rekey_suite_create() 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); suite_add_tcase(s, tc); From 851ee4eeb7c76de38d34fb34ccac77ae55806f2e Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 26 May 2016 15:08:09 +0200 Subject: [PATCH 36/84] ike-rekey: Establish new IKE_SA earlier as responder, but only if no collision Moving to the new SA only after receiving the DELETE for the old SA was not ideal as it rendered the new SA unusable (because it simply didn't exist in the manager) if the DELETE was delayed/got dropped. --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index a85df114c..ef61a53f1 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 @@ -123,7 +124,6 @@ static void establish_new(private_ike_rekey_t *this) 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); } @@ -232,7 +232,12 @@ METHOD(task_t, build_r, status_t, return SUCCESS; } charon->bus->set_sa(charon->bus, this->ike_sa); - this->ike_sa->set_state(this->ike_sa, IKE_REKEYING); + + 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); + 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); From b7fac1d96ec266a6ebc99b8d4fb4cf9ee5b2e9e5 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 26 May 2016 16:35:18 +0200 Subject: [PATCH 37/84] ike-sa-manager: Make sure rng is defined before destroying it in flush() This allows calling flush() multiple times. --- src/libcharon/sa/ike_sa_manager.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libcharon/sa/ike_sa_manager.c b/src/libcharon/sa/ike_sa_manager.c index 9b9ad93a2..63c9fdfa8 100644 --- a/src/libcharon/sa/ike_sa_manager.c +++ b/src/libcharon/sa/ike_sa_manager.c @@ -2188,7 +2188,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; From 5d10ef316da480a71720c5abe006f1c7066cd67f Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 26 May 2016 16:57:31 +0200 Subject: [PATCH 38/84] unit-tests: Provide a wrapper around bus_t::add_listener and unregister them during cleanup In case listeners on the stack are triggered while cleaning up after a test failed (e.g. via ike_sa_manager_t::flush) remaining listeners defined on the stack would cause a segmentation fault. --- .../tests/utils/exchange_test_helper.c | 20 +++++++++++++++++++ .../tests/utils/exchange_test_helper.h | 12 +++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index 038cca07a..d2187b985 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -18,6 +18,7 @@ #include "mock_ipsec.h" #include "mock_nonce_gen.h" +#include #include typedef struct private_exchange_test_helper_t private_exchange_test_helper_t; @@ -42,6 +43,11 @@ struct private_exchange_test_helper_t { * IKE_SA SPI counter */ refcount_t ike_spi; + + /** + * List of registered listeners + */ + array_t *listeners; }; /** @@ -240,6 +246,13 @@ METHOD(exchange_test_helper_t, establish_sa, void, 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 */ @@ -291,6 +304,7 @@ void exchange_test_helper_init(char *plugins) .sender = mock_sender_create(), .establish_sa = _establish_sa, .process_message = _process_message, + .add_listener = _add_listener, }, .creds = mem_cred_create(), ); @@ -327,13 +341,19 @@ void exchange_test_helper_init(char *plugins) 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); /* can't let charon do it as it happens too late */ 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 index 6530f7564..6b19ec4a8 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.h +++ b/src/libcharon/tests/utils/exchange_test_helper.h @@ -67,6 +67,18 @@ struct exchange_test_helper_t { */ void (*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 { From 7c6e0c2979ca0616539895db28b91314a97f9dc4 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 26 May 2016 17:00:10 +0200 Subject: [PATCH 39/84] unit-tests: Use wrapper for add_listener in bus_t related asserts --- src/libcharon/tests/utils/exchange_test_asserts.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libcharon/tests/utils/exchange_test_asserts.h b/src/libcharon/tests/utils/exchange_test_asserts.h index 906760c8b..32afcc2e4 100644 --- a/src/libcharon/tests/utils/exchange_test_asserts.h +++ b/src/libcharon/tests/utils/exchange_test_asserts.h @@ -183,7 +183,7 @@ do { \ .name = #n, \ ##__VA_ARGS__ \ }; \ - charon->bus->add_listener(charon->bus, &_hook_listener.listener) + exchange_test_helper->add_listener(exchange_test_helper, &_hook_listener.listener) /** * Enforce the most recently initialized hook assertion. @@ -337,7 +337,7 @@ bool exchange_test_asserts_message(listener_t *this, ike_sa_t *ike_sa, .rules = _rules, \ .num_rules = countof(_rules), \ }; \ - charon->bus->add_listener(charon->bus, &_listener.listener); \ + exchange_test_helper->add_listener(exchange_test_helper, &_listener.listener); \ }) #endif /** EXCHANGE_TEST_ASSERTS_H_ @}*/ From b76c1decd4fcb18a897d7f584fbe34a5e440a792 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 27 May 2016 10:07:03 +0200 Subject: [PATCH 40/84] unit-tests: Return status from process_message() --- src/libcharon/tests/utils/exchange_test_helper.c | 7 +++++-- src/libcharon/tests/utils/exchange_test_helper.h | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index d2187b985..c535dfb38 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -179,17 +179,20 @@ METHOD(backend_t, create_peer_cfg_enumerator, enumerator_t*, return enumerator_create_single(this->peer_cfg, NULL); } -METHOD(exchange_test_helper_t, process_message, void, +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; + if (!message) { message = this->public.sender->dequeue(this->public.sender); } charon->bus->set_sa(charon->bus, ike_sa); - ike_sa->process_message(ike_sa, message); + status = ike_sa->process_message(ike_sa, message); charon->bus->set_sa(charon->bus, NULL); message->destroy(message); + return status; } METHOD(exchange_test_helper_t, establish_sa, void, diff --git a/src/libcharon/tests/utils/exchange_test_helper.h b/src/libcharon/tests/utils/exchange_test_helper.h index 6b19ec4a8..e1fdb012a 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.h +++ b/src/libcharon/tests/utils/exchange_test_helper.h @@ -64,9 +64,10 @@ struct exchange_test_helper_t { * @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() */ - void (*process_message)(exchange_test_helper_t *this, ike_sa_t *sa, - message_t *message); + status_t (*process_message)(exchange_test_helper_t *this, ike_sa_t *sa, + message_t *message); /** * Register a listener with the bus. From 735bd4ca14adb752ce6ad6f0b2dbc40173710f19 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 27 May 2016 10:17:53 +0200 Subject: [PATCH 41/84] unit-tests: Make sure to flush the IKE_SA manager before destroying the sender As the static plugin that creates and destroys the default sender was not initialized because of the missing socket the daemon won't destroy our sender. Test cases will eventually have to flush the IKE_SA manager to satisfy the leak detective. However, in case of a test failure and if there are IKE_SAs in the manager the daemon will flush the SAs when deinitializing, which will cause deletes to get sent. This crashes if the sender is already destroyed. --- src/libcharon/tests/utils/exchange_test_helper.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index c535dfb38..ab562271f 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -354,7 +354,9 @@ void exchange_test_helper_deinit() } lib->credmgr->remove_set(lib->credmgr, &this->creds->set); this->creds->destroy(this->creds); - /* can't let charon do it as it happens too late */ + /* 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); From d7b3ee6ccab5959cd3b6ed2a10ada52bc9297a75 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 27 May 2016 10:31:11 +0200 Subject: [PATCH 42/84] unit-tests: Add asserts against IKE_SAs --- src/libcharon/tests/utils/sa_asserts.h | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/libcharon/tests/utils/sa_asserts.h b/src/libcharon/tests/utils/sa_asserts.h index 0d1b9faed..7afa3b55b 100644 --- a/src/libcharon/tests/utils/sa_asserts.h +++ b/src/libcharon/tests/utils/sa_asserts.h @@ -24,6 +24,51 @@ #ifndef SA_ASSERTS_H_ #define SA_ASSERTS_H_ +#include + +/** + * 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. */ From b5695bbffce1aff8f51b67a05150789c7020899e Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 27 May 2016 10:31:42 +0200 Subject: [PATCH 43/84] unit-tests: Add tests for IKE_SA rekeying --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/exchange_tests.h | 1 + src/libcharon/tests/suites/test_ike_rekey.c | 109 ++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 src/libcharon/tests/suites/test_ike_rekey.c diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index c83514546..4e2bdfd02 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -26,6 +26,7 @@ libcharon_tests_LDADD = \ exchange_tests_SOURCES = \ suites/test_child_delete.c \ suites/test_child_rekey.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 \ diff --git a/src/libcharon/tests/exchange_tests.h b/src/libcharon/tests/exchange_tests.h index 8be5f542d..0d0c192d8 100644 --- a/src/libcharon/tests/exchange_tests.h +++ b/src/libcharon/tests/exchange_tests.h @@ -13,5 +13,6 @@ * for more details. */ +TEST_SUITE(ike_rekey_suite_create) TEST_SUITE(child_delete_suite_create) TEST_SUITE(child_rekey_suite_create) 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..d2e365ed1 --- /dev/null +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -0,0 +1,109 @@ +/* + * 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 . + * + * 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 +#include +#include + +/** + * Regular IKE_SA rekey 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); + + assert_hook_not_called(ike_rekey); + call_ikesa(a, rekey); + assert_ike_sa_state(a, IKE_REKEYING); + assert_hook(); + + /* 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_REKEYING); + 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 + +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); + suite_add_tcase(s, tc); + + return s; +} From d6ac7496e639d2af5fdceccaf285e60f93fd39bf Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 27 May 2016 19:16:03 +0200 Subject: [PATCH 44/84] ike-rekey: Add the name/ID of the redundant IKE_SAs to the log messages --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index ef61a53f1..b67f28f67 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -311,20 +311,25 @@ METHOD(task_t, process_i, status_t, /* 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) + min(this_nonce.len, other_nonce.len)) > 0) { /* 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); + 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); + 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)); + charon->ike_sa_manager->checkin(charon->ike_sa_manager, + other->new_sa); other->new_sa = NULL; } else { - DBG1(DBG_IKE, "IKE_SA rekey collision lost, " - "deleting redundant IKE_SA"); + 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)); @@ -338,8 +343,8 @@ METHOD(task_t, process_i, status_t, } else { - charon->ike_sa_manager->checkin( - charon->ike_sa_manager, this->new_sa); + 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; From bb3899739d74356b2f365b73917cd0c8eedbdc92 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Sat, 28 May 2016 09:34:29 +0200 Subject: [PATCH 45/84] ikev2: Add a new state to track rekeyed IKE_SAs This makes handling such IKE_SAs more specifically compared to keeping them in state IKE_CONNECTING or IKE_ESTABLISHED (which we did when we lost a collision - even triggering the ike_updown event), or using IKE_REKEYING for them, which would also be ambiguous. For instance, we can now reject anything but DELETES for such SAs. --- src/libcharon/sa/ike_sa.c | 5 ++- src/libcharon/sa/ike_sa.h | 5 +++ src/libcharon/sa/ike_sa_manager.c | 3 +- src/libcharon/sa/ikev2/task_manager_v2.c | 35 +++++++++++++----- src/libcharon/sa/ikev2/tasks/ike_delete.c | 6 ++- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 41 ++++++--------------- src/libcharon/tests/suites/test_ike_rekey.c | 2 +- 7 files changed, 53 insertions(+), 44 deletions(-) diff --git a/src/libcharon/sa/ike_sa.c b/src/libcharon/sa/ike_sa.c index b7d71e4d6..e81f6ceef 100644 --- a/src/libcharon/sa/ike_sa.c +++ b/src/libcharon/sa/ike_sa.c @@ -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; diff --git a/src/libcharon/sa/ike_sa.h b/src/libcharon/sa/ike_sa.h index 2122867de..199a32c98 100644 --- a/src/libcharon/sa/ike_sa.h +++ b/src/libcharon/sa/ike_sa.h @@ -308,6 +308,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 */ diff --git a/src/libcharon/sa/ike_sa_manager.c b/src/libcharon/sa/ike_sa_manager.c index 63c9fdfa8..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; diff --git a/src/libcharon/sa/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c index 702c383b1..26e5b48a8 100644 --- a/src/libcharon/sa/ikev2/task_manager_v2.c +++ b/src/libcharon/sa/ikev2/task_manager_v2.c @@ -535,6 +535,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 +612,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); } @@ -909,9 +911,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 +951,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 +1017,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 +1367,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 +1409,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)) diff --git a/src/libcharon/sa/ikev2/tasks/ike_delete.c b/src/libcharon/sa/ikev2/tasks/ike_delete.c index e972dba07..72d656aae 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_delete.c +++ b/src/libcharon/sa/ikev2/tasks/ike_delete.c @@ -68,7 +68,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; } @@ -119,11 +120,12 @@ METHOD(task_t, process_r, status_t, switch (this->ike_sa->get_state(this->ike_sa)) { + case IKE_REKEYING: 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: diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index b67f28f67..334749a61 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -84,7 +84,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; } @@ -118,21 +117,11 @@ static void establish_new(private_ike_rekey_t *this) } this->new_sa = NULL; charon->bus->set_sa(charon->bus, this->ike_sa); + + this->ike_sa->set_state(this->ike_sa, IKE_REKEYED); } } -METHOD(task_t, process_r_delete, status_t, - private_ike_rekey_t *this, message_t *message) -{ - 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); -} - METHOD(task_t, build_i_delete, status_t, private_ike_rekey_t *this, message_t *message) { @@ -236,21 +225,12 @@ METHOD(task_t, build_r, status_t, 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); - this->ike_sa->set_state(this->ike_sa, IKE_REKEYING); + /* 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); } - - /* 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); - - return NEED_MORE; + return SUCCESS; } METHOD(task_t, process_i, status_t, @@ -316,11 +296,13 @@ METHOD(task_t, process_i, status_t, /* 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); + 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; @@ -335,7 +317,8 @@ METHOD(task_t, process_i, status_t, 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); + /* 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) { diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c index d2e365ed1..8fa6ea315 100644 --- a/src/libcharon/tests/suites/test_ike_rekey.c +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -51,7 +51,7 @@ START_TEST(test_regular) 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_REKEYING); + 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); From 498a46d22f8c3ae51a01bdf6cba8c7857e608acf Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Sat, 28 May 2016 10:10:18 +0200 Subject: [PATCH 46/84] unit-tests: Add test for simple IKE rekey collision --- src/libcharon/tests/suites/test_ike_rekey.c | 176 +++++++++++++++++++- 1 file changed, 171 insertions(+), 5 deletions(-) diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c index 8fa6ea315..ef0253a08 100644 --- a/src/libcharon/tests/suites/test_ike_rekey.c +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -20,7 +20,17 @@ #include /** - * Regular IKE_SA rekey either initiated by the original initiator or + * 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) @@ -42,10 +52,7 @@ START_TEST(test_regular) assert_hook_not_called(ike_updown); assert_hook_not_called(child_updown); - assert_hook_not_called(ike_rekey); - call_ikesa(a, rekey); - assert_ike_sa_state(a, IKE_REKEYING); - assert_hook(); + initiate_rekey(a); /* CREATE_CHILD_SA { SA, Ni, KEi } --> */ assert_hook_rekey(ike_rekey, 1, 3); @@ -94,6 +101,161 @@ START_TEST(test_regular) } 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 + Suite *ike_rekey_suite_create() { Suite *s; @@ -105,5 +267,9 @@ Suite *ike_rekey_suite_create() tcase_add_loop_test(tc, test_regular, 0, 2); suite_add_tcase(s, tc); + tc = tcase_create("collisions rekey"); + tcase_add_loop_test(tc, test_collision, 0, 4); + suite_add_tcase(s, tc); + return s; } From 40d9a4c892bb18440bc463ba7bc63e0fa31195c3 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 30 May 2016 15:39:38 +0200 Subject: [PATCH 47/84] unit-tests: Only deliver messages to the SA they are addressed to --- src/libcharon/tests/utils/exchange_test_helper.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/libcharon/tests/utils/exchange_test_helper.c b/src/libcharon/tests/utils/exchange_test_helper.c index ab562271f..f32906d5d 100644 --- a/src/libcharon/tests/utils/exchange_test_helper.c +++ b/src/libcharon/tests/utils/exchange_test_helper.c @@ -182,16 +182,24 @@ METHOD(backend_t, create_peer_cfg_enumerator, enumerator_t*, 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; + status_t status = FAILED; + ike_sa_id_t *id; if (!message) { message = this->public.sender->dequeue(this->public.sender); } - charon->bus->set_sa(charon->bus, ike_sa); - status = ike_sa->process_message(ike_sa, message); - charon->bus->set_sa(charon->bus, NULL); + 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; } From 78e8152d018ebb13ccc3d5693dc57ba164c161cf Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 30 May 2016 16:27:47 +0200 Subject: [PATCH 48/84] ike-delete: No need to wait for a response in case of concurrent deletes RFC 7296 explicitly says we SHOULD reply as usual and forget about our own close request. --- src/libcharon/sa/ikev2/tasks/ike_delete.c | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_delete.c b/src/libcharon/sa/ikev2/tasks/ike_delete.c index 72d656aae..c5b82a21e 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_delete.c +++ b/src/libcharon/sa/ikev2/tasks/ike_delete.c @@ -45,11 +45,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, @@ -128,9 +123,6 @@ METHOD(task_t, process_r, status_t, case IKE_REKEYED: this->rekeyed = TRUE; break; - case IKE_DELETING: - this->simultaneous = TRUE; - break; default: break; } @@ -143,11 +135,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); @@ -166,7 +153,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, From 72c295df5b6d5fb2732af7c9d454c7f78115b828 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 30 May 2016 16:30:56 +0200 Subject: [PATCH 49/84] unit-tests: Add tests for IKE SA deletion --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/exchange_tests.h | 1 + src/libcharon/tests/suites/test_ike_delete.c | 137 +++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 src/libcharon/tests/suites/test_ike_delete.c diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 4e2bdfd02..222d23f54 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -26,6 +26,7 @@ libcharon_tests_LDADD = \ exchange_tests_SOURCES = \ 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 \ diff --git a/src/libcharon/tests/exchange_tests.h b/src/libcharon/tests/exchange_tests.h index 0d0c192d8..2e322b68c 100644 --- a/src/libcharon/tests/exchange_tests.h +++ b/src/libcharon/tests/exchange_tests.h @@ -13,6 +13,7 @@ * for more details. */ +TEST_SUITE(ike_delete_suite_create) TEST_SUITE(ike_rekey_suite_create) TEST_SUITE(child_delete_suite_create) TEST_SUITE(child_rekey_suite_create) 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 . + * + * 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 +#include +#include + +/** + * 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; +} From c0976a5e01f0ceacb9a7d44821a5afd7ad3cbdce Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 30 May 2016 16:53:37 +0200 Subject: [PATCH 50/84] ike-rekey: Respond with TEMPORARY_FAILURE if we are deleting the SA --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index 334749a61..9373e9643 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -208,6 +208,11 @@ METHOD(task_t, process_r, status_t, METHOD(task_t, build_r, status_t, private_ike_rekey_t *this, message_t *message) { + if (this->ike_sa->get_state(this->ike_sa) == IKE_DELETING) + { + 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 */ From 8a78e41f295dbe16b590db411747ba6abb6c318c Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 30 May 2016 17:10:51 +0200 Subject: [PATCH 51/84] ike-rekey: Handle TEMPORARY_FAILURE notify --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 30 +++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index 9373e9643..89503fa86 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -69,6 +69,22 @@ struct private_ike_rekey_t { task_t *collision; }; +/** + * 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 */ @@ -251,6 +267,11 @@ METHOD(task_t, process_i, status_t, this->ike_sa->get_id(this->ike_sa), TRUE)); return SUCCESS; } + if (message->get_notify(message, TEMPORARY_FAILURE)) + { + schedule_delayed_rekey(this); + return SUCCESS; + } switch (this->ike_init->task.process(&this->ike_init->task, message)) { @@ -260,14 +281,7 @@ METHOD(task_t, process_i, status_t, this->collision->get_type(this->collision) == TASK_IKE_DELETE || this->collision->get_type(this->collision) == TASK_IKE_REAUTH))) { - 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: From 7015994a947b4d6cfa700d0e3bc029a81e6141b0 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 30 May 2016 17:26:28 +0200 Subject: [PATCH 52/84] unit-tests: Add tests for collisions between IKE_SA rekeying and deletion --- src/libcharon/tests/suites/test_ike_rekey.c | 174 ++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c index ef0253a08..453493341 100644 --- a/src/libcharon/tests/suites/test_ike_rekey.c +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -17,6 +17,7 @@ #include #include +#include #include /** @@ -256,6 +257,174 @@ START_TEST(test_collision) } 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; @@ -271,5 +440,10 @@ Suite *ike_rekey_suite_create() tcase_add_loop_test(tc, test_collision, 0, 4); 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; } From 83191e56b2673b421a396c5d1468a1c9133cbb44 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 30 May 2016 17:59:42 +0200 Subject: [PATCH 53/84] ike-rekey: Respond with TEMPORARY_FAILURE if CHILD_SAs are currently rekeyed/deleted/established --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index 89503fa86..828e7396d 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -67,6 +67,11 @@ 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; }; /** @@ -186,6 +191,7 @@ METHOD(task_t, process_r, status_t, 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; } @@ -201,6 +207,7 @@ METHOD(task_t, process_r, status_t, /* 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); + this->failed_temporarily = TRUE; return NEED_MORE; default: break; @@ -224,7 +231,7 @@ METHOD(task_t, process_r, status_t, METHOD(task_t, build_r, status_t, private_ike_rekey_t *this, message_t *message) { - if (this->ike_sa->get_state(this->ike_sa) == IKE_DELETING) + if (this->failed_temporarily) { message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); return SUCCESS; From 869b7075fcf021f9fb58f469965de4acef1f2012 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 30 May 2016 18:07:53 +0200 Subject: [PATCH 54/84] child-create: Respond with TEMPORARY_FAILURE while rekeying/deleting IKE_SA --- src/libcharon/sa/ikev2/tasks/child_create.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index ea56e161e..33247aa33 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -1232,13 +1232,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; } From 7b3eccfff4c8de7e23fd6bb6425253c828e0a7c1 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 30 May 2016 18:30:51 +0200 Subject: [PATCH 55/84] unit-tests: Add tests for IKE/CHILD rekey collisions --- src/libcharon/tests/suites/test_child_rekey.c | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index eb01dfa1d..098579e9a 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -1097,6 +1097,168 @@ END_TEST */ +/** + * 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; @@ -1123,5 +1285,13 @@ Suite *child_rekey_suite_create() 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; } From 144bebe4b2df3829cc8e02de546b1e5ef888dcee Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 10:08:03 +0200 Subject: [PATCH 56/84] child-delete: Reply as usual when concurrently rekeying the IKE_SA As per RFC 7296, 2.25.2 (what we did before was the behavior described in RFC 4718). --- src/libcharon/sa/ikev2/tasks/child_delete.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_delete.c b/src/libcharon/sa/ikev2/tasks/child_delete.c index b79ccf374..6fa8836ac 100644 --- a/src/libcharon/sa/ikev2/tasks/child_delete.c +++ b/src/libcharon/sa/ikev2/tasks/child_delete.c @@ -382,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); } From 0a2cad40a6fd5a60fc97a3620d4292c9a193d950 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 10:09:29 +0200 Subject: [PATCH 57/84] unit-tests: Add tests for IKE/CHILD delete collisions --- .../tests/suites/test_child_delete.c | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/src/libcharon/tests/suites/test_child_delete.c b/src/libcharon/tests/suites/test_child_delete.c index 51b3f059d..437e919c7 100644 --- a/src/libcharon/tests/suites/test_child_delete.c +++ b/src/libcharon/tests/suites/test_child_delete.c @@ -18,6 +18,7 @@ #include #include #include +#include #include /** @@ -181,6 +182,162 @@ START_TEST(test_collision_drop) } 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; @@ -197,5 +354,13 @@ Suite *child_delete_suite_create() 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; } From 1cca20705abc41939536e03769d2ef9c18557229 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 12:18:44 +0200 Subject: [PATCH 58/84] ikev2: Check for collisions after handling IKE deletion --- src/libcharon/sa/ikev2/task_manager_v2.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libcharon/sa/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c index 26e5b48a8..5f7d81b59 100644 --- a/src/libcharon/sa/ikev2/task_manager_v2.c +++ b/src/libcharon/sa/ikev2/task_manager_v2.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007-2015 Tobias Brunner + * Copyright (C) 2007-2016 Tobias Brunner * Copyright (C) 2007-2010 Martin Willi * Hochschule fuer Technik Rapperswil * @@ -849,6 +849,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; } From 13da616118df48daae1a82bad43829929ec5ad04 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 12:19:53 +0200 Subject: [PATCH 59/84] ike-rekey: Add method to check if there was a rekey collision --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 8 ++++++++ src/libcharon/sa/ikev2/tasks/ike_rekey.h | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index 828e7396d..2d0b8f294 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -380,6 +380,13 @@ 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) { @@ -444,6 +451,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 @@ -37,6 +38,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. * From 6270bbde6a24bdc31def5ba08ae068968fbaa662 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 12:21:01 +0200 Subject: [PATCH 60/84] ike-delete: Handle deletes while rekeying differently if there was a collision We treat these as if we concluded the rekeying, the active ike-rekey task will handle the collision afterwards. --- src/libcharon/sa/ikev2/tasks/ike_delete.c | 42 +++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_delete.c b/src/libcharon/sa/ikev2/tasks/ike_delete.c index c5b82a21e..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 #include - +#include typedef struct private_ike_delete_t private_ike_delete_t; @@ -89,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) { @@ -116,6 +144,16 @@ 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); From 1b989dd4c54735ff6dd4b96a58d8302b4787367a Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 12:22:32 +0200 Subject: [PATCH 61/84] ike-rekey: Properly handle situation if the peer did not notice the rekey collision We conclude the rekeying before deleting the IKE_SA. Waiting for the potential TEMPORARY_FAILURE notify is no good because if that response does not reach us the peer will not retransmit it upon our retransmits of the rekey request if it already deleted the IKE_SA after receiving our response to the delete. --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index 2d0b8f294..e99536e2d 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -392,6 +392,17 @@ METHOD(ike_rekey_t, collide, void, { DBG1(DBG_IKE, "detected %N collision with %N", task_type_names, TASK_IKE_REKEY, task_type_names, other->get_type(other)); + if (other->get_type(other) == TASK_IKE_DELETE) + { + if (this->collision && + this->collision->get_type(this->collision) == TASK_IKE_REKEY) + { + DBG1(DBG_IKE, "peer did not notice IKE_SA rekey collision"); + other->destroy(other); + establish_new((private_ike_rekey_t*)this->collision); + return; + } + } DESTROY_IF(this->collision); this->collision = other; } From 724d65900cca997377d6a6ed684bb5dc88a127d6 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 14:14:26 +0200 Subject: [PATCH 62/84] ike-rekey: Ignore colliding rekey tasks that did not create an IKE_SA This simplifies collision handling and we don't need to know about these tasks when concluding the rekeying we initiated. --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 120 ++++++++++++----------- 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index e99536e2d..715fa033e 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -304,63 +304,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) + 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) { - 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); - - /* 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, - 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; + this->new_sa->destroy(this->new_sa); } else { - 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) - { - 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); } @@ -392,16 +382,34 @@ METHOD(ike_rekey_t, collide, void, { DBG1(DBG_IKE, "detected %N collision with %N", task_type_names, TASK_IKE_REKEY, task_type_names, other->get_type(other)); - if (other->get_type(other) == TASK_IKE_DELETE) + + switch (other->get_type(other)) { - if (this->collision && - this->collision->get_type(this->collision) == TASK_IKE_REKEY) + case TASK_IKE_DELETE: + if (this->collision && + this->collision->get_type(this->collision) == TASK_IKE_REKEY) + { + DBG1(DBG_IKE, "peer did not notice IKE_SA rekey collision"); + other->destroy(other); + establish_new((private_ike_rekey_t*)this->collision); + return; + } + break; + case TASK_IKE_REKEY: { - DBG1(DBG_IKE, "peer did not notice IKE_SA rekey collision"); - other->destroy(other); - establish_new((private_ike_rekey_t*)this->collision); - return; + 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; From 74265761844810985615ff2243145d53408c9b69 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 14:35:22 +0200 Subject: [PATCH 63/84] ike-rekey: There is no passive reauth task, so it will never collide with one --- src/libcharon/sa/ikev2/task_manager_v2.c | 6 ++---- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libcharon/sa/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c index 5f7d81b59..d00896d8d 100644 --- a/src/libcharon/sa/ikev2/task_manager_v2.c +++ b/src/libcharon/sa/ikev2/task_manager_v2.c @@ -761,8 +761,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); @@ -771,8 +770,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); diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index 715fa033e..d7f81e1de 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -284,9 +284,8 @@ 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 (!this->collision || + this->collision->get_type(this->collision) != TASK_IKE_DELETE) { schedule_delayed_rekey(this); } From f67199378df96b330ec5dbf35abd44a0cfb5efa1 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 14:41:19 +0200 Subject: [PATCH 64/84] ike-rekey: Handle undetected collisions also if delete is delayed If the peer does not detect the rekey collision and deletes the old IKE_SA and then receives the colliding rekey request it will respond with TEMPORARY_FAILURE. That notify may arrive before the DELETE does, in which case we may just conclude the rekeying initiated by the peer. Also, since the IKE_SA is destroyed in any case when we receive a delete there is no point in storing the delete task in collide() as process_i() in the ike-rekey task will never be called. --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 42 +++++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index d7f81e1de..dadf872df 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -261,6 +261,28 @@ METHOD(task_t, build_r, status_t, return SUCCESS; } +/** + * 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, private_ike_rekey_t *this, message_t *message) { @@ -274,18 +296,12 @@ METHOD(task_t, process_i, status_t, this->ike_sa->get_id(this->ike_sa), TRUE)); return SUCCESS; } - if (message->get_notify(message, TEMPORARY_FAILURE)) - { - schedule_delayed_rekey(this); - return SUCCESS; - } switch (this->ike_init->task.process(&this->ike_init->task, message)) { case FAILED: /* rekeying failed, fallback to old SA */ - if (!this->collision || - this->collision->get_type(this->collision) != TASK_IKE_DELETE) + if (!conclude_undetected_collision(this)) { schedule_delayed_rekey(this); } @@ -385,15 +401,9 @@ METHOD(ike_rekey_t, collide, void, switch (other->get_type(other)) { case TASK_IKE_DELETE: - if (this->collision && - this->collision->get_type(this->collision) == TASK_IKE_REKEY) - { - DBG1(DBG_IKE, "peer did not notice IKE_SA rekey collision"); - other->destroy(other); - establish_new((private_ike_rekey_t*)this->collision); - return; - } - break; + conclude_undetected_collision(this); + other->destroy(other); + return; case TASK_IKE_REKEY: { private_ike_rekey_t *rekey = (private_ike_rekey_t*)other; From 566134b25a96863d08a1b4b8a0ec150c1eed0dcf Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 14:47:05 +0200 Subject: [PATCH 65/84] unit-tests: Add tests for IKE_SA rekeying if collision is not detected by one peer --- src/libcharon/tests/suites/test_ike_rekey.c | 340 ++++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c index 453493341..2ec06aae9 100644 --- a/src/libcharon/tests/suites/test_ike_rekey.c +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -257,6 +257,343 @@ START_TEST(test_collision) } 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. @@ -438,6 +775,9 @@ Suite *ike_rekey_suite_create() tc = tcase_create("collisions rekey"); tcase_add_loop_test(tc, test_collision, 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"); From 2e33d1f9ae8684dda2fde1839402739a29d3cf86 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 31 May 2016 18:19:17 +0200 Subject: [PATCH 66/84] unit-tests: Add test for rekey collision if one CREATE_CHILD_SA response is delayed --- src/libcharon/tests/suites/test_ike_rekey.c | 221 ++++++++++++++++++++ 1 file changed, 221 insertions(+) diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c index 2ec06aae9..8cbff2798 100644 --- a/src/libcharon/tests/suites/test_ike_rekey.c +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -257,6 +257,226 @@ START_TEST(test_collision) } 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: @@ -775,6 +995,7 @@ Suite *ike_rekey_suite_create() 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_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); From 09711198a7cc90a932e110b20ef587682c1bd01f Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 1 Jun 2016 15:45:01 +0200 Subject: [PATCH 67/84] ike-rekey: Make sure to ignore task when detecting collisions if ike-init subtask failed For instance, if INVALID_KE_PAYLOAD is returned we don't want this task to affect any active rekeying (no new SA has been established so far). --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index dadf872df..44684bca6 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -242,9 +242,10 @@ METHOD(task_t, build_r, status_t, 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; } From 2f211ced9c4ade46f6914777b093a206f2e24515 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 1 Jun 2016 11:14:20 +0200 Subject: [PATCH 68/84] proposal: Parse modpnone as MODP_NONE(0) --- src/libstrongswan/crypto/proposal/proposal_keywords_static.txt | 1 + 1 file changed, 1 insertion(+) 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 From aae95101484400bf93e6143636e27047b68e2971 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 1 Jun 2016 11:28:30 +0200 Subject: [PATCH 69/84] proposal: Handle MODP_NONE in both directions when selecting proposals --- src/libcharon/config/proposal.c | 20 ++++-- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/libcharon_tests.h | 1 + src/libcharon/tests/suites/test_proposal.c | 81 ++++++++++++++++++++++ 4 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 src/libcharon/tests/suites/test_proposal.c diff --git a/src/libcharon/config/proposal.c b/src/libcharon/config/proposal.c index 6675c1d6d..d676dec08 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; } diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 222d23f54..3e60072f3 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -3,6 +3,7 @@ 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 \ diff --git a/src/libcharon/tests/libcharon_tests.h b/src/libcharon/tests/libcharon_tests.h index d17ea041d..f770f464d 100644 --- a/src/libcharon/tests/libcharon_tests.h +++ b/src/libcharon/tests/libcharon_tests.h @@ -24,6 +24,7 @@ * @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_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 . + * + * 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 + +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; +} From f5e8bc18fda03b8d0baf2b54883ddb18f6059807 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 1 Jun 2016 14:53:23 +0200 Subject: [PATCH 70/84] proposal: Remove MODP_NONE from IKE proposals parsed from strings --- src/libcharon/config/proposal.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libcharon/config/proposal.c b/src/libcharon/config/proposal.c index d676dec08..a83acec23 100644 --- a/src/libcharon/config/proposal.c +++ b/src/libcharon/config/proposal.c @@ -453,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) From 3a40d572c6289f0ebb34ab8f094d9a4baa5b8433 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 1 Jun 2016 11:44:33 +0200 Subject: [PATCH 71/84] ike-cfg: Add option to prefer supplied proposals over locally configured ones --- src/libcharon/config/ike_cfg.c | 63 ++++++++++++------- src/libcharon/config/ike_cfg.h | 7 ++- .../sa/ikev1/tasks/aggressive_mode.c | 4 +- src/libcharon/sa/ikev1/tasks/main_mode.c | 4 +- src/libcharon/sa/ikev2/tasks/ike_init.c | 2 +- 5 files changed, 48 insertions(+), 32 deletions(-) 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) { - proposals->reset_enumerator(proposals, supplied_enum); + 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); + } - while (supplied_enum->enumerate(supplied_enum, (void**)&supplied)) + while (prefer_enum->enumerate(prefer_enum, (void**)&proposal)) + { + if (prefer_self) { - selected = stored->select(stored, supplied, private); + proposals->reset_enumerator(proposals, match_enum); + } + else + { + 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/sa/ikev1/tasks/aggressive_mode.c b/src/libcharon/sa/ikev1/tasks/aggressive_mode.c index af3a13924..9bc6a6e35 100644 --- a/src/libcharon/sa/ikev1/tasks/aggressive_mode.c +++ b/src/libcharon/sa/ikev1/tasks/aggressive_mode.c @@ -402,7 +402,7 @@ METHOD(task_t, process_r, 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) { @@ -640,7 +640,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..e702b61c1 100644 --- a/src/libcharon/sa/ikev1/tasks/main_mode.c +++ b/src/libcharon/sa/ikev1/tasks/main_mode.c @@ -394,7 +394,7 @@ METHOD(task_t, process_r, 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) { @@ -641,7 +641,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/ikev2/tasks/ike_init.c b/src/libcharon/sa/ikev2/tasks/ike_init.c index b96969ebe..cb51cfe25 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_init.c +++ b/src/libcharon/sa/ikev2/tasks/ike_init.c @@ -379,7 +379,7 @@ static void process_payloads(private_ike_init_t *this, message_t *message) private = this->ike_sa->supports_extension(this->ike_sa, EXT_STRONGSWAN); this->proposal = this->config->select_proposal(this->config, - proposal_list, private); + proposal_list, private, TRUE); if (!this->proposal) { charon->bus->alert(charon->bus, ALERT_PROPOSAL_MISMATCH_IKE, From f2ea230b91d050a4fddc84a55068107046b1708f Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 1 Jun 2016 12:03:21 +0200 Subject: [PATCH 72/84] child-cfg: Add option to prefer supplied proposals over locally configured ones --- src/libcharon/config/child_cfg.c | 49 +++++++++++++-------- src/libcharon/config/child_cfg.h | 4 +- src/libcharon/sa/ikev1/tasks/quick_mode.c | 8 ++-- src/libcharon/sa/ikev2/tasks/child_create.c | 2 +- 4 files changed, 39 insertions(+), 24 deletions(-) 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); - - /* compare all stored proposals with all supplied. Stored ones are preferred. */ - while (stored_enum->enumerate(stored_enum, &stored)) + if (prefer_self) { - stored = stored->clone(stored); - while (supplied_enum->enumerate(supplied_enum, &supplied)) + 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); + } + + while (prefer_enum->enumerate(prefer_enum, &proposal)) + { + 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/sa/ikev1/tasks/quick_mode.c b/src/libcharon/sa/ikev1/tasks/quick_mode.c index a6cc19390..70bc37016 100644 --- a/src/libcharon/sa/ikev1/tasks/quick_mode.c +++ b/src/libcharon/sa/ikev1/tasks/quick_mode.c @@ -1109,8 +1109,8 @@ 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); + this->proposal = this->config->select_proposal(this->config, list, + FALSE, private, TRUE); list->destroy_offset(list, offsetof(proposal_t, destroy)); get_lifetimes(this); @@ -1323,8 +1323,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/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index 33247aa33..79208dbdf 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -482,7 +482,7 @@ static status_t select_and_install(private_child_create_t *this, private = this->ike_sa->supports_extension(this->ike_sa, EXT_STRONGSWAN); this->proposal = this->config->select_proposal(this->config, - this->proposals, no_dh, private); + this->proposals, no_dh, private, TRUE); if (this->proposal == NULL) { DBG1(DBG_IKE, "no acceptable proposal found"); From 0a954d678979624806a2d250208530291da34e32 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 1 Jun 2016 12:18:56 +0200 Subject: [PATCH 73/84] ike: Add configuration option to switch to preferring supplied proposals over local ones --- conf/options/charon.opt | 5 +++++ src/libcharon/sa/ikev1/tasks/aggressive_mode.c | 5 ++++- src/libcharon/sa/ikev1/tasks/main_mode.c | 8 +++++--- src/libcharon/sa/ikev1/tasks/quick_mode.c | 6 ++++-- src/libcharon/sa/ikev2/tasks/child_create.c | 6 ++++-- src/libcharon/sa/ikev2/tasks/ike_init.c | 6 ++++-- 6 files changed, 26 insertions(+), 10 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/sa/ikev1/tasks/aggressive_mode.c b/src/libcharon/sa/ikev1/tasks/aggressive_mode.c index 9bc6a6e35..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, TRUE); + list, FALSE, prefer_configured); 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 e702b61c1..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, TRUE); + list, private, prefer_configured); 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 70bc37016..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); + 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, TRUE); + FALSE, private, prefer_configured); list->destroy_offset(list, offsetof(proposal_t, destroy)); get_lifetimes(this); diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index 79208dbdf..13064e32b 100644 --- a/src/libcharon/sa/ikev2/tasks/child_create.c +++ b/src/libcharon/sa/ikev2/tasks/child_create.c @@ -464,7 +464,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 +481,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, TRUE); + this->proposals, no_dh, private, prefer_configured); if (this->proposal == NULL) { DBG1(DBG_IKE, "no acceptable proposal found"); diff --git a/src/libcharon/sa/ikev2/tasks/ike_init.c b/src/libcharon/sa/ikev2/tasks/ike_init.c index cb51cfe25..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, TRUE); + proposal_list, private, prefer_configured); if (!this->proposal) { charon->bus->alert(charon->bus, ALERT_PROPOSAL_MISMATCH_IKE, From 46cbdcace9174c308a6240c9392057fe81be3195 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 1 Jun 2016 15:49:45 +0200 Subject: [PATCH 74/84] unit-tests: Add tests for IKE rekeying if INVALID_KE_PAYLOAD notifies are received --- src/libcharon/tests/suites/test_ike_rekey.c | 470 ++++++++++++++++++++ 1 file changed, 470 insertions(+) diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c index 8cbff2798..ba39657a4 100644 --- a/src/libcharon/tests/suites/test_ike_rekey.c +++ b/src/libcharon/tests/suites/test_ike_rekey.c @@ -102,6 +102,105 @@ START_TEST(test_regular) } 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. @@ -257,6 +356,374 @@ START_TEST(test_collision) } 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 @@ -991,10 +1458,13 @@ Suite *ike_rekey_suite_create() 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); From 2045cadc1cdb5e290ab1deb63d884da0f321a0e6 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 2 Jun 2016 15:13:24 +0200 Subject: [PATCH 75/84] ike-rekey: Return TEMPORARY_FAILURE when concurrently creating a CHILD_SA --- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 49 +++++++++++++++++------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c index 44684bca6..2f0552a33 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -182,38 +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"); - this->failed_temporarily = TRUE; - 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); - this->failed_temporarily = TRUE; - 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); From 4e2e7d4fecdc583827b8ce7c8429f4d4bce8df0d Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 2 Jun 2016 15:24:36 +0200 Subject: [PATCH 76/84] ike: Reduce RETRY_INTERVAL a bit Retry exchanges between 5 and 15 seconds after a temporary failure. --- src/libcharon/sa/ike_sa.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libcharon/sa/ike_sa.h b/src/libcharon/sa/ike_sa.h index 199a32c98..596399d2d 100644 --- a/src/libcharon/sa/ike_sa.h +++ b/src/libcharon/sa/ike_sa.h @@ -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. From 208678e6e1b8222073cc684392d08c839edeb50e Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 2 Jun 2016 15:50:11 +0200 Subject: [PATCH 77/84] ikev2: Add possibility to delay initiation of a queued task Such a task is not initiated unless a certain time has passed. This allows delaying certain tasks but avoids problems if we'd do this via a scheduled job (e.g. if the IKE_SA is rekeyed in the meantime). If the IKE_SA is rekeyed the delay of such tasks is reset when the tasks are adopted i.e. they get executed immediately on the new IKE_SA. This hasn't been implemented for IKEv1 yet. --- src/libcharon/sa/ike_sa.c | 11 +- src/libcharon/sa/ike_sa.h | 13 +- src/libcharon/sa/ikev1/task_manager_v1.c | 13 +- src/libcharon/sa/ikev2/task_manager_v2.c | 203 ++++++++++++++++------- src/libcharon/sa/task_manager.h | 15 +- 5 files changed, 189 insertions(+), 66 deletions(-) diff --git a/src/libcharon/sa/ike_sa.c b/src/libcharon/sa/ike_sa.c index e81f6ceef..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 @@ -2620,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) { @@ -2938,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 596399d2d..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 @@ -1123,6 +1123,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. * 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/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c index d00896d8d..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-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 #include #include +#include #ifdef ME #include #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 @@ -181,6 +165,22 @@ struct private_task_manager_t { bool make_before_break; }; +/** + * 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 */ @@ -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; } @@ -1524,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); @@ -1544,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); } /** @@ -1555,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; @@ -1639,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); @@ -1670,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); @@ -1801,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); @@ -1840,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, @@ -1855,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); @@ -1874,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); @@ -1886,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) { @@ -1902,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(); } @@ -1938,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/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. From b92f11c3bd2b526bb751fc18ba60bd999359edc0 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 2 Jun 2016 16:01:48 +0200 Subject: [PATCH 78/84] child-create: Retry creating the CHILD_SA if TEMPORARY_FAILURE is received We queue a delayed task that is initiated after a while. --- src/libcharon/sa/ikev2/tasks/child_create.c | 37 ++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_create.c b/src/libcharon/sa/ikev2/tasks/child_create.c index 13064e32b..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 #include #include - +#include typedef struct private_child_create_t private_child_create_t; @@ -204,6 +204,25 @@ struct private_child_create_t { bool retry; }; +/** + * 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 */ @@ -1434,7 +1453,6 @@ METHOD(task_t, process_i, status_t, case FAILED_CP_REQUIRED: case TS_UNACCEPTABLE: case INVALID_SELECTORS: - case TEMPORARY_FAILURE: { DBG1(DBG_IKE, "received %N notify, no CHILD_SA built", notify_type_names, type); @@ -1444,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; From b4f24ac0f65c24844369491d5b0c0d4546a51720 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 2 Jun 2016 16:03:30 +0200 Subject: [PATCH 79/84] unit-tests: Add test for collision between IKE_SA rekey and CHILD_SA creation --- src/libcharon/tests/Makefile.am | 1 + src/libcharon/tests/exchange_tests.h | 1 + .../tests/suites/test_child_create.c | 106 ++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/libcharon/tests/suites/test_child_create.c diff --git a/src/libcharon/tests/Makefile.am b/src/libcharon/tests/Makefile.am index 3e60072f3..b8670246b 100644 --- a/src/libcharon/tests/Makefile.am +++ b/src/libcharon/tests/Makefile.am @@ -25,6 +25,7 @@ libcharon_tests_LDADD = \ exchange_tests_SOURCES = \ + suites/test_child_create.c \ suites/test_child_delete.c \ suites/test_child_rekey.c \ suites/test_ike_delete.c \ diff --git a/src/libcharon/tests/exchange_tests.h b/src/libcharon/tests/exchange_tests.h index 2e322b68c..30086721f 100644 --- a/src/libcharon/tests/exchange_tests.h +++ b/src/libcharon/tests/exchange_tests.h @@ -15,5 +15,6 @@ 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/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 . + * + * 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 +#include +#include +#include +#include + +/** + * 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; +} From 6d8041d46b75a1f2b023a00f2eac5228263c9f17 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 2 Jun 2016 17:23:35 +0200 Subject: [PATCH 80/84] child-rekey: Ignore failed colliding CHILD_SA rekeyings If a passive rekeying fails due to an INVALID_KE_PAYLOAD we don't want to consider this task later when resolving collisions. This previously might have caused the wrong SA to get deleted/installed based on the nonces in the unsuccessful exchange. --- src/libcharon/sa/ikev2/tasks/child_rekey.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 5873a84af..95e4a3dae 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -455,9 +455,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; } From d707a1973362386a5feb4c16f5dba44de01d1ac6 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 2 Jun 2016 17:28:03 +0200 Subject: [PATCH 81/84] unit-tests: Add test for CHILD_SA rekey if a retry due to an INVALID_KE_PAYLOAD is delayed --- src/libcharon/tests/suites/test_child_rekey.c | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index 098579e9a..b4f7868e7 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -703,7 +703,10 @@ START_TEST(test_collision_ke_invalid) { { 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 */ @@ -817,6 +820,145 @@ START_TEST(test_collision_ke_invalid) } 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. @@ -1277,6 +1419,7 @@ Suite *child_rekey_suite_create() 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"); From de4e4687ff8357c30483c5cba39c7cc2a24bbb6c Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 2 Jun 2016 18:15:27 +0200 Subject: [PATCH 82/84] Report test coverage of libcharon and starter --- src/libcharon/Makefile.am | 3 +++ src/starter/Makefile.am | 3 +++ 2 files changed, 6 insertions(+) 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/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 = \ From 15cea08adc6a2700c401a23ce1838d33f31e26f5 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 10 Jun 2016 16:00:25 +0200 Subject: [PATCH 83/84] child-rekey: Only rekey installed CHILD_SAs Depending on the lifetimes a CHILD_SA we rekeyed as responder might expire shortly afterwards. We don't want to rekey it again. When retrying due to an INVALID_KE_PAYLOAD notify the expected state is CHILD_REKEYING if it is anything else (e.g. due to a collision) we ignore it. We also abort the exchange properly if we don't find the CHILD_SA, no need for an empty INFORMATIONAL exchange anymore. --- src/libcharon/sa/ikev2/tasks/child_rekey.c | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c index 95e4a3dae..c04ec141f 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -160,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); From 5435a9a062453b460125c450c755d46c6c98684c Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 10 Jun 2016 16:41:47 +0200 Subject: [PATCH 84/84] unit-tests: Add tests for expires after CHILD_SA rekeying --- src/libcharon/tests/suites/test_child_rekey.c | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c index b4f7868e7..fcac49388 100644 --- a/src/libcharon/tests/suites/test_child_rekey.c +++ b/src/libcharon/tests/suites/test_child_rekey.c @@ -184,6 +184,133 @@ START_TEST(test_regular_ke_invalid) } 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. @@ -1411,6 +1538,8 @@ Suite *child_rekey_suite_create() 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");