From 908be0651a5a97609cc80799e9c5dafdf702f56e Mon Sep 17 00:00:00 2001 From: Vladimir Medvedkin Date: Mon, 21 Oct 2019 15:35:46 +0100 Subject: [PATCH] app/test-sad: add test application for IPsec SAD Introduce new application to provide user to evaluate and perform custom functional and performance tests for IPsec SAD implementation. According to our measurements on SKX for 1M entries average lookup cost is ~80 cycles, average add cost ~500 cycles. Signed-off-by: Vladimir Medvedkin Acked-by: Akhil Goyal Acked-by: Konstantin Ananyev Tested-by: Konstantin Ananyev --- MAINTAINERS | 1 + app/Makefile | 1 + app/meson.build | 3 +- app/test-sad/Makefile | 18 + app/test-sad/main.c | 667 +++++++++++++++++++++++++ app/test-sad/meson.build | 6 + doc/guides/rel_notes/release_19_11.rst | 4 +- 7 files changed, 698 insertions(+), 2 deletions(-) create mode 100644 app/test-sad/Makefile create mode 100644 app/test-sad/main.c create mode 100644 app/test-sad/meson.build diff --git a/MAINTAINERS b/MAINTAINERS index 1dda19eba5..f0f555b375 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1198,6 +1198,7 @@ F: app/test/test_ipsec.c F: doc/guides/prog_guide/ipsec_lib.rst M: Vladimir Medvedkin F: app/test/test_ipsec_sad.c +F: app/test-sad/ Flow Classify - EXPERIMENTAL M: Bernard Iremonger diff --git a/app/Makefile b/app/Makefile index 28acbceca9..db9d2d5380 100644 --- a/app/Makefile +++ b/app/Makefile @@ -10,6 +10,7 @@ DIRS-$(CONFIG_RTE_LIBRTE_PDUMP) += pdump DIRS-$(CONFIG_RTE_LIBRTE_ACL) += test-acl DIRS-$(CONFIG_RTE_LIBRTE_CMDLINE) += test-cmdline DIRS-$(CONFIG_RTE_LIBRTE_PIPELINE) += test-pipeline +DIRS-$(CONFIG_RTE_LIBRTE_IPSEC) += test-sad ifeq ($(CONFIG_RTE_LIBRTE_BBDEV),y) DIRS-$(CONFIG_RTE_TEST_BBDEV) += test-bbdev diff --git a/app/meson.build b/app/meson.build index b0e6afbbe9..71109cc422 100644 --- a/app/meson.build +++ b/app/meson.build @@ -15,7 +15,8 @@ apps = [ 'test-crypto-perf', 'test-eventdev', 'test-pipeline', - 'test-pmd'] + 'test-pmd', + 'test-sad'] # for BSD only lib_execinfo = cc.find_library('execinfo', required: false) diff --git a/app/test-sad/Makefile b/app/test-sad/Makefile new file mode 100644 index 0000000000..9b354132e1 --- /dev/null +++ b/app/test-sad/Makefile @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2010-2014 Intel Corporation + +include $(RTE_SDK)/mk/rte.vars.mk + +ifeq ($(CONFIG_RTE_LIBRTE_IPSEC),y) + +APP = testsad + +CFLAGS += $(WERROR_FLAGS) +CFLAGS += -DALLOW_EXPERIMENTAL_API + +# all source are stored in SRCS-y +SRCS-y := main.c + +include $(RTE_SDK)/mk/rte.app.mk + +endif diff --git a/app/test-sad/main.c b/app/test-sad/main.c new file mode 100644 index 0000000000..22e10cbe66 --- /dev/null +++ b/app/test-sad/main.c @@ -0,0 +1,667 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2019 Intel Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#define PRINT_USAGE_START "%s [EAL options] --\n" + +#define GET_CB_FIELD(in, fd, base, lim, dlm) do { \ + unsigned long val; \ + char *end_fld; \ + errno = 0; \ + val = strtoul((in), &end_fld, (base)); \ + if (errno != 0 || end_fld[0] != (dlm) || val > (lim)) \ + return -EINVAL; \ + (fd) = (typeof(fd))val; \ + (in) = end_fld + 1; \ +} while (0) + +#define DEF_RULE_NUM 0x10000 +#define DEF_TUPLES_NUM 0x100000 +#define BURST_SZ_MAX 64 + +static struct { + const char *prgname; + const char *rules_file; + const char *tuples_file; + uint32_t nb_rules; + uint32_t nb_tuples; + uint32_t nb_rules_32; + uint32_t nb_rules_64; + uint32_t nb_rules_96; + uint32_t nb_tuples_rnd; + uint32_t burst_sz; + uint8_t fract_32; + uint8_t fract_64; + uint8_t fract_96; + uint8_t fract_rnd_tuples; + int ipv6; + int verbose; + int parallel_lookup; + int concurrent_rw; +} config = { + .rules_file = NULL, + .tuples_file = NULL, + .nb_rules = DEF_RULE_NUM, + .nb_tuples = DEF_TUPLES_NUM, + .nb_rules_32 = 0, + .nb_rules_64 = 0, + .nb_rules_96 = 0, + .nb_tuples_rnd = 0, + .burst_sz = BURST_SZ_MAX, + .fract_32 = 90, + .fract_64 = 9, + .fract_96 = 1, + .fract_rnd_tuples = 0, + .ipv6 = 0, + .verbose = 0, + .parallel_lookup = 0, + .concurrent_rw = 0 +}; + +enum { + CB_RULE_SPI, + CB_RULE_DIP, + CB_RULE_SIP, + CB_RULE_LEN, + CB_RULE_NUM, +}; + +static char line[LINE_MAX]; +struct rule { + union rte_ipsec_sad_key tuple; + int rule_type; +}; + +static struct rule *rules_tbl; +static struct rule *tuples_tbl; + +static int +parse_distrib(const char *in) +{ + int a, b, c; + + GET_CB_FIELD(in, a, 0, UINT8_MAX, '/'); + GET_CB_FIELD(in, b, 0, UINT8_MAX, '/'); + GET_CB_FIELD(in, c, 0, UINT8_MAX, 0); + + if ((a + b + c) != 100) + return -EINVAL; + + config.fract_32 = a; + config.fract_64 = b; + config.fract_96 = c; + + return 0; +} + +static void +print_config(void) +{ + fprintf(stdout, + "Rules total: %u\n" + "Configured rules distribution SPI/SPI_DIP/SIP_DIP_SIP:" + "%u/%u/%u\n" + "SPI only rules: %u\n" + "SPI_DIP rules: %u\n" + "SPI_DIP_SIP rules: %u\n" + "Lookup tuples: %u\n" + "Lookup burst size %u\n" + "Configured fraction of random tuples: %u\n" + "Random lookup tuples: %u\n", + config.nb_rules, config.fract_32, config.fract_64, + config.fract_96, config.nb_rules_32, config.nb_rules_64, + config.nb_rules_96, config.nb_tuples, config.burst_sz, + config.fract_rnd_tuples, config.nb_tuples_rnd); +} + +static void +print_usage(void) +{ + fprintf(stdout, + PRINT_USAGE_START + "[-f ]\n" + "[-t ]\n" + "[-n ]\n" + "[-l ]\n" + "[-6 ]\n" + "[-d <\"/\" separated rules length distribution" + "(if -f is not specified)>]\n" + "[-r ]\n" + "[-b ]\n" + "[-v ]\n" + "[-p ]\n" + "[-c ]\n", + config.prgname); + +} + +static int +get_str_num(FILE *f, int num) +{ + int n_lines = 0; + + if (f != NULL) { + while (fgets(line, sizeof(line), f) != NULL) + n_lines++; + rewind(f); + } else { + n_lines = num; + } + return n_lines; +} + +static int +parse_file(FILE *f, struct rule *tbl, int rule_tbl) +{ + int ret, i, j = 0; + char *s, *sp, *in[CB_RULE_NUM]; + static const char *dlm = " \t\n"; + int string_tok_nb = RTE_DIM(in); + + string_tok_nb -= (rule_tbl == 0) ? 1 : 0; + while (fgets(line, sizeof(line), f) != NULL) { + s = line; + for (i = 0; i != string_tok_nb; i++) { + in[i] = strtok_r(s, dlm, &sp); + if (in[i] == NULL) + return -EINVAL; + s = NULL; + } + GET_CB_FIELD(in[CB_RULE_SPI], tbl[j].tuple.v4.spi, 0, + UINT32_MAX, 0); + + if (config.ipv6) + ret = inet_pton(AF_INET6, in[CB_RULE_DIP], + &tbl[j].tuple.v6.dip); + else + ret = inet_pton(AF_INET, in[CB_RULE_DIP], + &tbl[j].tuple.v4.dip); + if (ret != 1) + return -EINVAL; + if (config.ipv6) + ret = inet_pton(AF_INET6, in[CB_RULE_SIP], + &tbl[j].tuple.v6.sip); + else + ret = inet_pton(AF_INET, in[CB_RULE_SIP], + &tbl[j].tuple.v4.sip); + if (ret != 1) + return -EINVAL; + if ((rule_tbl) && (in[CB_RULE_LEN] != NULL)) { + if (strcmp(in[CB_RULE_LEN], "SPI_DIP_SIP") == 0) { + tbl[j].rule_type = RTE_IPSEC_SAD_SPI_DIP_SIP; + config.nb_rules_96++; + } else if (strcmp(in[CB_RULE_LEN], "SPI_DIP") == 0) { + tbl[j].rule_type = RTE_IPSEC_SAD_SPI_DIP; + config.nb_rules_64++; + } else if (strcmp(in[CB_RULE_LEN], "SPI") == 0) { + tbl[j].rule_type = RTE_IPSEC_SAD_SPI_ONLY; + config.nb_rules_32++; + } else { + return -EINVAL; + } + } + j++; + } + return 0; +} + +static uint64_t +get_rnd_rng(uint64_t l, uint64_t u) +{ + if (l == u) + return l; + else + return (rte_rand() % (u - l) + l); +} + +static void +get_random_rules(struct rule *tbl, uint32_t nb_rules, int rule_tbl) +{ + unsigned int i, j, rnd; + int rule_type; + double edge = 0; + double step; + + step = (double)UINT32_MAX / nb_rules; + for (i = 0; i < nb_rules; i++, edge += step) { + rnd = rte_rand() % 100; + if (rule_tbl) { + tbl[i].tuple.v4.spi = get_rnd_rng((uint64_t)edge, + (uint64_t)(edge + step)); + if (config.ipv6) { + for (j = 0; j < 16; j++) { + tbl[i].tuple.v6.dip[j] = rte_rand(); + tbl[i].tuple.v6.sip[j] = rte_rand(); + } + } else { + tbl[i].tuple.v4.dip = rte_rand(); + tbl[i].tuple.v4.sip = rte_rand(); + } + if (rnd >= (100UL - config.fract_32)) { + rule_type = RTE_IPSEC_SAD_SPI_ONLY; + config.nb_rules_32++; + } else if (rnd >= (100UL - (config.fract_32 + + config.fract_64))) { + rule_type = RTE_IPSEC_SAD_SPI_DIP; + config.nb_rules_64++; + } else { + rule_type = RTE_IPSEC_SAD_SPI_DIP_SIP; + config.nb_rules_96++; + } + tbl[i].rule_type = rule_type; + } else { + if (rnd >= 100UL - config.fract_rnd_tuples) { + tbl[i].tuple.v4.spi = + get_rnd_rng((uint64_t)edge, + (uint64_t)(edge + step)); + if (config.ipv6) { + for (j = 0; j < 16; j++) { + tbl[i].tuple.v6.dip[j] = + rte_rand(); + tbl[i].tuple.v6.sip[j] = + rte_rand(); + } + } else { + tbl[i].tuple.v4.dip = rte_rand(); + tbl[i].tuple.v4.sip = rte_rand(); + } + config.nb_tuples_rnd++; + } else { + tbl[i].tuple.v4.spi = rules_tbl[i % + config.nb_rules].tuple.v4.spi; + if (config.ipv6) { + int r_idx = i % config.nb_rules; + memcpy(tbl[i].tuple.v6.dip, + rules_tbl[r_idx].tuple.v6.dip, + sizeof(tbl[i].tuple.v6.dip)); + memcpy(tbl[i].tuple.v6.sip, + rules_tbl[r_idx].tuple.v6.sip, + sizeof(tbl[i].tuple.v6.sip)); + } else { + tbl[i].tuple.v4.dip = rules_tbl[i % + config.nb_rules].tuple.v4.dip; + tbl[i].tuple.v4.sip = rules_tbl[i % + config.nb_rules].tuple.v4.sip; + } + } + } + } +} + +static void +tbl_init(struct rule **tbl, uint32_t *n_entries, + const char *file_name, int rule_tbl) +{ + FILE *f = NULL; + int ret; + const char *rules = "rules"; + const char *tuples = "tuples"; + + if (file_name != NULL) { + f = fopen(file_name, "r"); + if (f == NULL) + rte_exit(-EINVAL, "failed to open file: %s\n", + file_name); + } + + printf("init %s table...", (rule_tbl) ? rules : tuples); + *n_entries = get_str_num(f, *n_entries); + printf("%d entries\n", *n_entries); + *tbl = rte_zmalloc(NULL, sizeof(struct rule) * *n_entries, + RTE_CACHE_LINE_SIZE); + if (*tbl == NULL) + rte_exit(-ENOMEM, "failed to allocate tbl\n"); + + if (f != NULL) { + printf("parse file %s\n", file_name); + ret = parse_file(f, *tbl, rule_tbl); + if (ret != 0) + rte_exit(-EINVAL, "failed to parse file %s\n" + "rules file must be: " + " " + " " + " " + "\n" + "tuples file must be: " + " " + " " + "\n", + file_name); + } else { + printf("generate random values in %s table\n", + (rule_tbl) ? rules : tuples); + get_random_rules(*tbl, *n_entries, rule_tbl); + } + if (f != NULL) + fclose(f); +} + +static void +parse_opts(int argc, char **argv) +{ + int opt, ret; + char *endptr; + + while ((opt = getopt(argc, argv, "f:t:n:d:l:r:6b:vpc")) != -1) { + switch (opt) { + case 'f': + config.rules_file = optarg; + break; + case 't': + config.tuples_file = optarg; + break; + case 'n': + errno = 0; + config.nb_rules = strtoul(optarg, &endptr, 10); + if ((errno != 0) || (config.nb_rules == 0) || + (endptr[0] != 0)) { + print_usage(); + rte_exit(-EINVAL, "Invalid option -n\n"); + } + break; + case 'd': + ret = parse_distrib(optarg); + if (ret != 0) { + print_usage(); + rte_exit(-EINVAL, "Invalid option -d\n"); + } + break; + case 'b': + errno = 0; + config.burst_sz = strtoul(optarg, &endptr, 10); + if ((errno != 0) || (config.burst_sz == 0) || + (config.burst_sz > BURST_SZ_MAX) || + (endptr[0] != 0)) { + print_usage(); + rte_exit(-EINVAL, "Invalid option -b\n"); + } + break; + case 'l': + errno = 0; + config.nb_tuples = strtoul(optarg, &endptr, 10); + if ((errno != 0) || (config.nb_tuples == 0) || + (endptr[0] != 0)) { + print_usage(); + rte_exit(-EINVAL, "Invalid option -l\n"); + } + break; + case 'r': + errno = 0; + config.fract_rnd_tuples = strtoul(optarg, &endptr, 10); + if ((errno != 0) || (config.fract_rnd_tuples == 0) || + (config.fract_rnd_tuples >= 100) || + (endptr[0] != 0)) { + print_usage(); + rte_exit(-EINVAL, "Invalid option -r\n"); + } + break; + case '6': + config.ipv6 = 1; + break; + case 'v': + config.verbose = 1; + break; + case 'p': + config.parallel_lookup = 1; + break; + case 'c': + config.concurrent_rw = 1; + break; + default: + print_usage(); + rte_exit(-EINVAL, "Invalid options\n"); + } + } +} + +static void +print_addr(int af, const void *addr) +{ + char str[INET6_ADDRSTRLEN]; + const char *ret; + + ret = inet_ntop(af, addr, str, sizeof(str)); + if (ret != NULL) + printf("%s", str); +} + +static void +print_tuple(int af, uint32_t spi, const void *dip, const void *sip) +{ + + printf(""); +} + +static void +print_result(const union rte_ipsec_sad_key *key, void *res) +{ + struct rule *rule = res; + const struct rte_ipsec_sadv4_key *v4; + const struct rte_ipsec_sadv6_key *v6; + const char *spi_only = "SPI_ONLY"; + const char *spi_dip = "SPI_DIP"; + const char *spi_dip_sip = "SPI_DIP_SIP"; + const char *rule_type; + const void *dip, *sip; + uint32_t spi; + int af; + + af = (config.ipv6) ? AF_INET6 : AF_INET; + v4 = &key->v4; + v6 = &key->v6; + spi = (config.ipv6 == 0) ? v4->spi : v6->spi; + dip = (config.ipv6 == 0) ? &v4->dip : (const void *)v6->dip; + sip = (config.ipv6 == 0) ? &v4->sip : (const void *)v6->sip; + + if (res == NULL) { + printf("TUPLE: "); + print_tuple(af, spi, dip, sip); + printf(" not found\n"); + return; + } + + switch (rule->rule_type) { + case RTE_IPSEC_SAD_SPI_ONLY: + rule_type = spi_only; + break; + case RTE_IPSEC_SAD_SPI_DIP: + rule_type = spi_dip; + break; + case RTE_IPSEC_SAD_SPI_DIP_SIP: + rule_type = spi_dip_sip; + break; + default: + return; + } + + print_tuple(af, spi, dip, sip); + v4 = &rule->tuple.v4; + v6 = &rule->tuple.v6; + spi = (config.ipv6 == 0) ? v4->spi : v6->spi; + dip = (config.ipv6 == 0) ? &v4->dip : (const void *)v6->dip; + sip = (config.ipv6 == 0) ? &v4->sip : (const void *)v6->sip; + printf("\n\tpoints to RULE ID %zu ", + RTE_PTR_DIFF(res, rules_tbl)/sizeof(struct rule)); + print_tuple(af, spi, dip, sip); + printf(" %s\n", rule_type); +} + +static int +lookup(void *arg) +{ + int ret; + unsigned int i, j; + const union rte_ipsec_sad_key *keys[BURST_SZ_MAX]; + void *vals[BURST_SZ_MAX]; + uint64_t start, acc = 0; + uint32_t burst_sz; + struct rte_ipsec_sad *sad = arg; + + burst_sz = RTE_MIN(config.burst_sz, config.nb_tuples); + for (i = 0; i < config.nb_tuples; i += burst_sz) { + for (j = 0; j < burst_sz; j++) + keys[j] = (union rte_ipsec_sad_key *) + (&tuples_tbl[i + j].tuple); + start = rte_rdtsc_precise(); + ret = rte_ipsec_sad_lookup(sad, keys, vals, burst_sz); + acc += rte_rdtsc_precise() - start; + if (ret < 0) + rte_exit(-EINVAL, "Lookup failed\n"); + if (config.verbose) { + for (j = 0; j < burst_sz; j++) + print_result(keys[j], vals[j]); + } + } + printf("Average lookup cycles %.2Lf, lookups/sec: %.2Lf\n", + (long double)acc / config.nb_tuples, + (long double)config.nb_tuples * rte_get_tsc_hz() / acc); + + return 0; +} + +static void +add_rules(struct rte_ipsec_sad *sad, uint32_t fract) +{ + int32_t ret; + uint32_t i, j, f, fn, n; + uint64_t start, tm[fract + 1]; + uint32_t nm[fract + 1]; + + f = (config.nb_rules > fract) ? config.nb_rules / fract : 1; + + for (n = 0, j = 0; n != config.nb_rules; n = fn, j++) { + + fn = n + f; + fn = fn > config.nb_rules ? config.nb_rules : fn; + + start = rte_rdtsc_precise(); + for (i = n; i != fn; i++) { + ret = rte_ipsec_sad_add(sad, + &rules_tbl[i].tuple, + rules_tbl[i].rule_type, &rules_tbl[i]); + if (ret != 0) + rte_exit(ret, "%s failed @ %u-th rule\n", + __func__, i); + } + tm[j] = rte_rdtsc_precise() - start; + nm[j] = fn - n; + } + + for (i = 0; i != j; i++) + printf("ADD %u rules, %.2Lf cycles/rule, %.2Lf ADD/sec\n", + nm[i], (long double)tm[i] / nm[i], + (long double)nm[i] * rte_get_tsc_hz() / tm[i]); +} + +static void +del_rules(struct rte_ipsec_sad *sad, uint32_t fract) +{ + int32_t ret; + uint32_t i, j, f, fn, n; + uint64_t start, tm[fract + 1]; + uint32_t nm[fract + 1]; + + f = (config.nb_rules > fract) ? config.nb_rules / fract : 1; + + for (n = 0, j = 0; n != config.nb_rules; n = fn, j++) { + + fn = n + f; + fn = fn > config.nb_rules ? config.nb_rules : fn; + + start = rte_rdtsc_precise(); + for (i = n; i != fn; i++) { + ret = rte_ipsec_sad_del(sad, + &rules_tbl[i].tuple, + rules_tbl[i].rule_type); + if (ret != 0 && ret != -ENOENT) + rte_exit(ret, "%s failed @ %u-th rule\n", + __func__, i); + } + tm[j] = rte_rdtsc_precise() - start; + nm[j] = fn - n; + } + + for (i = 0; i != j; i++) + printf("DEL %u rules, %.2Lf cycles/rule, %.2Lf DEL/sec\n", + nm[i], (long double)tm[i] / nm[i], + (long double)nm[i] * rte_get_tsc_hz() / tm[i]); +} + +int +main(int argc, char **argv) +{ + int ret; + struct rte_ipsec_sad *sad; + struct rte_ipsec_sad_conf conf; + unsigned int lcore_id; + + ret = rte_eal_init(argc, argv); + if (ret < 0) + rte_panic("Cannot init EAL\n"); + + argc -= ret; + argv += ret; + + config.prgname = argv[0]; + + parse_opts(argc, argv); + tbl_init(&rules_tbl, &config.nb_rules, config.rules_file, 1); + tbl_init(&tuples_tbl, &config.nb_tuples, config.tuples_file, 0); + if (config.rules_file != NULL) { + config.fract_32 = (100 * config.nb_rules_32) / config.nb_rules; + config.fract_64 = (100 * config.nb_rules_64) / config.nb_rules; + config.fract_96 = (100 * config.nb_rules_96) / config.nb_rules; + } + if (config.tuples_file != NULL) { + config.fract_rnd_tuples = 0; + config.nb_tuples_rnd = 0; + } + conf.socket_id = -1; + conf.max_sa[RTE_IPSEC_SAD_SPI_ONLY] = config.nb_rules_32 * 5 / 4; + conf.max_sa[RTE_IPSEC_SAD_SPI_DIP] = config.nb_rules_64 * 5 / 4; + conf.max_sa[RTE_IPSEC_SAD_SPI_DIP_SIP] = config.nb_rules_96 * 5 / 4; + if (config.ipv6) + conf.flags |= RTE_IPSEC_SAD_FLAG_IPV6; + if (config.concurrent_rw) + conf.flags |= RTE_IPSEC_SAD_FLAG_RW_CONCURRENCY; + sad = rte_ipsec_sad_create("test", &conf); + if (sad == NULL) + rte_exit(-rte_errno, "can not allocate SAD table\n"); + + print_config(); + + add_rules(sad, 10); + if (config.parallel_lookup) + rte_eal_mp_remote_launch(lookup, sad, SKIP_MASTER); + + lookup(sad); + if (config.parallel_lookup) + RTE_LCORE_FOREACH_SLAVE(lcore_id) + if (rte_eal_wait_lcore(lcore_id) < 0) + return -1; + + del_rules(sad, 10); + + return 0; +} diff --git a/app/test-sad/meson.build b/app/test-sad/meson.build new file mode 100644 index 0000000000..31f9aab632 --- /dev/null +++ b/app/test-sad/meson.build @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2019 Intel Corporation + +allow_experimental_apis = true +sources = files('main.c') +deps += ['ipsec', 'net'] diff --git a/doc/guides/rel_notes/release_19_11.rst b/doc/guides/rel_notes/release_19_11.rst index a00938b7c7..0421afd31d 100644 --- a/doc/guides/rel_notes/release_19_11.rst +++ b/doc/guides/rel_notes/release_19_11.rst @@ -138,7 +138,9 @@ New Features * **Updated the IPSec library.** - Added SA Database API to ``librte_ipsec``. + Added SA Database API to ``librte_ipsec``. A new test-sad application is also + introduced to evaluate and perform custom functional and performance tests + for IPsec SAD implementation. * **Introduced FIFO for NTB PMD.** -- 2.20.1