/*
 * Copyright (C) 2016-2024 Tobias Brunner
 * Copyright (C) 2008 Martin Willi
 *
 * Copyright (C) secunet Security Networks AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

#include "ha_dispatcher.h"

#include <daemon.h>
#include <sa/ikev2/keymat_v2.h>
#include <sa/ikev1/keymat_v1.h>
#include <processing/jobs/callback_job.h>
#include <processing/jobs/adopt_children_job.h>

typedef struct private_ha_dispatcher_t private_ha_dispatcher_t;
typedef struct ha_key_exchange_t ha_key_exchange_t;

/**
 * Private data of an ha_dispatcher_t object.
 */
struct private_ha_dispatcher_t {

	/**
	 * Public ha_dispatcher_t interface.
	 */
	ha_dispatcher_t public;

	/**
	 * socket to pull messages from
	 */
	ha_socket_t *socket;

	/**
	 * segments to control
	 */
	ha_segments_t *segments;

	/**
	 * Cache for resync
	 */
	ha_cache_t *cache;

	/**
	 * Kernel helper
	 */
	ha_kernel_t *kernel;

	/**
	 * HA enabled pool
	 */
	ha_attribute_t *attr;
};

/**
 * KE implementation for HA synced KE shared secrets
 */
struct ha_key_exchange_t {

	/**
	 * Public interface
	 */
	key_exchange_t ke;

	/**
	 * Shared secret
	 */
	chunk_t secret;

	/**
	 * Own public value (IKEv1 only)
	 */
	chunk_t pub;
};

METHOD(key_exchange_t, ke_get_shared_secret, bool,
	ha_key_exchange_t *this, chunk_t *secret)
{
	*secret = chunk_clone(this->secret);
	return TRUE;
}

METHOD(key_exchange_t, ke_get_public_key, bool,
	ha_key_exchange_t *this, chunk_t *value)
{
	*value = chunk_clone(this->pub);
	return TRUE;
}

METHOD(key_exchange_t, ke_destroy, void,
	ha_key_exchange_t *this)
{
	free(this);
}

/**
 * Create a HA synced KE implementation
 */
static key_exchange_t *ha_key_exchange_create(chunk_t secret, chunk_t pub)
{
	ha_key_exchange_t *this;

	INIT(this,
		.ke = {
			.get_shared_secret = _ke_get_shared_secret,
			.get_public_key = _ke_get_public_key,
			.destroy = _ke_destroy,
		},
		.secret = secret,
		.pub = pub,
	);

	return &this->ke;
}

/**
 * Add the given KE methods to a proposal
 */
static void add_ke_methods_to_proposal(proposal_t *proposal, uint16_t ke_alg,
									   chunk_t add_kes)
{
	int i, count;

	if (ke_alg)
	{
		proposal->add_algorithm(proposal, KEY_EXCHANGE_METHOD, ke_alg, 0);
	}
	count = min(add_kes.len / sizeof(uint16_t), MAX_ADDITIONAL_KEY_EXCHANGES);
	for (i = 0; i < count; i++)
	{
		ke_alg = ntohs(((uint16_t*)add_kes.ptr)[i]);
		if (ke_alg)
		{
			proposal->add_algorithm(proposal, i + ADDITIONAL_KEY_EXCHANGE_1,
									ke_alg, 0);
		}
	}
}

/**
 * Process messages of type IKE_ADD
 */
