diff options
Diffstat (limited to 'src/plugins/nat')
-rw-r--r-- | src/plugins/nat/CMakeLists.txt | 56 | ||||
-rw-r--r-- | src/plugins/nat/pnat/.clang-format | 3 | ||||
-rw-r--r-- | src/plugins/nat/pnat/FEATURE.yaml | 12 | ||||
-rw-r--r-- | src/plugins/nat/pnat/pnat.api | 155 | ||||
-rw-r--r-- | src/plugins/nat/pnat/pnat.c | 372 | ||||
-rw-r--r-- | src/plugins/nat/pnat/pnat.h | 121 | ||||
-rw-r--r-- | src/plugins/nat/pnat/pnat.md | 37 | ||||
-rw-r--r-- | src/plugins/nat/pnat/pnat_api.c | 207 | ||||
-rw-r--r-- | src/plugins/nat/pnat/pnat_cli.c | 274 | ||||
-rw-r--r-- | src/plugins/nat/pnat/pnat_node.c | 83 | ||||
-rw-r--r-- | src/plugins/nat/pnat/pnat_node.h | 196 | ||||
-rw-r--r-- | src/plugins/nat/pnat/pnat_test.c | 504 | ||||
-rw-r--r-- | src/plugins/nat/pnat/pnat_test_stubs.h | 214 | ||||
-rw-r--r-- | src/plugins/nat/test/test_pnat.py | 203 |
14 files changed, 2437 insertions, 0 deletions
diff --git a/src/plugins/nat/CMakeLists.txt b/src/plugins/nat/CMakeLists.txt index 83c148f9658..d3f3eac3c05 100644 --- a/src/plugins/nat/CMakeLists.txt +++ b/src/plugins/nat/CMakeLists.txt @@ -139,3 +139,59 @@ add_vpp_plugin(nat64 LINK_LIBRARIES nat ) + +add_vpp_plugin(pnat + SOURCES + pnat/pnat.c + pnat/pnat_cli.c + pnat/pnat_api.c + pnat/pnat_node.c + + API_FILES + pnat/pnat.api +) + +# Unit tests +add_vpp_executable(test_pnat + SOURCES + pnat/pnat_test.c + pnat/pnat_node.c + pnat/pnat.c + ../../vnet/ip/ip_checksum.c + LINK_LIBRARIES vppinfra vlib + NO_INSTALL +) + +if("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.13" AND "${CMAKE_C_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang") + set(TARGET_NAME test_pnat) + set(COV_SOURCES ${CMAKE_SOURCE_DIR}/plugins/nat/pnat/pnat.c ${CMAKE_SOURCE_DIR}/plugins/nat/pnat/pnat_node.h ${CMAKE_SOURCE_DIR}/plugins/nat/pnat/pnat_node.c) + + message("Building with llvm Code Coverage Tools ${TARGET_NAME}") + target_compile_options(${TARGET_NAME} PRIVATE -fprofile-instr-generate -fcoverage-mapping) + target_link_options(${TARGET_NAME} PRIVATE -fprofile-instr-generate -fcoverage-mapping) + target_compile_options(${TARGET_NAME} PRIVATE -fsanitize=address) + target_link_options(${TARGET_NAME} PRIVATE -fsanitize=address) + + # llvm-cov + add_custom_target(${TARGET_NAME}-ccov-preprocessing + COMMAND LLVM_PROFILE_FILE=${TARGET_NAME}.profraw $<TARGET_FILE:${TARGET_NAME}> + COMMAND llvm-profdata merge -sparse ${TARGET_NAME}.profraw -o ${TARGET_NAME}.profdata + DEPENDS ${TARGET_NAME}) + + add_custom_target(${TARGET_NAME}-ccov-show + COMMAND llvm-cov show $<TARGET_FILE:${TARGET_NAME}> -instr-profile=${TARGET_NAME}.profdata -show-line-counts-or-regions ${COV_SOURCES} + DEPENDS ${TARGET_NAME}-ccov-preprocessing) + + add_custom_target(${TARGET_NAME}-ccov-report + COMMAND llvm-cov report -show-functions $<TARGET_FILE:${TARGET_NAME}> -instr-profile=${TARGET_NAME}.profdata ${COV_SOURCES} + DEPENDS ${TARGET_NAME}-ccov-preprocessing) + + add_custom_target(${TARGET_NAME}-ccov + COMMAND llvm-cov show $<TARGET_FILE:${TARGET_NAME}> -instr-profile=${TARGET_NAME}.profdata -show-line-counts-or-regions -output-dir=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET_NAME}-llvm-cov -format="html" ${COV_SOURCES} + DEPENDS ${TARGET_NAME}-ccov-preprocessing) + + add_custom_command(TARGET ${TARGET_NAME}-ccov POST_BUILD + COMMAND ; + COMMENT "Open ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET_NAME}-llvm-cov/index.html in your browser to view the coverage report." +) +endif() diff --git a/src/plugins/nat/pnat/.clang-format b/src/plugins/nat/pnat/.clang-format new file mode 100644 index 00000000000..6f25dc76efc --- /dev/null +++ b/src/plugins/nat/pnat/.clang-format @@ -0,0 +1,3 @@ +SortIncludes: false +BasedOnStyle: LLVM +IndentWidth: 4 diff --git a/src/plugins/nat/pnat/FEATURE.yaml b/src/plugins/nat/pnat/FEATURE.yaml new file mode 100644 index 00000000000..56f553a6081 --- /dev/null +++ b/src/plugins/nat/pnat/FEATURE.yaml @@ -0,0 +1,12 @@ +--- +name: Policy 1:1 NAT +maintainer: Ole Troan <ot@cisco.com> +description: "Match packet against rule and translate according to given instructions. + Rules are kept in a flow cache bihash. Instructions in a pool of translation entries. + + For a given interface/direction all rules must use the same lookup mask. E.g. SA + SP. + + A dynamic NAT would punt to slow path on a miss in the flow cache, in this case the miss behaviour is configurable. + Default behaviour is pass packet along unchanged." +state: experimental +properties: [API, CLI, MULTITHREAD] diff --git a/src/plugins/nat/pnat/pnat.api b/src/plugins/nat/pnat/pnat.api new file mode 100644 index 00000000000..c18c89445e9 --- /dev/null +++ b/src/plugins/nat/pnat/pnat.api @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2021 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 = "0.0.1"; + +import "vnet/interface_types.api"; +import "vnet/ip/ip_types.api"; + +enum pnat_mask +{ + PNAT_SA = 0x1, + PNAT_DA = 0x2, + PNAT_SPORT = 0x4, + PNAT_DPORT = 0x8, +}; + +enum pnat_attachment_point +{ + PNAT_IP4_INPUT, + PNAT_IP4_OUTPUT, + PNAT_ATTACHMENT_POINT_MAX, +}; + +typedef pnat_5tuple +{ + vl_api_ip4_address_t src; + vl_api_ip4_address_t dst; + vl_api_ip_proto_t proto; + u16 sport; + u16 dport; + vl_api_pnat_mask_t mask; +}; + +autoendian define pnat_binding_add +{ + u32 client_index; + u32 context; + vl_api_pnat_5tuple_t match; + vl_api_pnat_5tuple_t rewrite; +}; + +autoendian define pnat_binding_add_reply +{ + u32 context; + i32 retval; + u32 binding_index; +}; + +autoendian autoreply define pnat_binding_del +{ + u32 client_index; + u32 context; + u32 binding_index; +}; + +autoendian autoreply define pnat_binding_attach +{ + u32 client_index; + u32 context; + vl_api_interface_index_t sw_if_index; + vl_api_pnat_attachment_point_t attachment; + u32 binding_index; +}; + +autoendian autoreply define pnat_binding_detach +{ + u32 client_index; + u32 context; + vl_api_interface_index_t sw_if_index; + vl_api_pnat_attachment_point_t attachment; + u32 binding_index; +}; + +service { + rpc pnat_bindings_get returns pnat_bindings_get_reply + stream pnat_bindings_details; + rpc pnat_interfaces_get returns pnat_interfaces_get_reply + stream pnat_interfaces_details; +}; + +define pnat_bindings_get +{ + u32 client_index; + u32 context; + u32 cursor; +}; + +define pnat_bindings_get_reply +{ + u32 context; + i32 retval; + u32 cursor; +}; + +define pnat_bindings_details +{ + u32 context; + vl_api_pnat_5tuple_t match; + vl_api_pnat_5tuple_t rewrite; +}; + +define pnat_interfaces_get +{ + u32 client_index; + u32 context; + u32 cursor; +}; + +define pnat_interfaces_get_reply +{ + u32 context; + i32 retval; + u32 cursor; +}; + +define pnat_interfaces_details +{ + u32 context; + vl_api_interface_index_t sw_if_index; + bool enabled[2]; /* PNAT_ATTACHMENT_POINT_MAX */ + vl_api_pnat_mask_t lookup_mask[2]; /* PNAT_ATTACHMENT_POINT_MAX */ +}; + +counters pnat { + none { + severity info; + type counter64; + units "packets"; + description "successfully rewritten"; + }; + + rewrite { + severity error; + type counter64; + units "packets"; + description "rewrite failed"; + }; +}; + +paths { + "/err/pnat-input" "pnat"; + "/err/pnat-output" "pnat"; +}; diff --git a/src/plugins/nat/pnat/pnat.c b/src/plugins/nat/pnat/pnat.c new file mode 100644 index 00000000000..335d2750ce7 --- /dev/null +++ b/src/plugins/nat/pnat/pnat.c @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2021 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 "pnat.h" +#include <arpa/inet.h> +#include <stdbool.h> +#include <vlib/vlib.h> +#include <vnet/feature/feature.h> +#include <vnet/fib/fib_table.h> +#include <vnet/ip/format.h> +#include <vnet/ip/ip4.h> +#include <vnet/ip/ip4_packet.h> +#include <vnet/ip/reass/ip4_sv_reass.h> +#include <vppinfra/clib_error.h> + +/* + * This is the main control plane part of the PNAT (Policy 1:1 NAT) feature. + */ + +pnat_main_t pnat_main; + +/* + * Do a lookup in the interface vector (interface_by_sw_if_index) + * and return pool entry. + */ +pnat_interface_t *pnat_interface_by_sw_if_index(u32 sw_if_index) { + pnat_main_t *pm = &pnat_main; + + if (!pm->interface_by_sw_if_index || + sw_if_index > (vec_len(pm->interface_by_sw_if_index) - 1)) + return 0; + u32 index = pm->interface_by_sw_if_index[sw_if_index]; + if (index == ~0) + return 0; + if (pool_is_free_index(pm->interfaces, index)) + return 0; + return pool_elt_at_index(pm->interfaces, index); +} + +static pnat_mask_fast_t pnat_mask2fast(pnat_mask_t lookup_mask) { + pnat_mask_fast_t m = {0}; + + if (lookup_mask & PNAT_SA) + m.as_u64[0] = 0xffffffff00000000; + if (lookup_mask & PNAT_DA) + m.as_u64[0] |= 0x00000000ffffffff; + m.as_u64[1] = 0xffffffff00000000; + if (lookup_mask & PNAT_SPORT) + m.as_u64[1] |= 0x00000000ffff0000; + if (lookup_mask & PNAT_DPORT) + m.as_u64[1] |= 0x000000000000ffff; + return m; +} + +/* + * Create new PNAT interface object and register the pnat feature in the + * corresponding feature chain. + * Also enable shallow virtual reassembly, to ensure that we have + * L4 ports available for all packets we receive. + */ +static clib_error_t *pnat_enable_interface(u32 sw_if_index, + pnat_attachment_point_t attachment, + pnat_mask_t mask) { + pnat_main_t *pm = &pnat_main; + pnat_interface_t *interface = pnat_interface_by_sw_if_index(sw_if_index); + + if (!interface) { + pool_get_zero(pm->interfaces, interface); + interface->sw_if_index = sw_if_index; + vec_validate_init_empty(pm->interface_by_sw_if_index, sw_if_index, ~0); + pm->interface_by_sw_if_index[sw_if_index] = interface - pm->interfaces; + } + + char *nodename; + char *arcname; + bool input = false; + switch (attachment) { + case PNAT_IP4_INPUT: + nodename = "pnat-input"; + arcname = "ip4-unicast"; + input = true; + break; + + case PNAT_IP4_OUTPUT: + nodename = "pnat-output"; + arcname = "ip4-output"; + break; + default: + return clib_error_return(0, "Unknown attachment point %u %u", + sw_if_index, attachment); + } + + if (!interface->enabled[attachment]) { + if (vnet_feature_enable_disable(arcname, nodename, sw_if_index, 1, 0, + 0) != 0) + return clib_error_return(0, "PNAT feature enable failed on %u", + sw_if_index); + + if (input) { + /* TODO: Make shallow virtual reassembly configurable */ + ip4_sv_reass_enable_disable_with_refcnt(sw_if_index, 1); + } else { + ip4_sv_reass_output_enable_disable_with_refcnt(sw_if_index, 1); + } + + interface->lookup_mask[attachment] = mask; + interface->lookup_mask_fast[attachment] = pnat_mask2fast(mask); + interface->enabled[attachment] = true; + + } else { + pnat_mask_t current_mask = interface->lookup_mask[attachment]; + if (current_mask != mask) { + return clib_error_return(0, + "PNAT lookup mask must be consistent per " + "interface/direction %u", + sw_if_index); + } + } + + interface->refcount++; + + return 0; +} + +/* + * Delete interface object when no rules reference the interface. + */ +static int pnat_disable_interface(u32 sw_if_index, + pnat_attachment_point_t attachment) { + pnat_main_t *pm = &pnat_main; + pnat_interface_t *interface = pnat_interface_by_sw_if_index(sw_if_index); + + if (!interface) + return 0; + if (interface->refcount == 0) + return 0; + + if (interface->enabled[attachment] && attachment == PNAT_IP4_INPUT) { + ip4_sv_reass_enable_disable_with_refcnt(sw_if_index, 0); + if (vnet_feature_enable_disable("ip4-unicast", "pnat-input", + sw_if_index, 0, 0, 0) != 0) + return -1; + } + if (interface->enabled[attachment] && attachment == PNAT_IP4_OUTPUT) { + ip4_sv_reass_output_enable_disable_with_refcnt(sw_if_index, 0); + if (vnet_feature_enable_disable("ip4-output", "pnat-output", + sw_if_index, 0, 0, 0) != 0) + return -1; + } + + interface->lookup_mask[attachment] = 0; + interface->enabled[attachment] = false; + + interface->refcount--; + if (interface->refcount == 0) { + pm->interface_by_sw_if_index[sw_if_index] = ~0; + pool_put(pm->interfaces, interface); + } + return 0; +} + +/* + * From a 5-tuple (with mask) calculate the key used in the flow cache lookup. + */ +static inline void pnat_calc_key_from_5tuple(u32 sw_if_index, + pnat_attachment_point_t attachment, + pnat_5tuple_t *match, + clib_bihash_kv_16_8_t *kv) { + pnat_mask_fast_t mask = pnat_mask2fast(match->mask); + ip4_address_t src, dst; + clib_memcpy(&src, &match->src, 4); + clib_memcpy(&dst, &match->dst, 4); + pnat_calc_key(sw_if_index, attachment, src, dst, match->proto, + htons(match->sport), htons(match->dport), mask, kv); +} + +/* + * Map between the 5-tuple mask and the instruction set of the rewrite node. + */ +pnat_instructions_t pnat_instructions_from_mask(pnat_mask_t m) { + pnat_instructions_t i = 0; + + if (m & PNAT_SA) + i |= PNAT_INSTR_SOURCE_ADDRESS; + if (m & PNAT_DA) + i |= PNAT_INSTR_DESTINATION_ADDRESS; + if (m & PNAT_SPORT) + i |= PNAT_INSTR_SOURCE_PORT; + if (m & PNAT_DPORT) + i |= PNAT_INSTR_DESTINATION_PORT; + return i; +} + +/* + * "Init" the PNAT datastructures. Called upon first creation of a PNAT rule. + * TODO: Make number of buckets configurable. + */ +static void pnat_enable(void) { + pnat_main_t *pm = &pnat_main; + if (pm->enabled) + return; + + /* Create new flow cache table */ + clib_bihash_init_16_8(&pm->flowhash, "PNAT flow hash", + PNAT_FLOW_HASH_BUCKETS, 0); + + pm->enabled = true; +} +static void pnat_disable(void) { + pnat_main_t *pm = &pnat_main; + + if (!pm->enabled) + return; + if (pool_elts(pm->translations)) + return; + + /* Delete flow cache table */ + clib_bihash_free_16_8(&pm->flowhash); + + pm->enabled = false; +} + +/* + * Ensure that a new rule lookup mask matches what's installed on interface + */ +static int pnat_interface_check_mask(u32 sw_if_index, + pnat_attachment_point_t attachment, + pnat_mask_t mask) { + pnat_interface_t *interface = pnat_interface_by_sw_if_index(sw_if_index); + if (!interface) + return 0; + if (!interface->enabled[attachment]) + return 0; + if (interface->lookup_mask[attachment] != mask) + return -1; + + return 0; +} + +int pnat_binding_add(pnat_5tuple_t *match, pnat_5tuple_t *rewrite, u32 *index) { + pnat_main_t *pm = &pnat_main; + + *index = -1; + + /* If we aren't matching or rewriting, why are we here? */ + if (match->mask == 0 || rewrite->mask == 0) + return -1; + + /* Check if protocol is set if ports are set */ + if ((match->dport || match->sport) && + (match->proto != IP_API_PROTO_UDP && match->proto != IP_API_PROTO_TCP)) + return -2; + + /* Create pool entry */ + pnat_translation_t *t; + pool_get_zero(pm->translations, t); + memcpy(&t->post_da, &rewrite->dst, 4); + memcpy(&t->post_sa, &rewrite->src, 4); + t->post_sp = rewrite->sport; + t->post_dp = rewrite->dport; + t->instructions = pnat_instructions_from_mask(rewrite->mask); + + /* These are only used for show commands and trace */ + t->match = *match; + + /* Rewrite of protocol is not supported, ignore. */ + t->rewrite = *rewrite; + t->rewrite.proto = 0; + + *index = t - pm->translations; + + return 0; +} +u32 pnat_flow_lookup(u32 sw_if_index, pnat_attachment_point_t attachment, + pnat_5tuple_t *match) { + pnat_main_t *pm = &pnat_main; + clib_bihash_kv_16_8_t kv, value; + pnat_calc_key_from_5tuple(sw_if_index, attachment, match, &kv); + if (clib_bihash_search_16_8(&pm->flowhash, &kv, &value) == 0) { + return value.value; + } + return ~0; +} + +int pnat_binding_attach(u32 sw_if_index, pnat_attachment_point_t attachment, + u32 binding_index) { + pnat_main_t *pm = &pnat_main; + + if (!pm->translations || + pool_is_free_index(pm->translations, binding_index)) + return -1; + + pnat_translation_t *t = pool_elt_at_index(pm->translations, binding_index); + + if (pnat_interface_check_mask(sw_if_index, attachment, t->match.mask) != 0) + return -2; + + pnat_enable(); + + /* Verify non-duplicate */ + clib_bihash_kv_16_8_t kv, value; + pnat_calc_key_from_5tuple(sw_if_index, attachment, &t->match, &kv); + if (clib_bihash_search_16_8(&pm->flowhash, &kv, &value) == 0) { + return -3; + } + + /* Create flow cache */ + kv.value = binding_index; + if (clib_bihash_add_del_16_8(&pm->flowhash, &kv, 1)) { + pool_put(pm->translations, t); + return -4; + } + + /* Register interface */ + pnat_enable_interface(sw_if_index, attachment, t->match.mask); + + return 0; +} + +int pnat_binding_detach(u32 sw_if_index, pnat_attachment_point_t attachment, + u32 binding_index) { + pnat_main_t *pm = &pnat_main; + + if (!pm->translations || + pool_is_free_index(pm->translations, binding_index)) + return -1; + + pnat_translation_t *t = pool_elt_at_index(pm->translations, binding_index); + + /* Verify non-duplicate */ + clib_bihash_kv_16_8_t kv; + pnat_calc_key_from_5tuple(sw_if_index, attachment, &t->match, &kv); + if (clib_bihash_add_del_16_8(&pm->flowhash, &kv, 0)) { + return -2; + } + + /* Deregister interface */ + pnat_disable_interface(sw_if_index, attachment); + + pnat_disable(); + + return 0; +} + +/* + * Delete a translation using the index returned from pnat_add_translation. + */ +int pnat_binding_del(u32 index) { + pnat_main_t *pm = &pnat_main; + + if (pool_is_free_index(pm->translations, index)) { + clib_warning("Binding delete: translation does not exist: %d", index); + return -1; + } + + pnat_translation_t *t = pool_elt_at_index(pm->translations, index); + pool_put(pm->translations, t); + + return 0; +} diff --git a/src/plugins/nat/pnat/pnat.h b/src/plugins/nat/pnat/pnat.h new file mode 100644 index 00000000000..c5869ce3ca6 --- /dev/null +++ b/src/plugins/nat/pnat/pnat.h @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2021 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_pnat_h +#define included_pnat_h + +#include <stdbool.h> +#include <vnet/ip/ip4_packet.h> +#include <vppinfra/bihash_16_8.h> + +#define PNAT_FLOW_HASH_BUCKETS 256 + +/* Definitions from pnat.api */ +#include <pnat/pnat.api_types.h> +typedef vl_api_pnat_5tuple_t pnat_5tuple_t; +typedef vl_api_pnat_mask_t pnat_mask_t; +typedef vl_api_pnat_attachment_point_t pnat_attachment_point_t; + +/* Rewrite instructions */ +typedef enum { + PNAT_INSTR_NONE = 1 << 0, + PNAT_INSTR_SOURCE_ADDRESS = 1 << 1, + PNAT_INSTR_SOURCE_PORT = 1 << 2, + PNAT_INSTR_DESTINATION_ADDRESS = 1 << 3, + PNAT_INSTR_DESTINATION_PORT = 1 << 4, +} pnat_instructions_t; + +typedef struct { + u64 as_u64[2]; +} pnat_mask_fast_t; + +/* Session cache entries */ +typedef struct { + /* What to translate to */ + pnat_instructions_t instructions; + + /* Stored in network byte order */ + ip4_address_t post_sa; + ip4_address_t post_da; + u16 post_sp; + u16 post_dp; + + /* Used for trace/show commands */ + pnat_5tuple_t match; + pnat_5tuple_t rewrite; +} pnat_translation_t; + +/* Interface object */ +typedef struct { + u32 sw_if_index; + pnat_mask_t lookup_mask[PNAT_ATTACHMENT_POINT_MAX]; + pnat_mask_fast_t lookup_mask_fast[PNAT_ATTACHMENT_POINT_MAX]; + + /* Feature chain enabled on interface */ + bool enabled[PNAT_ATTACHMENT_POINT_MAX]; + + u32 refcount; +} pnat_interface_t; + +/* Globals */ +typedef struct { + bool enabled; + + clib_bihash_16_8_t flowhash; /* Bi-directional */ + + /* Interface pool */ + pnat_interface_t *interfaces; + u32 *interface_by_sw_if_index; + + /* Translations pool */ + pnat_translation_t *translations; + + u16 msg_id_base; +} pnat_main_t; +extern pnat_main_t pnat_main; + +pnat_interface_t *pnat_interface_by_sw_if_index(u32 sw_if_index); + +/* Packet trace information */ +typedef struct { + u32 pool_index; + pnat_5tuple_t match; + pnat_5tuple_t rewrite; +} pnat_trace_t; + +int pnat_binding_add(pnat_5tuple_t *match, pnat_5tuple_t *rewrite, + u32 *binding_index); +int pnat_binding_del(u32 binding_index); +int pnat_binding_attach(u32 sw_if_index, pnat_attachment_point_t attachment, + u32 binding_index); +int pnat_binding_detach(u32 sw_if_index, pnat_attachment_point_t attachment, + u32 binding_index); +u32 pnat_flow_lookup(u32 sw_if_index, pnat_attachment_point_t attachment, + pnat_5tuple_t *match); + +static inline void +pnat_calc_key(u32 sw_if_index, pnat_attachment_point_t attachment, + ip4_address_t src, ip4_address_t dst, u8 protocol, u16 sport, + u16 dport, pnat_mask_fast_t mask, clib_bihash_kv_16_8_t *kv) { + kv->key[0] = kv->key[1] = 0; + kv->key[0] = (u64)src.as_u32 << 32 | dst.as_u32; + kv->key[0] &= mask.as_u64[0]; + kv->key[1] |= + (u64)protocol << 56 | (u64)sw_if_index << 36 | (u64)attachment << 32; + kv->key[1] |= sport << 16 | dport; + kv->key[1] &= mask.as_u64[1]; +} + +#endif diff --git a/src/plugins/nat/pnat/pnat.md b/src/plugins/nat/pnat/pnat.md new file mode 100644 index 00000000000..a7c33766b90 --- /dev/null +++ b/src/plugins/nat/pnat/pnat.md @@ -0,0 +1,37 @@ +# PNAT: 1:1 match and rewrite programmable NAT + +PNAT is a stateless statically configured, match and rewrite plugin. +It uses a set of match and rewrite rules that are applied on the IP +input and output feature paths. A PNAT rule is unidirectional. + +The match is done using up to a 6-tuple; IP source and destination address, +IP protocol, transport layer source and destination ports, and FIB table / interface index. + +While multiple match/rewrite rules can be applied to an interface (per direction), the match +pattern must be the same across all rules on that interface/direction. + +If required in the future, matching could be done using the general classifier, allowing matching +on any protocol field, as well having an ordered set of match patterns. + +If the packet does not match, it will by default be passed to the next graph node in the feature chain. +If desired a different miss behaviour could be implemented, e.g. similarly to dynamic NAT, the packet punted to a slow path. + +## Rewrite instructions + +``` c +typedef enum { + PNAT_INSTR_NONE = 1 << 0, + PNAT_INSTR_SOURCE_ADDRESS = 1 << 1, + PNAT_INSTR_SOURCE_PORT = 1 << 2, + PNAT_INSTR_DESTINATION_ADDRESS = 1 << 3, + PNAT_INSTR_DESTINATION_PORT = 1 << 4, +} pnat_instructions_t; +``` + +These are the supported rewrite instructions. +The IP checksum and the TCP/UDP checksum are incrementally updated as required. + +There are only a few "sanity checks" on the rewrites. For example, the rewrite in the outbound direction +is applied on the ip-output feature chain. If one were to rewrite the IP destination address, the routing +decision and determination of the next-hop has already been done, and the packet would still be forwarded +to the original next-hop. diff --git a/src/plugins/nat/pnat/pnat_api.c b/src/plugins/nat/pnat/pnat_api.c new file mode 100644 index 00000000000..dad658c3e9e --- /dev/null +++ b/src/plugins/nat/pnat/pnat_api.c @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2021 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 "pnat.h" +#include <pnat/pnat.api_enum.h> +#include <pnat/pnat.api_types.h> +#include <vlibmemory/api.h> +#include <vnet/fib/fib_table.h> +#include <vnet/ip/ip.h> +#include <vnet/ip/ip_types_api.h> +#include <vnet/ip/reass/ip4_sv_reass.h> +#include <vnet/ip/reass/ip6_full_reass.h> +#include <vnet/ip/reass/ip6_sv_reass.h> + +/* + * This file contains the API handlers for the pnat.api + */ + +#define REPLY_MSG_ID_BASE pm->msg_id_base +#include <vlibapi/api_helper_macros.h> + +static void vl_api_pnat_binding_add_t_handler(vl_api_pnat_binding_add_t *mp) { + pnat_main_t *pm = &pnat_main; + vl_api_pnat_binding_add_reply_t *rmp; + u32 binding_index; + int rv = pnat_binding_add(&mp->match, &mp->rewrite, &binding_index); + REPLY_MACRO2_END(VL_API_PNAT_BINDING_ADD_REPLY, + ({ rmp->binding_index = binding_index; })); +} + +static void +vl_api_pnat_binding_attach_t_handler(vl_api_pnat_binding_attach_t *mp) { + pnat_main_t *pm = &pnat_main; + vl_api_pnat_binding_attach_reply_t *rmp; + int rv; + + /* Ensure that the interface exists */ + if (!vnet_sw_if_index_is_api_valid(mp->sw_if_index)) { + rv = VNET_API_ERROR_INVALID_SW_IF_INDEX; + goto bad_sw_if_index; + } + + rv = + pnat_binding_attach(mp->sw_if_index, mp->attachment, mp->binding_index); + +bad_sw_if_index: + REPLY_MACRO_END(VL_API_PNAT_BINDING_ATTACH_REPLY); +} + +static void +vl_api_pnat_binding_detach_t_handler(vl_api_pnat_binding_detach_t *mp) { + pnat_main_t *pm = &pnat_main; + vl_api_pnat_binding_detach_reply_t *rmp; + int rv; + + /* Ensure that the interface exists */ + if (!vnet_sw_if_index_is_api_valid(mp->sw_if_index)) { + rv = VNET_API_ERROR_INVALID_SW_IF_INDEX; + goto bad_sw_if_index; + } + + rv = + pnat_binding_detach(mp->sw_if_index, mp->attachment, mp->binding_index); + +bad_sw_if_index: + REPLY_MACRO_END(VL_API_PNAT_BINDING_DETACH_REPLY); +} + +static void vl_api_pnat_binding_del_t_handler(vl_api_pnat_binding_del_t *mp) { + pnat_main_t *pm = &pnat_main; + vl_api_pnat_binding_del_reply_t *rmp; + int rv = pnat_binding_del(mp->binding_index); + REPLY_MACRO_END(VL_API_PNAT_BINDING_DEL_REPLY); +} + +/* + * Workaround for a bug in vppapigen that doesn't register the endian handler + * for _details messages. When that's fixed it should be possible to use + * REPLY_MACRO_DETAILS4_END and not have to care about endian-ness in the + * handler itself. + */ +#define vl_endianfun +#include <pnat/pnat.api.h> +#undef vl_endianfun +static void send_bindings_details(u32 index, vl_api_registration_t *rp, + u32 context) { + pnat_main_t *pm = &pnat_main; + vl_api_pnat_bindings_details_t *rmp; + pnat_translation_t *t = pool_elt_at_index(pm->translations, index); + + /* Make sure every field is initiated (or don't skip the clib_memset()) */ + + REPLY_MACRO_DETAILS4(VL_API_PNAT_BINDINGS_DETAILS, rp, context, ({ + rmp->match = t->match; + rmp->rewrite = t->rewrite; + + /* Endian hack until apigen registers _details + * endian functions */ + vl_api_pnat_bindings_details_t_endian(rmp); + rmp->_vl_msg_id = htons(rmp->_vl_msg_id); + rmp->context = htonl(rmp->context); + })); +} + +static void vl_api_pnat_bindings_get_t_handler(vl_api_pnat_bindings_get_t *mp) { + pnat_main_t *pm = &pnat_main; + vl_api_pnat_bindings_get_reply_t *rmp; + + i32 rv = 0; + + if (pool_elts(pm->translations) == 0) { + REPLY_MACRO(VL_API_PNAT_BINDINGS_GET_REPLY); + return; + } + + /* + * "cursor" comes from the get call, and allows client to continue a dump + */ + REPLY_AND_DETAILS_MACRO(VL_API_PNAT_BINDINGS_GET_REPLY, pm->translations, ({ + send_bindings_details(cursor, rp, mp->context); + })); +} + +static void send_interfaces_details(u32 index, vl_api_registration_t *rp, + u32 context) { + pnat_main_t *pm = &pnat_main; + vl_api_pnat_interfaces_details_t *rmp; + pnat_interface_t *i = pool_elt_at_index(pm->interfaces, index); + + /* Make sure every field is initiated (or don't skip the clib_memset()) */ + + REPLY_MACRO_DETAILS4( + VL_API_PNAT_INTERFACES_DETAILS, rp, context, ({ + rmp->sw_if_index = i->sw_if_index; + clib_memcpy(rmp->enabled, i->enabled, PNAT_ATTACHMENT_POINT_MAX); + clib_memcpy(rmp->lookup_mask, i->lookup_mask, + sizeof(i->lookup_mask) * PNAT_ATTACHMENT_POINT_MAX); + + /* Endian hack until apigen registers _details + * endian functions */ + vl_api_pnat_interfaces_details_t_endian(rmp); + rmp->_vl_msg_id = htons(rmp->_vl_msg_id); + rmp->context = htonl(rmp->context); + })); +} + +static void +vl_api_pnat_interfaces_get_t_handler(vl_api_pnat_interfaces_get_t *mp) { + pnat_main_t *pm = &pnat_main; + vl_api_pnat_interfaces_get_reply_t *rmp; + + i32 rv = 0; + + if (pool_elts(pm->interfaces) == 0) { + REPLY_MACRO(VL_API_PNAT_INTERFACES_GET_REPLY); + return; + } + + /* + * "cursor" comes from the get call, and allows client to continue a dump + */ + REPLY_AND_DETAILS_MACRO( + VL_API_PNAT_INTERFACES_GET_REPLY, pm->interfaces, + ({ send_interfaces_details(cursor, rp, mp->context); })); +} + +/* API definitions */ +#include <vnet/format_fns.h> +#include <pnat/pnat.api.c> + +/* Set up the API message handling tables */ +clib_error_t *pnat_plugin_api_hookup(vlib_main_t *vm) { + pnat_main_t *pm = &pnat_main; + + pm->msg_id_base = setup_message_id_table(); + + return 0; +} + +/* + * Register the plugin and hook up the API + */ +#include <vnet/plugin/plugin.h> +VLIB_PLUGIN_REGISTER() = { + .version = "0.0.1", + .description = "Policy 1:1 NAT", +}; + +clib_error_t *pnat_init(vlib_main_t *vm) { + pnat_main_t *pm = &pnat_main; + memset(pm, 0, sizeof(*pm)); + + return pnat_plugin_api_hookup(vm); +} + +VLIB_INIT_FUNCTION(pnat_init); diff --git a/src/plugins/nat/pnat/pnat_cli.c b/src/plugins/nat/pnat/pnat_cli.c new file mode 100644 index 00000000000..2d389013c08 --- /dev/null +++ b/src/plugins/nat/pnat/pnat_cli.c @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2021 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 <stdbool.h> +#include <vlib/vlib.h> +#include <vnet/feature/feature.h> +#include <vnet/ip/ip.h> +#include <vnet/ip/ip4.h> +#include <vnet/ip/ip4_packet.h> +#include <vppinfra/clib_error.h> +#include <vppinfra/pool.h> +#include "pnat.h" + +/* + * This file contains the handlers for the (unsupported) VPP debug CLI. + */ +u8 *format_pnat_5tuple(u8 *s, va_list *args) { + pnat_5tuple_t *t = va_arg(*args, pnat_5tuple_t *); + s = format(s, "{"); + if (t->mask & PNAT_SA) + s = format(s, "%U", format_ip4_address, &t->src); + else + s = format(s, "*"); + if (t->mask & PNAT_SPORT) + s = format(s, ":%u,", t->sport); + else + s = format(s, ":*,"); + if (t->proto > 0) + s = format(s, "%U,", format_ip_protocol, t->proto); + else + s = format(s, "*,"); + if (t->mask & PNAT_DA) + s = format(s, "%U", format_ip4_address, &t->dst); + else + s = format(s, "*"); + if (t->mask & PNAT_DPORT) + s = format(s, ":%u", t->dport); + else + s = format(s, ":*"); + s = format(s, "}"); + return s; +} + +u8 *format_pnat_translation(u8 *s, va_list *args) { + u32 index = va_arg(*args, u32); + pnat_translation_t *t = va_arg(*args, pnat_translation_t *); + s = format(s, "[%d] match: %U rewrite: %U", index, format_pnat_5tuple, + &t->match, format_pnat_5tuple, &t->rewrite); + return s; +} + +static u8 *format_pnat_mask(u8 *s, va_list *args) { + pnat_mask_t t = va_arg(*args, pnat_mask_t); + if (t & PNAT_SA) + s = format(s, "SA "); + if (t & PNAT_SPORT) + s = format(s, "SP "); + if (t & PNAT_DA) + s = format(s, "DA "); + if (t & PNAT_DPORT) + s = format(s, "DP"); + return s; +} + +static u8 *format_pnat_interface(u8 *s, va_list *args) { + pnat_interface_t *interface = va_arg(*args, pnat_interface_t *); + s = format(s, "sw_if_index: %d", interface->sw_if_index); + if (interface->enabled[PNAT_IP4_INPUT]) { + s = format(s, " input mask: %U", format_pnat_mask, + interface->lookup_mask[PNAT_IP4_INPUT]); + } + if (interface->enabled[PNAT_IP4_OUTPUT]) { + s = format(s, " output mask: %U", format_pnat_mask, + interface->lookup_mask[PNAT_IP4_OUTPUT]); + } + return s; +} + +uword unformat_pnat_5tuple(unformat_input_t *input, va_list *args) { + pnat_5tuple_t *t = va_arg(*args, pnat_5tuple_t *); + u32 dport, sport; + while (1) { + if (unformat(input, "src %U", unformat_ip4_address, &t->src)) + t->mask |= PNAT_SA; + else if (unformat(input, "dst %U", unformat_ip4_address, &t->dst)) + t->mask |= PNAT_DA; + else if (unformat(input, "sport %d", &sport)) { + if (sport < 0 || sport > 65535) + return 0; + t->mask |= PNAT_SPORT; + t->sport = sport; + } else if (unformat(input, "dport %d", &dport)) { + if (dport < 0 || dport > 65535) + return 0; + t->mask |= PNAT_DPORT; + t->dport = dport; + } else if (unformat(input, "proto %U", unformat_ip_protocol, &t->proto)) + ; + else + break; + } + return 1; +} + +static clib_error_t *set_pnat_translation_command_fn(vlib_main_t *vm, + unformat_input_t *input, + vlib_cli_command_t *cmd) { + unformat_input_t _line_input, *line_input = &_line_input; + clib_error_t *error = 0; + bool in = false, out = false; + bool match_set = false, rewrite_set = false; + bool add = true; + u32 sw_if_index = ~0; + pnat_5tuple_t match = {0}; + pnat_5tuple_t rewrite = {0}; + + /* Get a line of input. */ + if (!unformat_user(input, unformat_line_input, line_input)) + return 0; + + while (unformat_check_input(line_input) != UNFORMAT_END_OF_INPUT) { + if (unformat(line_input, "match %U", unformat_pnat_5tuple, &match)) + match_set = true; + else if (unformat(line_input, "rewrite %U", unformat_pnat_5tuple, + &rewrite)) + rewrite_set = true; + else if (unformat(line_input, "interface %U", + unformat_vnet_sw_interface, vnet_get_main(), + &sw_if_index)) + ; + else if (unformat(line_input, "in")) { + in = true; + } else if (unformat(line_input, "out")) { + out = true; + } else if (unformat(line_input, "del")) { + add = false; + } else { + error = clib_error_return(0, "unknown input `%U'", + format_unformat_error, line_input); + goto done; + } + } + if (sw_if_index == ~0) { + error = clib_error_return(0, "interface is required `%U'", + format_unformat_error, line_input); + goto done; + } + if ((in && out) || (!in && !out)) { + error = clib_error_return(0, "in or out is required `%U'", + format_unformat_error, line_input); + goto done; + } + if (!match_set) { + error = clib_error_return(0, "missing parameter: match `%U'", + format_unformat_error, line_input); + goto done; + } + if (!rewrite_set) { + error = clib_error_return(0, "missing parameter: rewrite `%U'", + format_unformat_error, line_input); + goto done; + } + + if ((match.dport || match.sport) && + (match.proto != 17 && match.proto != 6)) { + error = clib_error_return(0, "missing protocol (TCP|UDP): match `%U'", + format_unformat_error, line_input); + goto done; + } + pnat_attachment_point_t attachment = in ? PNAT_IP4_INPUT : PNAT_IP4_OUTPUT; + + if (add) { + u32 binding_index; + int rv = pnat_binding_add(&match, &rewrite, &binding_index); + if (rv) { + error = clib_error_return(0, "Adding binding failed %d", rv); + goto done; + } + rv = pnat_binding_attach(sw_if_index, attachment, binding_index); + if (rv) { + pnat_binding_del(binding_index); + error = clib_error_return( + 0, "Attaching binding to interface failed %d", rv); + goto done; + } + } else { + /* Lookup binding and lookup interface if both exists proceed with + * delete */ + u32 binding_index = pnat_flow_lookup(sw_if_index, attachment, &match); + if (binding_index == ~0) { + error = clib_error_return(0, "Binding does not exist"); + goto done; + } + pnat_attachment_point_t attachment = + in ? PNAT_IP4_INPUT : PNAT_IP4_OUTPUT; + int rv = pnat_binding_detach(sw_if_index, attachment, binding_index); + if (rv) { + error = clib_error_return(0, "Detaching binding failed %d %d", + binding_index, rv); + goto done; + } + rv = pnat_binding_del(binding_index); + if (rv) { + error = clib_error_return(0, "Deleting translation failed %d %d", + binding_index, rv); + goto done; + } + } + +done: + unformat_free(line_input); + + return error; +} + +VLIB_CLI_COMMAND(set_pnat_translation_command, static) = { + .path = "set pnat translation", + .short_help = "set pnat translation interface <name> match <5-tuple> " + "rewrite <5-tuple> {in|out} [del]", + .function = set_pnat_translation_command_fn, +}; + +static clib_error_t * +show_pnat_translations_command_fn(vlib_main_t *vm, unformat_input_t *input, + vlib_cli_command_t *cmd) { + pnat_main_t *pm = &pnat_main; + pnat_translation_t *s; + clib_error_t *error = 0; + + /* Get a line of input. */ + pool_foreach(s, pm->translations) { + vlib_cli_output(vm, "%U", format_pnat_translation, s - pm->translations, + s); + } + return error; +} + +VLIB_CLI_COMMAND(show_pnat_translations_command, static) = { + .path = "show pnat translations", + .short_help = "show pnat translations", + .function = show_pnat_translations_command_fn, +}; + +static clib_error_t *show_pnat_interfaces_command_fn(vlib_main_t *vm, + unformat_input_t *input, + vlib_cli_command_t *cmd) { + pnat_main_t *pm = &pnat_main; + pnat_interface_t *interface; + clib_error_t *error = 0; + + /* Get a line of input. */ + pool_foreach(interface, pm->interfaces) { + vlib_cli_output(vm, "%U", format_pnat_interface, interface); + } + return error; +} + +VLIB_CLI_COMMAND(show_pnat_interfaces_command, static) = { + .path = "show pnat interfaces", + .short_help = "show pnat interfaces", + .function = show_pnat_interfaces_command_fn, +}; diff --git a/src/plugins/nat/pnat/pnat_node.c b/src/plugins/nat/pnat/pnat_node.c new file mode 100644 index 00000000000..0209b49b0aa --- /dev/null +++ b/src/plugins/nat/pnat/pnat_node.c @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021 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. + */ + +/* + * Policy NAT. + * Match packet against rule in a hash and translate according to given + * instructions. Rules are kept in a flow-cache bihash. Instructions in a pool + * of translation entries. + * + * All rules for a given interface/direction must use the same lookup pattern. + * E.g. SA+SP. + * + * A dynamic NAT would punt to slow path on a miss in the flow cache, in this + * case the miss behaviour is configurable. Default behaviour is pass packet + * along unchanged. + * + * The data structures are shared and assuming that updates to the tables are + * rare. Data-structures are protected depending on the API/CLI barriers. + */ + +#include <stdbool.h> +#include <vlib/vlib.h> +#include <pnat/pnat.api_enum.h> /* For error counters */ +#include "pnat_node.h" /* Graph nodes */ + +VLIB_NODE_FN(pnat_input_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) { + return pnat_node_inline(vm, node, frame, PNAT_IP4_INPUT, VLIB_RX); +} +VLIB_NODE_FN(pnat_output_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) { + return pnat_node_inline(vm, node, frame, PNAT_IP4_OUTPUT, VLIB_TX); +} + +VLIB_REGISTER_NODE(pnat_input_node) = { + .name = "pnat-input", + .vector_size = sizeof(u32), + .format_trace = format_pnat_trace, + .type = VLIB_NODE_TYPE_INTERNAL, + .n_errors = PNAT_N_ERROR, + .error_counters = pnat_error_counters, + .n_next_nodes = PNAT_N_NEXT, + .next_nodes = + { + [PNAT_NEXT_DROP] = "error-drop", + }, +}; + +VLIB_REGISTER_NODE(pnat_output_node) = { + .name = "pnat-output", + .vector_size = sizeof(u32), + .format_trace = format_pnat_trace, + .type = VLIB_NODE_TYPE_INTERNAL, + .n_errors = PNAT_N_ERROR, + .error_counters = pnat_error_counters, + .sibling_of = "pnat-input", +}; + +/* Hook up features */ +VNET_FEATURE_INIT(pnat_input, static) = { + .arc_name = "ip4-unicast", + .node_name = "pnat-input", + .runs_after = VNET_FEATURES("acl-plugin-in-ip4-fa", + "ip4-sv-reassembly-feature"), +}; +VNET_FEATURE_INIT(pnat_output, static) = { + .arc_name = "ip4-output", + .node_name = "pnat-output", + .runs_after = VNET_FEATURES("acl-plugin-out-ip4-fa", + "ip4-sv-reassembly-output-feature"), +}; diff --git a/src/plugins/nat/pnat/pnat_node.h b/src/plugins/nat/pnat/pnat_node.h new file mode 100644 index 00000000000..3f2235509fa --- /dev/null +++ b/src/plugins/nat/pnat/pnat_node.h @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2021 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_pnat_node_h +#define included_pnat_node_h + +#include "pnat.h" +#include <pnat/pnat.api_enum.h> +#include <vnet/feature/feature.h> +#include <vnet/udp/udp_packet.h> +#include <vnet/ip/format.h> + +/* PNAT next-nodes */ +typedef enum { PNAT_NEXT_DROP, PNAT_N_NEXT } pnat_next_t; + +// u8 *format_pnat_key(u8 *s, va_list *args); +u8 *format_pnat_5tuple(u8 *s, va_list *args); +static inline u8 *format_pnat_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 *); + pnat_trace_t *t = va_arg(*args, pnat_trace_t *); + + s = format(s, "pnat: index %d\n", t->pool_index); + if (t->pool_index != ~0) { + s = format(s, " match: %U\n", format_pnat_5tuple, &t->match); + s = format(s, " rewrite: %U", format_pnat_5tuple, &t->rewrite); + } + return s; +} + +/* + * Given a packet and rewrite instructions from a translation modify packet. + */ +static u32 pnat_rewrite_ip4(u32 pool_index, ip4_header_t *ip) { + pnat_main_t *pm = &pnat_main; + if (pool_is_free_index(pm->translations, pool_index)) + return PNAT_ERROR_REWRITE; + pnat_translation_t *t = pool_elt_at_index(pm->translations, pool_index); + + ip_csum_t csumd = 0; + + if (t->instructions & PNAT_INSTR_DESTINATION_ADDRESS) { + csumd = ip_csum_sub_even(csumd, ip->dst_address.as_u32); + csumd = ip_csum_add_even(csumd, t->post_da.as_u32); + ip->dst_address = t->post_da; + } + if (t->instructions & PNAT_INSTR_SOURCE_ADDRESS) { + csumd = ip_csum_sub_even(csumd, ip->src_address.as_u32); + csumd = ip_csum_add_even(csumd, t->post_sa.as_u32); + ip->src_address = t->post_sa; + } + + ip_csum_t csum = ip->checksum; + csum = ip_csum_sub_even(csum, csumd); + ip->checksum = ip_csum_fold(csum); + ASSERT(ip->checksum == ip4_header_checksum(ip)); + + /* L4 ports */ + if (ip->protocol == IP_PROTOCOL_TCP) { + tcp_header_t *tcp = ip4_next_header(ip); + ip_csum_t l4csum = tcp->checksum; + if (t->instructions & PNAT_INSTR_DESTINATION_PORT) { + l4csum = ip_csum_sub_even(l4csum, tcp->dst_port); + l4csum = ip_csum_add_even(l4csum, clib_net_to_host_u16(t->post_dp)); + tcp->dst_port = clib_net_to_host_u16(t->post_dp); + } + if (t->instructions & PNAT_INSTR_SOURCE_PORT) { + l4csum = ip_csum_sub_even(l4csum, tcp->src_port); + l4csum = ip_csum_add_even(l4csum, clib_net_to_host_u16(t->post_sp)); + tcp->src_port = clib_net_to_host_u16(t->post_sp); + } + l4csum = ip_csum_sub_even(l4csum, csumd); + tcp->checksum = ip_csum_fold(l4csum); + } else if (ip->protocol == IP_PROTOCOL_UDP) { + udp_header_t *udp = ip4_next_header(ip); + ip_csum_t l4csum = udp->checksum; + if (t->instructions & PNAT_INSTR_DESTINATION_PORT) { + l4csum = ip_csum_sub_even(l4csum, udp->dst_port); + l4csum = ip_csum_add_even(l4csum, clib_net_to_host_u16(t->post_dp)); + udp->dst_port = clib_net_to_host_u16(t->post_dp); + } + if (t->instructions & PNAT_INSTR_SOURCE_PORT) { + l4csum = ip_csum_sub_even(l4csum, udp->src_port); + l4csum = ip_csum_add_even(l4csum, clib_net_to_host_u16(t->post_sp)); + udp->src_port = clib_net_to_host_u16(t->post_sp); + } + if (udp->checksum) { + l4csum = ip_csum_sub_even(l4csum, csumd); + udp->checksum = ip_csum_fold(l4csum); + } + } + return PNAT_ERROR_NONE; +} + +/* + * Lookup the packet tuple in the flow cache, given the lookup mask. + * If a binding is found, rewrite the packet according to instructions, + * otherwise follow configured default action (forward, punt or drop) + */ +static_always_inline uword pnat_node_inline(vlib_main_t *vm, + vlib_node_runtime_t *node, + vlib_frame_t *frame, + pnat_attachment_point_t attachment, + int dir) { + pnat_main_t *pm = &pnat_main; + u32 n_left_from, *from; + u16 nexts[VLIB_FRAME_SIZE] = {0}, *next = nexts; + u32 pool_indicies[VLIB_FRAME_SIZE], *pi = pool_indicies; + vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b = bufs; + clib_bihash_kv_16_8_t kv, value; + ip4_header_t *ip0; + + from = vlib_frame_vector_args(frame); + n_left_from = frame->n_vectors; + vlib_get_buffers(vm, from, b, n_left_from); + pnat_interface_t *interface; + + /* Stage 1: build vector of flow hash (based on lookup mask) */ + while (n_left_from > 0) { + u32 sw_if_index0 = vnet_buffer(b[0])->sw_if_index[dir]; + u16 sport0 = vnet_buffer(b[0])->ip.reass.l4_src_port; + u16 dport0 = vnet_buffer(b[0])->ip.reass.l4_dst_port; + u32 iph_offset = vnet_buffer(b[0])->ip.reass.save_rewrite_length; + ip0 = (ip4_header_t *)(vlib_buffer_get_current(b[0]) + iph_offset); + interface = pnat_interface_by_sw_if_index(sw_if_index0); + ASSERT(interface); + pnat_mask_fast_t mask = interface->lookup_mask_fast[attachment]; + pnat_calc_key(sw_if_index0, attachment, ip0->src_address, + ip0->dst_address, ip0->protocol, sport0, dport0, mask, + &kv); + /* By default pass packet to next node in the feature chain */ + vnet_feature_next_u16(next, b[0]); + + if (clib_bihash_search_16_8(&pm->flowhash, &kv, &value) == 0) { + /* Cache hit */ + *pi = value.value; + u32 iph_offset = vnet_buffer(b[0])->ip.reass.save_rewrite_length; + ip0 = (ip4_header_t *)(vlib_buffer_get_current(b[0]) + iph_offset); + u32 errno0 = pnat_rewrite_ip4(value.value, ip0); + if (PREDICT_FALSE(errno0)) { + next[0] = PNAT_NEXT_DROP; + b[0]->error = node->errors[errno0]; + } + } else { + /* Cache miss */ + *pi = ~0; + } + next += 1; + + /*next: */ + n_left_from -= 1; + b += 1; + pi += 1; + } + + /* Packet trace */ + if (PREDICT_FALSE((node->flags & VLIB_NODE_FLAG_TRACE))) { + u32 i; + b = bufs; + pi = pool_indicies; + for (i = 0; i < frame->n_vectors; i++) { + if (b[0]->flags & VLIB_BUFFER_IS_TRACED) { + pnat_trace_t *t = vlib_add_trace(vm, node, b[0], sizeof(*t)); + if (*pi != ~0) { + if (!pool_is_free_index(pm->translations, *pi)) { + pnat_translation_t *tr = + pool_elt_at_index(pm->translations, *pi); + t->match = tr->match; + t->rewrite = tr->rewrite; + } + } + t->pool_index = *pi; + b += 1; + pi += 1; + } else + break; + } + } + + vlib_buffer_enqueue_to_next(vm, node, from, nexts, frame->n_vectors); + + return frame->n_vectors; +} +#endif diff --git a/src/plugins/nat/pnat/pnat_test.c b/src/plugins/nat/pnat/pnat_test.c new file mode 100644 index 00000000000..762b4bdb3f0 --- /dev/null +++ b/src/plugins/nat/pnat/pnat_test.c @@ -0,0 +1,504 @@ +/* + * Copyright (c) 2021 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 <stdbool.h> +#include <assert.h> +#include <vlib/vlib.h> +#include <vnet/feature/feature.h> +#include <vppinfra/clib_error.h> +#include <vnet/ip/ip4_packet.h> +#include <vnet/udp/udp.h> +#include <vppinfra/bihash_16_8.h> +#include <vppinfra/bihash_template.c> +#include <vnet/fib/ip4_fib.h> +#include "pnat.h" +#include <pnat/pnat.api_enum.h> /* For error counters */ +#include <arpa/inet.h> +#include "pnat_test_stubs.h" + +/* +** Buffer management in test setup +** Allocate buffers return vector of buffer indicies. +** +** Setup frame with buffers when calling function. +** Global vector of all buffers with their indicies? +** Convert buffer index to pointer? +*/ +struct buffers { + u8 data[2048]; +}; +struct buffers buffers[256]; +struct buffers expected[256]; +u32 *buffers_vector = 0; + +static u32 *buffer_init(u32 *vector, int count) { + int i; + for (i = 0; i < count; i++) { + vec_add1(vector, i); + } + return vector; +} +#define PNAT_TEST_DEBUG 0 + +u32 *results_bi = 0; /* global vector of result buffers */ +u16 *results_next = 0; +vlib_node_runtime_t *node; + +#define log_info(M, ...) \ + fprintf(stderr, "\033[32;1m[OK] " M "\033[0m\n", ##__VA_ARGS__) +#define log_error(M, ...) \ + fprintf(stderr, "\033[31;1m[ERROR] (%s:%d:) " M "\033[0m\n", __FILE__, \ + __LINE__, ##__VA_ARGS__) +#define test_assert(A, M, ...) \ + if (!(A)) { \ + log_error(M, ##__VA_ARGS__); \ + assert(A); \ + } else { \ + log_info(M, ##__VA_ARGS__); \ + } + +/* + * Always return the frame of generated packets + */ +#define vlib_frame_vector_args test_vlib_frame_vector_args +void *test_vlib_frame_vector_args(vlib_frame_t *f) { return buffers_vector; } + +/* Synthetic value for vnet_feature_next */ +#define NEXT_PASSTHROUGH 4242 + +#define vnet_feature_next_u16 test_vnet_feature_next_u16 +void vnet_feature_next_u16(u16 *next0, vlib_buffer_t *b0) { + *next0 = NEXT_PASSTHROUGH; +} + +/* Gather output packets */ +#define vlib_buffer_enqueue_to_next test_vlib_buffer_enqueue_to_next +void test_vlib_buffer_enqueue_to_next(vlib_main_t *vm, + vlib_node_runtime_t *node, u32 *buffers, + u16 *nexts, uword count) { + vec_add(results_next, nexts, count); + vec_add(results_bi, buffers, count); +} + +pnat_trace_t trace = {0}; +#define vlib_add_trace test_vlib_add_trace +void *test_vlib_add_trace(vlib_main_t *vm, vlib_node_runtime_t *r, + vlib_buffer_t *b, u32 n_data_bytes) { + return &trace; +} + +#define vlib_get_buffers test_vlib_get_buffers +void test_vlib_get_buffers(vlib_main_t *vm, u32 *bi, vlib_buffer_t **b, + int count) { + int i; + for (i = 0; i < count; i++) { + b[i] = (vlib_buffer_t *)&buffers[bi[i]]; + } +} + +vlib_buffer_t *test_vlib_get_buffer(u32 bi) { + return (vlib_buffer_t *)&buffers[bi]; +} + +/* Must be included here to allow the above functions to override */ +#include "pnat_node.h" + +/*** TESTS ***/ + +typedef struct { + char *src; + char *dst; + u8 proto; + u16 sport; + u16 dport; +} test_5tuple_t; + +typedef struct { + char *name; + test_5tuple_t send; + test_5tuple_t expect; + u32 expect_next_index; +} test_t; + +test_t tests[] = { + { + .name = "da rewritten", + .send = {"1.1.1.1", "2.2.2.2", 17, 80, 6871}, + .expect = {"1.1.1.1", "1.2.3.4", 17, 80, 6871}, + .expect_next_index = NEXT_PASSTHROUGH, + }, + { + .name = "unchanged", + .send = {"1.1.1.1", "2.2.2.2", 17, 80, 8080}, + .expect = {"1.1.1.1", "2.2.2.2", 17, 80, 8080}, + .expect_next_index = NEXT_PASSTHROUGH, + }, + { + .name = "tcp da", + .send = {"1.1.1.1", "2.2.2.2", 6, 80, 6871}, + .expect = {"1.1.1.1", "1.2.3.4", 6, 80, 6871}, + .expect_next_index = NEXT_PASSTHROUGH, + }, + { + .name = "tcp da ports", + .send = {"1.1.1.1", "2.2.2.2", 6, 80, 6872}, + .expect = {"1.1.1.1", "1.2.3.4", 6, 53, 8000}, + .expect_next_index = NEXT_PASSTHROUGH, + }, +}; + +/* Rules */ +typedef struct { + test_5tuple_t match; + test_5tuple_t rewrite; + bool in; + u32 index; +} rule_t; + +rule_t rules[] = { + { + .match = {.dst = "2.2.2.2", .proto = 17, .dport = 6871}, + .rewrite = {.dst = "1.2.3.4"}, + .in = true, + }, + { + .match = {.dst = "2.2.2.2", .proto = 6, .dport = 6871}, + .rewrite = {.dst = "1.2.3.4"}, + .in = true, + }, + { + .match = {.dst = "2.2.2.2", .proto = 6, .dport = 6872}, + .rewrite = {.dst = "1.2.3.4", .sport = 53, .dport = 8000}, + .in = true, + }, + { + .match = {.dst = "2.2.2.2", .proto = 6, .dport = 6873}, + .rewrite = {.dst = "1.2.3.4", .sport = 53, .dport = 8000}, + .in = true, + }, +}; + +static int fill_packets(vlib_main_t *vm, vlib_buffer_t *b, + test_5tuple_t *test) { + b->flags |= VLIB_BUFFER_IS_TRACED; + + ip4_header_t *ip = (ip4_header_t *)vlib_buffer_get_current(b); + memset(ip, 0, sizeof(*ip)); + ip->ip_version_and_header_length = 0x45; + ip->ttl = 64; + inet_pton(AF_INET, test->src, &ip->src_address.as_u32); + inet_pton(AF_INET, test->dst, &ip->dst_address.as_u32); + ip->protocol = test->proto; + + if (test->proto == IP_PROTOCOL_UDP) { + udp_header_t *udp = ip4_next_header(ip); + memset(udp, 0, sizeof(*udp)); + udp->dst_port = htons(test->dport); + udp->src_port = htons(test->sport); + udp->length = htons(8); + vnet_buffer(b)->ip.reass.l4_src_port = udp->src_port; + vnet_buffer(b)->ip.reass.l4_dst_port = udp->dst_port; + b->current_length = 28; + ip->length = htons(b->current_length); + ip->checksum = ip4_header_checksum(ip); + udp->checksum = ip4_tcp_udp_compute_checksum(vm, b, ip); + } else if (test->proto == IP_PROTOCOL_TCP) { + tcp_header_t *tcp = ip4_next_header(ip); + memset(tcp, 0, sizeof(*tcp)); + tcp->dst_port = htons(test->dport); + tcp->src_port = htons(test->sport); + vnet_buffer(b)->ip.reass.l4_src_port = tcp->src_port; + vnet_buffer(b)->ip.reass.l4_dst_port = tcp->dst_port; + b->current_length = sizeof(ip4_header_t) + sizeof(tcp_header_t); + ip->length = htons(b->current_length); + ip->checksum = ip4_header_checksum(ip); + tcp->checksum = ip4_tcp_udp_compute_checksum(vm, b, ip); + } else { + b->current_length = sizeof(ip4_header_t); + ip->length = htons(b->current_length); + ip->checksum = ip4_header_checksum(ip); + vnet_buffer(b)->ip.reass.l4_src_port = 0; + vnet_buffer(b)->ip.reass.l4_dst_port = 0; + } + + return 0; +} + +static void ruleto5tuple(test_5tuple_t *r, pnat_5tuple_t *t) { + if (r->src) { + inet_pton(AF_INET, r->src, &t->src); + t->mask |= PNAT_SA; + } + if (r->dst) { + inet_pton(AF_INET, r->dst, &t->dst); + t->mask |= PNAT_DA; + } + if (r->dport) { + t->dport = r->dport; + t->mask |= PNAT_DPORT; + } + if (r->sport) { + t->sport = r->sport; + t->mask |= PNAT_SPORT; + } + t->proto = r->proto; +} + +static void add_translation(rule_t *r) { + pnat_5tuple_t match = {0}; + pnat_5tuple_t rewrite = {0}; + + ruleto5tuple(&r->match, &match); + ruleto5tuple(&r->rewrite, &rewrite); + + int rv = pnat_binding_add(&match, &rewrite, &r->index); + assert(rv == 0); + + rv = pnat_binding_attach(0, PNAT_IP4_INPUT, r->index); + assert(rv == 0); +} + +static void del_translation(rule_t *r) { + int rv = pnat_binding_detach(0, PNAT_IP4_INPUT, r->index); + assert(rv == 0); + + rv = pnat_binding_del(r->index); + assert(rv == 0); +} + +static void validate_packet(vlib_main_t *vm, char *name, u32 bi, + vlib_buffer_t *expected_b) { + vlib_buffer_t *b = test_vlib_get_buffer(bi); + assert(b); + + ip4_header_t *ip = (ip4_header_t *)vlib_buffer_get_current(b); + ip4_header_t *expected_ip = + (ip4_header_t *)vlib_buffer_get_current(expected_b); + +#if PNAT_TEST_DEBUG + clib_warning("Received packet: %U", format_ip4_header, ip, 20); + clib_warning("Expected packet: %U", format_ip4_header, expected_ip, 20); + tcp_header_t *tcp = ip4_next_header(ip); + clib_warning("IP: %U TCP: %U", format_ip4_header, ip, sizeof(*ip), + format_tcp_header, tcp, sizeof(*tcp)); + tcp = ip4_next_header(expected_ip); + clib_warning("IP: %U TCP: %U", format_ip4_header, expected_ip, sizeof(*ip), + format_tcp_header, tcp, sizeof(*tcp)); +#endif + + u32 flags = ip4_tcp_udp_validate_checksum(vm, b); + assert((flags & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0); + flags = ip4_tcp_udp_validate_checksum(vm, expected_b); + assert((flags & VNET_BUFFER_F_L4_CHECKSUM_CORRECT) != 0); + assert(b->current_length == expected_b->current_length); + + test_assert(memcmp(ip, expected_ip, b->current_length) == 0, "%s", name); +} + +extern vlib_node_registration_t pnat_input_node; + +static void test_table(test_t *t, int no_tests) { + // walk through table of tests + int i; + vlib_main_t *vm = &vlib_global_main; + + /* Generate packet data */ + for (i = 0; i < no_tests; i++) { + // create input buffer(s) + fill_packets(vm, (vlib_buffer_t *)&buffers[i], &t[i].send); + fill_packets(vm, (vlib_buffer_t *)&expected[i], &t[i].expect); + } + + /* send packets through graph node */ + vlib_frame_t frame = {.n_vectors = no_tests}; + node->flags |= VLIB_NODE_FLAG_TRACE; + + pnat_node_inline(vm, node, &frame, PNAT_IP4_INPUT, VLIB_RX); + + /* verify tests */ + for (i = 0; i < no_tests; i++) { + assert(t[i].expect_next_index == results_next[i]); + validate_packet(vm, t[i].name, results_bi[i], + (vlib_buffer_t *)&expected[i]); + } + vec_free(results_next); + vec_free(results_bi); +} + +static void test_performance(void) { + pnat_main_t *pm = &pnat_main; + int i; + vlib_main_t *vm = &vlib_global_main; + + for (i = 0; i < sizeof(rules) / sizeof(rules[0]); i++) { + add_translation(&rules[i]); + } + assert(pool_elts(pm->translations) == sizeof(rules) / sizeof(rules[0])); + + int no_tests = sizeof(tests) / sizeof(tests[0]); + /* Generate packet data */ + for (i = 0; i < VLIB_FRAME_SIZE; i++) { + // create input buffer(s) + fill_packets(vm, (vlib_buffer_t *)&buffers[i], + &tests[i % no_tests].send); + // fill_packets(vm, (vlib_buffer_t *)&expected[i], &tests[i % + // no_tests].expect); + } + + /* send packets through graph node */ + vlib_frame_t frame = {.n_vectors = VLIB_FRAME_SIZE}; + node->flags &= ~VLIB_NODE_FLAG_TRACE; + + int j; + for (j = 0; j < 10000; j++) { + pnat_node_inline(vm, node, &frame, PNAT_IP4_INPUT, VLIB_RX); + +#if 0 + for (i = 0; i < VLIB_FRAME_SIZE; i++) { + assert(tests[i % no_tests].expect_next_index == results_next[i]); + validate_packet(vm, tests[i % no_tests].name, results_bi[i], (vlib_buffer_t *)&expected[i]); + } +#endif + vec_free(results_next); + vec_free(results_bi); + } + + for (i = 0; i < sizeof(rules) / sizeof(rules[0]); i++) { + del_translation(&rules[i]); + } + assert(pool_elts(pm->translations) == 0); + assert(pool_elts(pm->interfaces) == 0); +} + +static void test_packets(void) { + pnat_main_t *pm = &pnat_main; + int i; + for (i = 0; i < sizeof(rules) / sizeof(rules[0]); i++) { + add_translation(&rules[i]); + } + assert(pool_elts(pm->translations) == sizeof(rules) / sizeof(rules[0])); + + test_table(tests, sizeof(tests) / sizeof(tests[0])); + + for (i = 0; i < sizeof(rules) / sizeof(rules[0]); i++) { + del_translation(&rules[i]); + } + assert(pool_elts(pm->translations) == 0); + assert(pool_elts(pm->interfaces) == 0); +} +static void test_attach(void) { + pnat_attachment_point_t attachment = PNAT_IP4_INPUT; + u32 binding_index = 0; + u32 sw_if_index = 0; + int rv = pnat_binding_attach(sw_if_index, attachment, binding_index); + test_assert(rv == -1, "binding_attach - nothing to attach"); + + rv = pnat_binding_detach(sw_if_index, attachment, 1234); + test_assert(rv == -1, "binding_detach - nothing to detach"); + + pnat_5tuple_t match = {.mask = PNAT_SA}; + pnat_5tuple_t rewrite = {.mask = PNAT_SA}; + rv = pnat_binding_add(&match, &rewrite, &binding_index); + assert(rv == 0); + + rv = pnat_binding_attach(sw_if_index, attachment, binding_index); + test_assert(rv == 0, "binding_attach - rule"); + + rv = pnat_binding_detach(sw_if_index, attachment, binding_index); + test_assert(rv == 0, "binding_detach - rule"); + + rv = pnat_binding_del(binding_index); + assert(rv == 0); +} + +static void test_del_before_detach(void) { + pnat_attachment_point_t attachment = PNAT_IP4_INPUT; + u32 binding_index = 0; + u32 sw_if_index = 0; + + /* Ensure 5-tuple here will not duplicate with other tests cause this will + * not be removed from flow cache */ + rule_t rule = { + .match = {.dst = "123.123.123.123", .proto = 17, .dport = 6871}, + .rewrite = {.dst = "1.2.3.4"}, + .in = true, + }; + + add_translation(&rule); + + int rv = pnat_binding_del(binding_index); + assert(rv == 0); + + test_t test = { + .name = "hit missing rule", + .send = {"1.1.1.1", "123.123.123.123", 17, 80, 6871}, + .expect = {"1.1.1.1", "123.123.123.123", 17, 80, 6871}, + .expect_next_index = PNAT_NEXT_DROP, + }; + + test_table(&test, 1); + + /* For now if you have deleted before detach, can't find key */ + rv = pnat_binding_detach(sw_if_index, attachment, binding_index); + test_assert(rv == -1, "binding_detach - failure"); + + /* Re-add the rule and try again */ + pnat_5tuple_t match = {0}; + pnat_5tuple_t rewrite = {0}; + ruleto5tuple(&rule.match, &match); + ruleto5tuple(&rule.rewrite, &rewrite); + rv = pnat_binding_add(&match, &rewrite, &binding_index); + assert(rv == 0); + rv = pnat_binding_detach(sw_if_index, attachment, binding_index); + test_assert(rv == 0, "binding_detach - pass"); + rv = pnat_binding_del(binding_index); + assert(rv == 0); +} + +static void test_api(void) { + test_attach(); + test_del_before_detach(); +} + +/* + * Unit testing: + * 1) Table of packets and expected outcomes. Run through + * 2) Performance tests. Measure instructions, cache behaviour etc. + */ +clib_error_t *ip_checksum_init(vlib_main_t *vm); + +int main(int argc, char **argv) { + + clib_mem_init(0, 3ULL << 30); + + vlib_main_t *vm = &vlib_global_main; + + buffers_vector = buffer_init(buffers_vector, 256); + + assert(vlib_node_main_init(vm) == 0); + + ip_checksum_init(vm); + + u32 node_index = vlib_register_node(vm, &pnat_input_node); + node = vlib_node_get_runtime(vm, node_index); + assert(node); + + /* Test API */ + test_api(); + + test_packets(); + + test_performance(); +} diff --git a/src/plugins/nat/pnat/pnat_test_stubs.h b/src/plugins/nat/pnat/pnat_test_stubs.h new file mode 100644 index 00000000000..2801398407c --- /dev/null +++ b/src/plugins/nat/pnat/pnat_test_stubs.h @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2021 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_pnat_test_stubs_h +#define included_pnat_test_stubs_h + +void os_panic(void) {} +void os_exit(int code) {} +u32 ip4_fib_table_get_index_for_sw_if_index(u32 sw_if_index) { return 0; } +#include <vpp/stats/stat_segment.h> +clib_error_t *stat_segment_register_gauge(u8 *names, + stat_segment_update_fn update_fn, + u32 index) { + return 0; +}; +#include <vnet/feature/feature.h> +vnet_feature_main_t feature_main; +void classify_get_trace_chain(void){}; + +/* Format an IP4 address. */ +u8 *format_ip4_address(u8 *s, va_list *args) { + u8 *a = va_arg(*args, u8 *); + return format(s, "%d.%d.%d.%d", a[0], a[1], a[2], a[3]); +} + +u8 *format_pnat_5tuple(u8 *s, va_list *args) { return 0; } + +vl_counter_t pnat_error_counters[10]; + +int ip4_sv_reass_enable_disable_with_refcnt(u32 sw_if_index, int is_enable) { + return 0; +} +int ip4_sv_reass_output_enable_disable_with_refcnt(u32 sw_if_index, + int is_enable) { + return 0; +} +int vnet_feature_enable_disable(const char *arc_name, const char *node_name, + u32 sw_if_index, int enable_disable, + void *feature_config, + u32 n_feature_config_bytes) { + return 0; +} +vnet_main_t *vnet_get_main(void) { return 0; } + +static struct { + vec_header_t h; + vlib_main_t *vm; +} __attribute__((packed)) __bootstrap_vlib_main_vector + __attribute__((aligned(CLIB_CACHE_LINE_BYTES))) = { + .h.len = 1, + .vm = &vlib_global_main, +}; + +vlib_main_t **vlib_mains = &__bootstrap_vlib_main_vector.vm; + +/* Compute TCP/UDP/ICMP4 checksum in software. */ +u16 ip4_tcp_udp_compute_checksum(vlib_main_t *vm, vlib_buffer_t *p0, + ip4_header_t *ip0) { + ip_csum_t sum0; + u32 ip_header_length, payload_length_host_byte_order; + + /* Initialize checksum with ip header. */ + ip_header_length = ip4_header_bytes(ip0); + payload_length_host_byte_order = + clib_net_to_host_u16(ip0->length) - ip_header_length; + sum0 = clib_host_to_net_u32(payload_length_host_byte_order + + (ip0->protocol << 16)); + + if (BITS(uword) == 32) { + sum0 = ip_csum_with_carry(sum0, + clib_mem_unaligned(&ip0->src_address, u32)); + sum0 = ip_csum_with_carry(sum0, + clib_mem_unaligned(&ip0->dst_address, u32)); + } else + sum0 = ip_csum_with_carry(sum0, + clib_mem_unaligned(&ip0->src_address, u64)); + return ip_calculate_l4_checksum(vm, p0, sum0, + payload_length_host_byte_order, (u8 *)ip0, + ip_header_length, NULL); +} + +u32 ip4_tcp_udp_validate_checksum(vlib_main_t *vm, vlib_buffer_t *p0) { + ip4_header_t *ip0 = vlib_buffer_get_current(p0); + udp_header_t *udp0; + u16 sum16; + + ASSERT(ip0->protocol == IP_PROTOCOL_TCP || + ip0->protocol == IP_PROTOCOL_UDP); + + udp0 = (void *)(ip0 + 1); + if (ip0->protocol == IP_PROTOCOL_UDP && udp0->checksum == 0) { + p0->flags |= (VNET_BUFFER_F_L4_CHECKSUM_COMPUTED | + VNET_BUFFER_F_L4_CHECKSUM_CORRECT); + return p0->flags; + } + + sum16 = ip4_tcp_udp_compute_checksum(vm, p0, ip0); + + p0->flags |= (VNET_BUFFER_F_L4_CHECKSUM_COMPUTED | + ((sum16 == 0) << VNET_BUFFER_F_LOG2_L4_CHECKSUM_CORRECT)); + + return p0->flags; +} +u8 *format_tcp_header(u8 *s, va_list *args) { + tcp_header_t *tcp = va_arg(*args, tcp_header_t *); + u32 max_header_bytes = va_arg(*args, u32); + u32 header_bytes; + u32 indent; + + /* Nothing to do. */ + if (max_header_bytes < sizeof(tcp[0])) + return format(s, "TCP header truncated"); + + indent = format_get_indent(s); + indent += 2; + header_bytes = tcp_header_bytes(tcp); + + s = format(s, "TCP: %d -> %d", clib_net_to_host_u16(tcp->src), + clib_net_to_host_u16(tcp->dst)); + + s = format(s, "\n%Useq. 0x%08x ack 0x%08x", format_white_space, indent, + clib_net_to_host_u32(tcp->seq_number), + clib_net_to_host_u32(tcp->ack_number)); + + s = format(s, "\n%Utcp header: %d bytes", format_white_space, indent, + tcp->flags, header_bytes); + + s = format(s, "\n%Uwindow %d, checksum 0x%04x", format_white_space, indent, + clib_net_to_host_u16(tcp->window), + clib_net_to_host_u16(tcp->checksum)); + return s; +} + +/* Format an IP4 header. */ +u8 *format_ip4_header(u8 *s, va_list *args) { + ip4_header_t *ip = va_arg(*args, ip4_header_t *); + u32 max_header_bytes = va_arg(*args, u32); + u32 ip_version, header_bytes; + u32 indent; + + /* Nothing to do. */ + if (max_header_bytes < sizeof(ip[0])) + return format(s, "IP header truncated"); + + indent = format_get_indent(s); + indent += 2; + + ip_version = (ip->ip_version_and_header_length >> 4); + header_bytes = (ip->ip_version_and_header_length & 0xf) * sizeof(u32); + + s = format(s, "%d: %U -> %U", ip->protocol, format_ip4_address, + ip->src_address.data, format_ip4_address, ip->dst_address.data); + + /* Show IP version and header length only with unexpected values. */ + if (ip_version != 4 || header_bytes != sizeof(ip4_header_t)) + s = format(s, "\n%Uversion %d, header length %d", format_white_space, + indent, ip_version, header_bytes); + + s = format(s, "\n%Utos 0x%02x, ttl %d, length %d, checksum 0x%04x", + format_white_space, indent, ip->tos, ip->ttl, + clib_net_to_host_u16(ip->length), + clib_net_to_host_u16(ip->checksum)); + + /* Check and report invalid checksums. */ + { + if (!ip4_header_checksum_is_valid(ip)) + s = format(s, " (should be 0x%04x)", + clib_net_to_host_u16(ip4_header_checksum(ip))); + } + + { + u32 f = clib_net_to_host_u16(ip->flags_and_fragment_offset); + u32 o; + + s = format(s, "\n%Ufragment id 0x%04x", format_white_space, indent, + clib_net_to_host_u16(ip->fragment_id)); + + /* Fragment offset. */ + o = 8 * (f & 0x1fff); + f ^= f & 0x1fff; + if (o != 0) + s = format(s, " offset %d", o); + + if (f != 0) { + s = format(s, ", flags "); +#define _(l) \ + if (f & IP4_HEADER_FLAG_##l) \ + s = format(s, #l); + _(MORE_FRAGMENTS); + _(DONT_FRAGMENT); + _(CONGESTION); +#undef _ + } + /* Fragment packet but not the first. */ + if (o != 0) + return s; + } + + return s; +} + +#endif diff --git a/src/plugins/nat/test/test_pnat.py b/src/plugins/nat/test/test_pnat.py new file mode 100644 index 00000000000..5e52fa9f135 --- /dev/null +++ b/src/plugins/nat/test/test_pnat.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +"""Policy 1:1 NAT functional tests""" + +import unittest +from scapy.layers.inet import Ether, IP, UDP, ICMP +from framework import VppTestCase, VppTestRunner +from vpp_papi import VppEnum + + +class TestPNAT(VppTestCase): + """ PNAT Test Case """ + maxDiff = None + + @classmethod + def setUpClass(cls): + super(TestPNAT, cls).setUpClass() + cls.create_pg_interfaces(range(2)) + cls.interfaces = list(cls.pg_interfaces) + + @classmethod + def tearDownClass(cls): + super(TestPNAT, cls).tearDownClass() + + def setUp(self): + super(TestPNAT, self).setUp() + for i in self.interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + + def tearDown(self): + super(TestPNAT, self).tearDown() + if not self.vpp_dead: + for i in self.pg_interfaces: + i.unconfig_ip4() + i.admin_down() + + def validate(self, rx, expected): + self.assertEqual(rx, expected.__class__(expected)) + + def validate_bytes(self, rx, expected): + self.assertEqual(rx, expected) + + def ping_check(self): + """ Verify non matching traffic works. """ + p_ether = Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) + + icmpecho = (IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) / + ICMP()) + reply = (IP(src=self.pg0.local_ip4, dst=self.pg0.remote_ip4) / + ICMP(type='echo-reply')) + rx = self.send_and_expect(self.pg0, p_ether/icmpecho * 1, self.pg0) + for p in rx: + reply[IP].id = p[IP].id + self.validate(p[1], reply) + + def test_pnat(self): + """ PNAT test """ + + PNAT_IP4_INPUT = VppEnum.vl_api_pnat_attachment_point_t.PNAT_IP4_INPUT + PNAT_IP4_OUTPUT = \ + VppEnum.vl_api_pnat_attachment_point_t.PNAT_IP4_OUTPUT + + tests = [ + { + 'input': PNAT_IP4_INPUT, + 'sw_if_index': self.pg0.sw_if_index, + 'match': {'mask': 0xa, 'dst': '10.10.10.10', 'proto': 17, + 'dport': 6871}, + 'rewrite': {'mask': 0x2, 'dst': self.pg1.remote_ip4}, + 'send': (IP(src=self.pg0.remote_ip4, dst='10.10.10.10') / + UDP(dport=6871)), + 'reply': (IP(src=self.pg0.remote_ip4, + dst=self.pg1.remote_ip4) / + UDP(dport=6871)) + }, + { + 'input': PNAT_IP4_OUTPUT, + 'sw_if_index': self.pg1.sw_if_index, + 'match': {'mask': 0x9, 'src': self.pg0.remote_ip4, 'proto': 17, + 'dport': 6871}, + 'rewrite': {'mask': 0x1, 'src': '11.11.11.11'}, + 'send': (IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + UDP(dport=6871)), + 'reply': (IP(src='11.11.11.11', dst=self.pg1.remote_ip4) / + UDP(dport=6871)) + }, + { + 'input': PNAT_IP4_INPUT, + 'sw_if_index': self.pg0.sw_if_index, + 'match': {'mask': 0xa, 'dst': '10.10.10.10', 'proto': 17, + 'dport': 6871}, + 'rewrite': {'mask': 0xa, 'dst': self.pg1.remote_ip4, + 'dport': 5555}, + 'send': (IP(src=self.pg0.remote_ip4, dst='10.10.10.10') / + UDP(dport=6871)), + 'reply': (IP(src=self.pg0.remote_ip4, + dst=self.pg1.remote_ip4) / + UDP(dport=5555)) + }, + { + 'input': PNAT_IP4_INPUT, + 'sw_if_index': self.pg0.sw_if_index, + 'match': {'mask': 0xa, 'dst': self.pg1.remote_ip4, 'proto': 17, + 'dport': 6871}, + 'rewrite': {'mask': 0x8, 'dport': 5555}, + 'send': (IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + UDP(dport=6871, chksum=0)), + 'reply': (IP(src=self.pg0.remote_ip4, + dst=self.pg1.remote_ip4) / + UDP(dport=5555, chksum=0)) + }, + { + 'input': PNAT_IP4_INPUT, + 'sw_if_index': self.pg0.sw_if_index, + 'match': {'mask': 0x2, 'dst': self.pg1.remote_ip4, 'proto': 1}, + 'rewrite': {'mask': 0x1, 'src': '8.8.8.8'}, + 'send': (IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + ICMP()), + 'reply': IP(src='8.8.8.8', dst=self.pg1.remote_ip4)/ICMP(), + }, + ] + + p_ether = Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) + for t in tests: + rv = self.vapi.pnat_binding_add(match=t['match'], + rewrite=t['rewrite']) + self.vapi.pnat_binding_attach(sw_if_index=t['sw_if_index'], + attachment=t['input'], + binding_index=rv.binding_index) + + reply = t['reply'] + reply[IP].ttl -= 1 + rx = self.send_and_expect(self.pg0, p_ether/t['send']*1, self.pg1) + for p in rx: + # p.show2() + self.validate(p[1], reply) + + self.ping_check() + + self.vapi.pnat_binding_detach(sw_if_index=t['sw_if_index'], + attachment=t['input'], + binding_index=rv.binding_index) + self.vapi.pnat_binding_del(binding_index=rv.binding_index) + + def test_pnat_show(self): + """ PNAT show tests """ + + PNAT_IP4_INPUT = VppEnum.vl_api_pnat_attachment_point_t.PNAT_IP4_INPUT + PNAT_IP4_OUTPUT = \ + VppEnum.vl_api_pnat_attachment_point_t.PNAT_IP4_OUTPUT + + tests = [ + { + 'input': PNAT_IP4_INPUT, + 'sw_if_index': self.pg0.sw_if_index, + 'match': {'mask': 0xa, 'dst': '10.10.10.10', 'proto': 17, + 'dport': 6871}, + 'rewrite': {'mask': 0x2, 'dst': self.pg1.remote_ip4}, + 'send': (IP(src=self.pg0.remote_ip4, dst='10.10.10.10') / + UDP(dport=6871)), + 'reply': (IP(src=self.pg0.remote_ip4, + dst=self.pg1.remote_ip4) / + UDP(dport=6871)) + }, + { + 'input': PNAT_IP4_OUTPUT, + 'sw_if_index': self.pg1.sw_if_index, + 'match': {'mask': 0x9, 'src': self.pg0.remote_ip4, 'proto': 17, + 'dport': 6871}, + 'rewrite': {'mask': 0x1, 'src': '11.11.11.11'}, + 'send': (IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + UDP(dport=6871)), + 'reply': (IP(src='11.11.11.11', dst=self.pg1.remote_ip4) / + UDP(dport=6871)) + }, + ] + binding_index = [] + for t in tests: + rv = self.vapi.pnat_binding_add(match=t['match'], + rewrite=t['rewrite']) + binding_index.append(rv.binding_index) + self.vapi.pnat_binding_attach(sw_if_index=t['sw_if_index'], + attachment=t['input'], + binding_index=rv.binding_index) + + rv, l = self.vapi.pnat_bindings_get() + self.assertEqual(len(l), len(tests)) + + rv, l = self.vapi.pnat_interfaces_get() + self.assertEqual(len(l), 2) + + self.logger.info(self.vapi.cli("show pnat translations")) + self.logger.info(self.vapi.cli("show pnat interfaces")) + + for i, t in enumerate(tests): + self.vapi.pnat_binding_detach(sw_if_index=t['sw_if_index'], + attachment=t['input'], + binding_index=binding_index[i]) + self.vapi.pnat_binding_del(binding_index=binding_index[i]) + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) |