From 32f5e00c46acbad7bae51f8d8b367c102d666a68 Mon Sep 17 00:00:00 2001 From: Akeel Ali <701916+AkeelAli@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:50:32 -0400 Subject: [PATCH 1/7] First Version of VPP Tunnel Termination Plugin --- .../docker-sonic-vpp/conf/startup.conf.tmpl | 1 + .../docker-syncd-vpp/conf/startup.conf.tmpl | 1 + .../vpp/plugins/tunterm_acl/CMakeLists.txt | 29 + platform/vpp/plugins/tunterm_acl/FEATURE.yaml | 13 + .../vpp/plugins/tunterm_acl/tunterm_acl.api | 87 +++ .../vpp/plugins/tunterm_acl/tunterm_acl_api.c | 346 ++++++++++ .../vpp/plugins/tunterm_acl/tunterm_acl_api.h | 45 ++ .../vpp/plugins/tunterm_acl/tunterm_acl_cli.c | 70 ++ .../plugins/tunterm_acl/tunterm_acl_decap.c | 627 ++++++++++++++++++ .../plugins/tunterm_acl/tunterm_acl_node.c | 216 ++++++ .../tunterm_acl/tunterm_acl_redirect.c | 257 +++++++ .../tunterm_acl/tunterm_acl_redirect.h | 37 ++ platform/vpp/vppbld/Makefile | 3 + platform/vpp/vppbld/vpp.patch | 13 + 14 files changed, 1745 insertions(+) create mode 100644 platform/vpp/plugins/tunterm_acl/CMakeLists.txt create mode 100644 platform/vpp/plugins/tunterm_acl/FEATURE.yaml create mode 100644 platform/vpp/plugins/tunterm_acl/tunterm_acl.api create mode 100644 platform/vpp/plugins/tunterm_acl/tunterm_acl_api.c create mode 100644 platform/vpp/plugins/tunterm_acl/tunterm_acl_api.h create mode 100644 platform/vpp/plugins/tunterm_acl/tunterm_acl_cli.c create mode 100644 platform/vpp/plugins/tunterm_acl/tunterm_acl_decap.c create mode 100644 platform/vpp/plugins/tunterm_acl/tunterm_acl_node.c create mode 100644 platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.c create mode 100644 platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h diff --git a/platform/vpp/docker-sonic-vpp/conf/startup.conf.tmpl b/platform/vpp/docker-sonic-vpp/conf/startup.conf.tmpl index a6897f4..a449d77 100644 --- a/platform/vpp/docker-sonic-vpp/conf/startup.conf.tmpl +++ b/platform/vpp/docker-sonic-vpp/conf/startup.conf.tmpl @@ -227,6 +227,7 @@ plugins { plugin linux_nl_plugin.so { enable } plugin acl_plugin.so { enable } plugin vxlan_plugin.so { enable } + plugin tunterm_acl_plugin.so { enable } ## Enable all plugins by default and then selectively disable specific plugins # plugin dpdk_plugin.so { disable } diff --git a/platform/vpp/docker-syncd-vpp/conf/startup.conf.tmpl b/platform/vpp/docker-syncd-vpp/conf/startup.conf.tmpl index a6897f4..a449d77 100644 --- a/platform/vpp/docker-syncd-vpp/conf/startup.conf.tmpl +++ b/platform/vpp/docker-syncd-vpp/conf/startup.conf.tmpl @@ -227,6 +227,7 @@ plugins { plugin linux_nl_plugin.so { enable } plugin acl_plugin.so { enable } plugin vxlan_plugin.so { enable } + plugin tunterm_acl_plugin.so { enable } ## Enable all plugins by default and then selectively disable specific plugins # plugin dpdk_plugin.so { disable } diff --git a/platform/vpp/plugins/tunterm_acl/CMakeLists.txt b/platform/vpp/plugins/tunterm_acl/CMakeLists.txt new file mode 100644 index 0000000..75f5e4f --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/CMakeLists.txt @@ -0,0 +1,29 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include_directories(${CMAKE_SOURCE_DIR}) + +# for generated API headers: +include_directories(${CMAKE_BINARY_DIR}) + +add_vpp_plugin(tunterm_acl + SOURCES + tunterm_acl_api.c + tunterm_acl_cli.c + tunterm_acl_decap.c + tunterm_acl_redirect.c + tunterm_acl_node.c + + API_FILES + tunterm_acl.api +) diff --git a/platform/vpp/plugins/tunterm_acl/FEATURE.yaml b/platform/vpp/plugins/tunterm_acl/FEATURE.yaml new file mode 100644 index 0000000..0b04dc5 --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/FEATURE.yaml @@ -0,0 +1,13 @@ +--- +name: Tunnel Termination ACL +maintainer: Akeel Ali +features: + - Applies ACL after Tunnel Decap + - Current support is limited to a specific use-case + - Tunnel: IPv4 VxLAN Tunnel Termination + - Fields: DST IPv4/6 Classification + - Action: Redirect (ip4/6-rewrite) +description: "Tunnel Termination ACL plugin" +state: experimental +properties: [CLI, API] + diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl.api b/platform/vpp/plugins/tunterm_acl/tunterm_acl.api new file mode 100644 index 0000000..8f66922 --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl.api @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +option version = "1.0.0"; +import "vnet/interface_types.api"; +import "vnet/fib/fib_types.api"; + +typedef tunterm_acl_rule { + vl_api_address_t dst; + + /* TODO: Add support for multiple paths */ + vl_api_fib_path_t path; +}; + +/** \brief Replace an existing tunterm acl in-place or create a new one + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param tunterm_acl_index - an existing tunterm index (0..0xfffffffe) to replace, or 0xffffffff to make a new one + @param is_ipv6 - is this an IPv6 acl + @param count - number of rules + @r - Rules for this tunterm acl +*/ + +define tunterm_acl_add_replace +{ + u32 client_index; + u32 context; + u32 tunterm_acl_index; /* ~0 to add, existing # to replace */ + bool is_ipv6; + u32 count; + vl_api_tunterm_acl_rule_t r[count]; +}; + +/** \brief Reply to add/replace tunterm acl + @param context - returned sender context, to match reply w/ request + @param tunterm_acl_index - index of the updated or newly created tunterm acl + @param retval 0 - no error +*/ + +define tunterm_acl_add_replace_reply +{ + u32 context; + u32 tunterm_acl_index; + i32 retval; +}; + +/** \brief Delete a tunterm acl + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param tunterm_acl_index - tunterm index to delete +*/ + +autoreply define tunterm_acl_del +{ + u32 client_index; + u32 context; + u32 tunterm_acl_index; +}; + +/** \brief Add/remove a tunterm acl index to/from an interface + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param is_add - add or delete the tunterm index + @param sw_if_index - the interface to/from which we add/remove the tunterm acl + @param tunterm_acl_index - index of tunterm acl for the operation +*/ + +autoreply define tunterm_acl_interface_add_del +{ + u32 client_index; + u32 context; + bool is_add; + vl_api_interface_index_t sw_if_index; + u32 tunterm_acl_index; +}; diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.c b/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.c new file mode 100644 index 0000000..8ec460d --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.c @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2024 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @file + * @brief Tunnel Terminated Plugin API Code + */ + +#include +#include +#include + +#include +#include + +#include +#include + +#define REPLY_MSG_ID_BASE sm->msg_id_base +#include + +/* *INDENT-OFF* */ +VLIB_PLUGIN_REGISTER () = { + .version = TUNTERM_ACL_PLUGIN_BUILD_VER, + .description = "Tunnel Terminated ACL Plugin", +}; +/* *INDENT-ON* */ + +tunterm_acl_main_t tunterm_acl_main; + +#include +#include +#include + +#include +#include +#include + +#include "tunterm_acl_redirect.h" + +static int update_classify_table_and_sessions (bool is_ipv6, u32 count, vl_api_tunterm_acl_rule_t rules[], u32 * tunterm_acl_index) +{ + tunterm_acl_main_t * sm = &tunterm_acl_main; + vnet_classify_main_t *cm = &vnet_classify_main; + u32 table_index = *tunterm_acl_index; + int rv = 0; +#define CLASSIFY_TABLE_VECTOR_SIZE 16 + u8 mask[7*CLASSIFY_TABLE_VECTOR_SIZE]; + + clib_memset (mask, 0, sizeof (mask)); + u32 nbuckets = 2; + u32 memory_size = 2 << 22; + u32 skip = 5; + u32 match; + + /* Create the table if it's an add operation */ + if (table_index == ~0) { + if (is_ipv6) { + match = 2; + for (int i = 8; i <= 23; i++) { + mask[i] = 0xff; + } + } else { + match = 1; + for (int i = 0; i <= 3; i++) { + mask[i] = 0xff; + } + } + + rv = vnet_classify_add_del_table (cm, mask, nbuckets /* nbuckets */, memory_size /* memory_size */, + skip /* skip */, match, ~0 /* next_table_index */, + ~0 /* miss_next_index */, &table_index, + 0 /* current_data_flag */, 0 /* current_data_offset */, + 1 /* is_add */, 0 /* del_chain */); + + if (rv != 0) { + clib_warning ("vnet_classify_add_del_table failed"); + return rv; + } + + *tunterm_acl_index = table_index; + + vec_validate_init_empty (sm->classify_table_index_is_v6, table_index, 0); + sm->classify_table_index_is_v6[table_index] = is_ipv6; + } + + /* Make sure table AF is not being changed in the replace case */ + if (sm->classify_table_index_is_v6[table_index] != is_ipv6) { + clib_error ("Table AF mismatch"); + return VNET_API_ERROR_INVALID_VALUE_2; + } + + /* First clear anything already in the table */ + rv = tunterm_acl_redirect_clear (vlib_get_main (), table_index); + + if (rv != 0) { + clib_warning ("tunterm_acl_redirect_clear failed"); + return rv; + } + + /* Now add the new stuff */ + for (int i = 0; i < count; i++) { + /* (1) Process Route */ + fib_route_path_t *paths_ = 0; + u8 n_paths = 1; //rules[i].n_paths; + if (n_paths <= 0) { + return VNET_API_ERROR_NO_PATHS_IN_ROUTE; + } + + for (int j = 0; j < n_paths; j++) { + fib_route_path_t path; + clib_memset(&path, 0, sizeof(path)); + + if ((rv = fib_api_path_decode (&rules[i].path, &path))) { + vec_free (paths_); + return rv; + } + vec_add1 (paths_, path); + } + + /* (2) Process DST IP */ + if (is_ipv6 != rules[i].dst.af) { + return VNET_API_ERROR_INVALID_VALUE_3; + vec_free (paths_); + } + + ip46_address_t dst; + ip_address_decode (&rules[i].dst, &dst); + + u8 *match_vec = 0; + clib_memset (mask, 0, sizeof (mask)); + + if (is_ipv6) { + for (int i = 0; i < 16; i++) { + mask[8 + i + skip * CLASSIFY_TABLE_VECTOR_SIZE] = dst.ip6.as_u8[i]; + } + } else { + for (int i = 0; i < 4; i++) { + mask[i + skip * CLASSIFY_TABLE_VECTOR_SIZE] = dst.ip4.data[i]; + } + } + + /* (3) Add Session */ + vec_validate (match_vec, sizeof (mask) - 1); + clib_memcpy (match_vec, mask, sizeof (mask)); + + rv = tunterm_acl_redirect_add (vlib_get_main (), table_index, + 0 /* opaque_index */, is_ipv6 ? DPO_PROTO_IP6 : DPO_PROTO_IP4, + match_vec /* match */, paths_); + + vec_free(paths_); + vec_free(match_vec); + + if (rv != 0) { + clib_warning ("tunterm_acl_redirect_add failed"); + return rv; + } + } + + return rv; +} + +static int +verify_message_len (void *mp, u64 expected_len, char *where) +{ + u32 supplied_len = vl_msg_api_get_msg_length (mp); + if (supplied_len < expected_len) { + clib_warning ("%s: Supplied message length %d is less than expected %d", + where, supplied_len, expected_len); + return 0; + } else { + return 1; + } +} + +/** + * @brief Plugin API message handler. + */ +static void +vl_api_tunterm_acl_add_replace_t_handler (vl_api_tunterm_acl_add_replace_t * mp) +{ + vl_api_tunterm_acl_add_replace_reply_t *rmp; + tunterm_acl_main_t * sm = &tunterm_acl_main; + int rv = -1; + u32 tunterm_acl_index = ntohl (mp->tunterm_acl_index); + u32 acl_count = ntohl (mp->count); + u64 expected_len = sizeof (*mp) + acl_count * sizeof (mp->r[0]); + + if (verify_message_len (mp, expected_len, "tunterm_acl_add_replace")) { + rv = update_classify_table_and_sessions(mp->is_ipv6, acl_count, mp->r, &tunterm_acl_index); + } else { + rv = VNET_API_ERROR_INVALID_VALUE; + } + + REPLY_MACRO2(VL_API_TUNTERM_ACL_ADD_REPLACE_REPLY, + ({ + rmp->tunterm_acl_index = htonl(tunterm_acl_index); + })); +} + +static void +vl_api_tunterm_acl_del_t_handler (vl_api_tunterm_acl_del_t * mp) +{ + tunterm_acl_main_t * sm = &tunterm_acl_main; + vnet_classify_main_t *cm = &vnet_classify_main; + vl_api_tunterm_acl_del_reply_t *rmp; + u32 tunterm_acl_index = ntohl (mp->tunterm_acl_index); + int rv = 0; + + if (tunterm_acl_index == ~0) { + rv = VNET_API_ERROR_INVALID_VALUE; + goto exit; + } + + /* If tunterm index still being used, reject delete */ + for (int i = 0; i < vec_len(sm->classify_table_index_by_sw_if_index_v4); i++) { + if (sm->classify_table_index_by_sw_if_index_v4[i] == tunterm_acl_index) { + rv = VNET_API_ERROR_RSRC_IN_USE; + goto exit; + } + } + + for (int i = 0; i < vec_len(sm->classify_table_index_by_sw_if_index_v6); i++) { + if (sm->classify_table_index_by_sw_if_index_v6[i] == tunterm_acl_index) { + rv = VNET_API_ERROR_RSRC_IN_USE; + goto exit; + } + } + + /* First clear all the redirect sessions on that table */ + rv = tunterm_acl_redirect_clear (vlib_get_main (), tunterm_acl_index); + if (rv != 0) { + clib_warning ("tunterm_acl_redirect_clear failed"); + goto exit; + } + + /* Then delete the classify table */ + vnet_classify_delete_table_index (cm, tunterm_acl_index, 1 /* del_chain */); + + sm->classify_table_index_is_v6[tunterm_acl_index] = 0; + +exit: + REPLY_MACRO (VL_API_TUNTERM_ACL_DEL_REPLY); +} + +static void +vl_api_tunterm_acl_interface_add_del_t_handler (vl_api_tunterm_acl_interface_add_del_t * mp) +{ + tunterm_acl_main_t * sm = &tunterm_acl_main; + vnet_interface_main_t *im = &sm->vnet_main->interface_main; + u32 sw_if_index = ntohl (mp->sw_if_index); + u32 tunterm_acl_index = ntohl (mp->tunterm_acl_index); + vl_api_tunterm_acl_interface_add_del_reply_t *rmp; + int rv = 0; + + if (tunterm_acl_index == ~0) { + rv = VNET_API_ERROR_INVALID_VALUE; + goto exit; + } + + if (pool_is_free_index (im->sw_interfaces, sw_if_index)) { + rv = VNET_API_ERROR_INVALID_SW_IF_INDEX; + goto exit; + } + + /* make sure both are init as you can get v6 packets while only v4 acl installed. node will do a lookup in v6 table and crash otherwise */ + vec_validate_init_empty (sm->classify_table_index_by_sw_if_index_v6, sw_if_index, ~0); + vec_validate_init_empty (sm->classify_table_index_by_sw_if_index_v4, sw_if_index, ~0); + + bool is_ipv6 = sm->classify_table_index_is_v6[tunterm_acl_index]; + + if (mp->is_add) { + + /* First setup forwarding data, then enable */ + if (is_ipv6) { + sm->classify_table_index_by_sw_if_index_v6[sw_if_index] = tunterm_acl_index; + } else { + sm->classify_table_index_by_sw_if_index_v4[sw_if_index] = tunterm_acl_index; + } + + if (!(vnet_feature_is_enabled("ip4-unicast", "tunterm-ip4-vxlan-bypass", sw_if_index))) { + rv = vnet_feature_enable_disable ("ip4-unicast", "tunterm-ip4-vxlan-bypass", + sw_if_index, mp->is_add, 0, 0); + } + } else { + u32 tunterm_acl_index_v4 = sm->classify_table_index_by_sw_if_index_v4[sw_if_index]; + u32 tunterm_acl_index_v6 = sm->classify_table_index_by_sw_if_index_v6[sw_if_index]; + + if (tunterm_acl_index != tunterm_acl_index_v4 && tunterm_acl_index != tunterm_acl_index_v6) { + /* tunterm being removed is not attached */ + rv = VNET_API_ERROR_INVALID_VALUE_2; + goto exit; + } + + /* if last tunterm being removed from intf, then disable */ + if (tunterm_acl_index_v4 == ~0 || tunterm_acl_index_v6 == ~0) { + rv = vnet_feature_enable_disable ("ip4-unicast", "tunterm-ip4-vxlan-bypass", + sw_if_index, mp->is_add, 0, 0); + + if (rv != 0) { + goto exit; + } + } + + /* finally, remove forwarding data */ + if (is_ipv6) { + sm->classify_table_index_by_sw_if_index_v6[sw_if_index] = ~0; + } else { + sm->classify_table_index_by_sw_if_index_v4[sw_if_index] = ~0; + } + } + +exit: + REPLY_MACRO (VL_API_TUNTERM_ACL_INTERFACE_ADD_DEL_REPLY); +} + +/* API definitions */ +#include + +/** + * @brief Initialize the tunterm plugin. + */ +static clib_error_t * tunterm_acl_init (vlib_main_t * vm) +{ + tunterm_acl_main_t * sm = &tunterm_acl_main; + + sm->vnet_main = vnet_get_main (); + + /* Add our API messages to the global name_crc hash table */ + sm->msg_id_base = setup_message_id_table (); + + return tunterm_acl_redirect_init(vm); +} + +VLIB_INIT_FUNCTION (tunterm_acl_init); diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.h b/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.h new file mode 100644 index 0000000..b9192e3 --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef __included_tunterm_acl_api_h__ +#define __included_tunterm_acl_api_h__ + +#include +#include +#include + +#include +#include +#include + +typedef struct { + /* API message ID base */ + u16 msg_id_base; + + /* convenience */ + vnet_main_t * vnet_main; + + u32 *classify_table_index_by_sw_if_index_v4; + u32 *classify_table_index_by_sw_if_index_v6; + bool *classify_table_index_is_v6; +} tunterm_acl_main_t; + +extern tunterm_acl_main_t tunterm_acl_main; + +extern vlib_node_registration_t tunterm_acl_node; + +#define TUNTERM_ACL_PLUGIN_BUILD_VER "1.0" + +#endif /* __included_tunterm_acl_api_h__ */ + diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_cli.c b/platform/vpp/plugins/tunterm_acl/tunterm_acl_cli.c new file mode 100644 index 0000000..69d3784 --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_cli.c @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @file + * @brief Tunnel Terminated Plugin CLI handling. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +static clib_error_t * +show_tunterm_acl_interfaces_command_fn (vlib_main_t * vm, + unformat_input_t * input, + vlib_cli_command_t * cmd) +{ + tunterm_acl_main_t * sm = &tunterm_acl_main; + vnet_main_t * vnm = vnet_get_main (); + vnet_interface_main_t * im = &vnm->interface_main; + vlib_cli_output (vm, "Interface\tIndex\tIPv4 Tunterm Index\tIPv6 Tunterm Index"); + + /* Iterate over all interfaces */ + vnet_sw_interface_t * swif; + pool_foreach (swif, im->sw_interfaces) { + u32 sw_if_index = swif->sw_if_index; + + /* Check if the interface has "tunterm-ip4-vxlan-bypass" enabled */ + if (vnet_feature_is_enabled("ip4-unicast", "tunterm-ip4-vxlan-bypass", sw_if_index)) { + + u32 tunterm_acl_index_v4 = sm->classify_table_index_by_sw_if_index_v4[sw_if_index]; + u32 tunterm_acl_index_v6 = sm->classify_table_index_by_sw_if_index_v6[sw_if_index]; + + vlib_cli_output (vm, "%U\t%u\t%u\t%u", + format_vnet_sw_if_index_name, vnm, sw_if_index, + sw_if_index, tunterm_acl_index_v4, tunterm_acl_index_v6); + } + } + + return 0; +} + +/* *INDENT-OFF* */ +VLIB_CLI_COMMAND (show_tunterm_acl_interfaces_command, static) = { + .path = "show tunterm interfaces", + .short_help = "show tunterm interfaces", + .function = show_tunterm_acl_interfaces_command_fn, +}; +/* *INDENT-ON* */ diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_decap.c b/platform/vpp/plugins/tunterm_acl/tunterm_acl_decap.c new file mode 100644 index 0000000..fd6b16b --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_decap.c @@ -0,0 +1,627 @@ +/* + * tunterm_acl_decap.c: vxlan tunnel decap packet processing + * + * Copyright (c) 2024 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include + +vxlan_main_t* tunterm_acl_vxlan_main; + +typedef enum +{ + IP_VXLAN_BYPASS_NEXT_DROP, + IP_VXLAN_BYPASS_NEXT_VXLAN, + IP_VXLAN_BYPASS_NEXT_TUNTERM, + IP_VXLAN_BYPASS_N_NEXT, +} tunterm_acl_ip_vxlan_bypass_next_t; + + +typedef vxlan4_tunnel_key_t last_tunnel_cache4; + +static const vxlan_decap_info_t decap_not_found = { + .sw_if_index = ~0, + .next_index = VXLAN_INPUT_NEXT_DROP, + .error = VXLAN_ERROR_NO_SUCH_TUNNEL +}; + +static const vxlan_decap_info_t decap_bad_flags = { + .sw_if_index = ~0, + .next_index = VXLAN_INPUT_NEXT_DROP, + .error = VXLAN_ERROR_BAD_FLAGS +}; + +vxlan_decap_info_t +tunterm_acl_vxlan4_find_tunnel (vxlan_main_t * vxm, last_tunnel_cache4 * cache, + u32 fib_index, ip4_header_t * ip4_0, + vxlan_header_t * vxlan0, u32 * stats_sw_if_index) +{ + if (PREDICT_FALSE (vxlan0->flags != VXLAN_FLAGS_I)) + return decap_bad_flags; + + /* Make sure VXLAN tunnel exist according to packet S/D IP, UDP port, VRF, + * and VNI */ + u32 dst = ip4_0->dst_address.as_u32; + u32 src = ip4_0->src_address.as_u32; + udp_header_t *udp = ip4_next_header (ip4_0); + vxlan4_tunnel_key_t key4 = { + .key[0] = ((u64) dst << 32) | src, + .key[1] = ((u64) udp->dst_port << 48) | ((u64) fib_index << 32) | + vxlan0->vni_reserved, + }; + + if (PREDICT_TRUE + (key4.key[0] == cache->key[0] && key4.key[1] == cache->key[1])) + { + /* cache hit */ + vxlan_decap_info_t di = {.as_u64 = cache->value }; + *stats_sw_if_index = di.sw_if_index; + return di; + } + + int rv = clib_bihash_search_inline_16_8 (&vxm->vxlan4_tunnel_by_key, &key4); + if (PREDICT_TRUE (rv == 0)) + { + *cache = key4; + vxlan_decap_info_t di = {.as_u64 = key4.value }; + *stats_sw_if_index = di.sw_if_index; + return di; + } + + /* try multicast */ + if (PREDICT_TRUE (!ip4_address_is_multicast (&ip4_0->dst_address))) + return decap_not_found; + + /* search for mcast decap info by mcast address */ + key4.key[0] = dst; + rv = clib_bihash_search_inline_16_8 (&vxm->vxlan4_tunnel_by_key, &key4); + if (rv != 0) + return decap_not_found; + + /* search for unicast tunnel using the mcast tunnel local(src) ip */ + vxlan_decap_info_t mdi = {.as_u64 = key4.value }; + key4.key[0] = ((u64) mdi.local_ip.as_u32 << 32) | src; + rv = clib_bihash_search_inline_16_8 (&vxm->vxlan4_tunnel_by_key, &key4); + if (PREDICT_FALSE (rv != 0)) + return decap_not_found; + + /* mcast traffic does not update the cache */ + *stats_sw_if_index = mdi.sw_if_index; + vxlan_decap_info_t di = {.as_u64 = key4.value }; + return di; +} + +typedef vxlan6_tunnel_key_t last_tunnel_cache6; + +always_inline vxlan_decap_info_t +tunterm_acl_vxlan6_find_tunnel (vxlan_main_t * vxm, last_tunnel_cache6 * cache, + u32 fib_index, ip6_header_t * ip6_0, + vxlan_header_t * vxlan0, u32 * stats_sw_if_index) +{ + if (PREDICT_FALSE (vxlan0->flags != VXLAN_FLAGS_I)) + return decap_bad_flags; + + /* Make sure VXLAN tunnel exist according to packet SIP, UDP port, VRF, and + * VNI */ + udp_header_t *udp = ip6_next_header (ip6_0); + vxlan6_tunnel_key_t key6 = { + .key[0] = ip6_0->src_address.as_u64[0], + .key[1] = ip6_0->src_address.as_u64[1], + .key[2] = ((u64) udp->dst_port << 48) | ((u64) fib_index << 32) | + vxlan0->vni_reserved, + }; + + if (PREDICT_FALSE + (clib_bihash_key_compare_24_8 (key6.key, cache->key) == 0)) + { + int rv = + clib_bihash_search_inline_24_8 (&vxm->vxlan6_tunnel_by_key, &key6); + if (PREDICT_FALSE (rv != 0)) + return decap_not_found; + + *cache = key6; + } + vxlan_tunnel_t *t0 = pool_elt_at_index (vxm->tunnels, cache->value); + + /* Validate VXLAN tunnel SIP against packet DIP */ + if (PREDICT_TRUE (ip6_address_is_equal (&ip6_0->dst_address, &t0->src.ip6))) + *stats_sw_if_index = t0->sw_if_index; + else + { + /* try multicast */ + if (PREDICT_TRUE (!ip6_address_is_multicast (&ip6_0->dst_address))) + return decap_not_found; + + /* Make sure mcast VXLAN tunnel exist by packet DIP and VNI */ + key6.key[0] = ip6_0->dst_address.as_u64[0]; + key6.key[1] = ip6_0->dst_address.as_u64[1]; + int rv = + clib_bihash_search_inline_24_8 (&vxm->vxlan6_tunnel_by_key, &key6); + if (PREDICT_FALSE (rv != 0)) + return decap_not_found; + + vxlan_tunnel_t *mcast_t0 = pool_elt_at_index (vxm->tunnels, key6.value); + *stats_sw_if_index = mcast_t0->sw_if_index; + } + + vxlan_decap_info_t di = { + .sw_if_index = t0->sw_if_index, + .next_index = t0->decap_next_index, + }; + return di; +} + +always_inline uword +tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, + vlib_node_runtime_t * node, + vlib_frame_t * frame, u32 is_ip4) +{ + vxlan_main_t *vxm = tunterm_acl_vxlan_main; + + u32 *from, *to_next, n_left_from, n_left_to_next, next_index; + vlib_node_runtime_t *error_node = + vlib_node_get_runtime (vm, ip4_input_node.index); + vtep4_key_t last_vtep4; /* last IPv4 address / fib index + matching a local VTEP address */ + vtep6_key_t last_vtep6; /* last IPv6 address / fib index + matching a local VTEP address */ + vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b = bufs; + + last_tunnel_cache4 last4; + last_tunnel_cache6 last6; + + from = vlib_frame_vector_args (frame); + n_left_from = frame->n_vectors; + next_index = node->cached_next_index; + + vlib_get_buffers (vm, from, bufs, n_left_from); + + if (node->flags & VLIB_NODE_FLAG_TRACE) + ip4_forward_next_trace (vm, node, frame, VLIB_TX); + + if (is_ip4) + { + vtep4_key_init (&last_vtep4); + clib_memset (&last4, 0xff, sizeof last4); + } + else + { + vtep6_key_init (&last_vtep6); + clib_memset (&last6, 0xff, sizeof last6); + } + + while (n_left_from > 0) + { + vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next); + + while (n_left_from >= 4 && n_left_to_next >= 2) + { + vlib_buffer_t *b0, *b1; + ip4_header_t *ip40, *ip41; + ip6_header_t *ip60, *ip61; + udp_header_t *udp0, *udp1; + vxlan_header_t *vxlan0, *vxlan1; + u32 bi0, ip_len0, udp_len0, flags0, next0; + u32 bi1, ip_len1, udp_len1, flags1, next1; + i32 len_diff0, len_diff1; + u8 error0, good_udp0, proto0; + u8 error1, good_udp1, proto1; + u32 stats_if0 = ~0, stats_if1 = ~0; + + /* Prefetch next iteration. */ + { + vlib_prefetch_buffer_header (b[2], LOAD); + vlib_prefetch_buffer_header (b[3], LOAD); + + CLIB_PREFETCH (b[2]->data, 2 * CLIB_CACHE_LINE_BYTES, LOAD); + CLIB_PREFETCH (b[3]->data, 2 * CLIB_CACHE_LINE_BYTES, LOAD); + } + + bi0 = to_next[0] = from[0]; + bi1 = to_next[1] = from[1]; + from += 2; + n_left_from -= 2; + to_next += 2; + n_left_to_next -= 2; + + b0 = b[0]; + b1 = b[1]; + b += 2; + if (is_ip4) + { + ip40 = vlib_buffer_get_current (b0); + ip41 = vlib_buffer_get_current (b1); + } + else + { + ip60 = vlib_buffer_get_current (b0); + ip61 = vlib_buffer_get_current (b1); + } + + /* Setup packet for next IP feature */ + vnet_feature_next (&next0, b0); + vnet_feature_next (&next1, b1); + + if (is_ip4) + { + /* Treat IP frag packets as "experimental" protocol for now + until support of IP frag reassembly is implemented */ + proto0 = ip4_is_fragment (ip40) ? 0xfe : ip40->protocol; + proto1 = ip4_is_fragment (ip41) ? 0xfe : ip41->protocol; + } + else + { + proto0 = ip60->protocol; + proto1 = ip61->protocol; + } + + /* Process packet 0 */ + if (proto0 != IP_PROTOCOL_UDP) + goto exit0; /* not UDP packet */ + + if (is_ip4) + udp0 = ip4_next_header (ip40); + else + udp0 = ip6_next_header (ip60); + + u32 fi0 = vlib_buffer_get_ip_fib_index (b0, is_ip4); + vxlan0 = vlib_buffer_get_current (b0) + sizeof (udp_header_t) + + sizeof (ip4_header_t); + + vxlan_decap_info_t di0 = + is_ip4 ? + tunterm_acl_vxlan4_find_tunnel (vxm, &last4, fi0, ip40, vxlan0, &stats_if0) : + tunterm_acl_vxlan6_find_tunnel (vxm, &last6, fi0, ip60, vxlan0, &stats_if0); + + if (PREDICT_FALSE (di0.sw_if_index == ~0)) + goto exit0; /* unknown interface */ + + /* Validate DIP against VTEPs */ + if (is_ip4) + { +#ifdef CLIB_HAVE_VEC512 + if (!vtep4_check_vector (&vxm->vtep_table, b0, ip40, &last_vtep4, + &vxm->vtep4_u512)) +#else + if (!vtep4_check (&vxm->vtep_table, b0, ip40, &last_vtep4)) +#endif + goto exit0; /* no local VTEP for VXLAN packet */ + } + else + { + if (!vtep6_check (&vxm->vtep_table, b0, ip60, &last_vtep6)) + goto exit0; /* no local VTEP for VXLAN packet */ + } + + flags0 = b0->flags; + good_udp0 = (flags0 & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0; + + /* Don't verify UDP checksum for packets with explicit zero checksum. */ + good_udp0 |= udp0->checksum == 0; + + /* Verify UDP length */ + if (is_ip4) + ip_len0 = clib_net_to_host_u16 (ip40->length); + else + ip_len0 = clib_net_to_host_u16 (ip60->payload_length); + udp_len0 = clib_net_to_host_u16 (udp0->length); + len_diff0 = ip_len0 - udp_len0; + + /* Verify UDP checksum */ + if (PREDICT_FALSE (!good_udp0)) + { + if (is_ip4) + flags0 = ip4_tcp_udp_validate_checksum (vm, b0); + else + flags0 = ip6_tcp_udp_icmp_validate_checksum (vm, b0); + good_udp0 = (flags0 & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0; + } + + if (is_ip4) + { + error0 = good_udp0 ? 0 : IP4_ERROR_UDP_CHECKSUM; + error0 = (len_diff0 >= 0) ? error0 : IP4_ERROR_UDP_LENGTH; + } + else + { + error0 = good_udp0 ? 0 : IP6_ERROR_UDP_CHECKSUM; + error0 = (len_diff0 >= 0) ? error0 : IP6_ERROR_UDP_LENGTH; + } + + next0 = error0 ? + IP_VXLAN_BYPASS_NEXT_DROP : IP_VXLAN_BYPASS_NEXT_TUNTERM; + b0->error = error0 ? error_node->errors[error0] : 0; + + /* vxlan-input node expect current at VXLAN header */ + if (is_ip4) + vlib_buffer_advance (b0, + sizeof (ip4_header_t) + + sizeof (udp_header_t)); + else + vlib_buffer_advance (b0, + sizeof (ip6_header_t) + + sizeof (udp_header_t)); + + exit0: + /* Process packet 1 */ + if (proto1 != IP_PROTOCOL_UDP) + goto exit1; /* not UDP packet */ + + if (is_ip4) + udp1 = ip4_next_header (ip41); + else + udp1 = ip6_next_header (ip61); + + u32 fi1 = vlib_buffer_get_ip_fib_index (b1, is_ip4); + vxlan1 = vlib_buffer_get_current (b1) + sizeof (udp_header_t) + + sizeof (ip4_header_t); + + vxlan_decap_info_t di1 = + is_ip4 ? + tunterm_acl_vxlan4_find_tunnel (vxm, &last4, fi1, ip41, vxlan1, &stats_if1) : + tunterm_acl_vxlan6_find_tunnel (vxm, &last6, fi1, ip61, vxlan1, &stats_if1); + + if (PREDICT_FALSE (di1.sw_if_index == ~0)) + goto exit1; /* unknown interface */ + + /* Validate DIP against VTEPs */ + if (is_ip4) + { +#ifdef CLIB_HAVE_VEC512 + if (!vtep4_check_vector (&vxm->vtep_table, b1, ip41, &last_vtep4, + &vxm->vtep4_u512)) +#else + if (!vtep4_check (&vxm->vtep_table, b1, ip41, &last_vtep4)) +#endif + goto exit1; /* no local VTEP for VXLAN packet */ + } + else + { + if (!vtep6_check (&vxm->vtep_table, b1, ip61, &last_vtep6)) + goto exit1; /* no local VTEP for VXLAN packet */ + } + + flags1 = b1->flags; + good_udp1 = (flags1 & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0; + + /* Don't verify UDP checksum for packets with explicit zero checksum. */ + good_udp1 |= udp1->checksum == 0; + + /* Verify UDP length */ + if (is_ip4) + ip_len1 = clib_net_to_host_u16 (ip41->length); + else + ip_len1 = clib_net_to_host_u16 (ip61->payload_length); + udp_len1 = clib_net_to_host_u16 (udp1->length); + len_diff1 = ip_len1 - udp_len1; + + /* Verify UDP checksum */ + if (PREDICT_FALSE (!good_udp1)) + { + if (is_ip4) + flags1 = ip4_tcp_udp_validate_checksum (vm, b1); + else + flags1 = ip6_tcp_udp_icmp_validate_checksum (vm, b1); + good_udp1 = (flags1 & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0; + } + + if (is_ip4) + { + error1 = good_udp1 ? 0 : IP4_ERROR_UDP_CHECKSUM; + error1 = (len_diff1 >= 0) ? error1 : IP4_ERROR_UDP_LENGTH; + } + else + { + error1 = good_udp1 ? 0 : IP6_ERROR_UDP_CHECKSUM; + error1 = (len_diff1 >= 0) ? error1 : IP6_ERROR_UDP_LENGTH; + } + + next1 = error1 ? + IP_VXLAN_BYPASS_NEXT_DROP : IP_VXLAN_BYPASS_NEXT_TUNTERM; + b1->error = error1 ? error_node->errors[error1] : 0; + + /* vxlan-input node expect current at VXLAN header */ + if (is_ip4) + vlib_buffer_advance (b1, + sizeof (ip4_header_t) + + sizeof (udp_header_t)); + else + vlib_buffer_advance (b1, + sizeof (ip6_header_t) + + sizeof (udp_header_t)); + + exit1: + vlib_validate_buffer_enqueue_x2 (vm, node, next_index, + to_next, n_left_to_next, + bi0, bi1, next0, next1); + } + + while (n_left_from > 0 && n_left_to_next > 0) + { + vlib_buffer_t *b0; + ip4_header_t *ip40; + ip6_header_t *ip60; + udp_header_t *udp0; + vxlan_header_t *vxlan0; + u32 bi0, ip_len0, udp_len0, flags0, next0; + i32 len_diff0; + u8 error0, good_udp0, proto0; + u32 stats_if0 = ~0; + + bi0 = to_next[0] = from[0]; + from += 1; + n_left_from -= 1; + to_next += 1; + n_left_to_next -= 1; + + b0 = b[0]; + b++; + if (is_ip4) + ip40 = vlib_buffer_get_current (b0); + else + ip60 = vlib_buffer_get_current (b0); + + /* Setup packet for next IP feature */ + vnet_feature_next (&next0, b0); + + if (is_ip4) + /* Treat IP4 frag packets as "experimental" protocol for now + until support of IP frag reassembly is implemented */ + proto0 = ip4_is_fragment (ip40) ? 0xfe : ip40->protocol; + else + proto0 = ip60->protocol; + + if (proto0 != IP_PROTOCOL_UDP) + goto exit; /* not UDP packet */ + + if (is_ip4) + udp0 = ip4_next_header (ip40); + else + udp0 = ip6_next_header (ip60); + + u32 fi0 = vlib_buffer_get_ip_fib_index (b0, is_ip4); + vxlan0 = vlib_buffer_get_current (b0) + sizeof (udp_header_t) + + sizeof (ip4_header_t); + + vxlan_decap_info_t di0 = + is_ip4 ? + tunterm_acl_vxlan4_find_tunnel (vxm, &last4, fi0, ip40, vxlan0, &stats_if0) : + tunterm_acl_vxlan6_find_tunnel (vxm, &last6, fi0, ip60, vxlan0, &stats_if0); + + if (PREDICT_FALSE (di0.sw_if_index == ~0)) + goto exit; /* unknown interface */ + + /* Validate DIP against VTEPs */ + if (is_ip4) + { +#ifdef CLIB_HAVE_VEC512 + if (!vtep4_check_vector (&vxm->vtep_table, b0, ip40, &last_vtep4, + &vxm->vtep4_u512)) +#else + if (!vtep4_check (&vxm->vtep_table, b0, ip40, &last_vtep4)) +#endif + goto exit; /* no local VTEP for VXLAN packet */ + } + else + { + if (!vtep6_check (&vxm->vtep_table, b0, ip60, &last_vtep6)) + goto exit; /* no local VTEP for VXLAN packet */ + } + + flags0 = b0->flags; + good_udp0 = (flags0 & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0; + + /* Don't verify UDP checksum for packets with explicit zero checksum. */ + good_udp0 |= udp0->checksum == 0; + + /* Verify UDP length */ + if (is_ip4) + ip_len0 = clib_net_to_host_u16 (ip40->length); + else + ip_len0 = clib_net_to_host_u16 (ip60->payload_length); + udp_len0 = clib_net_to_host_u16 (udp0->length); + len_diff0 = ip_len0 - udp_len0; + + /* Verify UDP checksum */ + if (PREDICT_FALSE (!good_udp0)) + { + if (is_ip4) + flags0 = ip4_tcp_udp_validate_checksum (vm, b0); + else + flags0 = ip6_tcp_udp_icmp_validate_checksum (vm, b0); + good_udp0 = (flags0 & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0; + } + + if (is_ip4) + { + error0 = good_udp0 ? 0 : IP4_ERROR_UDP_CHECKSUM; + error0 = (len_diff0 >= 0) ? error0 : IP4_ERROR_UDP_LENGTH; + } + else + { + error0 = good_udp0 ? 0 : IP6_ERROR_UDP_CHECKSUM; + error0 = (len_diff0 >= 0) ? error0 : IP6_ERROR_UDP_LENGTH; + } + + next0 = error0 ? + IP_VXLAN_BYPASS_NEXT_DROP : IP_VXLAN_BYPASS_NEXT_TUNTERM; + b0->error = error0 ? error_node->errors[error0] : 0; + + /* vxlan-input node expect current at VXLAN header */ + if (is_ip4) + vlib_buffer_advance (b0, + sizeof (ip4_header_t) + + sizeof (udp_header_t)); + else + vlib_buffer_advance (b0, + sizeof (ip6_header_t) + + sizeof (udp_header_t)); + + exit: + vlib_validate_buffer_enqueue_x1 (vm, node, next_index, + to_next, n_left_to_next, + bi0, next0); + } + + vlib_put_next_frame (vm, node, next_index, n_left_to_next); + } + + return frame->n_vectors; +} + +VLIB_NODE_FN (tunterm_acl_ip4_vxlan_bypass_node) (vlib_main_t * vm, + vlib_node_runtime_t * node, + vlib_frame_t * frame) +{ + return tunterm_acl_ip_vxlan_bypass_inline (vm, node, frame, /* is_ip4 */ 1); +} + +VLIB_REGISTER_NODE (tunterm_acl_ip4_vxlan_bypass_node) = +{ + .name = "tunterm-ip4-vxlan-bypass", + .vector_size = sizeof (u32), + .n_next_nodes = IP_VXLAN_BYPASS_N_NEXT, + .next_nodes = { + [IP_VXLAN_BYPASS_NEXT_DROP] = "error-drop", + [IP_VXLAN_BYPASS_NEXT_VXLAN] = "vxlan4-input", + [IP_VXLAN_BYPASS_NEXT_TUNTERM] = "tunterm-acl", + }, + .format_buffer = format_ip4_header, + .format_trace = format_ip4_forward_next_trace, +}; + +static clib_error_t * +tunterm_acl_ip4_vxlan_bypass_init (vlib_main_t * vm) +{ + + tunterm_acl_vxlan_main = vlib_get_plugin_symbol ("vxlan_plugin.so", "vxlan_main"); + + if (tunterm_acl_vxlan_main == 0) + return clib_error_return (0, "cannot get vxlan_main symbol"); + + return 0; +} + +VLIB_INIT_FUNCTION (tunterm_acl_ip4_vxlan_bypass_init); + +VNET_FEATURE_INIT (tunterm_acl_ip4_vxlan_bypass, static) = +{ + .arc_name = "ip4-unicast", + .node_name = "tunterm-ip4-vxlan-bypass", + .runs_before = VNET_FEATURES ("ip4-lookup"), +}; \ No newline at end of file diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_node.c b/platform/vpp/plugins/tunterm_acl/tunterm_acl_node.c new file mode 100644 index 0000000..188a365 --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_node.c @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2024 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include +#include +#include +#include +#include +#include + +typedef struct +{ + u32 sw_if_index; + u32 next_index; + u32 index; +} tunterm_acl_trace_t; + +/* packet trace format function */ +static u8 * +format_tunterm_acl_trace (u8 * s, va_list * args) +{ + CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); + CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); + tunterm_acl_trace_t *t = va_arg (*args, tunterm_acl_trace_t *); + + s = format (s, "TUNTERM: sw_if_index %d next %d index %d", + t->sw_if_index, t->next_index, t->index); + return s; +} + +extern vlib_node_registration_t tunterm_acl_node; + +#define foreach_tunterm_acl_error \ +_(REDIRECTED, "Packets successfully redirected") \ +_(NO_CLASSIFY_TABLE, "No classify table found") \ +_(ACTION_NOT_SUPPORTED, "Match found, but action not supported") \ +_(UNSUPPORTED_ETHERTYPE, "Unsupported ethertype") \ +_(NO_MATCH, "No match found in classify table") \ + +typedef enum +{ +#define _(sym,str) TUNTERM_ACL_ERROR_##sym, + foreach_tunterm_acl_error +#undef _ + TUNTERM_ACL_N_ERROR, +} tunterm_acl_error_t; + +static char *tunterm_acl_error_strings[] = { +#define _(sym,string) string, + foreach_tunterm_acl_error +#undef _ +}; + +typedef enum +{ + TUNTERM_ACL_NEXT_DROP, + TUNTERM_ACL_NEXT_VXLAN4_INPUT, + TUNTERM_ACL_NEXT_IP4_REWRITE, + TUNTERM_ACL_NEXT_IP6_REWRITE, + TUNTERM_ACL_N_NEXT, +} tunterm_acl_next_t; + +#include +#include +#include +#include +#include + +VLIB_NODE_FN(tunterm_acl_node) (vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) { + u32 n_left_from, *from, *to_next; + tunterm_acl_next_t next_index; + vnet_classify_main_t *cm = &vnet_classify_main; + u32 table_index = ~0; + u32 pkts_redirected = 0; + u32 pkts_no_classify_table = 0; + u32 pkts_action_not_supported = 0; + u32 pkts_unsupported_ethertype = 0; + u32 pkts_no_match = 0; + + from = vlib_frame_vector_args(frame); + n_left_from = frame->n_vectors; + next_index = node->cached_next_index; + + while (n_left_from > 0) { + u32 n_left_to_next; + + vlib_get_next_frame(vm, node, next_index, to_next, n_left_to_next); + + while (n_left_from > 0 && n_left_to_next > 0) { + u32 bi0; + vlib_buffer_t *b0; + u32 next0 = TUNTERM_ACL_NEXT_VXLAN4_INPUT; // Default next index + u32 next_rewrite = TUNTERM_ACL_NEXT_IP4_REWRITE; + u32 sw_if_index0; + + bi0 = from[0]; + to_next[0] = bi0; + from += 1; + to_next += 1; + n_left_from -= 1; + n_left_to_next -= 1; + + b0 = vlib_get_buffer(vm, bi0); + sw_if_index0 = vnet_buffer(b0)->sw_if_index[VLIB_RX]; + + u8* etype = &(b0->data[b0->current_data + sizeof(vxlan_header_t) + 2*sizeof(mac_address_t)]); + u16 ethertype = (etype[0] << 8) | etype[1]; + + if (ethertype == 0x86DD) { + next0 = TUNTERM_ACL_NEXT_VXLAN4_INPUT; // Note we send to vxlan4-input not vxlan6-input as outer vxlan is v4 + next_rewrite = TUNTERM_ACL_NEXT_IP6_REWRITE; + table_index = tunterm_acl_main.classify_table_index_by_sw_if_index_v6[sw_if_index0]; + } else if (ethertype == 0x0800) { + next0 = TUNTERM_ACL_NEXT_VXLAN4_INPUT; + next_rewrite = TUNTERM_ACL_NEXT_IP4_REWRITE; + table_index = tunterm_acl_main.classify_table_index_by_sw_if_index_v4[sw_if_index0]; + } else { + // This shouldn't happen. Something is wrong. Drop the packet to bring attention to this. + next0 = TUNTERM_ACL_NEXT_DROP; + pkts_unsupported_ethertype++; + goto exit; + } + + if (PREDICT_FALSE(table_index == ~0)) { + // No classify table found for given proto. Continue to vxlan4-input. + pkts_no_classify_table++; + } else { + // Perform the classify table lookup + vnet_classify_table_t *t = pool_elt_at_index(cm->tables, table_index); + + u32 hash0 = vnet_classify_hash_packet (t, (b0->data)); + vnet_classify_entry_t *e = vnet_classify_find_entry(t, b0->data, hash0, vlib_time_now (vm)); + + if (e) { + if (e->action == CLASSIFY_ACTION_SET_METADATA) { + vlib_buffer_advance (b0, sizeof (vxlan_header_t)); + vlib_buffer_advance (b0, sizeof (ethernet_header_t)); + next0 = next_rewrite; + vnet_buffer (b0)->ip.adj_index[VLIB_TX] = e->metadata; + pkts_redirected++; + } else { + // Classify table hit, but action not supported. Continue to vxlan4-input. + pkts_action_not_supported++; + } + } else { + // No match found in classify table. Continue to vxlan4-input. + pkts_no_match++; + } + } + +exit: + if (PREDICT_FALSE((node->flags & VLIB_NODE_FLAG_TRACE) && (b0->flags & VLIB_BUFFER_IS_TRACED))) { + tunterm_acl_trace_t *t = vlib_add_trace(vm, node, b0, sizeof(*t)); + t->sw_if_index = sw_if_index0; + t->next_index = next0; + t->index = vnet_buffer (b0)->ip.adj_index[VLIB_TX]; + } + + // Verify speculative enqueue, maybe switch current next frame + vlib_validate_buffer_enqueue_x1(vm, node, next_index, to_next, n_left_to_next, bi0, next0); + } + + vlib_put_next_frame(vm, node, next_index, n_left_to_next); + } + + vlib_node_increment_counter (vm, tunterm_acl_node.index, TUNTERM_ACL_ERROR_REDIRECTED, pkts_redirected); + vlib_node_increment_counter (vm, tunterm_acl_node.index, TUNTERM_ACL_ERROR_NO_CLASSIFY_TABLE, pkts_no_classify_table); + vlib_node_increment_counter (vm, tunterm_acl_node.index, TUNTERM_ACL_ERROR_ACTION_NOT_SUPPORTED, pkts_action_not_supported); + vlib_node_increment_counter (vm, tunterm_acl_node.index, TUNTERM_ACL_ERROR_UNSUPPORTED_ETHERTYPE, pkts_unsupported_ethertype); + vlib_node_increment_counter (vm, tunterm_acl_node.index, TUNTERM_ACL_ERROR_NO_MATCH, pkts_no_match); + + return frame->n_vectors; +} + +/* *INDENT-OFF* */ +VLIB_REGISTER_NODE (tunterm_acl_node) = +{ + .name = "tunterm-acl", + .vector_size = sizeof (u32), + .format_trace = format_tunterm_acl_trace, + .type = VLIB_NODE_TYPE_INTERNAL, + + .n_errors = ARRAY_LEN(tunterm_acl_error_strings), + .error_strings = tunterm_acl_error_strings, + + .n_next_nodes = TUNTERM_ACL_N_NEXT, + + /* edit / add dispositions here */ + .next_nodes = { + [TUNTERM_ACL_NEXT_DROP] = "error-drop", + [TUNTERM_ACL_NEXT_VXLAN4_INPUT] = "vxlan4-input", + [TUNTERM_ACL_NEXT_IP4_REWRITE] = "ip4-rewrite", + [TUNTERM_ACL_NEXT_IP6_REWRITE] = "ip6-rewrite", + }, +}; +/* *INDENT-ON* */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.c b/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.c new file mode 100644 index 0000000..fa66bf1 --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.c @@ -0,0 +1,257 @@ +/* Copyright (c) 2024 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** + * @file + * @brief Tunnel Terminated Plugin Redirect Functionality. + */ +#include +#include +#include +#include +#include +#include + +typedef struct +{ + u8 *match_and_table_index; + dpo_id_t dpo; /* forwarding dpo */ + fib_node_t node; /* linkage into the FIB graph */ + fib_node_index_t pl; + u32 sibling; + u32 parent_node_index; + u32 opaque_index; + u32 table_index; + fib_forward_chain_type_t payload_type; + u8 is_ip6 : 1; +} tunterm_acl_redirect_t; + +typedef struct +{ + tunterm_acl_redirect_t *pool; + u32 *session_by_match_and_table_index; + fib_node_type_t fib_node_type; +} tunterm_acl_redirect_main_t; + +static tunterm_acl_redirect_main_t tunterm_acl_redirect_main; + +static int +tunterm_acl_redirect_stack (tunterm_acl_redirect_t *ipr) +{ + dpo_id_t dpo = DPO_INVALID; + + fib_path_list_contribute_forwarding (ipr->pl, ipr->payload_type, + fib_path_list_is_popular (ipr->pl) ? + FIB_PATH_LIST_FWD_FLAG_NONE : + FIB_PATH_LIST_FWD_FLAG_COLLAPSE, + &dpo); + dpo_stack_from_node (ipr->parent_node_index, &ipr->dpo, &dpo); + dpo_reset (&dpo); + + /* update session with new next_index */ + return vnet_classify_add_del_session ( + &vnet_classify_main, ipr->table_index, ipr->match_and_table_index, + ipr->dpo.dpoi_next_node /* hit_next_index */, ipr->opaque_index, + 0 /* advance */, CLASSIFY_ACTION_SET_METADATA, + ipr->dpo.dpoi_index /* metadata */, 1 /* is_add */); +} + +static tunterm_acl_redirect_t * +tunterm_acl_redirect_find (tunterm_acl_redirect_main_t *im, u32 table_index, + const u8 *match) +{ + /* we are adding the table index at the end of the match string so we + * can disambiguiate identical matches in different tables in + * im->session_by_match_and_table_index */ + u8 *match_and_table_index = vec_dup (match); + vec_add (match_and_table_index, (void *) &table_index, 4); + uword *p = + hash_get_mem (im->session_by_match_and_table_index, match_and_table_index); + vec_free (match_and_table_index); + if (!p) + return 0; + return pool_elt_at_index (im->pool, p[0]); +} + +__clib_export int +tunterm_acl_redirect_add (vlib_main_t *vm, u32 table_index, u32 opaque_index, + dpo_proto_t proto, const u8 *match, + const fib_route_path_t *rpaths) +{ + tunterm_acl_redirect_main_t *im = &tunterm_acl_redirect_main; + fib_forward_chain_type_t payload_type; + tunterm_acl_redirect_t *ipr; + const char *pname; + + payload_type = fib_forw_chain_type_from_dpo_proto (proto); + switch (payload_type) + { + case FIB_FORW_CHAIN_TYPE_UNICAST_IP4: + pname = "ip4-inacl"; + break; + case FIB_FORW_CHAIN_TYPE_UNICAST_IP6: + pname = "ip6-inacl"; + break; + default: + return VNET_API_ERROR_INVALID_ADDRESS_FAMILY; + } + + ipr = tunterm_acl_redirect_find (im, table_index, match); + if (ipr) + { + /* update to an existing session */ + fib_path_list_child_remove (ipr->pl, ipr->sibling); + dpo_reset (&ipr->dpo); + } + else + { + /* allocate a new entry */ + pool_get (im->pool, ipr); + fib_node_init (&ipr->node, im->fib_node_type); + ipr->match_and_table_index = vec_dup ((u8 *) match); + /* we are adding the table index at the end of the match string so we + * can disambiguiate identical matches in different tables in + * im->session_by_match_and_table_index */ + vec_add (ipr->match_and_table_index, (void *) &table_index, 4); + ipr->table_index = table_index; + hash_set_mem (im->session_by_match_and_table_index, + ipr->match_and_table_index, ipr - im->pool); + } + + ipr->payload_type = payload_type; + ipr->pl = fib_path_list_create ( + FIB_PATH_LIST_FLAG_SHARED | FIB_PATH_LIST_FLAG_NO_URPF, rpaths); + ipr->sibling = + fib_path_list_child_add (ipr->pl, im->fib_node_type, ipr - im->pool); + ipr->parent_node_index = vlib_get_node_by_name (vm, (u8 *) pname)->index; + ipr->opaque_index = opaque_index; + ipr->is_ip6 = payload_type == FIB_FORW_CHAIN_TYPE_UNICAST_IP6; + + return tunterm_acl_redirect_stack (ipr); +} +int +tunterm_acl_redirect_del_ipr (tunterm_acl_redirect_main_t *im, tunterm_acl_redirect_t *ipr) +{ + vnet_classify_main_t *cm = &vnet_classify_main; + int rv; + + rv = vnet_classify_add_del_session ( + cm, ipr->table_index, ipr->match_and_table_index, 0 /* hit_next_index */, + 0 /* opaque_index */, 0 /* advance */, 0 /* action */, 0 /* metadata */, + 0 /* is_add */); + if (rv) + return rv; + + hash_unset_mem (im->session_by_match_and_table_index, + ipr->match_and_table_index); + vec_free (ipr->match_and_table_index); + fib_path_list_child_remove (ipr->pl, ipr->sibling); + dpo_reset (&ipr->dpo); + pool_put (im->pool, ipr); + return 0; +} + +__clib_export int +tunterm_acl_redirect_del (vlib_main_t *vm, u32 table_index, const u8 *match) +{ + tunterm_acl_redirect_main_t *im = &tunterm_acl_redirect_main; + tunterm_acl_redirect_t *ipr; + int rv; + + ipr = tunterm_acl_redirect_find (im, table_index, match); + if (!ipr) + return VNET_API_ERROR_NO_SUCH_ENTRY; + + rv = tunterm_acl_redirect_del_ipr (im, ipr); + + return rv; +} + +int tunterm_acl_redirect_clear (vlib_main_t *vm, u32 table_index) { + tunterm_acl_redirect_main_t *im = &tunterm_acl_redirect_main; + tunterm_acl_redirect_t *ipr; + tunterm_acl_redirect_t **to_be_deleted = NULL; + int rv = 0; + + // Iterate through the pool to find all entries with the specified table_index + pool_foreach(ipr, im->pool) { + if (ipr->table_index == table_index) { + vec_add1(to_be_deleted, ipr); + } + } + + tunterm_acl_redirect_t **ipr_ptr; + vec_foreach(ipr_ptr, to_be_deleted) { + rv = tunterm_acl_redirect_del_ipr(im, *ipr_ptr); + if (rv) { + vec_free(to_be_deleted); + return rv; + } + } + + vec_free(to_be_deleted); + return 0; +} + +static fib_node_t * +tunterm_acl_redirect_get_node (fib_node_index_t index) +{ + tunterm_acl_redirect_main_t *im = &tunterm_acl_redirect_main; + tunterm_acl_redirect_t *ipr = pool_elt_at_index (im->pool, index); + return &ipr->node; +} + +static tunterm_acl_redirect_t * +tunterm_acl_redirect_get_from_node (fib_node_t *node) +{ + return ( + tunterm_acl_redirect_t *) (((char *) node) - + STRUCT_OFFSET_OF (tunterm_acl_redirect_t, node)); +} + +static void +tunterm_acl_redirect_last_lock_gone (fib_node_t *node) +{ + /* the lifetime of the entry is managed by the table. */ + ASSERT (0); +} + +/* A back walk has reached this entry */ +static fib_node_back_walk_rc_t +tunterm_acl_redirect_back_walk_notify (fib_node_t *node, + fib_node_back_walk_ctx_t *ctx) +{ + int rv; + tunterm_acl_redirect_t *ipr = tunterm_acl_redirect_get_from_node (node); + rv = tunterm_acl_redirect_stack (ipr); + ASSERT (0 == rv); + if (rv) + clib_warning ("tunterm_acl_redirect_stack() error %d", rv); + return FIB_NODE_BACK_WALK_CONTINUE; +} + +static const fib_node_vft_t tunterm_acl_redirect_vft = { + .fnv_get = tunterm_acl_redirect_get_node, + .fnv_last_lock = tunterm_acl_redirect_last_lock_gone, + .fnv_back_walk = tunterm_acl_redirect_back_walk_notify, +}; + +clib_error_t * +tunterm_acl_redirect_init (vlib_main_t *vm) +{ + tunterm_acl_redirect_main_t *im = &tunterm_acl_redirect_main; + im->session_by_match_and_table_index = + hash_create_vec (0, sizeof (u8), sizeof (u32)); + im->fib_node_type = fib_node_register_new_type ("tunterm-redirect", + &tunterm_acl_redirect_vft); + return 0; +} diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h b/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h new file mode 100644 index 0000000..f165d0b --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h @@ -0,0 +1,37 @@ +/* Copyright (c) 2024 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ + +#ifndef TUNTERM_ACL_REDIRECT_H_ +#define TUNTERM_ACL_REDIRECT_H_ + +#include + +clib_error_t * tunterm_acl_redirect_init (vlib_main_t *vm); + +int tunterm_acl_redirect_add (vlib_main_t *vm, u32 table_index, + u32 opaque_index, dpo_proto_t proto, + const u8 *match, const fib_route_path_t *rpaths); +int tunterm_acl_redirect_del (vlib_main_t *vm, u32 table_index, + const u8 *match); + +int tunterm_acl_redirect_clear (vlib_main_t *vm, u32 table_index); + +#endif /* TUNTERM_ACL_REDIRECT_H_ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ \ No newline at end of file diff --git a/platform/vpp/vppbld/Makefile b/platform/vpp/vppbld/Makefile index 41d0290..e6bd7f3 100644 --- a/platform/vpp/vppbld/Makefile +++ b/platform/vpp/vppbld/Makefile @@ -23,6 +23,7 @@ UID = $(shell id -u) GUID = $(shell id -g) VPP_REPO_DIR = repo +VPP_CUSTOM_PLUGINS_DIR = ../plugins MAIN_TARGET = $(VPPINFRA) DERIVED_TARGETS = $(VPP_MAIN) $(VPP_PLUGIN_CORE) $(VPP_PLUGIN_DPDK) \ $(VPP_PLUGIN_DEV) $(VPP_DEV) $(VPPINFRA_DEV) $(VPPDBG) @@ -68,11 +69,13 @@ build_vpp: repo_clone: rm -rf $(VPP_REPO_DIR) git clone $(VPP_URL) $(VPP_REPO_DIR) + cp -r $(VPP_CUSTOM_PLUGINS_DIR)/* $(VPP_REPO_DIR)/src/plugins/ pushd $(VPP_REPO_DIR) @VPP_SHA=`git log --until "$(VPP_LAG) days ago" -1 --format="%h"` git checkout $$VPP_SHA # 1. use installed libnl from sonic. # 2. use libbpf pulled by vpp + # 3. export vxlan main git apply ../vpp.patch popd diff --git a/platform/vpp/vppbld/vpp.patch b/platform/vpp/vppbld/vpp.patch index afc8cf0..74a6016 100644 --- a/platform/vpp/vppbld/vpp.patch +++ b/platform/vpp/vppbld/vpp.patch @@ -33,3 +33,16 @@ index b9285971f..c38acc598 100644 endef define xdp-tools_install_cmds +diff --git a/src/plugins/vxlan/vxlan.c b/src/plugins/vxlan/vxlan.c +index 0885550d2..8b8cd66e4 100644 +--- a/src/plugins/vxlan/vxlan.c ++++ b/src/plugins/vxlan/vxlan.c +@@ -43,7 +43,7 @@ + */ + + +-vxlan_main_t vxlan_main; ++__clib_export vxlan_main_t vxlan_main; + + static u32 + vxlan_eth_flag_change (vnet_main_t *vnm, vnet_hw_interface_t *hi, u32 flags) From 1ce65b3d41a01a2afe1b5b287486d66201d378f8 Mon Sep 17 00:00:00 2001 From: Akeel Ali <701916+AkeelAli@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:05:31 -0400 Subject: [PATCH 2/7] Add UT = UT: introduce test_tunterm_acl.py with various tests - Fix: add_replace empty-ACL ignores is_ipv6 as it is not set by sonic-vpp - Minor formatting & classify table memory size changes --- .../tunterm_acl/test/test_tunterm_acl.py | 423 ++++++++++++++++++ .../vpp/plugins/tunterm_acl/tunterm_acl_api.c | 17 +- .../plugins/tunterm_acl/tunterm_acl_decap.c | 2 +- .../tunterm_acl/tunterm_acl_redirect.h | 2 +- 4 files changed, 435 insertions(+), 9 deletions(-) create mode 100644 platform/vpp/plugins/tunterm_acl/test/test_tunterm_acl.py diff --git a/platform/vpp/plugins/tunterm_acl/test/test_tunterm_acl.py b/platform/vpp/plugins/tunterm_acl/test/test_tunterm_acl.py new file mode 100644 index 0000000..0d1db17 --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/test/test_tunterm_acl.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python3 + +import unittest +from framework import VppTestCase +from asfframework import VppTestRunner + +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.layers.inet import IP, UDP +from scapy.layers.inet6 import IPv6 +from scapy.layers.vxlan import VXLAN + +from vpp_ip_route import VppRoutePath +from vpp_vxlan_tunnel import VppVxlanTunnel +from vpp_l2 import L2_PORT_TYPE + +########################### +# Test Configuration variables +########################### + +# Scale +# Min 2 each if negative testing (or 1 each) +# Max around 100 ingress (as x2 for vxlan and test infra doesn't support sw_if_index > 255) +NUM_INGRESS_PGS = 2 +NUM_EGRESS_PGS = 8 + +# Debug +# Set to True to run tests individually (slow) +# Generates test teardown traces (including packet captures) for each test +RUN_TESTS_INDIVIDUALLY = False + +class TestVxlan(VppTestCase): + """VXLAN Test Case""" + + def __init__(self, *args): + VppTestCase.__init__(self, *args) + + def encapsulate(self, pkt, vni, src_mac, dst_mac, src_ip, dst_ip): + """ + Encapsulate the original payload frame by adding VXLAN header with its + UDP, IP and Ethernet fields + """ + return ( + Ether(src=src_mac, dst=dst_mac) + / IP(src=src_ip, dst=dst_ip) + / UDP(sport=self.dport, dport=self.dport, chksum=0) + / VXLAN(vni=vni, flags=self.flags) + / pkt + ) + + @classmethod + def createVxLANInterfaces(cls, port=4789): + cls.dport = port + + cls.single_tunnel_vni = 0x12345 + cls.single_tunnel_bd = 1 + + # Create VXLAN tunnels on ingress interfaces + cls.vxlan_tunnels = [] + for i in range(NUM_INGRESS_PGS): + ingress_pg = cls.pg_interfaces[NUM_EGRESS_PGS + i] + r = VppVxlanTunnel( + cls, + src=ingress_pg.local_ip4, + dst=ingress_pg.remote_ip4, + src_port=cls.dport, + dst_port=cls.dport, + vni=cls.single_tunnel_vni, + is_l3=True, + ) + r.add_vpp_config() + cls.vapi.sw_interface_set_l2_bridge( + rx_sw_if_index=r.sw_if_index, bd_id=cls.single_tunnel_bd + ) + cls.vxlan_tunnels.append(r) + + # Create loopback/BVI interface + cls.create_loopback_interfaces(1) + cls.loop0 = cls.lo_interfaces[0] + cls.loop0.admin_up() + cls.vapi.sw_interface_set_mac_address( + cls.loop0.sw_if_index, "00:00:00:00:00:02" + ) + cls.loop0.config_ip4() + cls.vapi.sw_interface_set_l2_bridge( + rx_sw_if_index=cls.loop0.sw_if_index, + bd_id=cls.single_tunnel_bd, + port_type=L2_PORT_TYPE.BVI, + ) + + # Enable v6 on BVI interface (for negative test to forward based on inner dst IP) + cls.vapi.sw_interface_ip6_enable_disable( + cls.loop0.sw_if_index, enable=1 + ) + + # Add default ACL permit per sonic-vpp use-case + from vpp_acl import AclRule, VppAcl, VppAclInterface + from ipaddress import IPv6Network + + rule_1 = AclRule(is_permit=1) + rule_2 = AclRule(is_permit=1) + rule_2.src_prefix = IPv6Network("0::0/0") + rule_2.dst_prefix = IPv6Network("0::0/0") + acl_1 = VppAcl(cls, rules=[rule_1, rule_2]) + acl_1.add_vpp_config() + + for i in range(NUM_INGRESS_PGS): + ingress_pg = cls.pg_interfaces[NUM_EGRESS_PGS + i] + acl_if = VppAclInterface(cls, sw_if_index=ingress_pg.sw_if_index, n_input=1, acls=[acl_1]) + acl_if.add_vpp_config() + + # TunTerm plugin Call + DST_IPs_v4 = [f"4.3.2.{i}" for i in range(NUM_EGRESS_PGS)] + DST_IPs_v6 = [f"2001:db8::{i+1}" for i in range(NUM_EGRESS_PGS)] + paths_v4 = [ + VppRoutePath(cls.pg_interfaces[i].remote_ip4, cls.pg_interfaces[i].sw_if_index).encode() + for i in range(NUM_EGRESS_PGS) + ] + paths_v6 = [ + VppRoutePath(cls.pg_interfaces[i].remote_ip6, cls.pg_interfaces[i].sw_if_index).encode() + for i in range(NUM_EGRESS_PGS) + ] + + cls.rules_v4 = [{"dst": dst_ip, "path": path} for dst_ip, path in zip(DST_IPs_v4, paths_v4)] + cls.rules_v6 = [{"dst": dst_ip, "path": path} for dst_ip, path in zip(DST_IPs_v6, paths_v6)] + reply_v4 = cls.vapi.tunterm_acl_add_replace(0xffffffff, False, len(cls.rules_v4), cls.rules_v4) + reply_v6 = cls.vapi.tunterm_acl_add_replace(0xffffffff, True, len(cls.rules_v6), cls.rules_v6) + cls.tunterm_acl_index_v4 = reply_v4.tunterm_acl_index + cls.tunterm_acl_index_v6 = reply_v6.tunterm_acl_index + + cls.vapi.tunterm_acl_add_replace(cls.tunterm_acl_index_v4, False, len(cls.rules_v4), cls.rules_v4) + cls.vapi.tunterm_acl_add_replace(cls.tunterm_acl_index_v6, True, len(cls.rules_v6), cls.rules_v6) + + for i in range(NUM_INGRESS_PGS): + ingress_pg = cls.pg_interfaces[NUM_EGRESS_PGS + i] + cls.vapi.tunterm_acl_interface_add_del(True, ingress_pg.sw_if_index, cls.tunterm_acl_index_v4) + cls.vapi.tunterm_acl_interface_add_del(True, ingress_pg.sw_if_index, cls.tunterm_acl_index_v6) + + @classmethod + def setUpClass(cls): + super(TestVxlan, cls).setUpClass() + + try: + cls.flags = 0x8 + + # Create pg interfaces. + cls.create_pg_interfaces(range(NUM_EGRESS_PGS + NUM_INGRESS_PGS)) + for pg in cls.pg_interfaces: + pg.admin_up() + pg.config_ip4() + pg.config_ip6() + pg.resolve_arp() + pg.resolve_ndp() + + cls.createVxLANInterfaces() + + except Exception: + cls.tearDownClass() + raise + + @classmethod + def tearDownClass(cls): + for i in range(NUM_INGRESS_PGS): + ingress_pg = cls.pg_interfaces[NUM_EGRESS_PGS + i] + cls.vapi.tunterm_acl_interface_add_del(False, ingress_pg.sw_if_index, cls.tunterm_acl_index_v4) + cls.vapi.tunterm_acl_interface_add_del(False, ingress_pg.sw_if_index, cls.tunterm_acl_index_v6) + + cls.vapi.tunterm_acl_del(cls.tunterm_acl_index_v4) + cls.vapi.tunterm_acl_del(cls.tunterm_acl_index_v6) + + super(TestVxlan, cls).tearDownClass() + + def setUp(self): + super(TestVxlan, self).setUp() + + def tearDown(self): + super(TestVxlan, self).tearDown() + + def show_commands_at_teardown(self): + self.logger.info(self.vapi.cli("show bridge-domain 1 detail")) + self.logger.info(self.vapi.cli("show vxlan tunnel")) + self.logger.info(self.vapi.cli("show node counters")) + self.logger.info(self.vapi.cli("show classify table verbose")) + self.logger.info(self.vapi.cli("show tunterm interfaces")) + self.logger.info(self.vapi.cli("show acl-plugin acl")) + self.logger.info(self.vapi.cli("show acl-plugin tables")) + + def create_frame_request(self, src_mac, dst_mac, src_ip, dst_ip, is_ipv6=False): + return ( + Ether(src=src_mac, dst=dst_mac) + / (IPv6(src=src_ip, dst=dst_ip) if is_ipv6 else IP(src=src_ip, dst=dst_ip)) + / UDP(sport=10000, dport=20000) + / Raw("\xa5" * 100) + ) + + def assert_eq_pkts(self, pkt1, pkt2): + """Verify the Ether, IP, UDP, payload are equal in both packets""" + self.assertEqual(pkt1[Ether].src, pkt2[Ether].src) + self.assertEqual(pkt1[Ether].dst, pkt2[Ether].dst) + if IP in pkt1 or IP in pkt2: + self.assertEqual(pkt1[IP].src, pkt2[IP].src) + self.assertEqual(pkt1[IP].dst, pkt2[IP].dst) + elif IPv6 in pkt1 or IPv6 in pkt2: + self.assertEqual(pkt1[IPv6].src, pkt2[IPv6].src) + self.assertEqual(pkt1[IPv6].dst, pkt2[IPv6].dst) + self.assertEqual(pkt1[UDP].sport, pkt2[UDP].sport) + self.assertEqual(pkt1[UDP].dport, pkt2[UDP].dport) + self.assertEqual(pkt1[Raw], pkt2[Raw]) + + def verify_packet_forwarding(self, out_pg, frame_request): + out = out_pg.get_capture(1) + pkt = out[0] + + frame_forwarded = frame_request.copy() + frame_forwarded[Ether].src = out_pg.local_mac + frame_forwarded[Ether].dst = out_pg.remote_mac + + self.assert_eq_pkts(pkt, frame_forwarded) + + def _test_decap(self, in_pg, out_pg, input_frame, is_v6=False): + """Decapsulation test + Send encapsulated frames from in_pg + Verify receipt of redirected decapsulated frames on output pg + """ + encapsulated_pkt = self.encapsulate(input_frame, self.single_tunnel_vni, in_pg.remote_mac, in_pg.local_mac, in_pg.remote_ip4, in_pg.local_ip4) + + in_pg.add_stream([encapsulated_pkt]) + out_pg.enable_capture() + self.pg_start() + + # Pick first received frame and check if it's the redirected frame + self.verify_packet_forwarding(out_pg, input_frame) + + self.logger.info("test_tunterm: Passed %s with input intf #%d (%s) and output %s" % + ("v6" if is_v6 else "v4", in_pg.pg_index - NUM_EGRESS_PGS + 1, in_pg.name, out_pg.name)) + + def _test_add_remove(self, pg): + reply_v4 = self.vapi.tunterm_acl_add_replace(0xffffffff, False, 1, [self.rules_v4[0]]) + reply_v6 = self.vapi.tunterm_acl_add_replace(0xffffffff, True, 1, [self.rules_v6[0]]) + tunterm_acl_index_v4 = reply_v4.tunterm_acl_index + tunterm_acl_index_v6 = reply_v6.tunterm_acl_index + + self.vapi.tunterm_acl_interface_add_del(True, pg.sw_if_index, tunterm_acl_index_v4) + self.vapi.tunterm_acl_interface_add_del(True, pg.sw_if_index, tunterm_acl_index_v6) + + # Shouldn't be able to delete the tunterm entry as it's still in use + with self.assertRaises(Exception): + self.vapi.tunterm_acl_del(tunterm_acl_index_v4) + with self.assertRaises(Exception): + self.vapi.tunterm_acl_del(tunterm_acl_index_v6) + + # Shouldn't be able to switch table AF during replace + with self.assertRaises(Exception): + self.vapi.tunterm_acl_add_del(tunterm_acl_index_v4, True, 1, [self.rules_v6[0]]) + with self.assertRaises(Exception): + self.vapi.tunterm_acl_add_del(tunterm_acl_index_v6, False, 1, [self.rules_v4[0]]) + + # Test a proper replace + if len(self.rules_v4) > 1: + reply = self.vapi.tunterm_acl_add_replace(tunterm_acl_index_v4, False, 1, [self.rules_v4[1]]) + tunterm_acl_index_v4_2 = reply.tunterm_acl_index + self.assertEqual(tunterm_acl_index_v4, tunterm_acl_index_v4_2) + if len(self.rules_v6) > 1: + reply = self.vapi.tunterm_acl_add_replace(tunterm_acl_index_v6, True, 1, [self.rules_v6[1]]) + tunterm_acl_index_v6_2 = reply.tunterm_acl_index + self.assertEqual(tunterm_acl_index_v6, tunterm_acl_index_v6_2) + + self.vapi.tunterm_acl_interface_add_del(False, pg.sw_if_index, tunterm_acl_index_v4) + self.vapi.tunterm_acl_interface_add_del(False, pg.sw_if_index, tunterm_acl_index_v6) + + self.vapi.tunterm_acl_del(tunterm_acl_index_v4) + self.vapi.tunterm_acl_del(tunterm_acl_index_v6) + + # Shouldn't be able to detach the tunterm as it's already been detached + with self.assertRaises(Exception): + self.vapi.tunterm_acl_interface_add_del(False, pg.sw_if_index, tunterm_acl_index_v4) + with self.assertRaises(Exception): + self.vapi.tunterm_acl_interface_add_del(False, pg.sw_if_index, tunterm_acl_index_v6) + + # Test empty acls + reply_v4 = self.vapi.tunterm_acl_add_replace(0xffffffff, False, 0, []) + reply_v4 = self.vapi.tunterm_acl_add_replace(reply_v4.tunterm_acl_index, False, 0, []) + reply_v6 = self.vapi.tunterm_acl_add_replace(0xffffffff, True, 0, []) + reply_v6 = self.vapi.tunterm_acl_add_replace(reply_v6.tunterm_acl_index, True, 0, []) + self.vapi.tunterm_acl_del(reply_v4.tunterm_acl_index) + self.vapi.tunterm_acl_del(reply_v6.tunterm_acl_index) + + def _run_decap_test(self, ingress_index, egress_index, is_ipv6=False): + self.remove_configured_vpp_objects_on_tear_down = False + out_pg = self.pg_interfaces[egress_index] + in_pg = self.pg_interfaces[NUM_EGRESS_PGS + ingress_index] + + src_ip = "2001:db9::1" if is_ipv6 else "1.2.3.4" + dst_ip = f"2001:db8::{egress_index + 1}" if is_ipv6 else f"4.3.2.{egress_index}" + + frame_request = self.create_frame_request( + "00:00:00:00:00:01", "00:00:00:00:00:02", + src_ip, dst_ip, is_ipv6 + ) + + self._test_add_remove(out_pg) # exercise the add/remove (no-op) + self._test_decap(in_pg, out_pg, frame_request, is_ipv6) + + ################# + # Test Creator + # - For every ingress and egress pair, create a decap/redirect test + ################# + @classmethod + def add_dynamic_tests(cls): + if RUN_TESTS_INDIVIDUALLY: + for ingress_index in range(NUM_INGRESS_PGS): + for egress_index in range(NUM_EGRESS_PGS): + def test_v4(self, ingress_index=ingress_index, egress_index=egress_index): + self._run_decap_test(ingress_index, egress_index, is_ipv6=False) + def test_v6(self, ingress_index=ingress_index, egress_index=egress_index): + self._run_decap_test(ingress_index, egress_index, is_ipv6=True) + setattr(cls, f"test_decap_v4_ingress_{ingress_index}_egress_{egress_index}", test_v4) + setattr(cls, f"test_decap_v6_ingress_{ingress_index}_egress_{egress_index}", test_v6) + else: + def test_v4(self): + for ingress_index in range(NUM_INGRESS_PGS): + for egress_index in range(NUM_EGRESS_PGS): + self._run_decap_test(ingress_index, egress_index, is_ipv6=False) + def test_v6(self): + for ingress_index in range(NUM_INGRESS_PGS): + for egress_index in range(NUM_EGRESS_PGS): + self._run_decap_test(ingress_index, egress_index, is_ipv6=True) + setattr(cls, "test_decap_v4", test_v4) + setattr(cls, "test_decap_v6", test_v6) + + ################# + # Negative Tests + # - Decap with unmatched inner DST IP (v4/v6) + # - Normal IP forwarding without encapsulation (v4/v6) + # - VXLAN packet without decap + ################# + + def _test_negative_decap(self, is_ipv6=False): + out_pg = self.pg_interfaces[0] + in_pg = self.pg_interfaces[NUM_EGRESS_PGS] + + frame_request = self.create_frame_request( + "00:00:00:00:00:01", "00:00:00:00:00:02", + "2001:db9::1" if is_ipv6 else "1.2.3.4", + out_pg.remote_ip6 if is_ipv6 else out_pg.remote_ip4, + is_ipv6 + ) + + encapsulated_pkt = self.encapsulate( + frame_request, self.single_tunnel_vni, in_pg.remote_mac, in_pg.local_mac, in_pg.remote_ip4, in_pg.local_ip4 + ) + + in_pg.add_stream([encapsulated_pkt]) + out_pg.enable_capture() + self.pg_start() + + self.verify_packet_forwarding(out_pg, frame_request) + self.logger.info(f"test_negative_decap_v{'6' if is_ipv6 else '4'}: Passed") + + def _test_negative_normal_ip(self, is_ipv6=False): + for src_pg in [self.pg_interfaces[NUM_EGRESS_PGS + i] for i in range(NUM_INGRESS_PGS)]: + for dst_pg in self.pg_interfaces[:NUM_EGRESS_PGS]: + frame_request = self.create_frame_request( + src_pg.remote_mac, src_pg.local_mac, + src_pg.remote_ip6 if is_ipv6 else src_pg.remote_ip4, + dst_pg.remote_ip6 if is_ipv6 else dst_pg.remote_ip4, + is_ipv6 + ) + + src_pg.add_stream([frame_request]) + dst_pg.enable_capture() + self.pg_start() + + self.verify_packet_forwarding(dst_pg, frame_request) + self.logger.info(f"test_negative_normal_ip{'6' if is_ipv6 else '4'}: Passed with {src_pg.name} to {dst_pg.name}") + + def test_negative_decap_v4(self): + """Negative test for VXLAN decap with unmatched inner DST IPv4""" + self._test_negative_decap(is_ipv6=False) + self.remove_configured_vpp_objects_on_tear_down = False + + def test_negative_decap_v6(self): + """Negative test for VXLAN decap with unmatched inner DST IPv6""" + self._test_negative_decap(is_ipv6=True) + self.remove_configured_vpp_objects_on_tear_down = False + + def test_negative_normal_ip4(self): + """Negative test for non-encaped IPv4 packet """ + self._test_negative_normal_ip(is_ipv6=False) + self.remove_configured_vpp_objects_on_tear_down = False + + def test_negative_normal_ip6(self): + """Negative test for non-encaped IPv6 packet """ + self._test_negative_normal_ip(is_ipv6=True) + self.remove_configured_vpp_objects_on_tear_down = False + + def test_negative_vxlan_no_decap(self): + """Negative test for non-terminated VXLAN packet""" + in_pg = self.pg_interfaces[NUM_EGRESS_PGS] + out_pg = self.pg_interfaces[NUM_EGRESS_PGS + 1] + frame_request = self.create_frame_request( + "00:00:00:00:00:01", "00:00:00:00:00:02", + "1.2.3.4", "4.3.2.1" + ) + + encapsulated_pkt = self.encapsulate( + frame_request, self.single_tunnel_vni + 1, in_pg.remote_mac, in_pg.local_mac, in_pg.remote_ip4, out_pg.remote_ip4 + ) + + in_pg.add_stream([encapsulated_pkt]) + out_pg.enable_capture() + self.pg_start() + + self.verify_packet_forwarding(out_pg, encapsulated_pkt) + self.logger.info(f"test_negative_vxlan_no_decap: Passed from {in_pg.name} to {out_pg.name}") + + self.remove_configured_vpp_objects_on_tear_down = False + + +TestVxlan.add_dynamic_tests() + +if __name__ == "__main__": + unittest.main(testRunner=VppTestRunner) diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.c b/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.c index 8ec460d..c999271 100644 --- a/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.c +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.c @@ -57,15 +57,18 @@ static int update_classify_table_and_sessions (bool is_ipv6, u32 count, vl_api_t int rv = 0; #define CLASSIFY_TABLE_VECTOR_SIZE 16 u8 mask[7*CLASSIFY_TABLE_VECTOR_SIZE]; - clib_memset (mask, 0, sizeof (mask)); - u32 nbuckets = 2; + + /* Table Configs (TBD optimal values for expected use-cases) */ + u32 nbuckets = 32; u32 memory_size = 2 << 22; u32 skip = 5; - u32 match; + u32 match = 1; /* Create the table if it's an add operation */ if (table_index == ~0) { + + /* Mask inner DIP */ if (is_ipv6) { match = 2; for (int i = 8; i <= 23; i++) { @@ -78,8 +81,8 @@ static int update_classify_table_and_sessions (bool is_ipv6, u32 count, vl_api_t } } - rv = vnet_classify_add_del_table (cm, mask, nbuckets /* nbuckets */, memory_size /* memory_size */, - skip /* skip */, match, ~0 /* next_table_index */, + rv = vnet_classify_add_del_table (cm, mask, nbuckets, memory_size, + skip, match, ~0 /* next_table_index */, ~0 /* miss_next_index */, &table_index, 0 /* current_data_flag */, 0 /* current_data_offset */, 1 /* is_add */, 0 /* del_chain */); @@ -96,8 +99,8 @@ static int update_classify_table_and_sessions (bool is_ipv6, u32 count, vl_api_t } /* Make sure table AF is not being changed in the replace case */ - if (sm->classify_table_index_is_v6[table_index] != is_ipv6) { - clib_error ("Table AF mismatch"); + if (count > 0 && sm->classify_table_index_is_v6[table_index] != is_ipv6) { + clib_warning ("Table AF mismatch"); return VNET_API_ERROR_INVALID_VALUE_2; } diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_decap.c b/platform/vpp/plugins/tunterm_acl/tunterm_acl_decap.c index fd6b16b..51b8c9f 100644 --- a/platform/vpp/plugins/tunterm_acl/tunterm_acl_decap.c +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_decap.c @@ -624,4 +624,4 @@ VNET_FEATURE_INIT (tunterm_acl_ip4_vxlan_bypass, static) = .arc_name = "ip4-unicast", .node_name = "tunterm-ip4-vxlan-bypass", .runs_before = VNET_FEATURES ("ip4-lookup"), -}; \ No newline at end of file +}; diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h b/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h index f165d0b..8f8ac02 100644 --- a/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h @@ -34,4 +34,4 @@ int tunterm_acl_redirect_clear (vlib_main_t *vm, u32 table_index); * Local Variables: * eval: (c-set-style "gnu") * End: - */ \ No newline at end of file + */ From f9f7d4551603d0797966faa2c86495d38b9635c6 Mon Sep 17 00:00:00 2001 From: Akeel Ali <701916+AkeelAli@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:36:44 -0400 Subject: [PATCH 3/7] Add Tunterm ACL Plugin documentation + Cleanup --- platform/vpp/plugins/tunterm_acl/FEATURE.yaml | 3 +- .../vpp/plugins/tunterm_acl/docs/README.rst | 58 ++++++++++++++++++ .../plugins/tunterm_acl/docs/tunterm_acl.png | Bin 0 -> 70807 bytes .../vpp/plugins/tunterm_acl/tunterm_acl.api | 2 +- .../vpp/plugins/tunterm_acl/tunterm_acl_api.c | 25 +++----- .../plugins/tunterm_acl/tunterm_acl_node.c | 6 +- .../tunterm_acl/tunterm_acl_redirect.h | 1 + 7 files changed, 71 insertions(+), 24 deletions(-) create mode 100644 platform/vpp/plugins/tunterm_acl/docs/README.rst create mode 100644 platform/vpp/plugins/tunterm_acl/docs/tunterm_acl.png diff --git a/platform/vpp/plugins/tunterm_acl/FEATURE.yaml b/platform/vpp/plugins/tunterm_acl/FEATURE.yaml index 0b04dc5..f78e0ff 100644 --- a/platform/vpp/plugins/tunterm_acl/FEATURE.yaml +++ b/platform/vpp/plugins/tunterm_acl/FEATURE.yaml @@ -1,6 +1,6 @@ --- name: Tunnel Termination ACL -maintainer: Akeel Ali +maintainer: Akeel Ali features: - Applies ACL after Tunnel Decap - Current support is limited to a specific use-case @@ -10,4 +10,3 @@ features: description: "Tunnel Termination ACL plugin" state: experimental properties: [CLI, API] - diff --git a/platform/vpp/plugins/tunterm_acl/docs/README.rst b/platform/vpp/plugins/tunterm_acl/docs/README.rst new file mode 100644 index 0000000..8e26202 --- /dev/null +++ b/platform/vpp/plugins/tunterm_acl/docs/README.rst @@ -0,0 +1,58 @@ +======================= +Tunterm ACL Plugin README +======================= + +Overview +-------- +The Tunterm ACL plugin is a self-contained plugin that applies ACL after Tunnel Decapsulation. + +It is currently designed to support a single specific use-case: + +IPv4 VxLAN tunnel termination and classification based on inner DST IPv4/6 fields, followed by a redirect action using ip4/6-rewrite. + +Plugin Structure +---------------- +The Tunterm ACL plugin consists of the following main parts: + +1. `tunterm_acl.api/tunterm_acl_api.c`: This file contains the tunterm-acl ACL API and handlers to setup and attach the classifier/sessions using an ACL-like API. + +2. `tunterm_acl_node.c`: This file contains the tunterm-acl node that performs the classification and redirect logic. Please note that at this point, there are no performance considerations implemented. + +3. `tunterm_acl_decap.c`: This file is a copy of the ip4-vxlan-bypass node, but instead of forwarding to vxlan4-input, it forwards to tunterm-acl. + +4. `tunterm_acl_redirect.c`: This file is a copy of the ip-session-redirect functions, augmented with additional functionality required for the plugin. + +5. `tunterm_acl_cli.c`: This file provides CLI capability, allowing you to see which interfaces have tunterm-acl enabled. + +6. `test_tunterm_acl.py`: This file contains unit tests that cover various positive and negative use-cases. + +Requirements +------------ +In order to use the Tunterm ACL plugin, the `vxlan_main` structure from the `vxlan` plugin must be exported: + +.. code-block:: c + + __clib_export vxlan_main_t vxlan_main; + +Functionality +------------- +The plugin introduces the following nodes and functionality: + +.. image:: tunterm_acl.png + :target: tunterm_acl.png + +Generalization Considerations +----------------------------- +If there is a need to generalize the plugin, the following considerations and work-items should be addressed: + +1. Modify vxlan bypass to allow for tunterm-acl insertion before vxlan4-input, while ensuring minimal impact on bypass performance. + +2. Add support for additional tunnel types, as currently only v4-vxlan-bypass is supported. + +3. Augment the ip-session-redirect plugin directly to support the added functionality and use it directly, instead of creating a copy. + +4. Enhance the ACL fields and actions to support additional fields, permit/deny actions, and explore the possibility of using the classifier API instead of a custom node. + +5. Implement performance considerations and enhancements to optimize the plugin's performance. + +Please note that the above considerations are suggestions and may require further analysis and implementation. diff --git a/platform/vpp/plugins/tunterm_acl/docs/tunterm_acl.png b/platform/vpp/plugins/tunterm_acl/docs/tunterm_acl.png new file mode 100644 index 0000000000000000000000000000000000000000..e04c56ddb03dd0df492a958b0b12d9f6db5291a1 GIT binary patch literal 70807 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 xiUDC27A^TEzRQVr4_wgafedSJ>i* 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 #include -/* *INDENT-OFF* */ -VLIB_PLUGIN_REGISTER () = { - .version = TUNTERM_ACL_PLUGIN_BUILD_VER, - .description = "Tunnel Terminated ACL Plugin", -}; -/* *INDENT-ON* */ - -tunterm_acl_main_t tunterm_acl_main; - #include #include #include @@ -49,6 +40,13 @@ tunterm_acl_main_t tunterm_acl_main; #include "tunterm_acl_redirect.h" +VLIB_PLUGIN_REGISTER () = { + .version = TUNTERM_ACL_PLUGIN_BUILD_VER, + .description = "Tunnel Terminated ACL Plugin", +}; + +tunterm_acl_main_t tunterm_acl_main; + static int update_classify_table_and_sessions (bool is_ipv6, u32 count, vl_api_tunterm_acl_rule_t rules[], u32 * tunterm_acl_index) { tunterm_acl_main_t * sm = &tunterm_acl_main; @@ -187,9 +185,6 @@ verify_message_len (void *mp, u64 expected_len, char *where) } } -/** - * @brief Plugin API message handler. - */ static void vl_api_tunterm_acl_add_replace_t_handler (vl_api_tunterm_acl_add_replace_t * mp) { @@ -277,7 +272,7 @@ vl_api_tunterm_acl_interface_add_del_t_handler (vl_api_tunterm_acl_interface_add goto exit; } - /* make sure both are init as you can get v6 packets while only v4 acl installed. node will do a lookup in v6 table and crash otherwise */ + /* make sure both are init as you can get v6 packets while only v4 acl installed */ vec_validate_init_empty (sm->classify_table_index_by_sw_if_index_v6, sw_if_index, ~0); vec_validate_init_empty (sm->classify_table_index_by_sw_if_index_v4, sw_if_index, ~0); @@ -331,16 +326,12 @@ vl_api_tunterm_acl_interface_add_del_t_handler (vl_api_tunterm_acl_interface_add /* API definitions */ #include -/** - * @brief Initialize the tunterm plugin. - */ static clib_error_t * tunterm_acl_init (vlib_main_t * vm) { tunterm_acl_main_t * sm = &tunterm_acl_main; sm->vnet_main = vnet_get_main (); - /* Add our API messages to the global name_crc hash table */ sm->msg_id_base = setup_message_id_table (); return tunterm_acl_redirect_init(vm); diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_node.c b/platform/vpp/plugins/tunterm_acl/tunterm_acl_node.c index 188a365..d65eda1 100644 --- a/platform/vpp/plugins/tunterm_acl/tunterm_acl_node.c +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_node.c @@ -19,7 +19,6 @@ #include #include #include - typedef struct { u32 sw_if_index; @@ -119,7 +118,8 @@ VLIB_NODE_FN(tunterm_acl_node) (vlib_main_t *vm, vlib_node_runtime_t *node, vlib u16 ethertype = (etype[0] << 8) | etype[1]; if (ethertype == 0x86DD) { - next0 = TUNTERM_ACL_NEXT_VXLAN4_INPUT; // Note we send to vxlan4-input not vxlan6-input as outer vxlan is v4 + // Note we send to vxlan4-input not vxlan6-input as outer vxlan is v4 + next0 = TUNTERM_ACL_NEXT_VXLAN4_INPUT; next_rewrite = TUNTERM_ACL_NEXT_IP6_REWRITE; table_index = tunterm_acl_main.classify_table_index_by_sw_if_index_v6[sw_if_index0]; } else if (ethertype == 0x0800) { @@ -184,7 +184,6 @@ VLIB_NODE_FN(tunterm_acl_node) (vlib_main_t *vm, vlib_node_runtime_t *node, vlib return frame->n_vectors; } -/* *INDENT-OFF* */ VLIB_REGISTER_NODE (tunterm_acl_node) = { .name = "tunterm-acl", @@ -205,7 +204,6 @@ VLIB_REGISTER_NODE (tunterm_acl_node) = [TUNTERM_ACL_NEXT_IP6_REWRITE] = "ip6-rewrite", }, }; -/* *INDENT-ON* */ /* * fd.io coding-style-patch-verification: ON diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h b/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h index 8f8ac02..654aa0d 100644 --- a/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h +++ b/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h @@ -21,6 +21,7 @@ clib_error_t * tunterm_acl_redirect_init (vlib_main_t *vm); int tunterm_acl_redirect_add (vlib_main_t *vm, u32 table_index, u32 opaque_index, dpo_proto_t proto, const u8 *match, const fib_route_path_t *rpaths); + int tunterm_acl_redirect_del (vlib_main_t *vm, u32 table_index, const u8 *match); From 935bb7992331e939ac92348a3ed225e1873aaa34 Mon Sep 17 00:00:00 2001 From: Akeel Ali <701916+AkeelAli@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:08:52 -0400 Subject: [PATCH 4/7] Move plugins to vppbld --- platform/vpp/vppbld/Makefile | 2 +- .../{ => vppbld}/plugins/tunterm_acl/CMakeLists.txt | 0 .../{ => vppbld}/plugins/tunterm_acl/FEATURE.yaml | 0 .../plugins/tunterm_acl/docs/README.rst | 0 .../plugins/tunterm_acl/docs/tunterm_acl.png | Bin .../plugins/tunterm_acl/test/test_tunterm_acl.py | 0 .../plugins/tunterm_acl/tunterm_acl.api | 0 .../plugins/tunterm_acl/tunterm_acl_api.c | 0 .../plugins/tunterm_acl/tunterm_acl_api.h | 0 .../plugins/tunterm_acl/tunterm_acl_cli.c | 0 .../plugins/tunterm_acl/tunterm_acl_decap.c | 0 .../plugins/tunterm_acl/tunterm_acl_node.c | 0 .../plugins/tunterm_acl/tunterm_acl_redirect.c | 0 .../plugins/tunterm_acl/tunterm_acl_redirect.h | 0 14 files changed, 1 insertion(+), 1 deletion(-) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/CMakeLists.txt (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/FEATURE.yaml (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/docs/README.rst (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/docs/tunterm_acl.png (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/test/test_tunterm_acl.py (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/tunterm_acl.api (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/tunterm_acl_api.c (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/tunterm_acl_api.h (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/tunterm_acl_cli.c (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/tunterm_acl_decap.c (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/tunterm_acl_node.c (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/tunterm_acl_redirect.c (100%) rename platform/vpp/{ => vppbld}/plugins/tunterm_acl/tunterm_acl_redirect.h (100%) diff --git a/platform/vpp/vppbld/Makefile b/platform/vpp/vppbld/Makefile index e6bd7f3..86892a3 100644 --- a/platform/vpp/vppbld/Makefile +++ b/platform/vpp/vppbld/Makefile @@ -23,7 +23,7 @@ UID = $(shell id -u) GUID = $(shell id -g) VPP_REPO_DIR = repo -VPP_CUSTOM_PLUGINS_DIR = ../plugins +VPP_CUSTOM_PLUGINS_DIR = plugins MAIN_TARGET = $(VPPINFRA) DERIVED_TARGETS = $(VPP_MAIN) $(VPP_PLUGIN_CORE) $(VPP_PLUGIN_DPDK) \ $(VPP_PLUGIN_DEV) $(VPP_DEV) $(VPPINFRA_DEV) $(VPPDBG) diff --git a/platform/vpp/plugins/tunterm_acl/CMakeLists.txt b/platform/vpp/vppbld/plugins/tunterm_acl/CMakeLists.txt similarity index 100% rename from platform/vpp/plugins/tunterm_acl/CMakeLists.txt rename to platform/vpp/vppbld/plugins/tunterm_acl/CMakeLists.txt diff --git a/platform/vpp/plugins/tunterm_acl/FEATURE.yaml b/platform/vpp/vppbld/plugins/tunterm_acl/FEATURE.yaml similarity index 100% rename from platform/vpp/plugins/tunterm_acl/FEATURE.yaml rename to platform/vpp/vppbld/plugins/tunterm_acl/FEATURE.yaml diff --git a/platform/vpp/plugins/tunterm_acl/docs/README.rst b/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst similarity index 100% rename from platform/vpp/plugins/tunterm_acl/docs/README.rst rename to platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst diff --git a/platform/vpp/plugins/tunterm_acl/docs/tunterm_acl.png b/platform/vpp/vppbld/plugins/tunterm_acl/docs/tunterm_acl.png similarity index 100% rename from platform/vpp/plugins/tunterm_acl/docs/tunterm_acl.png rename to platform/vpp/vppbld/plugins/tunterm_acl/docs/tunterm_acl.png diff --git a/platform/vpp/plugins/tunterm_acl/test/test_tunterm_acl.py b/platform/vpp/vppbld/plugins/tunterm_acl/test/test_tunterm_acl.py similarity index 100% rename from platform/vpp/plugins/tunterm_acl/test/test_tunterm_acl.py rename to platform/vpp/vppbld/plugins/tunterm_acl/test/test_tunterm_acl.py diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl.api b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl.api similarity index 100% rename from platform/vpp/plugins/tunterm_acl/tunterm_acl.api rename to platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl.api diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.c similarity index 100% rename from platform/vpp/plugins/tunterm_acl/tunterm_acl_api.c rename to platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.c diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_api.h b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.h similarity index 100% rename from platform/vpp/plugins/tunterm_acl/tunterm_acl_api.h rename to platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.h diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_cli.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_cli.c similarity index 100% rename from platform/vpp/plugins/tunterm_acl/tunterm_acl_cli.c rename to platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_cli.c diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_decap.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_decap.c similarity index 100% rename from platform/vpp/plugins/tunterm_acl/tunterm_acl_decap.c rename to platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_decap.c diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_node.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_node.c similarity index 100% rename from platform/vpp/plugins/tunterm_acl/tunterm_acl_node.c rename to platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_node.c diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.c similarity index 100% rename from platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.c rename to platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.c diff --git a/platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.h similarity index 100% rename from platform/vpp/plugins/tunterm_acl/tunterm_acl_redirect.h rename to platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.h From dfcca0ffc40360673e1f01cb76c286bda89e00c9 Mon Sep 17 00:00:00 2001 From: Akeel Ali <701916+AkeelAli@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:32:41 -0400 Subject: [PATCH 5/7] Bug fixes + README update --- .../plugins/tunterm_acl/docs/README.rst | 27 ++++++++++++------- .../plugins/tunterm_acl/tunterm_acl_api.c | 4 +-- .../plugins/tunterm_acl/tunterm_acl_cli.c | 10 +++++-- .../plugins/tunterm_acl/tunterm_acl_node.c | 12 +-------- .../tunterm_acl/tunterm_acl_redirect.c | 4 +-- 5 files changed, 29 insertions(+), 28 deletions(-) diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst b/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst index 8e26202..c6a8d63 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst +++ b/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst @@ -10,6 +10,16 @@ It is currently designed to support a single specific use-case: IPv4 VxLAN tunnel termination and classification based on inner DST IPv4/6 fields, followed by a redirect action using ip4/6-rewrite. +Plugin API +---------- +The Tunterm ACL plugin provides an API similar to the acl plugin API: + +1. `tunterm_acl_add_replace`: Create or replace an existing tunterm acl in-place. + +2. `tunterm_acl_del`: Delete a tunterm acl. + +3. `tunterm_acl_interface_add_del`: Add/remove a tunterm acl index to/from an interface. + Plugin Structure ---------------- The Tunterm ACL plugin consists of the following main parts: @@ -41,18 +51,15 @@ The plugin introduces the following nodes and functionality: .. image:: tunterm_acl.png :target: tunterm_acl.png -Generalization Considerations +Enhancements ----------------------------- -If there is a need to generalize the plugin, the following considerations and work-items should be addressed: - -1. Modify vxlan bypass to allow for tunterm-acl insertion before vxlan4-input, while ensuring minimal impact on bypass performance. - -2. Add support for additional tunnel types, as currently only v4-vxlan-bypass is supported. +Following are some enhancements that can be made to the plugin: +1. Add multi-path redirect support -3. Augment the ip-session-redirect plugin directly to support the added functionality and use it directly, instead of creating a copy. +2. Add multi-v4/v6 tunterm ACL support on a single interface -4. Enhance the ACL fields and actions to support additional fields, permit/deny actions, and explore the possibility of using the classifier API instead of a custom node. +3. Add VPP performance optimizations to tunterm-acl node -5. Implement performance considerations and enhancements to optimize the plugin's performance. +4. Add v6 outer vxlan bypass support -Please note that the above considerations are suggestions and may require further analysis and implementation. +5. Expose tunterm-acl stats via API \ No newline at end of file diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.c index eae0cc3..5c6927f 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.c +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.c @@ -114,7 +114,7 @@ static int update_classify_table_and_sessions (bool is_ipv6, u32 count, vl_api_t for (int i = 0; i < count; i++) { /* (1) Process Route */ fib_route_path_t *paths_ = 0; - u8 n_paths = 1; //rules[i].n_paths; + u8 n_paths = 1; //rules[i].n_paths; (TODO: support multiple paths) if (n_paths <= 0) { return VNET_API_ERROR_NO_PATHS_IN_ROUTE; } @@ -132,8 +132,8 @@ static int update_classify_table_and_sessions (bool is_ipv6, u32 count, vl_api_t /* (2) Process DST IP */ if (is_ipv6 != rules[i].dst.af) { - return VNET_API_ERROR_INVALID_VALUE_3; vec_free (paths_); + return VNET_API_ERROR_INVALID_VALUE_3; } ip46_address_t dst; diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_cli.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_cli.c index 69d3784..36c3528 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_cli.c +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_cli.c @@ -48,9 +48,15 @@ show_tunterm_acl_interfaces_command_fn (vlib_main_t * vm, /* Check if the interface has "tunterm-ip4-vxlan-bypass" enabled */ if (vnet_feature_is_enabled("ip4-unicast", "tunterm-ip4-vxlan-bypass", sw_if_index)) { + u32 tunterm_acl_index_v4 = ~0; + u32 tunterm_acl_index_v6 = ~0; - u32 tunterm_acl_index_v4 = sm->classify_table_index_by_sw_if_index_v4[sw_if_index]; - u32 tunterm_acl_index_v6 = sm->classify_table_index_by_sw_if_index_v6[sw_if_index]; + if (sw_if_index < vec_len(sm->classify_table_index_by_sw_if_index_v4)) { + tunterm_acl_index_v4 = sm->classify_table_index_by_sw_if_index_v4[sw_if_index]; + } + if (sw_if_index < vec_len(sm->classify_table_index_by_sw_if_index_v6)) { + tunterm_acl_index_v6 = sm->classify_table_index_by_sw_if_index_v6[sw_if_index]; + } vlib_cli_output (vm, "%U\t%u\t%u\t%u", format_vnet_sw_if_index_name, vnm, sw_if_index, diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_node.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_node.c index d65eda1..b025665 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_node.c +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_node.c @@ -66,8 +66,6 @@ typedef enum { TUNTERM_ACL_NEXT_DROP, TUNTERM_ACL_NEXT_VXLAN4_INPUT, - TUNTERM_ACL_NEXT_IP4_REWRITE, - TUNTERM_ACL_NEXT_IP6_REWRITE, TUNTERM_ACL_N_NEXT, } tunterm_acl_next_t; @@ -101,7 +99,6 @@ VLIB_NODE_FN(tunterm_acl_node) (vlib_main_t *vm, vlib_node_runtime_t *node, vlib u32 bi0; vlib_buffer_t *b0; u32 next0 = TUNTERM_ACL_NEXT_VXLAN4_INPUT; // Default next index - u32 next_rewrite = TUNTERM_ACL_NEXT_IP4_REWRITE; u32 sw_if_index0; bi0 = from[0]; @@ -118,13 +115,8 @@ VLIB_NODE_FN(tunterm_acl_node) (vlib_main_t *vm, vlib_node_runtime_t *node, vlib u16 ethertype = (etype[0] << 8) | etype[1]; if (ethertype == 0x86DD) { - // Note we send to vxlan4-input not vxlan6-input as outer vxlan is v4 - next0 = TUNTERM_ACL_NEXT_VXLAN4_INPUT; - next_rewrite = TUNTERM_ACL_NEXT_IP6_REWRITE; table_index = tunterm_acl_main.classify_table_index_by_sw_if_index_v6[sw_if_index0]; } else if (ethertype == 0x0800) { - next0 = TUNTERM_ACL_NEXT_VXLAN4_INPUT; - next_rewrite = TUNTERM_ACL_NEXT_IP4_REWRITE; table_index = tunterm_acl_main.classify_table_index_by_sw_if_index_v4[sw_if_index0]; } else { // This shouldn't happen. Something is wrong. Drop the packet to bring attention to this. @@ -147,7 +139,7 @@ VLIB_NODE_FN(tunterm_acl_node) (vlib_main_t *vm, vlib_node_runtime_t *node, vlib if (e->action == CLASSIFY_ACTION_SET_METADATA) { vlib_buffer_advance (b0, sizeof (vxlan_header_t)); vlib_buffer_advance (b0, sizeof (ethernet_header_t)); - next0 = next_rewrite; + next0 = (e->next_index < node->n_next_nodes) ? e->next_index : next0; vnet_buffer (b0)->ip.adj_index[VLIB_TX] = e->metadata; pkts_redirected++; } else { @@ -200,8 +192,6 @@ VLIB_REGISTER_NODE (tunterm_acl_node) = .next_nodes = { [TUNTERM_ACL_NEXT_DROP] = "error-drop", [TUNTERM_ACL_NEXT_VXLAN4_INPUT] = "vxlan4-input", - [TUNTERM_ACL_NEXT_IP4_REWRITE] = "ip4-rewrite", - [TUNTERM_ACL_NEXT_IP6_REWRITE] = "ip6-rewrite", }, }; diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.c index fa66bf1..e9c3a39 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.c +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.c @@ -96,10 +96,8 @@ tunterm_acl_redirect_add (vlib_main_t *vm, u32 table_index, u32 opaque_index, switch (payload_type) { case FIB_FORW_CHAIN_TYPE_UNICAST_IP4: - pname = "ip4-inacl"; - break; case FIB_FORW_CHAIN_TYPE_UNICAST_IP6: - pname = "ip6-inacl"; + pname = "tunterm-acl"; break; default: return VNET_API_ERROR_INVALID_ADDRESS_FAMILY; From b9a4ec7d14588f917618060d586ac7838a18250f Mon Sep 17 00:00:00 2001 From: AkeelAli <701916+AkeelAli@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:51:37 -0400 Subject: [PATCH 6/7] Add missing newline in README.rst --- platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst b/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst index c6a8d63..3612b4f 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst +++ b/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst @@ -54,6 +54,7 @@ The plugin introduces the following nodes and functionality: Enhancements ----------------------------- Following are some enhancements that can be made to the plugin: + 1. Add multi-path redirect support 2. Add multi-v4/v6 tunterm ACL support on a single interface @@ -62,4 +63,4 @@ Following are some enhancements that can be made to the plugin: 4. Add v6 outer vxlan bypass support -5. Expose tunterm-acl stats via API \ No newline at end of file +5. Expose tunterm-acl stats via API From aefb387b66f0190572c356421e73cdc429418223 Mon Sep 17 00:00:00 2001 From: AkeelAli <701916+AkeelAli@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:45:39 -0400 Subject: [PATCH 7/7] Classification and Formatting changes - Classify relative to start of inner DIP - Support inner vlan tag + add testing - Formatting of test and src code --- .../vppbld/plugins/tunterm_acl/FEATURE.yaml | 6 +- .../plugins/tunterm_acl/docs/README.rst | 2 +- .../tunterm_acl/test/test_tunterm_acl.py | 285 ++++++++---- .../plugins/tunterm_acl/tunterm_acl_api.c | 433 ++++++++++-------- .../plugins/tunterm_acl/tunterm_acl_api.h | 18 +- .../plugins/tunterm_acl/tunterm_acl_cli.c | 61 +-- .../plugins/tunterm_acl/tunterm_acl_decap.c | 169 ++++--- .../plugins/tunterm_acl/tunterm_acl_node.c | 284 +++++++----- .../tunterm_acl/tunterm_acl_redirect.c | 56 ++- .../tunterm_acl/tunterm_acl_redirect.h | 8 +- 10 files changed, 780 insertions(+), 542 deletions(-) diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/FEATURE.yaml b/platform/vpp/vppbld/plugins/tunterm_acl/FEATURE.yaml index f78e0ff..ecadd5e 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/FEATURE.yaml +++ b/platform/vpp/vppbld/plugins/tunterm_acl/FEATURE.yaml @@ -4,9 +4,9 @@ maintainer: Akeel Ali features: - Applies ACL after Tunnel Decap - Current support is limited to a specific use-case - - Tunnel: IPv4 VxLAN Tunnel Termination - - Fields: DST IPv4/6 Classification - - Action: Redirect (ip4/6-rewrite) + - IPv4 VxLAN Tunnel Termination (Tunnel) + - DST IPv4/6 Classification (Field) + - Redirect (Action) description: "Tunnel Termination ACL plugin" state: experimental properties: [CLI, API] diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst b/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst index 3612b4f..ab19003 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst +++ b/platform/vpp/vppbld/plugins/tunterm_acl/docs/README.rst @@ -8,7 +8,7 @@ The Tunterm ACL plugin is a self-contained plugin that applies ACL after Tunnel It is currently designed to support a single specific use-case: -IPv4 VxLAN tunnel termination and classification based on inner DST IPv4/6 fields, followed by a redirect action using ip4/6-rewrite. +IPv4 VxLAN tunnel termination and classification based on inner DST IPv4/6 fields, followed by a redirect action via a VPP FIB path. Plugin API ---------- diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/test/test_tunterm_acl.py b/platform/vpp/vppbld/plugins/tunterm_acl/test/test_tunterm_acl.py index 0d1db17..abfd8ae 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/test/test_tunterm_acl.py +++ b/platform/vpp/vppbld/plugins/tunterm_acl/test/test_tunterm_acl.py @@ -4,7 +4,7 @@ from framework import VppTestCase from asfframework import VppTestRunner -from scapy.layers.l2 import Ether +from scapy.layers.l2 import Ether, Dot1Q from scapy.packet import Raw from scapy.layers.inet import IP, UDP from scapy.layers.inet6 import IPv6 @@ -29,34 +29,21 @@ # Generates test teardown traces (including packet captures) for each test RUN_TESTS_INDIVIDUALLY = False -class TestVxlan(VppTestCase): - """VXLAN Test Case""" + +class TestTuntermAcl(VppTestCase): + """Tunnel Termination ACL Test Case""" def __init__(self, *args): VppTestCase.__init__(self, *args) - def encapsulate(self, pkt, vni, src_mac, dst_mac, src_ip, dst_ip): - """ - Encapsulate the original payload frame by adding VXLAN header with its - UDP, IP and Ethernet fields - """ - return ( - Ether(src=src_mac, dst=dst_mac) - / IP(src=src_ip, dst=dst_ip) - / UDP(sport=self.dport, dport=self.dport, chksum=0) - / VXLAN(vni=vni, flags=self.flags) - / pkt - ) - @classmethod - def createVxLANInterfaces(cls, port=4789): + def configTuntermACL(cls, port=4789): cls.dport = port cls.single_tunnel_vni = 0x12345 cls.single_tunnel_bd = 1 # Create VXLAN tunnels on ingress interfaces - cls.vxlan_tunnels = [] for i in range(NUM_INGRESS_PGS): ingress_pg = cls.pg_interfaces[NUM_EGRESS_PGS + i] r = VppVxlanTunnel( @@ -72,7 +59,6 @@ def createVxLANInterfaces(cls, port=4789): cls.vapi.sw_interface_set_l2_bridge( rx_sw_if_index=r.sw_if_index, bd_id=cls.single_tunnel_bd ) - cls.vxlan_tunnels.append(r) # Create loopback/BVI interface cls.create_loopback_interfaces(1) @@ -89,9 +75,7 @@ def createVxLANInterfaces(cls, port=4789): ) # Enable v6 on BVI interface (for negative test to forward based on inner dst IP) - cls.vapi.sw_interface_ip6_enable_disable( - cls.loop0.sw_if_index, enable=1 - ) + cls.vapi.sw_interface_ip6_enable_disable(cls.loop0.sw_if_index, enable=1) # Add default ACL permit per sonic-vpp use-case from vpp_acl import AclRule, VppAcl, VppAclInterface @@ -106,39 +90,61 @@ def createVxLANInterfaces(cls, port=4789): for i in range(NUM_INGRESS_PGS): ingress_pg = cls.pg_interfaces[NUM_EGRESS_PGS + i] - acl_if = VppAclInterface(cls, sw_if_index=ingress_pg.sw_if_index, n_input=1, acls=[acl_1]) + acl_if = VppAclInterface( + cls, sw_if_index=ingress_pg.sw_if_index, n_input=1, acls=[acl_1] + ) acl_if.add_vpp_config() # TunTerm plugin Call DST_IPs_v4 = [f"4.3.2.{i}" for i in range(NUM_EGRESS_PGS)] DST_IPs_v6 = [f"2001:db8::{i+1}" for i in range(NUM_EGRESS_PGS)] paths_v4 = [ - VppRoutePath(cls.pg_interfaces[i].remote_ip4, cls.pg_interfaces[i].sw_if_index).encode() + VppRoutePath( + cls.pg_interfaces[i].remote_ip4, cls.pg_interfaces[i].sw_if_index + ).encode() for i in range(NUM_EGRESS_PGS) ] paths_v6 = [ - VppRoutePath(cls.pg_interfaces[i].remote_ip6, cls.pg_interfaces[i].sw_if_index).encode() + VppRoutePath( + cls.pg_interfaces[i].remote_ip6, cls.pg_interfaces[i].sw_if_index + ).encode() for i in range(NUM_EGRESS_PGS) ] - cls.rules_v4 = [{"dst": dst_ip, "path": path} for dst_ip, path in zip(DST_IPs_v4, paths_v4)] - cls.rules_v6 = [{"dst": dst_ip, "path": path} for dst_ip, path in zip(DST_IPs_v6, paths_v6)] - reply_v4 = cls.vapi.tunterm_acl_add_replace(0xffffffff, False, len(cls.rules_v4), cls.rules_v4) - reply_v6 = cls.vapi.tunterm_acl_add_replace(0xffffffff, True, len(cls.rules_v6), cls.rules_v6) + cls.rules_v4 = [ + {"dst": dst_ip, "path": path} for dst_ip, path in zip(DST_IPs_v4, paths_v4) + ] + cls.rules_v6 = [ + {"dst": dst_ip, "path": path} for dst_ip, path in zip(DST_IPs_v6, paths_v6) + ] + reply_v4 = cls.vapi.tunterm_acl_add_replace( + 0xFFFFFFFF, False, len(cls.rules_v4), cls.rules_v4 + ) + reply_v6 = cls.vapi.tunterm_acl_add_replace( + 0xFFFFFFFF, True, len(cls.rules_v6), cls.rules_v6 + ) cls.tunterm_acl_index_v4 = reply_v4.tunterm_acl_index cls.tunterm_acl_index_v6 = reply_v6.tunterm_acl_index - cls.vapi.tunterm_acl_add_replace(cls.tunterm_acl_index_v4, False, len(cls.rules_v4), cls.rules_v4) - cls.vapi.tunterm_acl_add_replace(cls.tunterm_acl_index_v6, True, len(cls.rules_v6), cls.rules_v6) + cls.vapi.tunterm_acl_add_replace( + cls.tunterm_acl_index_v4, False, len(cls.rules_v4), cls.rules_v4 + ) + cls.vapi.tunterm_acl_add_replace( + cls.tunterm_acl_index_v6, True, len(cls.rules_v6), cls.rules_v6 + ) for i in range(NUM_INGRESS_PGS): ingress_pg = cls.pg_interfaces[NUM_EGRESS_PGS + i] - cls.vapi.tunterm_acl_interface_add_del(True, ingress_pg.sw_if_index, cls.tunterm_acl_index_v4) - cls.vapi.tunterm_acl_interface_add_del(True, ingress_pg.sw_if_index, cls.tunterm_acl_index_v6) + cls.vapi.tunterm_acl_interface_add_del( + True, ingress_pg.sw_if_index, cls.tunterm_acl_index_v4 + ) + cls.vapi.tunterm_acl_interface_add_del( + True, ingress_pg.sw_if_index, cls.tunterm_acl_index_v6 + ) @classmethod def setUpClass(cls): - super(TestVxlan, cls).setUpClass() + super(TestTuntermAcl, cls).setUpClass() try: cls.flags = 0x8 @@ -152,7 +158,7 @@ def setUpClass(cls): pg.resolve_arp() pg.resolve_ndp() - cls.createVxLANInterfaces() + cls.configTuntermACL() except Exception: cls.tearDownClass() @@ -162,19 +168,23 @@ def setUpClass(cls): def tearDownClass(cls): for i in range(NUM_INGRESS_PGS): ingress_pg = cls.pg_interfaces[NUM_EGRESS_PGS + i] - cls.vapi.tunterm_acl_interface_add_del(False, ingress_pg.sw_if_index, cls.tunterm_acl_index_v4) - cls.vapi.tunterm_acl_interface_add_del(False, ingress_pg.sw_if_index, cls.tunterm_acl_index_v6) + cls.vapi.tunterm_acl_interface_add_del( + False, ingress_pg.sw_if_index, cls.tunterm_acl_index_v4 + ) + cls.vapi.tunterm_acl_interface_add_del( + False, ingress_pg.sw_if_index, cls.tunterm_acl_index_v6 + ) cls.vapi.tunterm_acl_del(cls.tunterm_acl_index_v4) cls.vapi.tunterm_acl_del(cls.tunterm_acl_index_v6) - super(TestVxlan, cls).tearDownClass() + super(TestTuntermAcl, cls).tearDownClass() def setUp(self): - super(TestVxlan, self).setUp() + super(TestTuntermAcl, self).setUp() def tearDown(self): - super(TestVxlan, self).tearDown() + super(TestTuntermAcl, self).tearDown() def show_commands_at_teardown(self): self.logger.info(self.vapi.cli("show bridge-domain 1 detail")) @@ -185,9 +195,23 @@ def show_commands_at_teardown(self): self.logger.info(self.vapi.cli("show acl-plugin acl")) self.logger.info(self.vapi.cli("show acl-plugin tables")) - def create_frame_request(self, src_mac, dst_mac, src_ip, dst_ip, is_ipv6=False): + def encapsulate(self, pkt, vni, src_mac, dst_mac, src_ip, dst_ip): return ( Ether(src=src_mac, dst=dst_mac) + / IP(src=src_ip, dst=dst_ip) + / UDP(sport=self.dport, dport=self.dport, chksum=0) + / VXLAN(vni=vni, flags=self.flags) + / pkt + ) + + def create_frame_request( + self, src_mac, dst_mac, src_ip, dst_ip, is_ipv6=False, add_vlan=False + ): + ether_layer = Ether(src=src_mac, dst=dst_mac) + if add_vlan: + ether_layer /= Dot1Q(vlan=100) + return ( + ether_layer / (IPv6(src=src_ip, dst=dst_ip) if is_ipv6 else IP(src=src_ip, dst=dst_ip)) / UDP(sport=10000, dport=20000) / Raw("\xa5" * 100) @@ -218,30 +242,47 @@ def verify_packet_forwarding(self, out_pg, frame_request): self.assert_eq_pkts(pkt, frame_forwarded) def _test_decap(self, in_pg, out_pg, input_frame, is_v6=False): - """Decapsulation test - Send encapsulated frames from in_pg - Verify receipt of redirected decapsulated frames on output pg - """ - encapsulated_pkt = self.encapsulate(input_frame, self.single_tunnel_vni, in_pg.remote_mac, in_pg.local_mac, in_pg.remote_ip4, in_pg.local_ip4) + encapsulated_pkt = self.encapsulate( + input_frame, + self.single_tunnel_vni, + in_pg.remote_mac, + in_pg.local_mac, + in_pg.remote_ip4, + in_pg.local_ip4, + ) in_pg.add_stream([encapsulated_pkt]) out_pg.enable_capture() self.pg_start() - # Pick first received frame and check if it's the redirected frame self.verify_packet_forwarding(out_pg, input_frame) - self.logger.info("test_tunterm: Passed %s with input intf #%d (%s) and output %s" % - ("v6" if is_v6 else "v4", in_pg.pg_index - NUM_EGRESS_PGS + 1, in_pg.name, out_pg.name)) + self.logger.info( + "test_tunterm: Passed %s with input intf #%d (%s) and output %s" + % ( + "v6" if is_v6 else "v4", + in_pg.pg_index - NUM_EGRESS_PGS + 1, + in_pg.name, + out_pg.name, + ) + ) def _test_add_remove(self, pg): - reply_v4 = self.vapi.tunterm_acl_add_replace(0xffffffff, False, 1, [self.rules_v4[0]]) - reply_v6 = self.vapi.tunterm_acl_add_replace(0xffffffff, True, 1, [self.rules_v6[0]]) + reply_v4 = self.vapi.tunterm_acl_add_replace( + 0xFFFFFFFF, False, 1, [self.rules_v4[0]] + ) + reply_v6 = self.vapi.tunterm_acl_add_replace( + 0xFFFFFFFF, True, 1, [self.rules_v6[0]] + ) tunterm_acl_index_v4 = reply_v4.tunterm_acl_index tunterm_acl_index_v6 = reply_v6.tunterm_acl_index - self.vapi.tunterm_acl_interface_add_del(True, pg.sw_if_index, tunterm_acl_index_v4) - self.vapi.tunterm_acl_interface_add_del(True, pg.sw_if_index, tunterm_acl_index_v6) + self.vapi.tunterm_acl_interface_add_del( + True, pg.sw_if_index, tunterm_acl_index_v4 + ) + self.vapi.tunterm_acl_interface_add_del( + True, pg.sw_if_index, tunterm_acl_index_v6 + ) # Shouldn't be able to delete the tunterm entry as it's still in use with self.assertRaises(Exception): @@ -251,41 +292,63 @@ def _test_add_remove(self, pg): # Shouldn't be able to switch table AF during replace with self.assertRaises(Exception): - self.vapi.tunterm_acl_add_del(tunterm_acl_index_v4, True, 1, [self.rules_v6[0]]) + self.vapi.tunterm_acl_add_del( + tunterm_acl_index_v4, True, 1, [self.rules_v6[0]] + ) with self.assertRaises(Exception): - self.vapi.tunterm_acl_add_del(tunterm_acl_index_v6, False, 1, [self.rules_v4[0]]) + self.vapi.tunterm_acl_add_del( + tunterm_acl_index_v6, False, 1, [self.rules_v4[0]] + ) # Test a proper replace if len(self.rules_v4) > 1: - reply = self.vapi.tunterm_acl_add_replace(tunterm_acl_index_v4, False, 1, [self.rules_v4[1]]) + reply = self.vapi.tunterm_acl_add_replace( + tunterm_acl_index_v4, False, 1, [self.rules_v4[1]] + ) tunterm_acl_index_v4_2 = reply.tunterm_acl_index self.assertEqual(tunterm_acl_index_v4, tunterm_acl_index_v4_2) if len(self.rules_v6) > 1: - reply = self.vapi.tunterm_acl_add_replace(tunterm_acl_index_v6, True, 1, [self.rules_v6[1]]) + reply = self.vapi.tunterm_acl_add_replace( + tunterm_acl_index_v6, True, 1, [self.rules_v6[1]] + ) tunterm_acl_index_v6_2 = reply.tunterm_acl_index self.assertEqual(tunterm_acl_index_v6, tunterm_acl_index_v6_2) - self.vapi.tunterm_acl_interface_add_del(False, pg.sw_if_index, tunterm_acl_index_v4) - self.vapi.tunterm_acl_interface_add_del(False, pg.sw_if_index, tunterm_acl_index_v6) + self.vapi.tunterm_acl_interface_add_del( + False, pg.sw_if_index, tunterm_acl_index_v4 + ) + self.vapi.tunterm_acl_interface_add_del( + False, pg.sw_if_index, tunterm_acl_index_v6 + ) self.vapi.tunterm_acl_del(tunterm_acl_index_v4) self.vapi.tunterm_acl_del(tunterm_acl_index_v6) # Shouldn't be able to detach the tunterm as it's already been detached with self.assertRaises(Exception): - self.vapi.tunterm_acl_interface_add_del(False, pg.sw_if_index, tunterm_acl_index_v4) + self.vapi.tunterm_acl_interface_add_del( + False, pg.sw_if_index, tunterm_acl_index_v4 + ) with self.assertRaises(Exception): - self.vapi.tunterm_acl_interface_add_del(False, pg.sw_if_index, tunterm_acl_index_v6) + self.vapi.tunterm_acl_interface_add_del( + False, pg.sw_if_index, tunterm_acl_index_v6 + ) # Test empty acls - reply_v4 = self.vapi.tunterm_acl_add_replace(0xffffffff, False, 0, []) - reply_v4 = self.vapi.tunterm_acl_add_replace(reply_v4.tunterm_acl_index, False, 0, []) - reply_v6 = self.vapi.tunterm_acl_add_replace(0xffffffff, True, 0, []) - reply_v6 = self.vapi.tunterm_acl_add_replace(reply_v6.tunterm_acl_index, True, 0, []) + reply_v4 = self.vapi.tunterm_acl_add_replace(0xFFFFFFFF, False, 0, []) + reply_v4 = self.vapi.tunterm_acl_add_replace( + reply_v4.tunterm_acl_index, False, 0, [] + ) + reply_v6 = self.vapi.tunterm_acl_add_replace(0xFFFFFFFF, True, 0, []) + reply_v6 = self.vapi.tunterm_acl_add_replace( + reply_v6.tunterm_acl_index, True, 0, [] + ) self.vapi.tunterm_acl_del(reply_v4.tunterm_acl_index) self.vapi.tunterm_acl_del(reply_v6.tunterm_acl_index) - def _run_decap_test(self, ingress_index, egress_index, is_ipv6=False): + def _run_decap_test( + self, ingress_index, egress_index, is_ipv6=False, add_vlan=False + ): self.remove_configured_vpp_objects_on_tear_down = False out_pg = self.pg_interfaces[egress_index] in_pg = self.pg_interfaces[NUM_EGRESS_PGS + ingress_index] @@ -294,8 +357,7 @@ def _run_decap_test(self, ingress_index, egress_index, is_ipv6=False): dst_ip = f"2001:db8::{egress_index + 1}" if is_ipv6 else f"4.3.2.{egress_index}" frame_request = self.create_frame_request( - "00:00:00:00:00:01", "00:00:00:00:00:02", - src_ip, dst_ip, is_ipv6 + "00:00:00:00:00:01", "00:00:00:00:00:02", src_ip, dst_ip, is_ipv6, add_vlan ) self._test_add_remove(out_pg) # exercise the add/remove (no-op) @@ -310,24 +372,58 @@ def add_dynamic_tests(cls): if RUN_TESTS_INDIVIDUALLY: for ingress_index in range(NUM_INGRESS_PGS): for egress_index in range(NUM_EGRESS_PGS): - def test_v4(self, ingress_index=ingress_index, egress_index=egress_index): + + def test_v4( + self, ingress_index=ingress_index, egress_index=egress_index + ): self._run_decap_test(ingress_index, egress_index, is_ipv6=False) - def test_v6(self, ingress_index=ingress_index, egress_index=egress_index): + + def test_v6( + self, ingress_index=ingress_index, egress_index=egress_index + ): self._run_decap_test(ingress_index, egress_index, is_ipv6=True) - setattr(cls, f"test_decap_v4_ingress_{ingress_index}_egress_{egress_index}", test_v4) - setattr(cls, f"test_decap_v6_ingress_{ingress_index}_egress_{egress_index}", test_v6) + + setattr( + cls, + f"test_decap_v4_ingress_{ingress_index}_egress_{egress_index}", + test_v4, + ) + setattr( + cls, + f"test_decap_v6_ingress_{ingress_index}_egress_{egress_index}", + test_v6, + ) else: + def test_v4(self): for ingress_index in range(NUM_INGRESS_PGS): for egress_index in range(NUM_EGRESS_PGS): self._run_decap_test(ingress_index, egress_index, is_ipv6=False) + def test_v6(self): for ingress_index in range(NUM_INGRESS_PGS): for egress_index in range(NUM_EGRESS_PGS): self._run_decap_test(ingress_index, egress_index, is_ipv6=True) + setattr(cls, "test_decap_v4", test_v4) setattr(cls, "test_decap_v6", test_v6) + def test_v4_inner_vlan(self): + """Test for inner vlan tag (v4)""" + for ingress_index in range(NUM_INGRESS_PGS): + for egress_index in range(NUM_EGRESS_PGS): + self._run_decap_test( + ingress_index, egress_index, is_ipv6=False, add_vlan=True + ) + + def test_v6_inner_vlan(self): + """Test for inner vlan tag (v6)""" + for ingress_index in range(NUM_INGRESS_PGS): + for egress_index in range(NUM_EGRESS_PGS): + self._run_decap_test( + ingress_index, egress_index, is_ipv6=True, add_vlan=True + ) + ################# # Negative Tests # - Decap with unmatched inner DST IP (v4/v6) @@ -340,14 +436,20 @@ def _test_negative_decap(self, is_ipv6=False): in_pg = self.pg_interfaces[NUM_EGRESS_PGS] frame_request = self.create_frame_request( - "00:00:00:00:00:01", "00:00:00:00:00:02", + "00:00:00:00:00:01", + "00:00:00:00:00:02", "2001:db9::1" if is_ipv6 else "1.2.3.4", out_pg.remote_ip6 if is_ipv6 else out_pg.remote_ip4, - is_ipv6 + is_ipv6, ) encapsulated_pkt = self.encapsulate( - frame_request, self.single_tunnel_vni, in_pg.remote_mac, in_pg.local_mac, in_pg.remote_ip4, in_pg.local_ip4 + frame_request, + self.single_tunnel_vni, + in_pg.remote_mac, + in_pg.local_mac, + in_pg.remote_ip4, + in_pg.local_ip4, ) in_pg.add_stream([encapsulated_pkt]) @@ -358,13 +460,16 @@ def _test_negative_decap(self, is_ipv6=False): self.logger.info(f"test_negative_decap_v{'6' if is_ipv6 else '4'}: Passed") def _test_negative_normal_ip(self, is_ipv6=False): - for src_pg in [self.pg_interfaces[NUM_EGRESS_PGS + i] for i in range(NUM_INGRESS_PGS)]: + for src_pg in [ + self.pg_interfaces[NUM_EGRESS_PGS + i] for i in range(NUM_INGRESS_PGS) + ]: for dst_pg in self.pg_interfaces[:NUM_EGRESS_PGS]: frame_request = self.create_frame_request( - src_pg.remote_mac, src_pg.local_mac, + src_pg.remote_mac, + src_pg.local_mac, src_pg.remote_ip6 if is_ipv6 else src_pg.remote_ip4, dst_pg.remote_ip6 if is_ipv6 else dst_pg.remote_ip4, - is_ipv6 + is_ipv6, ) src_pg.add_stream([frame_request]) @@ -372,7 +477,9 @@ def _test_negative_normal_ip(self, is_ipv6=False): self.pg_start() self.verify_packet_forwarding(dst_pg, frame_request) - self.logger.info(f"test_negative_normal_ip{'6' if is_ipv6 else '4'}: Passed with {src_pg.name} to {dst_pg.name}") + self.logger.info( + f"test_negative_normal_ip{'6' if is_ipv6 else '4'}: Passed with {src_pg.name} to {dst_pg.name}" + ) def test_negative_decap_v4(self): """Negative test for VXLAN decap with unmatched inner DST IPv4""" @@ -385,12 +492,12 @@ def test_negative_decap_v6(self): self.remove_configured_vpp_objects_on_tear_down = False def test_negative_normal_ip4(self): - """Negative test for non-encaped IPv4 packet """ + """Negative test for non-encaped IPv4 packet""" self._test_negative_normal_ip(is_ipv6=False) self.remove_configured_vpp_objects_on_tear_down = False def test_negative_normal_ip6(self): - """Negative test for non-encaped IPv6 packet """ + """Negative test for non-encaped IPv6 packet""" self._test_negative_normal_ip(is_ipv6=True) self.remove_configured_vpp_objects_on_tear_down = False @@ -399,12 +506,16 @@ def test_negative_vxlan_no_decap(self): in_pg = self.pg_interfaces[NUM_EGRESS_PGS] out_pg = self.pg_interfaces[NUM_EGRESS_PGS + 1] frame_request = self.create_frame_request( - "00:00:00:00:00:01", "00:00:00:00:00:02", - "1.2.3.4", "4.3.2.1" + "00:00:00:00:00:01", "00:00:00:00:00:02", "1.2.3.4", "4.3.2.1" ) encapsulated_pkt = self.encapsulate( - frame_request, self.single_tunnel_vni + 1, in_pg.remote_mac, in_pg.local_mac, in_pg.remote_ip4, out_pg.remote_ip4 + frame_request, + self.single_tunnel_vni + 1, + in_pg.remote_mac, + in_pg.local_mac, + in_pg.remote_ip4, + out_pg.remote_ip4, ) in_pg.add_stream([encapsulated_pkt]) @@ -412,12 +523,14 @@ def test_negative_vxlan_no_decap(self): self.pg_start() self.verify_packet_forwarding(out_pg, encapsulated_pkt) - self.logger.info(f"test_negative_vxlan_no_decap: Passed from {in_pg.name} to {out_pg.name}") + self.logger.info( + f"test_negative_vxlan_no_decap: Passed from {in_pg.name} to {out_pg.name}" + ) self.remove_configured_vpp_objects_on_tear_down = False -TestVxlan.add_dynamic_tests() +TestTuntermAcl.add_dynamic_tests() if __name__ == "__main__": unittest.main(testRunner=VppTestRunner) diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.c index 5c6927f..b7911ed 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.c +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.c @@ -41,207 +41,224 @@ #include "tunterm_acl_redirect.h" VLIB_PLUGIN_REGISTER () = { - .version = TUNTERM_ACL_PLUGIN_BUILD_VER, - .description = "Tunnel Terminated ACL Plugin", + .version = TUNTERM_ACL_PLUGIN_BUILD_VER, + .description = "Tunnel Terminated ACL Plugin", }; tunterm_acl_main_t tunterm_acl_main; -static int update_classify_table_and_sessions (bool is_ipv6, u32 count, vl_api_tunterm_acl_rule_t rules[], u32 * tunterm_acl_index) +static int +update_classify_table_and_sessions (bool is_ipv6, u32 count, + vl_api_tunterm_acl_rule_t rules[], + u32 *tunterm_acl_index) { - tunterm_acl_main_t * sm = &tunterm_acl_main; + tunterm_acl_main_t *sm = &tunterm_acl_main; vnet_classify_main_t *cm = &vnet_classify_main; u32 table_index = *tunterm_acl_index; int rv = 0; -#define CLASSIFY_TABLE_VECTOR_SIZE 16 - u8 mask[7*CLASSIFY_TABLE_VECTOR_SIZE]; - clib_memset (mask, 0, sizeof (mask)); - /* Table Configs (TBD optimal values for expected use-cases) */ + // Classifier Table Configs +#define CLASSIFY_TABLE_VECTOR_SIZE 16 + u32 vectors_matched = 1; + u32 bytes_matched = + is_ipv6 ? sizeof (ip6_address_t) : sizeof (ip4_address_t); u32 nbuckets = 32; u32 memory_size = 2 << 22; - u32 skip = 5; - u32 match = 1; - - /* Create the table if it's an add operation */ - if (table_index == ~0) { - - /* Mask inner DIP */ - if (is_ipv6) { - match = 2; - for (int i = 8; i <= 23; i++) { - mask[i] = 0xff; - } - } else { - match = 1; - for (int i = 0; i <= 3; i++) { - mask[i] = 0xff; - } - } - rv = vnet_classify_add_del_table (cm, mask, nbuckets, memory_size, - skip, match, ~0 /* next_table_index */, - ~0 /* miss_next_index */, &table_index, - 0 /* current_data_flag */, 0 /* current_data_offset */, - 1 /* is_add */, 0 /* del_chain */); + u8 mask[vectors_matched * CLASSIFY_TABLE_VECTOR_SIZE]; + clib_memset (mask, 0, sizeof (mask)); - if (rv != 0) { - clib_warning ("vnet_classify_add_del_table failed"); - return rv; + /* Create the table if it's an add operation */ + if (table_index == ~0) + { + + /* Mask inner DIP */ + for (int i = 0; i < (bytes_matched); i++) + { + mask[i] = 0xff; + } + + rv = vnet_classify_add_del_table ( + cm, mask, nbuckets, memory_size, 0 /* skip */, vectors_matched, + ~0 /* next_table_index */, ~0 /* miss_next_index */, &table_index, + 0 /* current_data_flag */, 0 /* current_data_offset */, 1 /* is_add */, + 0 /* del_chain */); + + if (rv != 0) + { + clib_warning ("vnet_classify_add_del_table failed"); + return rv; + } + + *tunterm_acl_index = table_index; + + vec_validate_init_empty (sm->classify_table_index_is_v6, table_index, 0); + sm->classify_table_index_is_v6[table_index] = is_ipv6; } - *tunterm_acl_index = table_index; - - vec_validate_init_empty (sm->classify_table_index_is_v6, table_index, 0); - sm->classify_table_index_is_v6[table_index] = is_ipv6; - } - /* Make sure table AF is not being changed in the replace case */ - if (count > 0 && sm->classify_table_index_is_v6[table_index] != is_ipv6) { - clib_warning ("Table AF mismatch"); - return VNET_API_ERROR_INVALID_VALUE_2; - } + if (count > 0 && sm->classify_table_index_is_v6[table_index] != is_ipv6) + { + clib_warning ("Table AF mismatch"); + return VNET_API_ERROR_INVALID_VALUE_2; + } /* First clear anything already in the table */ rv = tunterm_acl_redirect_clear (vlib_get_main (), table_index); - if (rv != 0) { - clib_warning ("tunterm_acl_redirect_clear failed"); - return rv; - } - - /* Now add the new stuff */ - for (int i = 0; i < count; i++) { - /* (1) Process Route */ - fib_route_path_t *paths_ = 0; - u8 n_paths = 1; //rules[i].n_paths; (TODO: support multiple paths) - if (n_paths <= 0) { - return VNET_API_ERROR_NO_PATHS_IN_ROUTE; + if (rv != 0) + { + clib_warning ("tunterm_acl_redirect_clear failed"); + return rv; } - for (int j = 0; j < n_paths; j++) { - fib_route_path_t path; - clib_memset(&path, 0, sizeof(path)); - - if ((rv = fib_api_path_decode (&rules[i].path, &path))) { - vec_free (paths_); - return rv; - } - vec_add1 (paths_, path); - } + /* Now add the new stuff */ + for (int i = 0; i < count; i++) + { + /* (1) Process Route */ + fib_route_path_t *paths_ = 0; + u8 n_paths = 1; // rules[i].n_paths; (TODO: support multiple paths) + if (n_paths <= 0) + { + return VNET_API_ERROR_NO_PATHS_IN_ROUTE; + } + + for (int j = 0; j < n_paths; j++) + { + fib_route_path_t path; + clib_memset (&path, 0, sizeof (path)); + + if ((rv = fib_api_path_decode (&rules[i].path, &path))) + { + vec_free (paths_); + return rv; + } + vec_add1 (paths_, path); + } + + /* (2) Process DST IP */ + if (is_ipv6 != rules[i].dst.af) + { + vec_free (paths_); + return VNET_API_ERROR_INVALID_VALUE_3; + } + + ip46_address_t dst; + ip_address_decode (&rules[i].dst, &dst); + + u8 *match_vec = 0; + clib_memset (mask, 0, sizeof (mask)); + + for (int i = 0; i < bytes_matched; i++) + { + mask[i] = is_ipv6 ? dst.ip6.as_u8[i] : dst.ip4.as_u8[i]; + } + + /* (3) Add Session */ + vec_validate (match_vec, sizeof (mask) - 1); + clib_memcpy (match_vec, mask, sizeof (mask)); + + rv = tunterm_acl_redirect_add (vlib_get_main (), table_index, + 0 /* opaque_index */, + is_ipv6 ? DPO_PROTO_IP6 : DPO_PROTO_IP4, + match_vec /* match */, paths_); - /* (2) Process DST IP */ - if (is_ipv6 != rules[i].dst.af) { vec_free (paths_); - return VNET_API_ERROR_INVALID_VALUE_3; - } - - ip46_address_t dst; - ip_address_decode (&rules[i].dst, &dst); - - u8 *match_vec = 0; - clib_memset (mask, 0, sizeof (mask)); - - if (is_ipv6) { - for (int i = 0; i < 16; i++) { - mask[8 + i + skip * CLASSIFY_TABLE_VECTOR_SIZE] = dst.ip6.as_u8[i]; - } - } else { - for (int i = 0; i < 4; i++) { - mask[i + skip * CLASSIFY_TABLE_VECTOR_SIZE] = dst.ip4.data[i]; - } - } - - /* (3) Add Session */ - vec_validate (match_vec, sizeof (mask) - 1); - clib_memcpy (match_vec, mask, sizeof (mask)); + vec_free (match_vec); - rv = tunterm_acl_redirect_add (vlib_get_main (), table_index, - 0 /* opaque_index */, is_ipv6 ? DPO_PROTO_IP6 : DPO_PROTO_IP4, - match_vec /* match */, paths_); - - vec_free(paths_); - vec_free(match_vec); - - if (rv != 0) { - clib_warning ("tunterm_acl_redirect_add failed"); - return rv; + if (rv != 0) + { + clib_warning ("tunterm_acl_redirect_add failed"); + return rv; + } } - } return rv; } static int -verify_message_len (void *mp, u64 expected_len, char *where) +verify_message_len (void *mp, u64 expected_len, char *where) { u32 supplied_len = vl_msg_api_get_msg_length (mp); - if (supplied_len < expected_len) { - clib_warning ("%s: Supplied message length %d is less than expected %d", - where, supplied_len, expected_len); - return 0; - } else { - return 1; - } + if (supplied_len < expected_len) + { + clib_warning ("%s: Supplied message length %d is less than expected %d", + where, supplied_len, expected_len); + return 0; + } + else + { + return 1; + } } static void -vl_api_tunterm_acl_add_replace_t_handler (vl_api_tunterm_acl_add_replace_t * mp) +vl_api_tunterm_acl_add_replace_t_handler (vl_api_tunterm_acl_add_replace_t *mp) { vl_api_tunterm_acl_add_replace_reply_t *rmp; - tunterm_acl_main_t * sm = &tunterm_acl_main; + tunterm_acl_main_t *sm = &tunterm_acl_main; int rv = -1; u32 tunterm_acl_index = ntohl (mp->tunterm_acl_index); u32 acl_count = ntohl (mp->count); u64 expected_len = sizeof (*mp) + acl_count * sizeof (mp->r[0]); - if (verify_message_len (mp, expected_len, "tunterm_acl_add_replace")) { - rv = update_classify_table_and_sessions(mp->is_ipv6, acl_count, mp->r, &tunterm_acl_index); - } else { - rv = VNET_API_ERROR_INVALID_VALUE; - } + if (verify_message_len (mp, expected_len, "tunterm_acl_add_replace")) + { + rv = update_classify_table_and_sessions (mp->is_ipv6, acl_count, mp->r, + &tunterm_acl_index); + } + else + { + rv = VNET_API_ERROR_INVALID_VALUE; + } - REPLY_MACRO2(VL_API_TUNTERM_ACL_ADD_REPLACE_REPLY, - ({ - rmp->tunterm_acl_index = htonl(tunterm_acl_index); - })); + REPLY_MACRO2 (VL_API_TUNTERM_ACL_ADD_REPLACE_REPLY, + ({ rmp->tunterm_acl_index = htonl (tunterm_acl_index); })); } static void -vl_api_tunterm_acl_del_t_handler (vl_api_tunterm_acl_del_t * mp) +vl_api_tunterm_acl_del_t_handler (vl_api_tunterm_acl_del_t *mp) { - tunterm_acl_main_t * sm = &tunterm_acl_main; + tunterm_acl_main_t *sm = &tunterm_acl_main; vnet_classify_main_t *cm = &vnet_classify_main; vl_api_tunterm_acl_del_reply_t *rmp; u32 tunterm_acl_index = ntohl (mp->tunterm_acl_index); int rv = 0; - if (tunterm_acl_index == ~0) { - rv = VNET_API_ERROR_INVALID_VALUE; - goto exit; - } + if (tunterm_acl_index == ~0) + { + rv = VNET_API_ERROR_INVALID_VALUE; + goto exit; + } /* If tunterm index still being used, reject delete */ - for (int i = 0; i < vec_len(sm->classify_table_index_by_sw_if_index_v4); i++) { - if (sm->classify_table_index_by_sw_if_index_v4[i] == tunterm_acl_index) { - rv = VNET_API_ERROR_RSRC_IN_USE; - goto exit; + for (int i = 0; i < vec_len (sm->classify_table_index_by_sw_if_index_v4); + i++) + { + if (sm->classify_table_index_by_sw_if_index_v4[i] == tunterm_acl_index) + { + rv = VNET_API_ERROR_RSRC_IN_USE; + goto exit; + } } - } - for (int i = 0; i < vec_len(sm->classify_table_index_by_sw_if_index_v6); i++) { - if (sm->classify_table_index_by_sw_if_index_v6[i] == tunterm_acl_index) { - rv = VNET_API_ERROR_RSRC_IN_USE; - goto exit; + for (int i = 0; i < vec_len (sm->classify_table_index_by_sw_if_index_v6); + i++) + { + if (sm->classify_table_index_by_sw_if_index_v6[i] == tunterm_acl_index) + { + rv = VNET_API_ERROR_RSRC_IN_USE; + goto exit; + } } - } /* First clear all the redirect sessions on that table */ rv = tunterm_acl_redirect_clear (vlib_get_main (), tunterm_acl_index); - if (rv != 0) { - clib_warning ("tunterm_acl_redirect_clear failed"); - goto exit; - } + if (rv != 0) + { + clib_warning ("tunterm_acl_redirect_clear failed"); + goto exit; + } /* Then delete the classify table */ vnet_classify_delete_table_index (cm, tunterm_acl_index, 1 /* del_chain */); @@ -253,71 +270,98 @@ vl_api_tunterm_acl_del_t_handler (vl_api_tunterm_acl_del_t * mp) } static void -vl_api_tunterm_acl_interface_add_del_t_handler (vl_api_tunterm_acl_interface_add_del_t * mp) +vl_api_tunterm_acl_interface_add_del_t_handler ( + vl_api_tunterm_acl_interface_add_del_t *mp) { - tunterm_acl_main_t * sm = &tunterm_acl_main; + tunterm_acl_main_t *sm = &tunterm_acl_main; vnet_interface_main_t *im = &sm->vnet_main->interface_main; u32 sw_if_index = ntohl (mp->sw_if_index); u32 tunterm_acl_index = ntohl (mp->tunterm_acl_index); vl_api_tunterm_acl_interface_add_del_reply_t *rmp; int rv = 0; - if (tunterm_acl_index == ~0) { - rv = VNET_API_ERROR_INVALID_VALUE; - goto exit; - } - - if (pool_is_free_index (im->sw_interfaces, sw_if_index)) { - rv = VNET_API_ERROR_INVALID_SW_IF_INDEX; - goto exit; - } - - /* make sure both are init as you can get v6 packets while only v4 acl installed */ - vec_validate_init_empty (sm->classify_table_index_by_sw_if_index_v6, sw_if_index, ~0); - vec_validate_init_empty (sm->classify_table_index_by_sw_if_index_v4, sw_if_index, ~0); - - bool is_ipv6 = sm->classify_table_index_is_v6[tunterm_acl_index]; - - if (mp->is_add) { - - /* First setup forwarding data, then enable */ - if (is_ipv6) { - sm->classify_table_index_by_sw_if_index_v6[sw_if_index] = tunterm_acl_index; - } else { - sm->classify_table_index_by_sw_if_index_v4[sw_if_index] = tunterm_acl_index; - } - - if (!(vnet_feature_is_enabled("ip4-unicast", "tunterm-ip4-vxlan-bypass", sw_if_index))) { - rv = vnet_feature_enable_disable ("ip4-unicast", "tunterm-ip4-vxlan-bypass", - sw_if_index, mp->is_add, 0, 0); + if (tunterm_acl_index == ~0) + { + rv = VNET_API_ERROR_INVALID_VALUE; + goto exit; } - } else { - u32 tunterm_acl_index_v4 = sm->classify_table_index_by_sw_if_index_v4[sw_if_index]; - u32 tunterm_acl_index_v6 = sm->classify_table_index_by_sw_if_index_v6[sw_if_index]; - if (tunterm_acl_index != tunterm_acl_index_v4 && tunterm_acl_index != tunterm_acl_index_v6) { - /* tunterm being removed is not attached */ - rv = VNET_API_ERROR_INVALID_VALUE_2; + if (pool_is_free_index (im->sw_interfaces, sw_if_index)) + { + rv = VNET_API_ERROR_INVALID_SW_IF_INDEX; goto exit; } - /* if last tunterm being removed from intf, then disable */ - if (tunterm_acl_index_v4 == ~0 || tunterm_acl_index_v6 == ~0) { - rv = vnet_feature_enable_disable ("ip4-unicast", "tunterm-ip4-vxlan-bypass", - sw_if_index, mp->is_add, 0, 0); + /* make sure both are init as you can get v6 packets while only v4 acl + * installed */ + vec_validate_init_empty (sm->classify_table_index_by_sw_if_index_v6, + sw_if_index, ~0); + vec_validate_init_empty (sm->classify_table_index_by_sw_if_index_v4, + sw_if_index, ~0); - if (rv != 0) { - goto exit; - } - } + bool is_ipv6 = sm->classify_table_index_is_v6[tunterm_acl_index]; - /* finally, remove forwarding data */ - if (is_ipv6) { - sm->classify_table_index_by_sw_if_index_v6[sw_if_index] = ~0; - } else { - sm->classify_table_index_by_sw_if_index_v4[sw_if_index] = ~0; + if (mp->is_add) + { + + /* First setup forwarding data, then enable */ + if (is_ipv6) + { + sm->classify_table_index_by_sw_if_index_v6[sw_if_index] = + tunterm_acl_index; + } + else + { + sm->classify_table_index_by_sw_if_index_v4[sw_if_index] = + tunterm_acl_index; + } + + if (!(vnet_feature_is_enabled ("ip4-unicast", "tunterm-ip4-vxlan-bypass", + sw_if_index))) + { + rv = vnet_feature_enable_disable ("ip4-unicast", + "tunterm-ip4-vxlan-bypass", + sw_if_index, mp->is_add, 0, 0); + } + } + else + { + u32 tunterm_acl_index_v4 = + sm->classify_table_index_by_sw_if_index_v4[sw_if_index]; + u32 tunterm_acl_index_v6 = + sm->classify_table_index_by_sw_if_index_v6[sw_if_index]; + + if (tunterm_acl_index != tunterm_acl_index_v4 && + tunterm_acl_index != tunterm_acl_index_v6) + { + /* tunterm being removed is not attached */ + rv = VNET_API_ERROR_INVALID_VALUE_2; + goto exit; + } + + /* if last tunterm being removed from intf, then disable */ + if (tunterm_acl_index_v4 == ~0 || tunterm_acl_index_v6 == ~0) + { + rv = vnet_feature_enable_disable ("ip4-unicast", + "tunterm-ip4-vxlan-bypass", + sw_if_index, mp->is_add, 0, 0); + + if (rv != 0) + { + goto exit; + } + } + + /* finally, remove forwarding data */ + if (is_ipv6) + { + sm->classify_table_index_by_sw_if_index_v6[sw_if_index] = ~0; + } + else + { + sm->classify_table_index_by_sw_if_index_v4[sw_if_index] = ~0; + } } - } exit: REPLY_MACRO (VL_API_TUNTERM_ACL_INTERFACE_ADD_DEL_REPLY); @@ -326,15 +370,16 @@ vl_api_tunterm_acl_interface_add_del_t_handler (vl_api_tunterm_acl_interface_add /* API definitions */ #include -static clib_error_t * tunterm_acl_init (vlib_main_t * vm) +static clib_error_t * +tunterm_acl_init (vlib_main_t *vm) { - tunterm_acl_main_t * sm = &tunterm_acl_main; + tunterm_acl_main_t *sm = &tunterm_acl_main; - sm->vnet_main = vnet_get_main (); + sm->vnet_main = vnet_get_main (); sm->msg_id_base = setup_message_id_table (); - return tunterm_acl_redirect_init(vm); + return tunterm_acl_redirect_init (vm); } VLIB_INIT_FUNCTION (tunterm_acl_init); diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.h b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.h index b9192e3..06b5bda 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.h +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_api.h @@ -23,16 +23,17 @@ #include #include -typedef struct { - /* API message ID base */ - u16 msg_id_base; +typedef struct +{ + /* API message ID base */ + u16 msg_id_base; - /* convenience */ - vnet_main_t * vnet_main; + /* convenience */ + vnet_main_t *vnet_main; - u32 *classify_table_index_by_sw_if_index_v4; - u32 *classify_table_index_by_sw_if_index_v6; - bool *classify_table_index_is_v6; + u32 *classify_table_index_by_sw_if_index_v4; + u32 *classify_table_index_by_sw_if_index_v6; + bool *classify_table_index_is_v6; } tunterm_acl_main_t; extern tunterm_acl_main_t tunterm_acl_main; @@ -42,4 +43,3 @@ extern vlib_node_registration_t tunterm_acl_node; #define TUNTERM_ACL_PLUGIN_BUILD_VER "1.0" #endif /* __included_tunterm_acl_api_h__ */ - diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_cli.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_cli.c index 36c3528..e34c48e 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_cli.c +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_cli.c @@ -30,47 +30,54 @@ #include #include - static clib_error_t * -show_tunterm_acl_interfaces_command_fn (vlib_main_t * vm, - unformat_input_t * input, - vlib_cli_command_t * cmd) +show_tunterm_acl_interfaces_command_fn (vlib_main_t *vm, + unformat_input_t *input, + vlib_cli_command_t *cmd) { - tunterm_acl_main_t * sm = &tunterm_acl_main; - vnet_main_t * vnm = vnet_get_main (); - vnet_interface_main_t * im = &vnm->interface_main; - vlib_cli_output (vm, "Interface\tIndex\tIPv4 Tunterm Index\tIPv6 Tunterm Index"); + tunterm_acl_main_t *sm = &tunterm_acl_main; + vnet_main_t *vnm = vnet_get_main (); + vnet_interface_main_t *im = &vnm->interface_main; + vlib_cli_output (vm, + "Interface\tIndex\tIPv4 Tunterm Index\tIPv6 Tunterm Index"); /* Iterate over all interfaces */ - vnet_sw_interface_t * swif; - pool_foreach (swif, im->sw_interfaces) { - u32 sw_if_index = swif->sw_if_index; + vnet_sw_interface_t *swif; + pool_foreach (swif, im->sw_interfaces) + { + u32 sw_if_index = swif->sw_if_index; - /* Check if the interface has "tunterm-ip4-vxlan-bypass" enabled */ - if (vnet_feature_is_enabled("ip4-unicast", "tunterm-ip4-vxlan-bypass", sw_if_index)) { - u32 tunterm_acl_index_v4 = ~0; - u32 tunterm_acl_index_v6 = ~0; + /* Check if the interface has "tunterm-ip4-vxlan-bypass" enabled */ + if (vnet_feature_is_enabled ("ip4-unicast", "tunterm-ip4-vxlan-bypass", + sw_if_index)) + { + u32 tunterm_acl_index_v4 = ~0; + u32 tunterm_acl_index_v6 = ~0; - if (sw_if_index < vec_len(sm->classify_table_index_by_sw_if_index_v4)) { - tunterm_acl_index_v4 = sm->classify_table_index_by_sw_if_index_v4[sw_if_index]; - } - if (sw_if_index < vec_len(sm->classify_table_index_by_sw_if_index_v6)) { - tunterm_acl_index_v6 = sm->classify_table_index_by_sw_if_index_v6[sw_if_index]; - } + if (sw_if_index < + vec_len (sm->classify_table_index_by_sw_if_index_v4)) + { + tunterm_acl_index_v4 = + sm->classify_table_index_by_sw_if_index_v4[sw_if_index]; + } + if (sw_if_index < + vec_len (sm->classify_table_index_by_sw_if_index_v6)) + { + tunterm_acl_index_v6 = + sm->classify_table_index_by_sw_if_index_v6[sw_if_index]; + } - vlib_cli_output (vm, "%U\t%u\t%u\t%u", - format_vnet_sw_if_index_name, vnm, sw_if_index, - sw_if_index, tunterm_acl_index_v4, tunterm_acl_index_v6); + vlib_cli_output (vm, "%U\t%u\t%u\t%u", format_vnet_sw_if_index_name, + vnm, sw_if_index, sw_if_index, tunterm_acl_index_v4, + tunterm_acl_index_v6); + } } - } return 0; } -/* *INDENT-OFF* */ VLIB_CLI_COMMAND (show_tunterm_acl_interfaces_command, static) = { .path = "show tunterm interfaces", .short_help = "show tunterm interfaces", .function = show_tunterm_acl_interfaces_command_fn, }; -/* *INDENT-ON* */ diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_decap.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_decap.c index 51b8c9f..9e001b5 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_decap.c +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_decap.c @@ -21,7 +21,7 @@ #include -vxlan_main_t* tunterm_acl_vxlan_main; +vxlan_main_t *tunterm_acl_vxlan_main; typedef enum { @@ -31,7 +31,6 @@ typedef enum IP_VXLAN_BYPASS_N_NEXT, } tunterm_acl_ip_vxlan_bypass_next_t; - typedef vxlan4_tunnel_key_t last_tunnel_cache4; static const vxlan_decap_info_t decap_not_found = { @@ -40,16 +39,16 @@ static const vxlan_decap_info_t decap_not_found = { .error = VXLAN_ERROR_NO_SUCH_TUNNEL }; -static const vxlan_decap_info_t decap_bad_flags = { - .sw_if_index = ~0, - .next_index = VXLAN_INPUT_NEXT_DROP, - .error = VXLAN_ERROR_BAD_FLAGS -}; +static const vxlan_decap_info_t decap_bad_flags = { .sw_if_index = ~0, + .next_index = + VXLAN_INPUT_NEXT_DROP, + .error = + VXLAN_ERROR_BAD_FLAGS }; vxlan_decap_info_t -tunterm_acl_vxlan4_find_tunnel (vxlan_main_t * vxm, last_tunnel_cache4 * cache, - u32 fib_index, ip4_header_t * ip4_0, - vxlan_header_t * vxlan0, u32 * stats_sw_if_index) +tunterm_acl_vxlan4_find_tunnel (vxlan_main_t *vxm, last_tunnel_cache4 *cache, + u32 fib_index, ip4_header_t *ip4_0, + vxlan_header_t *vxlan0, u32 *stats_sw_if_index) { if (PREDICT_FALSE (vxlan0->flags != VXLAN_FLAGS_I)) return decap_bad_flags; @@ -65,11 +64,11 @@ tunterm_acl_vxlan4_find_tunnel (vxlan_main_t * vxm, last_tunnel_cache4 * cache, vxlan0->vni_reserved, }; - if (PREDICT_TRUE - (key4.key[0] == cache->key[0] && key4.key[1] == cache->key[1])) + if (PREDICT_TRUE (key4.key[0] == cache->key[0] && + key4.key[1] == cache->key[1])) { /* cache hit */ - vxlan_decap_info_t di = {.as_u64 = cache->value }; + vxlan_decap_info_t di = { .as_u64 = cache->value }; *stats_sw_if_index = di.sw_if_index; return di; } @@ -78,7 +77,7 @@ tunterm_acl_vxlan4_find_tunnel (vxlan_main_t * vxm, last_tunnel_cache4 * cache, if (PREDICT_TRUE (rv == 0)) { *cache = key4; - vxlan_decap_info_t di = {.as_u64 = key4.value }; + vxlan_decap_info_t di = { .as_u64 = key4.value }; *stats_sw_if_index = di.sw_if_index; return di; } @@ -94,7 +93,7 @@ tunterm_acl_vxlan4_find_tunnel (vxlan_main_t * vxm, last_tunnel_cache4 * cache, return decap_not_found; /* search for unicast tunnel using the mcast tunnel local(src) ip */ - vxlan_decap_info_t mdi = {.as_u64 = key4.value }; + vxlan_decap_info_t mdi = { .as_u64 = key4.value }; key4.key[0] = ((u64) mdi.local_ip.as_u32 << 32) | src; rv = clib_bihash_search_inline_16_8 (&vxm->vxlan4_tunnel_by_key, &key4); if (PREDICT_FALSE (rv != 0)) @@ -102,16 +101,16 @@ tunterm_acl_vxlan4_find_tunnel (vxlan_main_t * vxm, last_tunnel_cache4 * cache, /* mcast traffic does not update the cache */ *stats_sw_if_index = mdi.sw_if_index; - vxlan_decap_info_t di = {.as_u64 = key4.value }; + vxlan_decap_info_t di = { .as_u64 = key4.value }; return di; } typedef vxlan6_tunnel_key_t last_tunnel_cache6; always_inline vxlan_decap_info_t -tunterm_acl_vxlan6_find_tunnel (vxlan_main_t * vxm, last_tunnel_cache6 * cache, - u32 fib_index, ip6_header_t * ip6_0, - vxlan_header_t * vxlan0, u32 * stats_sw_if_index) +tunterm_acl_vxlan6_find_tunnel (vxlan_main_t *vxm, last_tunnel_cache6 *cache, + u32 fib_index, ip6_header_t *ip6_0, + vxlan_header_t *vxlan0, u32 *stats_sw_if_index) { if (PREDICT_FALSE (vxlan0->flags != VXLAN_FLAGS_I)) return decap_bad_flags; @@ -126,8 +125,7 @@ tunterm_acl_vxlan6_find_tunnel (vxlan_main_t * vxm, last_tunnel_cache6 * cache, vxlan0->vni_reserved, }; - if (PREDICT_FALSE - (clib_bihash_key_compare_24_8 (key6.key, cache->key) == 0)) + if (PREDICT_FALSE (clib_bihash_key_compare_24_8 (key6.key, cache->key) == 0)) { int rv = clib_bihash_search_inline_24_8 (&vxm->vxlan6_tunnel_by_key, &key6); @@ -167,19 +165,18 @@ tunterm_acl_vxlan6_find_tunnel (vxlan_main_t * vxm, last_tunnel_cache6 * cache, } always_inline uword -tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, - vlib_node_runtime_t * node, - vlib_frame_t * frame, u32 is_ip4) +tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t *vm, vlib_node_runtime_t *node, + vlib_frame_t *frame, u32 is_ip4) { vxlan_main_t *vxm = tunterm_acl_vxlan_main; u32 *from, *to_next, n_left_from, n_left_to_next, next_index; vlib_node_runtime_t *error_node = vlib_node_get_runtime (vm, ip4_input_node.index); - vtep4_key_t last_vtep4; /* last IPv4 address / fib index - matching a local VTEP address */ - vtep6_key_t last_vtep6; /* last IPv6 address / fib index - matching a local VTEP address */ + vtep4_key_t last_vtep4; /* last IPv4 address / fib index + matching a local VTEP address */ + vtep6_key_t last_vtep6; /* last IPv6 address / fib index + matching a local VTEP address */ vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b = bufs; last_tunnel_cache4 last4; @@ -260,7 +257,7 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, if (is_ip4) { /* Treat IP frag packets as "experimental" protocol for now - until support of IP frag reassembly is implemented */ + until support of IP frag reassembly is implemented */ proto0 = ip4_is_fragment (ip40) ? 0xfe : ip40->protocol; proto1 = ip4_is_fragment (ip41) ? 0xfe : ip41->protocol; } @@ -272,7 +269,7 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, /* Process packet 0 */ if (proto0 != IP_PROTOCOL_UDP) - goto exit0; /* not UDP packet */ + goto exit0; /* not UDP packet */ if (is_ip4) udp0 = ip4_next_header (ip40); @@ -284,9 +281,10 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, sizeof (ip4_header_t); vxlan_decap_info_t di0 = - is_ip4 ? - tunterm_acl_vxlan4_find_tunnel (vxm, &last4, fi0, ip40, vxlan0, &stats_if0) : - tunterm_acl_vxlan6_find_tunnel (vxm, &last6, fi0, ip60, vxlan0, &stats_if0); + is_ip4 ? tunterm_acl_vxlan4_find_tunnel (vxm, &last4, fi0, ip40, + vxlan0, &stats_if0) : + tunterm_acl_vxlan6_find_tunnel (vxm, &last6, fi0, ip60, + vxlan0, &stats_if0); if (PREDICT_FALSE (di0.sw_if_index == ~0)) goto exit0; /* unknown interface */ @@ -300,18 +298,19 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, #else if (!vtep4_check (&vxm->vtep_table, b0, ip40, &last_vtep4)) #endif - goto exit0; /* no local VTEP for VXLAN packet */ + goto exit0; /* no local VTEP for VXLAN packet */ } else { if (!vtep6_check (&vxm->vtep_table, b0, ip60, &last_vtep6)) - goto exit0; /* no local VTEP for VXLAN packet */ + goto exit0; /* no local VTEP for VXLAN packet */ } flags0 = b0->flags; good_udp0 = (flags0 & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0; - /* Don't verify UDP checksum for packets with explicit zero checksum. */ + /* Don't verify UDP checksum for packets with explicit zero checksum. + */ good_udp0 |= udp0->checksum == 0; /* Verify UDP length */ @@ -343,24 +342,22 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, error0 = (len_diff0 >= 0) ? error0 : IP6_ERROR_UDP_LENGTH; } - next0 = error0 ? - IP_VXLAN_BYPASS_NEXT_DROP : IP_VXLAN_BYPASS_NEXT_TUNTERM; + next0 = + error0 ? IP_VXLAN_BYPASS_NEXT_DROP : IP_VXLAN_BYPASS_NEXT_TUNTERM; b0->error = error0 ? error_node->errors[error0] : 0; /* vxlan-input node expect current at VXLAN header */ if (is_ip4) - vlib_buffer_advance (b0, - sizeof (ip4_header_t) + - sizeof (udp_header_t)); + vlib_buffer_advance (b0, sizeof (ip4_header_t) + + sizeof (udp_header_t)); else - vlib_buffer_advance (b0, - sizeof (ip6_header_t) + - sizeof (udp_header_t)); + vlib_buffer_advance (b0, sizeof (ip6_header_t) + + sizeof (udp_header_t)); exit0: /* Process packet 1 */ if (proto1 != IP_PROTOCOL_UDP) - goto exit1; /* not UDP packet */ + goto exit1; /* not UDP packet */ if (is_ip4) udp1 = ip4_next_header (ip41); @@ -372,9 +369,10 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, sizeof (ip4_header_t); vxlan_decap_info_t di1 = - is_ip4 ? - tunterm_acl_vxlan4_find_tunnel (vxm, &last4, fi1, ip41, vxlan1, &stats_if1) : - tunterm_acl_vxlan6_find_tunnel (vxm, &last6, fi1, ip61, vxlan1, &stats_if1); + is_ip4 ? tunterm_acl_vxlan4_find_tunnel (vxm, &last4, fi1, ip41, + vxlan1, &stats_if1) : + tunterm_acl_vxlan6_find_tunnel (vxm, &last6, fi1, ip61, + vxlan1, &stats_if1); if (PREDICT_FALSE (di1.sw_if_index == ~0)) goto exit1; /* unknown interface */ @@ -388,18 +386,19 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, #else if (!vtep4_check (&vxm->vtep_table, b1, ip41, &last_vtep4)) #endif - goto exit1; /* no local VTEP for VXLAN packet */ + goto exit1; /* no local VTEP for VXLAN packet */ } else { if (!vtep6_check (&vxm->vtep_table, b1, ip61, &last_vtep6)) - goto exit1; /* no local VTEP for VXLAN packet */ + goto exit1; /* no local VTEP for VXLAN packet */ } flags1 = b1->flags; good_udp1 = (flags1 & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0; - /* Don't verify UDP checksum for packets with explicit zero checksum. */ + /* Don't verify UDP checksum for packets with explicit zero checksum. + */ good_udp1 |= udp1->checksum == 0; /* Verify UDP length */ @@ -431,24 +430,22 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, error1 = (len_diff1 >= 0) ? error1 : IP6_ERROR_UDP_LENGTH; } - next1 = error1 ? - IP_VXLAN_BYPASS_NEXT_DROP : IP_VXLAN_BYPASS_NEXT_TUNTERM; + next1 = + error1 ? IP_VXLAN_BYPASS_NEXT_DROP : IP_VXLAN_BYPASS_NEXT_TUNTERM; b1->error = error1 ? error_node->errors[error1] : 0; /* vxlan-input node expect current at VXLAN header */ if (is_ip4) - vlib_buffer_advance (b1, - sizeof (ip4_header_t) + - sizeof (udp_header_t)); + vlib_buffer_advance (b1, sizeof (ip4_header_t) + + sizeof (udp_header_t)); else - vlib_buffer_advance (b1, - sizeof (ip6_header_t) + - sizeof (udp_header_t)); + vlib_buffer_advance (b1, sizeof (ip6_header_t) + + sizeof (udp_header_t)); exit1: - vlib_validate_buffer_enqueue_x2 (vm, node, next_index, - to_next, n_left_to_next, - bi0, bi1, next0, next1); + vlib_validate_buffer_enqueue_x2 (vm, node, next_index, to_next, + n_left_to_next, bi0, bi1, next0, + next1); } while (n_left_from > 0 && n_left_to_next > 0) @@ -487,7 +484,7 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, proto0 = ip60->protocol; if (proto0 != IP_PROTOCOL_UDP) - goto exit; /* not UDP packet */ + goto exit; /* not UDP packet */ if (is_ip4) udp0 = ip4_next_header (ip40); @@ -499,9 +496,10 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, sizeof (ip4_header_t); vxlan_decap_info_t di0 = - is_ip4 ? - tunterm_acl_vxlan4_find_tunnel (vxm, &last4, fi0, ip40, vxlan0, &stats_if0) : - tunterm_acl_vxlan6_find_tunnel (vxm, &last6, fi0, ip60, vxlan0, &stats_if0); + is_ip4 ? tunterm_acl_vxlan4_find_tunnel (vxm, &last4, fi0, ip40, + vxlan0, &stats_if0) : + tunterm_acl_vxlan6_find_tunnel (vxm, &last6, fi0, ip60, + vxlan0, &stats_if0); if (PREDICT_FALSE (di0.sw_if_index == ~0)) goto exit; /* unknown interface */ @@ -515,18 +513,19 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, #else if (!vtep4_check (&vxm->vtep_table, b0, ip40, &last_vtep4)) #endif - goto exit; /* no local VTEP for VXLAN packet */ + goto exit; /* no local VTEP for VXLAN packet */ } else { if (!vtep6_check (&vxm->vtep_table, b0, ip60, &last_vtep6)) - goto exit; /* no local VTEP for VXLAN packet */ + goto exit; /* no local VTEP for VXLAN packet */ } flags0 = b0->flags; good_udp0 = (flags0 & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0; - /* Don't verify UDP checksum for packets with explicit zero checksum. */ + /* Don't verify UDP checksum for packets with explicit zero checksum. + */ good_udp0 |= udp0->checksum == 0; /* Verify UDP length */ @@ -558,24 +557,21 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, error0 = (len_diff0 >= 0) ? error0 : IP6_ERROR_UDP_LENGTH; } - next0 = error0 ? - IP_VXLAN_BYPASS_NEXT_DROP : IP_VXLAN_BYPASS_NEXT_TUNTERM; + next0 = + error0 ? IP_VXLAN_BYPASS_NEXT_DROP : IP_VXLAN_BYPASS_NEXT_TUNTERM; b0->error = error0 ? error_node->errors[error0] : 0; /* vxlan-input node expect current at VXLAN header */ if (is_ip4) - vlib_buffer_advance (b0, - sizeof (ip4_header_t) + - sizeof (udp_header_t)); + vlib_buffer_advance (b0, sizeof (ip4_header_t) + + sizeof (udp_header_t)); else - vlib_buffer_advance (b0, - sizeof (ip6_header_t) + - sizeof (udp_header_t)); + vlib_buffer_advance (b0, sizeof (ip6_header_t) + + sizeof (udp_header_t)); exit: - vlib_validate_buffer_enqueue_x1 (vm, node, next_index, - to_next, n_left_to_next, - bi0, next0); + vlib_validate_buffer_enqueue_x1 (vm, node, next_index, to_next, + n_left_to_next, bi0, next0); } vlib_put_next_frame (vm, node, next_index, n_left_to_next); @@ -584,9 +580,8 @@ tunterm_acl_ip_vxlan_bypass_inline (vlib_main_t * vm, return frame->n_vectors; } -VLIB_NODE_FN (tunterm_acl_ip4_vxlan_bypass_node) (vlib_main_t * vm, - vlib_node_runtime_t * node, - vlib_frame_t * frame) +VLIB_NODE_FN (tunterm_acl_ip4_vxlan_bypass_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) { return tunterm_acl_ip_vxlan_bypass_inline (vm, node, frame, /* is_ip4 */ 1); } @@ -606,10 +601,11 @@ VLIB_REGISTER_NODE (tunterm_acl_ip4_vxlan_bypass_node) = }; static clib_error_t * -tunterm_acl_ip4_vxlan_bypass_init (vlib_main_t * vm) +tunterm_acl_ip4_vxlan_bypass_init (vlib_main_t *vm) { - tunterm_acl_vxlan_main = vlib_get_plugin_symbol ("vxlan_plugin.so", "vxlan_main"); + tunterm_acl_vxlan_main = + vlib_get_plugin_symbol ("vxlan_plugin.so", "vxlan_main"); if (tunterm_acl_vxlan_main == 0) return clib_error_return (0, "cannot get vxlan_main symbol"); @@ -619,8 +615,7 @@ tunterm_acl_ip4_vxlan_bypass_init (vlib_main_t * vm) VLIB_INIT_FUNCTION (tunterm_acl_ip4_vxlan_bypass_init); -VNET_FEATURE_INIT (tunterm_acl_ip4_vxlan_bypass, static) = -{ +VNET_FEATURE_INIT (tunterm_acl_ip4_vxlan_bypass, static) = { .arc_name = "ip4-unicast", .node_name = "tunterm-ip4-vxlan-bypass", .runs_before = VNET_FEATURES ("ip4-lookup"), diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_node.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_node.c index b025665..ecd6be9 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_node.c +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_node.c @@ -28,36 +28,36 @@ typedef struct /* packet trace format function */ static u8 * -format_tunterm_acl_trace (u8 * s, va_list * args) +format_tunterm_acl_trace (u8 *s, va_list *args) { CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); tunterm_acl_trace_t *t = va_arg (*args, tunterm_acl_trace_t *); - s = format (s, "TUNTERM: sw_if_index %d next %d index %d", - t->sw_if_index, t->next_index, t->index); + s = format (s, "TUNTERM: sw_if_index %d next %d index %d", t->sw_if_index, + t->next_index, t->index); return s; } extern vlib_node_registration_t tunterm_acl_node; -#define foreach_tunterm_acl_error \ -_(REDIRECTED, "Packets successfully redirected") \ -_(NO_CLASSIFY_TABLE, "No classify table found") \ -_(ACTION_NOT_SUPPORTED, "Match found, but action not supported") \ -_(UNSUPPORTED_ETHERTYPE, "Unsupported ethertype") \ -_(NO_MATCH, "No match found in classify table") \ +#define foreach_tunterm_acl_error \ + _ (REDIRECTED, "Packets successfully redirected") \ + _ (NO_CLASSIFY_TABLE, "No classify table found") \ + _ (ACTION_NOT_SUPPORTED, "Match found, but action not supported") \ + _ (UNSUPPORTED_ETHERTYPE, "Unsupported ethertype") \ + _ (NO_MATCH, "No match found in classify table") typedef enum { -#define _(sym,str) TUNTERM_ACL_ERROR_##sym, +#define _(sym, str) TUNTERM_ACL_ERROR_##sym, foreach_tunterm_acl_error #undef _ TUNTERM_ACL_N_ERROR, } tunterm_acl_error_t; static char *tunterm_acl_error_strings[] = { -#define _(sym,string) string, +#define _(sym, string) string, foreach_tunterm_acl_error #undef _ }; @@ -75,105 +75,175 @@ typedef enum #include #include -VLIB_NODE_FN(tunterm_acl_node) (vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) { - u32 n_left_from, *from, *to_next; - tunterm_acl_next_t next_index; - vnet_classify_main_t *cm = &vnet_classify_main; - u32 table_index = ~0; - u32 pkts_redirected = 0; - u32 pkts_no_classify_table = 0; - u32 pkts_action_not_supported = 0; - u32 pkts_unsupported_ethertype = 0; - u32 pkts_no_match = 0; - - from = vlib_frame_vector_args(frame); - n_left_from = frame->n_vectors; - next_index = node->cached_next_index; - - while (n_left_from > 0) { - u32 n_left_to_next; - - vlib_get_next_frame(vm, node, next_index, to_next, n_left_to_next); - - while (n_left_from > 0 && n_left_to_next > 0) { - u32 bi0; - vlib_buffer_t *b0; - u32 next0 = TUNTERM_ACL_NEXT_VXLAN4_INPUT; // Default next index - u32 sw_if_index0; - - bi0 = from[0]; - to_next[0] = bi0; - from += 1; - to_next += 1; - n_left_from -= 1; - n_left_to_next -= 1; - - b0 = vlib_get_buffer(vm, bi0); - sw_if_index0 = vnet_buffer(b0)->sw_if_index[VLIB_RX]; - - u8* etype = &(b0->data[b0->current_data + sizeof(vxlan_header_t) + 2*sizeof(mac_address_t)]); - u16 ethertype = (etype[0] << 8) | etype[1]; - - if (ethertype == 0x86DD) { - table_index = tunterm_acl_main.classify_table_index_by_sw_if_index_v6[sw_if_index0]; - } else if (ethertype == 0x0800) { - table_index = tunterm_acl_main.classify_table_index_by_sw_if_index_v4[sw_if_index0]; - } else { - // This shouldn't happen. Something is wrong. Drop the packet to bring attention to this. - next0 = TUNTERM_ACL_NEXT_DROP; - pkts_unsupported_ethertype++; - goto exit; - } - - if (PREDICT_FALSE(table_index == ~0)) { - // No classify table found for given proto. Continue to vxlan4-input. - pkts_no_classify_table++; - } else { - // Perform the classify table lookup - vnet_classify_table_t *t = pool_elt_at_index(cm->tables, table_index); - - u32 hash0 = vnet_classify_hash_packet (t, (b0->data)); - vnet_classify_entry_t *e = vnet_classify_find_entry(t, b0->data, hash0, vlib_time_now (vm)); - - if (e) { - if (e->action == CLASSIFY_ACTION_SET_METADATA) { - vlib_buffer_advance (b0, sizeof (vxlan_header_t)); - vlib_buffer_advance (b0, sizeof (ethernet_header_t)); - next0 = (e->next_index < node->n_next_nodes) ? e->next_index : next0; - vnet_buffer (b0)->ip.adj_index[VLIB_TX] = e->metadata; - pkts_redirected++; - } else { - // Classify table hit, but action not supported. Continue to vxlan4-input. - pkts_action_not_supported++; - } - } else { - // No match found in classify table. Continue to vxlan4-input. - pkts_no_match++; - } - } - -exit: - if (PREDICT_FALSE((node->flags & VLIB_NODE_FLAG_TRACE) && (b0->flags & VLIB_BUFFER_IS_TRACED))) { - tunterm_acl_trace_t *t = vlib_add_trace(vm, node, b0, sizeof(*t)); - t->sw_if_index = sw_if_index0; - t->next_index = next0; - t->index = vnet_buffer (b0)->ip.adj_index[VLIB_TX]; - } - - // Verify speculative enqueue, maybe switch current next frame - vlib_validate_buffer_enqueue_x1(vm, node, next_index, to_next, n_left_to_next, bi0, next0); - } - - vlib_put_next_frame(vm, node, next_index, n_left_to_next); +VLIB_NODE_FN (tunterm_acl_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) +{ + u32 n_left_from, *from, *to_next; + tunterm_acl_next_t next_index; + vnet_classify_main_t *cm = &vnet_classify_main; + u32 table_index = ~0; + u32 pkts_redirected = 0; + u32 pkts_no_classify_table = 0; + u32 pkts_action_not_supported = 0; + u32 pkts_unsupported_ethertype = 0; + u32 pkts_no_match = 0; + + from = vlib_frame_vector_args (frame); + n_left_from = frame->n_vectors; + next_index = node->cached_next_index; + + while (n_left_from > 0) + { + u32 n_left_to_next; + + vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next); + + while (n_left_from > 0 && n_left_to_next > 0) + { + u32 bi0; + vlib_buffer_t *b0; + u32 next0 = TUNTERM_ACL_NEXT_VXLAN4_INPUT; // Default next index + u32 sw_if_index0; + u32 inner_ether_offset, inner_ip_offset, inner_dip_offset; + bool is_inner_vlan_tagged = false; + + bi0 = from[0]; + to_next[0] = bi0; + from += 1; + to_next += 1; + n_left_from -= 1; + n_left_to_next -= 1; + + b0 = vlib_get_buffer (vm, bi0); + + sw_if_index0 = vnet_buffer (b0)->sw_if_index[VLIB_RX]; + + // bypass node leaves current_data at vxlan header + inner_ether_offset = b0->current_data + sizeof (vxlan_header_t); + inner_ip_offset = inner_ether_offset + sizeof (ethernet_header_t); + + u8 *etype = + &(b0->data[inner_ether_offset + 2 * sizeof (mac_address_t)]); + u16 ethertype = (etype[0] << 8) | etype[1]; + + // skip any VLAN tag + if (ethertype == 0x8100) + { + etype += sizeof (ethernet_vlan_header_t); + ethertype = (etype[0] << 8) | etype[1]; + + is_inner_vlan_tagged = true; + inner_ip_offset += sizeof (ethernet_vlan_header_t); + } + + if (ethertype == 0x86DD) + { + table_index = + tunterm_acl_main + .classify_table_index_by_sw_if_index_v6[sw_if_index0]; + // offset ip6 header until DIP + inner_dip_offset = inner_ip_offset + sizeof (ip6_header_t) - + sizeof (ip6_address_t); + } + else if (ethertype == 0x0800) + { + table_index = + tunterm_acl_main + .classify_table_index_by_sw_if_index_v4[sw_if_index0]; + // offset ip4 header until DIP + inner_dip_offset = inner_ip_offset + sizeof (ip4_header_t) - + sizeof (ip4_address_t); + } + else + { + // Unexpected ethertype. Continue to vxlan4-input. + pkts_unsupported_ethertype++; + goto exit; + } + + if (PREDICT_FALSE (table_index == ~0)) + { + // No classify table found for given proto. Continue to + // vxlan4-input. + pkts_no_classify_table++; + } + else + { + // Perform the classify table lookup + vnet_classify_table_t *t = + pool_elt_at_index (cm->tables, table_index); + + u32 hash0 = + vnet_classify_hash_packet (t, (b0->data + inner_dip_offset)); + vnet_classify_entry_t *e = vnet_classify_find_entry ( + t, (b0->data + inner_dip_offset), hash0, vlib_time_now (vm)); + + if (e) + { + if (e->action == CLASSIFY_ACTION_SET_METADATA) + { + vlib_buffer_advance (b0, sizeof (vxlan_header_t)); + vlib_buffer_advance (b0, sizeof (ethernet_header_t)); + if (is_inner_vlan_tagged) + { + vlib_buffer_advance ( + b0, sizeof (ethernet_vlan_header_t)); + } + next0 = (e->next_index < node->n_next_nodes) ? + e->next_index : + next0; + vnet_buffer (b0)->ip.adj_index[VLIB_TX] = e->metadata; + pkts_redirected++; + } + else + { + // Classify table hit, but action not supported. Continue + // to vxlan4-input. + pkts_action_not_supported++; + } + } + else + { + // No match found in classify table. Continue to + // vxlan4-input. + pkts_no_match++; + } + } + + exit: + if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && + (b0->flags & VLIB_BUFFER_IS_TRACED))) + { + tunterm_acl_trace_t *t = + vlib_add_trace (vm, node, b0, sizeof (*t)); + t->sw_if_index = sw_if_index0; + t->next_index = next0; + t->index = vnet_buffer (b0)->ip.adj_index[VLIB_TX]; + } + + // Verify speculative enqueue, maybe switch current next frame + vlib_validate_buffer_enqueue_x1 (vm, node, next_index, to_next, + n_left_to_next, bi0, next0); + } + + vlib_put_next_frame (vm, node, next_index, n_left_to_next); } - vlib_node_increment_counter (vm, tunterm_acl_node.index, TUNTERM_ACL_ERROR_REDIRECTED, pkts_redirected); - vlib_node_increment_counter (vm, tunterm_acl_node.index, TUNTERM_ACL_ERROR_NO_CLASSIFY_TABLE, pkts_no_classify_table); - vlib_node_increment_counter (vm, tunterm_acl_node.index, TUNTERM_ACL_ERROR_ACTION_NOT_SUPPORTED, pkts_action_not_supported); - vlib_node_increment_counter (vm, tunterm_acl_node.index, TUNTERM_ACL_ERROR_UNSUPPORTED_ETHERTYPE, pkts_unsupported_ethertype); - vlib_node_increment_counter (vm, tunterm_acl_node.index, TUNTERM_ACL_ERROR_NO_MATCH, pkts_no_match); + vlib_node_increment_counter (vm, tunterm_acl_node.index, + TUNTERM_ACL_ERROR_REDIRECTED, pkts_redirected); + vlib_node_increment_counter (vm, tunterm_acl_node.index, + TUNTERM_ACL_ERROR_NO_CLASSIFY_TABLE, + pkts_no_classify_table); + vlib_node_increment_counter (vm, tunterm_acl_node.index, + TUNTERM_ACL_ERROR_ACTION_NOT_SUPPORTED, + pkts_action_not_supported); + vlib_node_increment_counter (vm, tunterm_acl_node.index, + TUNTERM_ACL_ERROR_UNSUPPORTED_ETHERTYPE, + pkts_unsupported_ethertype); + vlib_node_increment_counter (vm, tunterm_acl_node.index, + TUNTERM_ACL_ERROR_NO_MATCH, pkts_no_match); - return frame->n_vectors; + return frame->n_vectors; } VLIB_REGISTER_NODE (tunterm_acl_node) = diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.c b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.c index e9c3a39..e3c85dd 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.c +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.c @@ -67,7 +67,7 @@ tunterm_acl_redirect_stack (tunterm_acl_redirect_t *ipr) static tunterm_acl_redirect_t * tunterm_acl_redirect_find (tunterm_acl_redirect_main_t *im, u32 table_index, - const u8 *match) + const u8 *match) { /* we are adding the table index at the end of the match string so we * can disambiguiate identical matches in different tables in @@ -84,8 +84,8 @@ tunterm_acl_redirect_find (tunterm_acl_redirect_main_t *im, u32 table_index, __clib_export int tunterm_acl_redirect_add (vlib_main_t *vm, u32 table_index, u32 opaque_index, - dpo_proto_t proto, const u8 *match, - const fib_route_path_t *rpaths) + dpo_proto_t proto, const u8 *match, + const fib_route_path_t *rpaths) { tunterm_acl_redirect_main_t *im = &tunterm_acl_redirect_main; fib_forward_chain_type_t payload_type; @@ -137,7 +137,8 @@ tunterm_acl_redirect_add (vlib_main_t *vm, u32 table_index, u32 opaque_index, return tunterm_acl_redirect_stack (ipr); } int -tunterm_acl_redirect_del_ipr (tunterm_acl_redirect_main_t *im, tunterm_acl_redirect_t *ipr) +tunterm_acl_redirect_del_ipr (tunterm_acl_redirect_main_t *im, + tunterm_acl_redirect_t *ipr) { vnet_classify_main_t *cm = &vnet_classify_main; int rv; @@ -174,29 +175,36 @@ tunterm_acl_redirect_del (vlib_main_t *vm, u32 table_index, const u8 *match) return rv; } -int tunterm_acl_redirect_clear (vlib_main_t *vm, u32 table_index) { +int +tunterm_acl_redirect_clear (vlib_main_t *vm, u32 table_index) +{ tunterm_acl_redirect_main_t *im = &tunterm_acl_redirect_main; tunterm_acl_redirect_t *ipr; tunterm_acl_redirect_t **to_be_deleted = NULL; int rv = 0; - // Iterate through the pool to find all entries with the specified table_index - pool_foreach(ipr, im->pool) { - if (ipr->table_index == table_index) { - vec_add1(to_be_deleted, ipr); - } - } + // Iterate through the pool to find all entries with the specified + // table_index + pool_foreach (ipr, im->pool) + { + if (ipr->table_index == table_index) + { + vec_add1 (to_be_deleted, ipr); + } + } tunterm_acl_redirect_t **ipr_ptr; - vec_foreach(ipr_ptr, to_be_deleted) { - rv = tunterm_acl_redirect_del_ipr(im, *ipr_ptr); - if (rv) { - vec_free(to_be_deleted); - return rv; + vec_foreach (ipr_ptr, to_be_deleted) + { + rv = tunterm_acl_redirect_del_ipr (im, *ipr_ptr); + if (rv) + { + vec_free (to_be_deleted); + return rv; + } } - } - vec_free(to_be_deleted); + vec_free (to_be_deleted); return 0; } @@ -211,9 +219,9 @@ tunterm_acl_redirect_get_node (fib_node_index_t index) static tunterm_acl_redirect_t * tunterm_acl_redirect_get_from_node (fib_node_t *node) { - return ( - tunterm_acl_redirect_t *) (((char *) node) - - STRUCT_OFFSET_OF (tunterm_acl_redirect_t, node)); + return (tunterm_acl_redirect_t *) (((char *) node) - + STRUCT_OFFSET_OF (tunterm_acl_redirect_t, + node)); } static void @@ -226,7 +234,7 @@ tunterm_acl_redirect_last_lock_gone (fib_node_t *node) /* A back walk has reached this entry */ static fib_node_back_walk_rc_t tunterm_acl_redirect_back_walk_notify (fib_node_t *node, - fib_node_back_walk_ctx_t *ctx) + fib_node_back_walk_ctx_t *ctx) { int rv; tunterm_acl_redirect_t *ipr = tunterm_acl_redirect_get_from_node (node); @@ -249,7 +257,7 @@ tunterm_acl_redirect_init (vlib_main_t *vm) tunterm_acl_redirect_main_t *im = &tunterm_acl_redirect_main; im->session_by_match_and_table_index = hash_create_vec (0, sizeof (u8), sizeof (u32)); - im->fib_node_type = fib_node_register_new_type ("tunterm-redirect", - &tunterm_acl_redirect_vft); + im->fib_node_type = + fib_node_register_new_type ("tunterm-redirect", &tunterm_acl_redirect_vft); return 0; } diff --git a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.h b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.h index 654aa0d..b62048e 100644 --- a/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.h +++ b/platform/vpp/vppbld/plugins/tunterm_acl/tunterm_acl_redirect.h @@ -16,14 +16,14 @@ #include -clib_error_t * tunterm_acl_redirect_init (vlib_main_t *vm); +clib_error_t *tunterm_acl_redirect_init (vlib_main_t *vm); int tunterm_acl_redirect_add (vlib_main_t *vm, u32 table_index, - u32 opaque_index, dpo_proto_t proto, - const u8 *match, const fib_route_path_t *rpaths); + u32 opaque_index, dpo_proto_t proto, + const u8 *match, const fib_route_path_t *rpaths); int tunterm_acl_redirect_del (vlib_main_t *vm, u32 table_index, - const u8 *match); + const u8 *match); int tunterm_acl_redirect_clear (vlib_main_t *vm, u32 table_index);