static void process_ike_add(private_ha_dispatcher_t *this, ha_message_t *message)
{
	ha_message_attribute_t attribute;
	ha_message_value_t value;
	enumerator_t *enumerator;
	ike_sa_t *ike_sa = NULL, *old_sa = NULL;
	ike_version_t version = IKEV2;
	uint16_t encr = 0, len = 0, integ = 0, prf = 0, old_prf = PRF_UNDEFINED;
	uint16_t ke_alg = 0;
	chunk_t nonce_i = chunk_empty, nonce_r = chunk_empty;
	chunk_t secret = chunk_empty, old_skd = chunk_empty;
	chunk_t add_secret = chunk_empty, add_kes = chunk_empty;
	chunk_t dh_local = chunk_empty, dh_remote = chunk_empty, psk = chunk_empty;
	host_t *other = NULL;
	bool ok = FALSE;
	auth_method_t method = AUTH_RSA;

	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		switch (attribute)
		{
			case HA_IKE_ID:
				ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager,
														  value.ike_sa_id);
				if (!ike_sa)
				{
					ike_sa = ike_sa_create(value.ike_sa_id,
						value.ike_sa_id->is_initiator(value.ike_sa_id), version);
				}
				break;
			case HA_IKE_REKEY_ID:
				old_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager,
														  value.ike_sa_id);
				break;
			case HA_REMOTE_ADDR:
				other = value.host->clone(value.host);
				break;
			case HA_IKE_VERSION:
				version = value.u8;
				break;
			case HA_NONCE_I:
				nonce_i = value.chunk;
				break;
			case HA_NONCE_R:
				nonce_r = value.chunk;
				break;
			case HA_SECRET:
				secret = value.chunk;
				break;
			case HA_ADD_SECRET:
				add_secret = value.chunk;
				break;
			case HA_LOCAL_DH:
				dh_local = value.chunk;
				break;
			case HA_REMOTE_DH:
				dh_remote = value.chunk;
				break;
			case HA_PSK:
				psk = value.chunk;
				break;
			case HA_OLD_SKD:
				old_skd = value.chunk;
				break;
			case HA_ALG_ENCR:
				encr = value.u16;
				break;
			case HA_ALG_ENCR_LEN:
				len = value.u16;
				break;
			case HA_ALG_INTEG:
				integ = value.u16;
				break;
			case HA_ALG_PRF:
				prf = value.u16;
				break;
			case HA_ALG_OLD_PRF:
				old_prf = value.u16;
				break;
			case HA_ALG_KE:
				ke_alg = value.u16;
				break;
			case HA_ALG_ADD_KES:
				add_kes = value.chunk;
				break;
			case HA_AUTH_METHOD:
				method = value.u16;
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);

	if (ike_sa)
	{
		proposal_t *proposal;
		key_exchange_t *ke;
		array_t *kes = NULL;
		bool key_update = FALSE;

		proposal = ike_sa->get_proposal(ike_sa);
		if (!proposal)
		{
			proposal = proposal_create(PROTO_IKE, 0);
			if (integ)
			{
				proposal->add_algorithm(proposal, INTEGRITY_ALGORITHM, integ, 0);
			}
			if (encr)
			{
				proposal->add_algorithm(proposal, ENCRYPTION_ALGORITHM, encr, len);
			}
			if (prf)
			{
				proposal->add_algorithm(proposal, PSEUDO_RANDOM_FUNCTION, prf, 0);
			}
			add_ke_methods_to_proposal(proposal, ke_alg, add_kes);
		}
		else
		{
			key_update = TRUE;
		}
		charon->bus->set_sa(charon->bus, ike_sa);
		ke = ha_key_exchange_create(secret, dh_local);
		array_insert_create(&kes, ARRAY_HEAD, ke);
		if (ike_sa->get_version(ike_sa) == IKEV2)
		{
			keymat_v2_t *keymat_v2 = (keymat_v2_t*)ike_sa->get_keymat(ike_sa);

			if (add_secret.len)
			{
				ke = ha_key_exchange_create(add_secret, chunk_empty);
				array_insert_create(&kes, ARRAY_TAIL, ke);
			}
			if (key_update)
			{
				old_prf = keymat_v2->get_skd(keymat_v2, &old_skd);
			}
			ok = keymat_v2->derive_ike_keys(keymat_v2, proposal, kes, nonce_i,
							nonce_r, ike_sa->get_id(ike_sa), old_prf, old_skd);
		}
		else if (ike_sa->get_version(ike_sa) == IKEV1)
		{
			keymat_v1_t *keymat_v1 = (keymat_v1_t*)ike_sa->get_keymat(ike_sa);
			shared_key_t *shared = NULL;

			if (psk.len)
			{
				method = AUTH_PSK;
				shared = shared_key_create(SHARED_IKE, chunk_clone(psk));
			}
			if (keymat_v1->create_hasher(keymat_v1, proposal))
			{
				ok = keymat_v1->derive_ike_keys(keymat_v1, proposal,
								ke, dh_remote, nonce_i, nonce_r,
								ike_sa->get_id(ike_sa), method, shared);
			}
			DESTROY_IF(shared);
		}
		array_destroy_offset(kes, offsetof(key_exchange_t, destroy));
		if (ok)
		{
			if (old_sa)
			{	/* register IKE_SA before calling inherit_post() so no scheduled
				 * jobs are lost */
				charon->ike_sa_manager->checkout_new(charon->ike_sa_manager,
													 ike_sa);
				ike_sa->inherit_pre(ike_sa, old_sa);
				ike_sa->inherit_post(ike_sa, old_sa);
				charon->ike_sa_manager->checkin_and_destroy(
												charon->ike_sa_manager, old_sa);
				old_sa = NULL;
			}
			if (other)
			{
				ike_sa->set_other_host(ike_sa, other);
				other = NULL;
			}
			if (!key_update)
			{
				ike_sa->set_state(ike_sa, IKE_CONNECTING);
				ike_sa->set_proposal(ike_sa, proposal);
			}
			this->cache->cache(this->cache, ike_sa, message);
			message = NULL;
			charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
		}
		else
		{
			DBG1(DBG_IKE, "HA keymat derivation failed");
			if (key_update)
			{
				charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager,
															ike_sa);
			}
			else
			{
				ike_sa->destroy(ike_sa);
				charon->bus->set_sa(charon->bus, NULL);
			}
		}
		if (!key_update)
		{
			proposal->destroy(proposal);
		}
	}
	if (old_sa)
	{
		charon->ike_sa_manager->checkin(charon->ike_sa_manager, old_sa);
	}
	DESTROY_IF(other);
	DESTROY_IF(message);
}

