From: Francois ten Krooden Date: Wed, 18 Oct 2023 09:52:09 +0000 (+0200) Subject: kernel-vpp: Add kernel plugin for the FD.io Vector Packet Processor (VPP) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a5fea1a07125afea1543a8fb7fe9a5f30eb3e715;p=thirdparty%2Fstrongswan.git kernel-vpp: Add kernel plugin for the FD.io Vector Packet Processor (VPP) This is quite simple at the moment and basically only provides installation of SAs and policies into VPP. Co-authored-by: Tobias Brunner --- diff --git a/conf/Makefile.am b/conf/Makefile.am index 362aaf0fdb..fddc5a0dd9 100644 --- a/conf/Makefile.am +++ b/conf/Makefile.am @@ -75,6 +75,7 @@ plugins = \ plugins/kernel-netlink.opt \ plugins/kernel-pfkey.opt \ plugins/kernel-pfroute.opt \ + plugins/kernel-vpp.opt \ plugins/load-tester.opt \ plugins/lookip.opt \ plugins/ntru.opt \ diff --git a/conf/plugins/kernel-vpp.opt b/conf/plugins/kernel-vpp.opt new file mode 100644 index 0000000000..ad615b72e2 --- /dev/null +++ b/conf/plugins/kernel-vpp.opt @@ -0,0 +1,14 @@ +charon.plugins.kernel-vpp.app_name = strongswan + Application name that should be used when connecting to the VPP API. + +charon.plugins.kernel-vpp.api_prefix = + Shared memory prefix used for the VPP API. + + Shared memory prefix used for the VPP API. This corresponds to the prefix + option in VPP's api-segment config section. + +charon.plugins.kernel-vpp.max_outstanding_requests = 64 + Maximum outstanding requests queued to VPP not answered yet. + +charon.plugins.kernel-vpp.response_queue_size = 32 + Size of the VPP API response queue. diff --git a/configure.ac b/configure.ac index 365f5cb967..8ecfb3d1f1 100644 --- a/configure.ac +++ b/configure.ac @@ -231,6 +231,7 @@ ARG_ENABL_SET([kernel-pfkey], [enable the PF_KEY kernel interface.]) ARG_ENABL_SET([kernel-pfroute], [enable the PF_ROUTE kernel interface.]) ARG_ENABL_SET([kernel-iph], [enable the Windows IP Helper based networking backend.]) ARG_ENABL_SET([kernel-libipsec],[enable the libipsec kernel interface.]) +ARG_ENABL_SET([kernel-vpp], [enable the FD.io VPP kernel interface.]) ARG_ENABL_SET([kernel-wfp], [enable the Windows Filtering Platform IPsec backend.]) ARG_DISBL_SET([socket-default], [disable default socket implementation for charon.]) ARG_ENABL_SET([socket-dynamic], [enable dynamic socket implementation for charon]) @@ -1598,6 +1599,7 @@ ADD_PLUGIN([attr], [c charon]) ADD_PLUGIN([attr-sql], [c charon]) ADD_PLUGIN([load-tester], [c charon]) ADD_PLUGIN([kernel-libipsec], [c charon cmd]) +ADD_PLUGIN([kernel-vpp], [c charon cmd]) ADD_PLUGIN([kernel-wfp], [c charon]) ADD_PLUGIN([kernel-iph], [c charon]) ADD_PLUGIN([kernel-pfkey], [c charon nm cmd]) @@ -1780,6 +1782,7 @@ AM_CONDITIONAL(USE_KERNEL_NETLINK, test x$kernel_netlink = xtrue) AM_CONDITIONAL(USE_KERNEL_PFKEY, test x$kernel_pfkey = xtrue) AM_CONDITIONAL(USE_KERNEL_PFROUTE, test x$kernel_pfroute = xtrue) AM_CONDITIONAL(USE_KERNEL_LIBIPSEC, test x$kernel_libipsec = xtrue) +AM_CONDITIONAL(USE_KERNEL_VPP, test x$kernel_vpp = xtrue) AM_CONDITIONAL(USE_KERNEL_WFP, test x$kernel_wfp = xtrue) AM_CONDITIONAL(USE_KERNEL_IPH, test x$kernel_iph = xtrue) AM_CONDITIONAL(USE_WHITELIST, test x$whitelist = xtrue) @@ -2127,6 +2130,7 @@ AC_CONFIG_FILES([ src/libcharon/plugins/kernel_pfkey/Makefile src/libcharon/plugins/kernel_pfroute/Makefile src/libcharon/plugins/kernel_libipsec/Makefile + src/libcharon/plugins/kernel_vpp/Makefile src/libcharon/plugins/kernel_wfp/Makefile src/libcharon/plugins/kernel_iph/Makefile src/libcharon/plugins/whitelist/Makefile diff --git a/src/libcharon/Makefile.am b/src/libcharon/Makefile.am index fd88237fee..aecc4601ab 100644 --- a/src/libcharon/Makefile.am +++ b/src/libcharon/Makefile.am @@ -587,6 +587,13 @@ if MONOLITHIC endif endif +if USE_KERNEL_VPP + SUBDIRS += plugins/kernel_vpp +if MONOLITHIC + libcharon_la_LIBADD += plugins/kernel_vpp/libstrongswan-kernel-vpp.la +endif +endif + if USE_KERNEL_WFP SUBDIRS += plugins/kernel_wfp if MONOLITHIC diff --git a/src/libcharon/plugins/kernel_vpp/Makefile.am b/src/libcharon/plugins/kernel_vpp/Makefile.am new file mode 100644 index 0000000000..040bcb6a8e --- /dev/null +++ b/src/libcharon/plugins/kernel_vpp/Makefile.am @@ -0,0 +1,20 @@ +AM_CPPFLAGS = \ + -I${linux_headers} \ + -I$(top_srcdir)/src/libstrongswan \ + -I$(top_srcdir)/src/libcharon + +AM_CFLAGS = \ + $(PLUGIN_CFLAGS) + +if MONOLITHIC +noinst_LTLIBRARIES = libstrongswan-kernel-vpp.la +else +plugin_LTLIBRARIES = libstrongswan-kernel-vpp.la +endif + +libstrongswan_kernel_vpp_la_SOURCES = \ + kernel_vpp_plugin.h kernel_vpp_plugin.c \ + kernel_vpp_ipsec.h kernel_vpp_ipsec.c + +libstrongswan_kernel_vpp_la_LDFLAGS = -module -avoid-version +libstrongswan_kernel_vpp_la_LIBADD = -lvapiclient -lvppapiclient -lvppinfra diff --git a/src/libcharon/plugins/kernel_vpp/kernel_vpp_ipsec.c b/src/libcharon/plugins/kernel_vpp/kernel_vpp_ipsec.c new file mode 100644 index 0000000000..0e603110c2 --- /dev/null +++ b/src/libcharon/plugins/kernel_vpp/kernel_vpp_ipsec.c @@ -0,0 +1,1133 @@ +/* + * Copyright (C) 2023 Tobias Brunner + * Copyright (c) 2022 Nanoteq Pty Ltd + * + * 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 . + * + * 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 "kernel_vpp_ipsec.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +DEFINE_VAPI_MSG_IDS_VPE_API_JSON +DEFINE_VAPI_MSG_IDS_IPSEC_API_JSON + +#define SA_STAT_SEGMENT_NAME "/net/ipsec/sa" + +/* default values for config options */ +#define VPP_APP_NAME "strongswan" +#define VPP_MAX_OUTSTANDING_REQUESTS 64 +#define VPP_RESPONSE_QUEUE_SIZE 32 + +/** base priority for installed policies */ +#define POLICY_PRIO_BASE 200000 + +typedef struct private_kernel_vpp_ipsec_t private_kernel_vpp_ipsec_t; + +/** + * Private data + */ +struct private_kernel_vpp_ipsec_t +{ + /** + * Public interface + */ + kernel_vpp_ipsec_t public; + + /** + * Mutex for accessing entries + */ + mutex_t *mutex; + + /** + * Next security association database entry ID to allocate + */ + refcount_t next_sad_id; + + /** + * Security policy database entry ID used for policies + */ + refcount_t spd_id; + + /** + * Hash table of IPsec SAs using policies (sa_t) + */ + hashtable_t *sas; + + /** + * RNG used to generate SPIs + */ + rng_t *rng; + + /** + * VPP API Context + */ + vapi_ctx_t vpp_ctx; +}; + +/** + * Destroy the given SA ID object + */ +static void ipsec_sa_id_destroy(kernel_ipsec_sa_id_t *id) +{ + id->src->destroy(id->src); + id->dst->destroy(id->dst); + free(id); +} + +/** + * Clone the given SA ID object + */ +static kernel_ipsec_sa_id_t *ipsec_sa_id_clone(kernel_ipsec_sa_id_t *id) +{ + kernel_ipsec_sa_id_t *clone = malloc_thing(kernel_ipsec_sa_id_t); + + *clone = *id; + clone->src = clone->src->clone(clone->src); + clone->dst = clone->dst->clone(clone->dst); + return clone; +} + +/** + * Hash function for IPsec SA entries + */ +static u_int ipsec_sa_id_hash(kernel_ipsec_sa_id_t *sa) +{ + return chunk_hash_inc(sa->src->get_address(sa->src), + chunk_hash_inc(sa->dst->get_address(sa->dst), + chunk_hash_inc(chunk_from_thing(sa->spi), + chunk_hash(chunk_from_thing(sa->proto))))); +} + +/** + * Equality function for IPsec SA entries + */ +static bool ipsec_sa_id_equals(kernel_ipsec_sa_id_t *sa, + kernel_ipsec_sa_id_t *other_sa) +{ + return sa->src->ip_equals(sa->src, other_sa->src) && + sa->dst->ip_equals(sa->dst, other_sa->dst) && + sa->spi == other_sa->spi && + sa->proto == other_sa->proto; +} + +/** + * IPsec SA entry + */ +typedef struct { + /** SA ID */ + kernel_ipsec_sa_id_t *id; + /** VPP SA ID */ + uint32_t sa_id; + /** VPP stats index for the SA */ + uint32_t stat_index; +} sa_t; + +/** + * Destroy an sa_t object + */ +static void sa_destroy(sa_t *sa) +{ + ipsec_sa_id_destroy(sa->id); + free(sa); +} + +/** + * Calculate the approximate prefix length for the port range in the given TS + */ +static uint8_t port_range_prefix(traffic_selector_t *ts) +{ + uint16_t from, to, bitmask = 0x8000; + uint8_t prefix; + + from = ts->get_from_port(ts); + to = ts->get_to_port(ts); + + for (prefix = 0; prefix < 16; prefix++) + { + if ((bitmask & from) != (bitmask & to)) + { + break; + } + bitmask >>= 1; + } + return prefix; +} + +/** + * Calculate the priority of a policy + * + * This is basically the same formula we use in the kernel-netlink interface, + * but some features are currently not or only partially supported by VPP. + * + * Note that larger values have a higher precedence in VPP as compared to the + * Linux kernel. + * + * bits 0-0: separate trap and regular policies (0..1) 1 bit + * bits 1-1: reserved for interface restriction (0..1) 1 bit + * bits 2-7: src + dst port mask bits (2 * 0..16) 6 bits + * bits 8-8: restriction to protocol (0..1) 1 bit + * bits 9-17: src + dst network mask bits (2 * 0..128) 9 bits + * 18 bits + * + * smallest value: 000000000 0 000000 0 0: 0, lowest priority = 200'000 + * largest value : 100000000 1 100000 0 1: 131'457, highest priority = 731'457 + */ +static uint32_t get_priority(kernel_ipsec_policy_id_t *id, + policy_priority_t prio) +{ + uint32_t priority = POLICY_PRIO_BASE; + uint8_t proto, src_prefix, dst_prefix, sport_prefix, dport_prefix; + + switch (prio) + { + case POLICY_PRIORITY_PASS: + priority += POLICY_PRIO_BASE; + /* fall-through */ + case POLICY_PRIORITY_ROUTED: + case POLICY_PRIORITY_DEFAULT: + priority += POLICY_PRIO_BASE; + /* fall-through */ + case POLICY_PRIORITY_FALLBACK: + break; + } + + /* since VPP supports ranges, these prefixes are just approximations */ + id->src_ts->to_subnet(id->src_ts, NULL, &src_prefix); + id->dst_ts->to_subnet(id->dst_ts, NULL, &dst_prefix); + sport_prefix = port_range_prefix(id->src_ts); + dport_prefix = port_range_prefix(id->dst_ts); + + proto = max(id->src_ts->get_protocol(id->src_ts), + id->dst_ts->get_protocol(id->dst_ts)); + + /* calculate priority */ + priority += (src_prefix + dst_prefix) * 512; + priority += proto ? 256 : 0; + priority += (sport_prefix + dport_prefix) * 4; + priority += (prio != POLICY_PRIORITY_ROUTED); + return priority; +} + +/** + * Convert a chunk containing an IP address to a VPP address + */ +static inline void chunk_to_vapi_addr(chunk_t chunk, vapi_type_address *addr) +{ + addr->af = (chunk.len == 4) ? ADDRESS_IP4 : ADDRESS_IP6; + memcpy(&addr->un, chunk.ptr, chunk.len); +} + +METHOD(kernel_ipsec_t, get_features, kernel_feature_t, + private_kernel_vpp_ipsec_t *this) +{ + return KERNEL_ESP_V3_TFC; +} + +METHOD(kernel_ipsec_t, get_spi, status_t, + private_kernel_vpp_ipsec_t *this, host_t *src, host_t *dst, + uint8_t protocol, uint32_t *spi) +{ + uint32_t spi_min, spi_max, spi_new; + + spi_min = lib->settings->get_int(lib->settings, "%s.spi_min", + KERNEL_SPI_MIN, lib->ns); + spi_max = lib->settings->get_int(lib->settings, "%s.spi_max", + KERNEL_SPI_MAX, lib->ns); + if (spi_min > spi_max) + { + spi_new = spi_min; + spi_min = spi_max; + spi_max = spi_new; + } + /* make sure the SPI is valid (not in range 0-255) */ + spi_min = max(spi_min, 0x00000100); + spi_max = max(spi_max, 0x00000100); + + if (!this->rng->get_bytes(this->rng, sizeof(spi_new), + (uint8_t*)&spi_new)) + { + DBG1(DBG_KNL, "failed to allocate SPI"); + return FAILED; + } + spi_new = spi_min + spi_new % (spi_max - spi_min + 1); + + DBG2(DBG_KNL, "allocated SPI %.8x", spi_new); + *spi = htonl(spi_new); + + return SUCCESS; +} + +METHOD(kernel_ipsec_t, get_cpi, status_t, + private_kernel_vpp_ipsec_t *this, host_t *src, host_t *dst, + uint16_t *cpi) +{ + /* IPComp is not supported in VPP */ + return NOT_SUPPORTED; +} + +/** + * Data for an expire callback job + */ +typedef struct { + /** Reference to kernel interface */ + private_kernel_vpp_ipsec_t *this; + /** SA to expire */ + kernel_ipsec_sa_id_t *id; + /** 0 if this is a hard expire, otherwise the offset in s (soft->hard) */ + uint32_t hard_offset; +} expire_data_t; + +/** + * Clean up expire data + */ +CALLBACK(expire_data_destroy, void, + expire_data_t *this) +{ + ipsec_sa_id_destroy(this->id); + free(this); +} + +CALLBACK(expire_job, job_requeue_t, + expire_data_t *data) +{ + private_kernel_vpp_ipsec_t *this = data->this; + uint32_t hard_offset = 0; + sa_t *entry; + + this->mutex->lock(this->mutex); + entry = this->sas->get(this->sas, data->id); + if (entry) + { + hard_offset = data->hard_offset; + if (hard_offset) + { + /* soft limit reached, schedule hard expire */ + data->hard_offset = 0; + } + } + this->mutex->unlock(this->mutex); + + if (entry) + { + charon->kernel->expire(charon->kernel, data->id->proto, data->id->spi, + data->id->dst, !hard_offset); + } + return hard_offset ? JOB_RESCHEDULE(hard_offset) : JOB_REQUEUE_NONE; +} + +/** + * Schedule an expire event for the given SA + */ +static void schedule_expire(private_kernel_vpp_ipsec_t *this, + kernel_ipsec_sa_id_t *id, lifetime_cfg_t *lifetime) +{ + expire_data_t *data; + callback_job_t *job; + uint32_t timeout; + + if (!lifetime->time.life) + { /* no expiration at all */ + return; + } + + INIT(data, + .this = this, + .id = ipsec_sa_id_clone(id), + ); + + /* schedule a rekey first, a hard timeout will be scheduled then, if any */ + data->hard_offset = lifetime->time.life - lifetime->time.rekey; + timeout = lifetime->time.rekey; + + if (lifetime->time.life <= lifetime->time.rekey || + lifetime->time.rekey == 0) + { /* no rekey, schedule hard timeout */ + data->hard_offset = 0; + timeout = lifetime->time.life; + } + + job = callback_job_create(expire_job, data, expire_data_destroy, NULL); + lib->scheduler->schedule_job(lib->scheduler, (job_t*)job, timeout); +} + +/** + * Map the given protocol to a supported VPP identifier + */ +static vapi_enum_ipsec_proto map_protocol(uint8_t proto) +{ + switch (proto) + { + case IPPROTO_ESP: + return IPSEC_API_PROTO_ESP; + case IPPROTO_AH: + return IPSEC_API_PROTO_AH; + default: + return -1; + } +} + +/** + * Map of supported encryption/AEAD algorithms + */ +static struct { + encryption_algorithm_t alg; + size_t key_len; + size_t salt_len; + vapi_enum_ipsec_crypto_alg api; +} enc_algs[] = { + {ENCR_NULL, 0, 0, IPSEC_API_CRYPTO_ALG_NONE}, + {ENCR_DES, 0, 0, IPSEC_API_CRYPTO_ALG_DES_CBC}, + {ENCR_3DES, 0, 0, IPSEC_API_CRYPTO_ALG_3DES_CBC}, + {ENCR_AES_CBC, 16, 0, IPSEC_API_CRYPTO_ALG_AES_CBC_128}, + {ENCR_AES_CBC, 24, 0, IPSEC_API_CRYPTO_ALG_AES_CBC_192}, + {ENCR_AES_CBC, 32, 0, IPSEC_API_CRYPTO_ALG_AES_CBC_256}, + {ENCR_AES_CTR, 16, 4, IPSEC_API_CRYPTO_ALG_AES_CTR_128}, + {ENCR_AES_CTR, 24, 4, IPSEC_API_CRYPTO_ALG_AES_CTR_192}, + {ENCR_AES_CTR, 32, 4, IPSEC_API_CRYPTO_ALG_AES_CTR_256}, + /* only a 128-bit ICV seems to be supported by VPP for AES-GCM */ + {ENCR_AES_GCM_ICV16, 16, 4, IPSEC_API_CRYPTO_ALG_AES_GCM_128}, + {ENCR_AES_GCM_ICV16, 24, 4, IPSEC_API_CRYPTO_ALG_AES_GCM_192}, + {ENCR_AES_GCM_ICV16, 32, 4, IPSEC_API_CRYPTO_ALG_AES_GCM_256}, + {ENCR_CHACHA20_POLY1305, 32, 4, IPSEC_API_CRYPTO_ALG_CHACHA20_POLY1305}, +}; + +/** + * Map the given algorithm and key size to an identifier + */ +static vapi_enum_ipsec_crypto_alg map_enc_alg(encryption_algorithm_t alg, + size_t key_len, size_t *salt_len) +{ + int i; + + for (i = 0; i < countof(enc_algs); i++) + { + if (enc_algs[i].alg == alg && + (!enc_algs[i].key_len || + (enc_algs[i].key_len + enc_algs[i].salt_len) == key_len)) + { + *salt_len = enc_algs[i].salt_len; + return enc_algs[i].api; + } + } + return -1; +} + +/** + * Map of supported integrity protection algorithms + */ +static struct { + integrity_algorithm_t alg; + vapi_enum_ipsec_integ_alg api; +} int_algs[] = { + {AUTH_HMAC_MD5_96, IPSEC_API_INTEG_ALG_MD5_96}, + {AUTH_HMAC_SHA1_96, IPSEC_API_INTEG_ALG_SHA1_96}, + {AUTH_HMAC_SHA2_256_96, IPSEC_API_INTEG_ALG_SHA_256_96}, + {AUTH_HMAC_SHA2_256_128, IPSEC_API_INTEG_ALG_SHA_256_128}, + {AUTH_HMAC_SHA2_384_192, IPSEC_API_INTEG_ALG_SHA_384_192}, + {AUTH_HMAC_SHA2_512_256, IPSEC_API_INTEG_ALG_SHA_512_256}, +}; + +/** + * Map the given algorithm to an identifier + */ +static vapi_enum_ipsec_integ_alg map_int_alg(integrity_algorithm_t alg) +{ + int i; + + for (i = 0; i < countof(int_algs); i++) + { + if (int_algs[i].alg == alg) + { + return int_algs[i].api; + } + } + return -1; +} + +/** + * Callback for adding/deleting SAs + */ +static vapi_error_e add_del_sad_cb(vapi_ctx_t ctx, void *user, vapi_error_e rv, + bool is_last, + vapi_payload_ipsec_sad_entry_add_del_v2_reply *p) +{ + vapi_payload_ipsec_sad_entry_add_del_v2_reply *reply = user; + + if (p) + { + *reply = *p; + } + else + { + reply->retval = VNET_API_ERROR_RESPONSE_NOT_READY; + } + return VAPI_OK; +} + +METHOD(kernel_ipsec_t, add_sa, status_t, + private_kernel_vpp_ipsec_t *this, kernel_ipsec_sa_id_t *id, + kernel_ipsec_add_sa_t *data) +{ + vapi_error_e rv; + vapi_msg_ipsec_sad_entry_add_del_v2 *msg; + vapi_type_ipsec_sad_entry_v2 *entry; + vapi_payload_ipsec_sad_entry_add_del_v2_reply reply; + uint32_t sad_id, flags = IPSEC_API_SAD_FLAG_NONE; + size_t salt_len = 0; + sa_t *sa; + + msg = vapi_alloc_ipsec_sad_entry_add_del_v2(this->vpp_ctx); + msg->payload.is_add = TRUE; + entry = &msg->payload.entry; + + entry->sad_id = sad_id = ref_get(&this->next_sad_id); + entry->spi = ntohl(id->spi); + + DBG2(DBG_KNL, "adding SAD entry with SPI %.8x and ID %d", + ntohl(id->spi), sad_id); + + entry->protocol = map_protocol(id->proto); + if (entry->protocol == -1) + { + DBG1(DBG_KNL, "unsupported IPsec protocol %d", id->proto); + vapi_msg_free(this->vpp_ctx, msg); + return FAILED; + } + + if (data->enc_alg != ENCR_UNDEFINED) + { + entry->crypto_algorithm = map_enc_alg(data->enc_alg, + data->enc_key.len, &salt_len); + if (entry->crypto_algorithm == -1) + { + DBG1(DBG_KNL, "algorithm %N with key size %u not supported by VPP", + encryption_algorithm_names, data->enc_alg, + data->enc_key.len * 8); + vapi_msg_free(this->vpp_ctx, msg); + return FAILED; + } + DBG2(DBG_KNL, " using encryption algorithm %N with key size %d", + encryption_algorithm_names, data->enc_alg, data->enc_key.len * 8); + + entry->crypto_key.length = min(sizeof(entry->crypto_key.data), + data->enc_key.len - salt_len); + memcpy(entry->crypto_key.data, data->enc_key.ptr, + entry->crypto_key.length); + /* VPP uses an uint32_t as salt, which is the only length we currently + * support, but the VAPI layer will convert the byte order */ + if (salt_len == 4) + { + entry->salt = untoh32(data->enc_key.ptr + entry->crypto_key.length); + } + } + + if (data->int_alg != AUTH_UNDEFINED) + { + entry->integrity_algorithm = map_int_alg(data->int_alg); + if (entry->integrity_algorithm == 1) + { + DBG1(DBG_KNL, "algorithm %N not supported by VPP", + integrity_algorithm_names, data->int_alg); + vapi_msg_free(this->vpp_ctx, msg); + return FAILED; + } + DBG2(DBG_KNL, " using integrity algorithm %N with key size %d", + integrity_algorithm_names, data->int_alg, data->int_key.len * 8); + + entry->integrity_key.length = min(sizeof(entry->integrity_key.data), + data->int_key.len); + memcpy(entry->integrity_key.data, data->int_key.ptr, + entry->integrity_key.length); + } + + if (data->esn) + { + flags |= IPSEC_API_SAD_FLAG_USE_ESN; + } + if (data->mode == MODE_TUNNEL) + { + flags |= IPSEC_API_SAD_FLAG_IS_TUNNEL; + if (id->src->get_family(id->src) == AF_INET6) + { + flags |= IPSEC_API_SAD_FLAG_IS_TUNNEL_V6; + } + } + if (data->encap) + { + flags |= IPSEC_API_SAD_FLAG_UDP_ENCAP; + entry->udp_src_port = id->src->get_port(id->src); + entry->udp_dst_port = id->dst->get_port(id->dst); + } + if (data->inbound) + { + flags |= IPSEC_API_SAD_FLAG_IS_INBOUND; + + if (data->replay_window) + { + flags |= IPSEC_API_SAD_FLAG_USE_ANTI_REPLAY; + } + } + entry->flags = flags; + + if (data->copy_df) + { + entry->tunnel_flags |= TUNNEL_API_ENCAP_DECAP_FLAG_ENCAP_COPY_DF; + } + if (data->copy_ecn) + { + entry->tunnel_flags |= TUNNEL_API_ENCAP_DECAP_FLAG_ENCAP_COPY_ECN | + TUNNEL_API_ENCAP_DECAP_FLAG_DECAP_COPY_ECN; + } + if (data->copy_dscp) + { + entry->tunnel_flags |= TUNNEL_API_ENCAP_DECAP_FLAG_ENCAP_COPY_DSCP; + } + + chunk_to_vapi_addr(id->src->get_address(id->src), &entry->tunnel_src); + chunk_to_vapi_addr(id->dst->get_address(id->dst), &entry->tunnel_dst); + + rv = vapi_ipsec_sad_entry_add_del_v2(this->vpp_ctx, msg, add_del_sad_cb, + &reply); + if (rv != VAPI_OK || reply.retval) + { + DBG1(DBG_KNL, "unable to add SAD entry with SPI %.8x (%d)", + ntohl(id->spi), rv ?: reply.retval); + return FAILED; + } + + INIT(sa, + .id = ipsec_sa_id_clone(id), + .sa_id = sad_id, + .stat_index = reply.stat_index, + ); + + this->mutex->lock(this->mutex); + this->sas->put(this->sas, sa->id, sa); + schedule_expire(this, id, data->lifetime); + this->mutex->unlock(this->mutex); + return SUCCESS; +} + +METHOD(kernel_ipsec_t, update_sa, status_t, + private_kernel_vpp_ipsec_t *this, kernel_ipsec_sa_id_t *id, + kernel_ipsec_update_sa_t *data) +{ + /* this is not currently supported by VPP */ + return NOT_SUPPORTED; +} + +/** + * Get the VPP statistics for a particular segment + */ +static stat_segment_data_t *get_vpp_statistics(char *segment_name) +{ + uint8_t **pattern = NULL; + stat_segment_data_t *result; + uint32_t *dir; + int rv; + + rv = stat_segment_connect(STAT_SEGMENT_SOCKET_FILE); + if (rv != 0) + { + DBG1(DBG_KNL, "unable to connect to VPP stats socket (%d)", rv); + return NULL; + } + + vec_add1(pattern, segment_name); + dir = stat_segment_ls(pattern); + result = stat_segment_dump(dir); + vec_free(dir); + vec_free(pattern); + + stat_segment_disconnect(); + return result; +} + +METHOD(kernel_ipsec_t, query_sa, status_t, + private_kernel_vpp_ipsec_t *this, kernel_ipsec_sa_id_t *id, + kernel_ipsec_query_sa_t *data, uint64_t *bytes, uint64_t *packets, + time_t *time) +{ + status_t status = FAILED; + stat_segment_data_t *stats; + sa_t *sa; + counter_t bytes_total = 0, packets_total = 0; + int i, j; + + DBG3(DBG_KNL, "querying SAD entry with SPI %.8x", ntohl(id->spi)); + + this->mutex->lock(this->mutex); + sa = this->sas->get(this->sas, id); + if (!sa) + { + DBG1(DBG_KNL, "required SA ID not found to query VPP"); + goto error; + } + + stats = get_vpp_statistics(SA_STAT_SEGMENT_NAME); + if (!stats) + { + DBG1(DBG_KNL, "unable to retrieve SA statistics from VPP"); + goto error; + } + + for (i = 0; i < vec_len(stats); i++) + { + if (stats[i].type != STAT_DIR_TYPE_COUNTER_VECTOR_COMBINED || + !stats[i].combined_counter_vec) + { + continue; + } + /* combine stats from all VPP threads */ + for (j = 0; j < vec_len(stats[i].combined_counter_vec); j++) + { + if (sa->stat_index <= vec_len(stats[i].combined_counter_vec[j])) + { + bytes_total += stats[i].combined_counter_vec[j][sa->stat_index].bytes; + packets_total += stats[i].combined_counter_vec[j][sa->stat_index].packets; + } + } + } + stat_segment_data_free(stats); + + if (bytes) + { + *bytes = bytes_total; + } + if (packets) + { + *packets = packets_total; + } + if (time) + { + *time = 0; + } + status = SUCCESS; + +error: + this->mutex->unlock(this->mutex); + return status; +} + +METHOD(kernel_ipsec_t, del_sa, status_t, + private_kernel_vpp_ipsec_t *this, kernel_ipsec_sa_id_t *id, + kernel_ipsec_del_sa_t *data) +{ + vapi_error_e rv; + vapi_msg_ipsec_sad_entry_add_del_v2 *msg; + vapi_payload_ipsec_sad_entry_add_del_v2_reply reply; + sa_t *sa; + + DBG2(DBG_KNL, "deleting SAD entry with SPI %.8x", ntohl(id->spi)); + + msg = vapi_alloc_ipsec_sad_entry_add_del_v2(this->vpp_ctx); + + this->mutex->lock(this->mutex); + sa = this->sas->remove(this->sas, id); + if (!sa) + { + DBG1(DBG_KNL, "unable to find SA entry with SPI %.8x", ntohl(id->spi)); + vapi_msg_free(this->vpp_ctx, msg); + this->mutex->unlock(this->mutex); + return NOT_FOUND; + } + this->mutex->unlock(this->mutex); + + msg->payload.is_add = FALSE; + msg->payload.entry.sad_id = sa->sa_id; + sa_destroy(sa); + + /* the code in VPP (ipsec_api.c) requires a number of the fields to be + * filled in, but it only cares about the sad_id when the deletion occurs */ + msg->payload.entry.spi = ntohl(id->spi); + msg->payload.entry.protocol = map_protocol(id->proto); + + rv = vapi_ipsec_sad_entry_add_del_v2(this->vpp_ctx, msg, add_del_sad_cb, + &reply); + if (rv != VAPI_OK || reply.retval) + { + DBG1(DBG_KNL, "unable to delete SAD entry with SPI %.8x (%d)", + ntohl(id->spi), rv ?: reply.retval); + return FAILED; + } + return SUCCESS; +} + +METHOD(kernel_ipsec_t, flush_sas, status_t, + private_kernel_vpp_ipsec_t *this) +{ + /* this is not being called by strongSwan anymore */ + return NOT_SUPPORTED; +} + +/** + * Callback for adding/deleting policies + */ +static vapi_error_e add_del_spd_entry_cb(vapi_ctx_t ctx, void *user, + vapi_error_e rv, bool is_last, + vapi_payload_ipsec_spd_entry_add_del_reply *p) +{ + vapi_payload_ipsec_spd_entry_add_del_reply *reply = user; + + if (p) + { + *reply = *p; + } + else + { + reply->retval = VNET_API_ERROR_RESPONSE_NOT_READY; + } + return VAPI_OK; +} + +/* + * Install/Delete a SPD Policy in vpp for use by a policy. + */ +static bool add_del_spd_entry(private_kernel_vpp_ipsec_t *this, + kernel_ipsec_policy_id_t *id, + kernel_ipsec_manage_policy_t *data, bool add) +{ + vapi_error_e rv; + vapi_msg_ipsec_spd_entry_add_del *msg; + vapi_type_ipsec_spd_entry *entry; + vapi_payload_ipsec_spd_entry_add_del_reply reply; + traffic_selector_t *local, *remote; + uint32_t auto_priority; + sa_t *sa; + + msg = vapi_alloc_ipsec_spd_entry_add_del(this->vpp_ctx); + msg->payload.is_add = add; + entry = &msg->payload.entry; + entry->spd_id = this->spd_id; + entry->is_outbound = (id->dir == POLICY_OUT); + + auto_priority = get_priority(id, data->prio); + entry->priority = data->manual_prio ?: auto_priority; + + DBG2(DBG_KNL, "%s policy %R === %R %N [priority %u]", + add ? "adding" : "deleting", id->src_ts, id->dst_ts, policy_dir_names, + id->dir, entry->priority); + + switch (data->type) + { + case POLICY_IPSEC: + /* when the policy is PROTECT, VPP must have a valid SA ID, + * trap policies/acquires are currently not supported */ + entry->policy = IPSEC_API_SPD_ACTION_PROTECT; + if (data->sa) + { + kernel_ipsec_sa_id_t sa_id = { + .src = data->src, + .dst = data->dst, + .proto = data->sa->esp.use ? IPPROTO_ESP : IPPROTO_AH, + .spi = (data->sa->esp.use ? data->sa->esp.spi : + data->sa->ah.spi), + }; + this->mutex->lock(this->mutex); + sa = this->sas->get(this->sas, &sa_id); + if (!sa) + { + this->mutex->unlock(this->mutex); + DBG1(DBG_KNL, "required SA ID not found for policy, trap " + "policies are not supported by VPP"); + vapi_msg_free(this->vpp_ctx, msg); + return FALSE; + } + entry->sa_id = sa->sa_id; + this->mutex->unlock(this->mutex); + } + break; + case POLICY_PASS: + entry->policy = IPSEC_API_SPD_ACTION_BYPASS; + break; + case POLICY_DROP: + entry->policy = IPSEC_API_SPD_ACTION_DISCARD; + break; + } + + /* src or dest proto may be "any" (0), use more restrictive one */ + entry->protocol = max(id->src_ts->get_protocol(id->src_ts), + id->dst_ts->get_protocol(id->dst_ts)); + if (id->dir == POLICY_OUT) + { + local = id->src_ts; + remote = id->dst_ts; + } + else + { + remote = id->src_ts; + local = id->dst_ts; + } + + chunk_to_vapi_addr(local->get_from_address(local), &entry->local_address_start); + chunk_to_vapi_addr(local->get_to_address(local), &entry->local_address_stop); + chunk_to_vapi_addr(remote->get_from_address(remote), &entry->remote_address_start); + chunk_to_vapi_addr(remote->get_to_address(remote), &entry->remote_address_stop); + + entry->local_port_start = local->get_from_port(local); + entry->local_port_stop = local->get_to_port(local); + entry->remote_port_start = remote->get_from_port(remote); + entry->remote_port_stop = remote->get_to_port(remote); + + rv = vapi_ipsec_spd_entry_add_del(this->vpp_ctx, msg, add_del_spd_entry_cb, + &reply); + if (rv != VAPI_OK || reply.retval) + { + DBG1(DBG_KNL, "unable to %s SPD entry (%d)", add ? "add" : "delete", + rv ?: reply.retval); + return FALSE; + } + return TRUE; +} + +METHOD(kernel_ipsec_t, add_policy, status_t, + private_kernel_vpp_ipsec_t *this, kernel_ipsec_policy_id_t *id, + kernel_ipsec_manage_policy_t *data) +{ + if (id->dir == POLICY_FWD) + { + /* forward policies are not supported by VPP */ + return SUCCESS; + } + if (!add_del_spd_entry(this, id, data, TRUE)) + { + return FAILED; + } + return SUCCESS; +} + +METHOD(kernel_ipsec_t, query_policy, status_t, + private_kernel_vpp_ipsec_t *this, kernel_ipsec_policy_id_t *id, + kernel_ipsec_query_policy_t *data, time_t *use_time) +{ + /* policy stats from VPP use byte/packet counts */ + return NOT_SUPPORTED; +} + +METHOD(kernel_ipsec_t, del_policy, status_t, + private_kernel_vpp_ipsec_t *this, kernel_ipsec_policy_id_t *id, + kernel_ipsec_manage_policy_t *data) +{ + if (id->dir == POLICY_FWD) + { + /* forward policies are not supported on VPP */ + return SUCCESS; + } + if (!add_del_spd_entry(this, id, data, FALSE)) + { + return FAILED; + } + return SUCCESS; +} + +METHOD(kernel_ipsec_t, flush_policies, status_t, + private_kernel_vpp_ipsec_t *this) +{ + /* this is not being called by strongSwan anymore */ + return NOT_SUPPORTED; +} + +METHOD(kernel_ipsec_t, bypass_socket, bool, + private_kernel_vpp_ipsec_t *this, int fd, int family) +{ + /* FIXME: we may have to install port-specific bypass policies and/or maybe + * some punting rules? */ + return TRUE; +} + +METHOD(kernel_ipsec_t, enable_udp_decap, bool, + private_kernel_vpp_ipsec_t *this, int fd, int family, uint16_t port) +{ + /* FIXME: not sure if necessary on VPP */ + return TRUE; +} + +/** + * Callback for managing SPD in VPP that simply collects the return value + */ +static vapi_error_e manage_vpp_spd_cb(vapi_ctx_t ctx, void *user, vapi_error_e rv, + bool is_last, + vapi_payload_ipsec_spd_add_del_reply *p) +{ + *((uint32_t*)user) = p ? p->retval : VNET_API_ERROR_RESPONSE_NOT_READY; + return VAPI_OK; +} + +/** + * Install or delete an SPD in VPP for use by policies. + */ +static bool manage_vpp_spd(private_kernel_vpp_ipsec_t *this, bool add, + uint32_t spd_id) +{ + vapi_error_e rv; + uint32_t retval = 0; + vapi_msg_ipsec_spd_add_del *msg; + + msg = vapi_alloc_ipsec_spd_add_del(this->vpp_ctx); + msg->payload.is_add = add; + msg->payload.spd_id = spd_id; + + rv = vapi_ipsec_spd_add_del(this->vpp_ctx, msg, manage_vpp_spd_cb, &retval); + if (rv != VAPI_OK || + (retval && add && retval != VNET_API_ERROR_ENTRY_ALREADY_EXISTS) || + (retval && !add && retval != VNET_API_ERROR_NO_SUCH_ENTRY)) + { + DBG1(DBG_KNL, "failed to %s VPP SPD with ID %d (%d)", + add ? "add" : "remove", spd_id, retval ?: rv); + return FALSE; + } + else if (retval) + { + DBG2(DBG_KNL, "SPD with ID %d %s, ignored", spd_id, + add ? "to be added already exists" : "to be removed doesn't exist"); + } + else + { + DBG2(DBG_KNL, "%s SPD with ID %d", add ? "added" : "removed", + spd_id); + } + return TRUE; +} + +METHOD(kernel_ipsec_t, destroy, void, + private_kernel_vpp_ipsec_t *this) +{ + this->mutex->destroy(this->mutex); + this->sas->destroy(this->sas); + DESTROY_IF(this->rng); + + if (this->vpp_ctx) + { + manage_vpp_spd(this, FALSE, this->spd_id); + vapi_disconnect(this->vpp_ctx); + vapi_ctx_free(this->vpp_ctx); + } + free(this); +} + +/** + * Callback that logs the VPP version + */ +static vapi_error_e log_version_cb(vapi_ctx_t ctx, void *user, vapi_error_e rv, + bool is_last, vapi_payload_show_version_reply *p) +{ + DBG2(DBG_KNL, "VPP (%s), version: %s, build date: %s", p->program, + p->version, p->build_date); + return VAPI_OK; +} + +/** + * Initialize the VPP API + */ +static vapi_ctx_t vpp_api_init() +{ + vapi_ctx_t ctx = NULL; + vapi_error_e rv; + + rv = vapi_ctx_alloc(&ctx); + if (rv == VAPI_OK) + { + rv = vapi_connect(ctx, + lib->settings->get_str(lib->settings, + "%s.plugins.kernel-vpp.app_name", + VPP_APP_NAME, lib->ns), + lib->settings->get_str(lib->settings, + "%s.plugins.kernel-vpp.api_prefix", + NULL, lib->ns), + lib->settings->get_int(lib->settings, + "%s.plugins.kernel-vpp.max_outstanding_requests", + VPP_MAX_OUTSTANDING_REQUESTS, lib->ns), + lib->settings->get_int(lib->settings, + "%s.plugins.kernel-vpp.response_queue_size", + VPP_RESPONSE_QUEUE_SIZE, lib->ns), + VAPI_MODE_BLOCKING, TRUE); + + if (rv == VAPI_OK) + { + vapi_msg_show_version *msg = vapi_alloc_show_version(ctx); + vapi_show_version(ctx, msg, log_version_cb, NULL); + return ctx; + } + else + { + DBG1(DBG_KNL, "unable to connect to VPP API (%d)", rv); + vapi_ctx_free(ctx); + } + } + else + { + DBG1(DBG_KNL, "unable to allocate VPP API context (%d)", rv); + } + return NULL; +} + +/* + * Described in header + */ +kernel_vpp_ipsec_t *kernel_vpp_ipsec_create() +{ + private_kernel_vpp_ipsec_t *this; + + INIT(this, + .public = { + .interface = { + .get_features = _get_features, + .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 = _flush_sas, + .add_policy = _add_policy, + .query_policy = _query_policy, + .del_policy = _del_policy, + .flush_policies = _flush_policies, + .bypass_socket = _bypass_socket, + .enable_udp_decap = _enable_udp_decap, + .destroy = _destroy, + }, + }, + .mutex = mutex_create(MUTEX_TYPE_DEFAULT), + .sas = hashtable_create((hashtable_hash_t)ipsec_sa_id_hash, + (hashtable_equals_t)ipsec_sa_id_equals, 32), + .vpp_ctx = vpp_api_init(), + .spd_id = 1, + ); + + if (!this->vpp_ctx || !manage_vpp_spd(this, TRUE, this->spd_id)) + { + destroy(this); + return NULL; + } + + this->rng = lib->crypto->create_rng(lib->crypto, RNG_WEAK); + if (!this->rng) + { + DBG1(DBG_KNL, "failed to create RNG for SPI generation"); + destroy(this); + return NULL; + } + return &this->public; +} diff --git a/src/libcharon/plugins/kernel_vpp/kernel_vpp_ipsec.h b/src/libcharon/plugins/kernel_vpp/kernel_vpp_ipsec.h new file mode 100644 index 0000000000..1ab413ecdf --- /dev/null +++ b/src/libcharon/plugins/kernel_vpp/kernel_vpp_ipsec.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 Nanoteq Pty Ltd + * + * 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 . + * + * 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. + */ + +/** + * @defgroup kernel_vpp_ipsec kernel_vpp_ipsec + * @{ @ingroup kernel_vpp + */ + +#ifndef KERNEL_VPP_IPSEC_H_ +#define KERNEL_VPP_IPSEC_H_ + +#include + +typedef struct kernel_vpp_ipsec_t kernel_vpp_ipsec_t; + +/** + * Implementation of the kernel-ipsec interface using FD.io VPP. + */ +struct kernel_vpp_ipsec_t { + + /** + * Implements kernel_ipsec_t interface + */ + kernel_ipsec_t interface; +}; + +/** + * Create a FD.io VPP kernel-ipsec interface instance. + * + * @return kernel_vpp_ipsec_t instance + */ +kernel_vpp_ipsec_t *kernel_vpp_ipsec_create(); + +#endif /** KERNEL_VPP_IPSEC_H_ @}*/ diff --git a/src/libcharon/plugins/kernel_vpp/kernel_vpp_plugin.c b/src/libcharon/plugins/kernel_vpp/kernel_vpp_plugin.c new file mode 100644 index 0000000000..64c83e20a2 --- /dev/null +++ b/src/libcharon/plugins/kernel_vpp/kernel_vpp_plugin.c @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021 Nanoteq Pty Ltd + * + * 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 . + * + * 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 "kernel_vpp_plugin.h" +#include "kernel_vpp_ipsec.h" + +typedef struct private_kernel_vpp_plugin_t private_kernel_vpp_plugin_t; + +/** + * Private data of this plugin + */ +struct private_kernel_vpp_plugin_t { + + /** + * Public interface + */ + kernel_vpp_plugin_t public; +}; + +METHOD(plugin_t, get_name, char*, + private_kernel_vpp_plugin_t *this) +{ + return "kernel-vpp"; +} + +METHOD(plugin_t, get_features, int, + private_kernel_vpp_plugin_t *this, plugin_feature_t *features[]) +{ + static plugin_feature_t f[] = { + PLUGIN_CALLBACK(kernel_ipsec_register, kernel_vpp_ipsec_create), + PLUGIN_PROVIDE(CUSTOM, "kernel-ipsec"), + PLUGIN_DEPENDS(RNG, RNG_WEAK), + }; + *features = f; + return countof(f); +} + +METHOD(plugin_t, destroy, void, + private_kernel_vpp_plugin_t *this) +{ + free(this); +} + +/* + * Described in header + */ +plugin_t *kernel_vpp_plugin_create() +{ + private_kernel_vpp_plugin_t *this; + + INIT(this, + .public = { + .plugin = { + .get_name = _get_name, + .get_features = _get_features, + .destroy = _destroy, + }, + }, + ); + + return &this->public.plugin; +} diff --git a/src/libcharon/plugins/kernel_vpp/kernel_vpp_plugin.h b/src/libcharon/plugins/kernel_vpp/kernel_vpp_plugin.h new file mode 100644 index 0000000000..26a9ebb00a --- /dev/null +++ b/src/libcharon/plugins/kernel_vpp/kernel_vpp_plugin.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Nanoteq Pty Ltd + * + * 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 . + * + * 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. + */ + +/** + * @defgroup kernel_vpp kernel_vpp + * @ingroup cplugins + * + * @defgroup kernel_vpp_plugin kernel_vpp_plugin + * @{ @ingroup kernel_vpp + */ + +#ifndef KERNEL_VPP_PLUGIN_H_ +#define KERNEL_VPP_PLUGIN_H_ + +#include + +typedef struct kernel_vpp_plugin_t kernel_vpp_plugin_t; + +/** + * FD.io VPP Interface plugin + * + * This plugin implements a kernel interface for strongSwan to communicate with + * the VPP process. + */ +struct kernel_vpp_plugin_t { + + /** + * Implements plugin interface + */ + plugin_t plugin; +}; + +#endif /** KERNEL_VPP_PLUGIN_H_ @}*/