/**
 * Apply all set conditions to the IKE_SA
 */
static void set_conditions(ike_sa_t *ike_sa, ike_condition_t conditions)
{
	ike_condition_t i, private = (conditions & COND_PRIVATE_MARKER);

	for (i = 0; i < (sizeof(i) * 8) - 1; ++i)
	{
		ike_condition_t cond = (1 << i) | private;

		ike_sa->set_condition(ike_sa, cond, (conditions & cond) == cond);
	}
}

/**
 * Apply all enabled extensions to the IKE_SA
 */
static void set_extensions(ike_sa_t *ike_sa, ike_extension_t extensions)
{
	ike_extension_t i, private = (extensions & EXT_PRIVATE_MARKER);

	for (i = 0; i < (sizeof(i) * 8) - 1; ++i)
	{
		ike_extension_t ext = (1 << i) | private;

		if ((extensions & ext) == ext)
		{
			ike_sa->enable_extension(ike_sa, ext);
		}
	}
}

/**
 * Process messages of type IKE_UPDATE
 */
static void process_ike_update(private_ha_dispatcher_t *this,
							   ha_message_t *message)
{
	ha_message_attribute_t attribute;
	ha_message_value_t value;
	enumerator_t *enumerator;
	ike_sa_t *ike_sa = NULL;
	peer_cfg_t *peer_cfg = NULL;
	auth_cfg_t *auth;
	bool received_vip = FALSE, first_local_vip = TRUE, first_peer_addr = TRUE;

	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		if (attribute != HA_IKE_ID && ike_sa == NULL)
		{
			/* must be first attribute */
			break;
		}
		switch (attribute)
		{
			case HA_IKE_ID:
				ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager,
														  value.ike_sa_id);
				break;
			case HA_LOCAL_ID:
				ike_sa->set_my_id(ike_sa, value.id->clone(value.id));
				break;
			case HA_REMOTE_ID:
				ike_sa->set_other_id(ike_sa, value.id->clone(value.id));
				break;
			case HA_REMOTE_EAP_ID:
				auth = auth_cfg_create();
				auth->add(auth, AUTH_RULE_EAP_IDENTITY, value.id->clone(value.id));
				ike_sa->add_auth_cfg(ike_sa, FALSE, auth);
				break;
			case HA_LOCAL_ADDR:
				ike_sa->set_my_host(ike_sa, value.host->clone(value.host));
				break;
			case HA_REMOTE_ADDR:
				ike_sa->set_other_host(ike_sa, value.host->clone(value.host));
				break;
			case HA_LOCAL_VIP:
				if (first_local_vip)
				{
					ike_sa->clear_virtual_ips(ike_sa, TRUE);
					first_local_vip = FALSE;
				}
				ike_sa->add_virtual_ip(ike_sa, TRUE, value.host);
				break;
			case HA_REMOTE_VIP:
				if (!received_vip)
				{
					ike_sa->clear_virtual_ips(ike_sa, FALSE);
				}
				ike_sa->add_virtual_ip(ike_sa, FALSE, value.host);
				received_vip = TRUE;
				break;
			case HA_PEER_ADDR:
				if (first_peer_addr)
				{
					ike_sa->clear_peer_addresses(ike_sa);
					first_peer_addr = FALSE;
				}
				ike_sa->add_peer_address(ike_sa, value.host->clone(value.host));
				break;
			case HA_CONFIG_NAME:
				peer_cfg = charon->backends->get_peer_cfg_by_name(
												charon->backends, value.str);
				if (peer_cfg)
				{
					ike_sa->set_peer_cfg(ike_sa, peer_cfg);
					peer_cfg->destroy(peer_cfg);
				}
				else
				{
					DBG1(DBG_IKE, "HA is missing nodes peer configuration");
					charon->ike_sa_manager->checkin_and_destroy(
												charon->ike_sa_manager, ike_sa);
					ike_sa = NULL;
				}
				break;
			case HA_EXTENSIONS:
				set_extensions(ike_sa, value.u32);
				break;
			case HA_CONDITIONS:
				set_conditions(ike_sa, value.u32);
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);

	if (ike_sa)
	{
		if (ike_sa->get_state(ike_sa) == IKE_CONNECTING &&
			ike_sa->get_peer_cfg(ike_sa))
		{
			DBG1(DBG_CFG, "installed HA passive IKE_SA '%s' %H[%Y]...%H[%Y]",
				 ike_sa->get_name(ike_sa),
				 ike_sa->get_my_host(ike_sa), ike_sa->get_my_id(ike_sa),
				 ike_sa->get_other_host(ike_sa), ike_sa->get_other_id(ike_sa));
			ike_sa->set_state(ike_sa, IKE_PASSIVE);
		}
		if (received_vip)
		{
			enumerator_t *pools, *vips;
			host_t *vip;
			char *pool;

			peer_cfg = ike_sa->get_peer_cfg(ike_sa);
			if (peer_cfg)
			{
				pools = peer_cfg->create_pool_enumerator(peer_cfg);
				while (pools->enumerate(pools, &pool))
				{
					vips = ike_sa->create_virtual_ip_enumerator(ike_sa, FALSE);
					while (vips->enumerate(vips, &vip))
					{
						this->attr->reserve(this->attr, pool, vip);
					}
					vips->destroy(vips);
				}
				pools->destroy(pools);
			}
		}
#ifdef USE_IKEV1
		if (ike_sa->get_version(ike_sa) == IKEV1)
		{
			lib->processor->queue_job(lib->processor, (job_t*)
							adopt_children_job_create(ike_sa->get_id(ike_sa)));
		}
#endif /* USE_IKEV1 */
		this->cache->cache(this->cache, ike_sa, message);
		charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
	}
	else
	{
		DBG1(DBG_CFG, "passive HA IKE_SA to update not found");
		message->destroy(message);
	}
}

/**
 * Process messages of type IKE_MID_INITIATOR/RESPONDER
 */
static void process_ike_mid(private_ha_dispatcher_t *this,
							   ha_message_t *message, bool initiator)
{
	ha_message_attribute_t attribute;
	ha_message_value_t value;
	enumerator_t *enumerator;
	ike_sa_t *ike_sa = NULL;
	uint32_t mid = 0;

	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		switch (attribute)
		{
			case HA_IKE_ID:
				ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager,
														  value.ike_sa_id);
				break;
			case HA_MID:
				mid = value.u32;
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);

	if (ike_sa)
	{
		if (mid)
		{
			ike_sa->set_message_id(ike_sa, initiator, mid);
		}
		this->cache->cache(this->cache, ike_sa, message);
		charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
	}
	else
	{
		message->destroy(message);
	}
}

/**
 * Process messages of type IKE_IV
 */
static void process_ike_iv(private_ha_dispatcher_t *this, ha_message_t *message)
{
	ha_message_attribute_t attribute;
	ha_message_value_t value;
	enumerator_t *enumerator;
	ike_sa_t *ike_sa = NULL;
	chunk_t iv = chunk_empty;

	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		switch (attribute)
		{
			case HA_IKE_ID:
				ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager,
														  value.ike_sa_id);
				break;
			case HA_IV:
				iv = value.chunk;
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);

	if (ike_sa)
	{
		if (ike_sa->get_version(ike_sa) == IKEV1)
		{
			if (iv.len)
			{
				keymat_v1_t *keymat;

				keymat = (keymat_v1_t*)ike_sa->get_keymat(ike_sa);
				if (keymat->update_iv(keymat, 0, iv))
				{
					keymat->confirm_iv(keymat, 0);
				}
			}
		}
		this->cache->cache(this->cache, ike_sa, message);
		charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
	}
	else
	{
		message->destroy(message);
	}
}

/**
 * Process messages of type IKE_DELETE
 */
static void process_ike_delete(private_ha_dispatcher_t *this,
							   ha_message_t *message)
{
	ha_message_attribute_t attribute;
	ha_message_value_t value;
	enumerator_t *enumerator;
	ike_sa_t *ike_sa = NULL;

	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		switch (attribute)
		{
			case HA_IKE_ID:
				ike_sa = charon->ike_sa_manager->checkout(
									charon->ike_sa_manager, value.ike_sa_id);
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);
	if (ike_sa)
	{
		this->cache->cache(this->cache, ike_sa, message);
		charon->ike_sa_manager->checkin_and_destroy(
						charon->ike_sa_manager, ike_sa);
	}
	else
	{
		message->destroy(message);
	}
}

/**
 * Lookup a child cfg from the peer cfg by name
 */
static child_cfg_t* find_child_cfg(ike_sa_t *ike_sa, char *name)
{
	peer_cfg_t *peer_cfg;
	child_cfg_t *current, *found = NULL;
	enumerator_t *enumerator;

	peer_cfg = ike_sa->get_peer_cfg(ike_sa);
	if (peer_cfg)
	{
		enumerator = peer_cfg->create_child_cfg_enumerator(peer_cfg);
		while (enumerator->enumerate(enumerator, &current))
		{
			if (streq(current->get_name(current), name))
			{
				found = current;
				break;
			}
		}
		enumerator->destroy(enumerator);
	}
	return found;
}

/**
 * Process messages of type CHILD_ADD
 */
static void process_child_add(private_ha_dispatcher_t *this,
							  ha_message_t *message)
{
	ha_message_attribute_t attribute;
	ha_message_value_t value;
	enumerator_t *enumerator;
	ike_sa_t *ike_sa = NULL;
	char *config_name = "";
	child_cfg_t *config = NULL;
	child_sa_t *child_sa;
	proposal_t *proposal;
	bool initiator = FALSE, failed = FALSE, ok = FALSE;
	uint32_t inbound_spi = 0, outbound_spi = 0;
	uint16_t inbound_cpi = 0, outbound_cpi = 0;
	uint8_t mode = MODE_TUNNEL, ipcomp = 0;
	uint16_t encr = 0, integ = 0, len = 0, ke_alg = 0;
	uint16_t esn = NO_EXT_SEQ_NUMBERS;
	chunk_t nonce_i = chunk_empty, nonce_r = chunk_empty, secret = chunk_empty;
	chunk_t add_secret = chunk_empty, add_kes = chunk_empty;
	chunk_t encr_i, integ_i, encr_r, integ_r;
	linked_list_t *local_ts, *remote_ts;
	key_exchange_t *ke = NULL;
	array_t *kes = NULL;

	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		switch (attribute)
		{
			case HA_IKE_ID:
				ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager,
														  value.ike_sa_id);
				break;
			case HA_CONFIG_NAME:
				config_name = value.str;
				break;
			case HA_INITIATOR:
				initiator = value.u8;
				break;
			case HA_INBOUND_SPI:
				inbound_spi = value.u32;
				break;
			case HA_OUTBOUND_SPI:
				outbound_spi = value.u32;
				break;
			case HA_INBOUND_CPI:
				inbound_cpi = value.u32;
				break;
			case HA_OUTBOUND_CPI:
				outbound_cpi = value.u32;
				break;
			case HA_IPSEC_MODE:
				mode = value.u8;
				break;
			case HA_IPCOMP:
				ipcomp = value.u8;
				break;
			case HA_ALG_ENCR:
				encr = value.u16;
				break;
			case HA_ALG_ENCR_LEN:
				len = value.u16;
				break;
			case HA_ALG_INTEG:
				integ = value.u16;
				break;
			case HA_ALG_KE:
				ke_alg = value.u16;
				break;
			case HA_ALG_ADD_KES:
				add_kes = value.chunk;
				break;
			case HA_ESN:
				esn = value.u16;
				break;
			case HA_NONCE_I:
				nonce_i = value.chunk;
				break;
			case HA_NONCE_R:
				nonce_r = value.chunk;
				break;
			case HA_SECRET:
				secret = value.chunk;
				break;
			case HA_ADD_SECRET:
				add_secret = value.chunk;
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);

	if (!ike_sa)
	{
		DBG1(DBG_CHD, "IKE_SA for HA CHILD_SA not found");
		message->destroy(message);
		return;
	}
	config = find_child_cfg(ike_sa, config_name);
	if (!config)
	{
		DBG1(DBG_CHD, "HA is missing nodes child configuration");
		charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
		message->destroy(message);
		return;
	}

	child_sa_create_t data = {
		.encap = ike_sa->has_condition(ike_sa, COND_NAT_ANY),
		.cpu = CPU_ID_MAX,
	};
	child_sa = child_sa_create(ike_sa->get_my_host(ike_sa),
							   ike_sa->get_other_host(ike_sa), config, &data);
	child_sa->set_mode(child_sa, mode);
	child_sa->set_protocol(child_sa, PROTO_ESP);
	child_sa->set_ipcomp(child_sa, ipcomp);

	proposal = proposal_create(PROTO_ESP, 0);
	if (integ)
	{
		proposal->add_algorithm(proposal, INTEGRITY_ALGORITHM, integ, 0);
	}
	if (encr)
	{
		proposal->add_algorithm(proposal, ENCRYPTION_ALGORITHM, encr, len);
	}
	add_ke_methods_to_proposal(proposal, ke_alg, add_kes);
	proposal->add_algorithm(proposal, EXTENDED_SEQUENCE_NUMBERS, esn, 0);

	if (secret.len)
	{
		ke = ha_key_exchange_create(secret, chunk_empty);
		array_insert_create(&kes, ARRAY_HEAD, ke);
	}
	if (add_secret.len)
	{
		ke = ha_key_exchange_create(add_secret, chunk_empty);
		array_insert_create(&kes, ARRAY_TAIL, ke);
	}
	if (ike_sa->get_version(ike_sa) == IKEV2)
	{
		keymat_v2_t *keymat_v2 = (keymat_v2_t*)ike_sa->get_keymat(ike_sa);

		ok = keymat_v2->derive_child_keys(keymat_v2, proposal, kes,
						nonce_i, nonce_r, &encr_i, &integ_i, &encr_r, &integ_r);
	}
	else if (ike_sa->get_version(ike_sa) == IKEV1)
	{
		keymat_v1_t *keymat_v1 = (keymat_v1_t*)ike_sa->get_keymat(ike_sa);
		uint32_t spi_i, spi_r;

		spi_i = initiator ? inbound_spi : outbound_spi;
		spi_r = initiator ? outbound_spi : inbound_spi;

		ok = keymat_v1->derive_child_keys(keymat_v1, proposal, ke, spi_i, spi_r,
						nonce_i, nonce_r, &encr_i, &integ_i, &encr_r, &integ_r);
	}
	array_destroy_offset(kes, offsetof(key_exchange_t, destroy));
	if (!ok)
	{
		DBG1(DBG_CHD, "HA CHILD_SA key derivation failed");
		child_sa->destroy(child_sa);
		proposal->destroy(proposal);
		charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
		message->destroy(message);
		return;
	}
	child_sa->set_proposal(child_sa, proposal);
	child_sa->set_state(child_sa, CHILD_INSTALLING);
	proposal->destroy(proposal);

	/* TODO: Change CHILD_SA API to avoid cloning twice */
	local_ts = linked_list_create();
	remote_ts = linked_list_create();
	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		switch (attribute)
		{
			case HA_LOCAL_TS:
				local_ts->insert_last(local_ts, value.ts->clone(value.ts));
				break;
			case HA_REMOTE_TS:
				remote_ts->insert_last(remote_ts, value.ts->clone(value.ts));
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);

	child_sa->set_policies(child_sa, local_ts, remote_ts);

	if (initiator)
	{
		if (child_sa->install(child_sa, encr_r, integ_r, inbound_spi,
							  inbound_cpi, initiator, TRUE, TRUE) != SUCCESS ||
			child_sa->install(child_sa, encr_i, integ_i, outbound_spi,
							  outbound_cpi, initiator, FALSE, TRUE) != SUCCESS)
		{
			failed = TRUE;
		}
	}
	else
	{
		if (child_sa->install(child_sa, encr_i, integ_i, inbound_spi,
							  inbound_cpi, initiator, TRUE, TRUE) != SUCCESS ||
			child_sa->install(child_sa, encr_r, integ_r, outbound_spi,
							  outbound_cpi, initiator, FALSE, TRUE) != SUCCESS)
		{
			failed = TRUE;
		}
	}
	chunk_clear(&encr_i);
	chunk_clear(&integ_i);
	chunk_clear(&encr_r);
	chunk_clear(&integ_r);

	if (failed)
	{
		DBG1(DBG_CHD, "HA CHILD_SA installation failed");
		child_sa->destroy(child_sa);
		local_ts->destroy_offset(local_ts, offsetof(traffic_selector_t, destroy));
		remote_ts->destroy_offset(remote_ts, offsetof(traffic_selector_t, destroy));
		charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
		message->destroy(message);
		return;
	}

#if DEBUG_LEVEL >= 1
	u_int seg_i, seg_o;

	seg_i = this->kernel->get_segment_spi(this->kernel,
								ike_sa->get_my_host(ike_sa), inbound_spi);
	seg_o = this->kernel->get_segment_spi(this->kernel,
								ike_sa->get_other_host(ike_sa), outbound_spi);
	DBG1(DBG_CFG, "installed HA CHILD_SA %s{%u} %#R === %#R "
		"(segment in: %d%s, out: %d%s)", child_sa->get_name(child_sa),
		child_sa->get_unique_id(child_sa), local_ts, remote_ts,
		seg_i, this->segments->is_active(this->segments, seg_i) ? "*" : "",
		seg_o, this->segments->is_active(this->segments, seg_o) ? "*" : "");
#endif /* DEBUG_LEVEL */

	child_sa->install_policies(child_sa);
	local_ts->destroy_offset(local_ts, offsetof(traffic_selector_t, destroy));
	remote_ts->destroy_offset(remote_ts, offsetof(traffic_selector_t, destroy));

	child_sa->set_state(child_sa, CHILD_INSTALLED);
	ike_sa->add_child_sa(ike_sa, child_sa);
	message->destroy(message);
	charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
}

/**
 * Process messages of type CHILD_DELETE
 */
static void process_child_delete(private_ha_dispatcher_t *this,
								 ha_message_t *message)
{
	ha_message_attribute_t attribute;
	ha_message_value_t value;
	enumerator_t *enumerator;
	ike_sa_t *ike_sa = NULL;
	child_sa_t *child_sa;
	uint32_t spi = 0;

	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		switch (attribute)
		{
			case HA_IKE_ID:
				ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager,
														  value.ike_sa_id);
				break;
			case HA_INBOUND_SPI:
				spi = value.u32;
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);

	if (ike_sa)
	{
		child_sa = ike_sa->get_child_sa(ike_sa, PROTO_ESP, spi, TRUE);
		if (child_sa)
		{
			ike_sa->destroy_child_sa(ike_sa, PROTO_ESP, spi);
		}
		charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
	}
	message->destroy(message);
}

/**
 * Process messages of type SEGMENT_TAKE/DROP
 */
static void process_segment(private_ha_dispatcher_t *this,
							ha_message_t *message, bool take)
{
	ha_message_attribute_t attribute;
	ha_message_value_t value;
	enumerator_t *enumerator;

	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		switch (attribute)
		{
			case HA_SEGMENT:
				if (take)
				{
					DBG1(DBG_CFG, "remote node takes segment %d", value.u16);
					this->segments->deactivate(this->segments, value.u16, FALSE);
				}
				else
				{
					DBG1(DBG_CFG, "remote node drops segment %d", value.u16);
					this->segments->activate(this->segments, value.u16, FALSE);
				}
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);
	message->destroy(message);
}

/**
 * Process messages of type STATUS
 */
static void process_status(private_ha_dispatcher_t *this,
						   ha_message_t *message)
{
	ha_message_attribute_t attribute;
	ha_message_value_t value;
	enumerator_t *enumerator;
	segment_mask_t mask = 0;

	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		switch (attribute)
		{
			case HA_SEGMENT:
				mask |= SEGMENTS_BIT(value.u16);
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);

	this->segments->handle_status(this->segments, mask);
	message->destroy(message);
}

/**
 * Process messages of type RESYNC
 */
static void process_resync(private_ha_dispatcher_t *this,
						   ha_message_t *message)
{
	ha_message_attribute_t attribute;
	ha_message_value_t value;
	enumerator_t *enumerator;

	enumerator = message->create_attribute_enumerator(message);
	while (enumerator->enumerate(enumerator, &attribute, &value))
	{
		switch (attribute)
		{
			case HA_SEGMENT:
				this->cache->resync(this->cache, value.u16);
				break;
			default:
				break;
		}
	}
	enumerator->destroy(enumerator);
	message->destroy(message);
}

/**
 * Dispatcher job function
 */
static job_requeue_t dispatch(private_ha_dispatcher_t *this)
{
	ha_message_t *message;
	ha_message_type_t type;

	message = this->socket->pull(this->socket);
	type = message->get_type(message);
	if (type != HA_STATUS)
	{
		DBG2(DBG_CFG, "received HA %N message", ha_message_type_names,
			 message->get_type(message));
	}
	switch (type)
	{
		case HA_IKE_ADD:
			process_ike_add(this, message);
			break;
		case HA_IKE_UPDATE:
			process_ike_update(this, message);
			break;
		case HA_IKE_MID_INITIATOR:
			process_ike_mid(this, message, TRUE);
			break;
		case HA_IKE_MID_RESPONDER:
			process_ike_mid(this, message, FALSE);
			break;
		case HA_IKE_IV:
			process_ike_iv(this, message);
			break;
		case HA_IKE_DELETE:
			process_ike_delete(this, message);
			break;
		case HA_CHILD_ADD:
			process_child_add(this, message);
			break;
		case HA_CHILD_DELETE:
			process_child_delete(this, message);
			break;
		case HA_SEGMENT_DROP:
			process_segment(this, message, FALSE);
			break;
		case HA_SEGMENT_TAKE:
			process_segment(this, message, TRUE);
			break;
		case HA_STATUS:
			process_status(this, message);
			break;
		case HA_RESYNC:
			process_resync(this, message);
			break;
		default:
			DBG1(DBG_CFG, "received unknown HA message type %d", type);
			message->destroy(message);
			break;
	}
	return JOB_REQUEUE_DIRECT;
}

METHOD(ha_dispatcher_t, destroy, void,
	private_ha_dispatcher_t *this)
{
	free(this);
}

/**
 * See header
 */
ha_dispatcher_t *ha_dispatcher_create(ha_socket_t *socket,
									ha_segments_t *segments, ha_cache_t *cache,
									ha_kernel_t *kernel, ha_attribute_t *attr)
{
	private_ha_dispatcher_t *this;


	INIT(this,
		.public = {
			.destroy = _destroy,
		},
		.socket = socket,
		.segments = segments,
		.cache = cache,
		.kernel = kernel,
		.attr = attr,
	);
	lib->processor->queue_job(lib->processor,
		(job_t*)callback_job_create_with_prio((callback_job_cb_t)dispatch, this,
				NULL, callback_job_cancel_thread, JOB_PRIO_CRITICAL));

	return &this->public;
}
