aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOle Troan <otroan@employees.org>2023-08-17 13:36:08 +0200
committerOle Troan <otroan@employees.org>2023-08-25 09:15:32 +0200
commit6ee3aa41c395d036c8c79a3681cc5ee6bc6fceb9 (patch)
treefcbb367a65e0c9fc932f62fcaf2d46ff622ba9dc
parentecb62d2e5d0af14e2de143a729abdf35e132e5d5 (diff)
npt66: network prefix translation for ipv6
This is the initial commit of a NPTv6 (RFC6296) implementation for VPP. It's restricted to a single internal to external binding and runs as an output/input feature on the egress interface. Type: feature Change-Id: I0e3497af97f1ebd99377b84dbf599ecea935ca24 Signed-off-by: Ole Troan <otroan@employees.org>
-rw-r--r--MAINTAINERS5
-rw-r--r--docs/spelling_wordlist.txt2
-rw-r--r--src/plugins/npt66/CMakeLists.txt17
-rw-r--r--src/plugins/npt66/FEATURE.yaml16
-rw-r--r--src/plugins/npt66/npt66.api18
-rw-r--r--src/plugins/npt66/npt66.c116
-rw-r--r--src/plugins/npt66/npt66.h28
-rw-r--r--src/plugins/npt66/npt66_api.c71
-rw-r--r--src/plugins/npt66/npt66_cli.c88
-rw-r--r--src/plugins/npt66/npt66_node.c289
-rw-r--r--test/test_npt66.py89
11 files changed, 739 insertions, 0 deletions
diff --git a/MAINTAINERS b/MAINTAINERS
index 84894a30553..6e9b535028f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -816,6 +816,11 @@ I: bpf_trace_filter
M: Mohammed Hawari <mohammed@hawari.fr>
F: src/plugins/bpf_trace_filter
+Plugin - NPTv6
+I: npt66
+M: Ole Troan <otroan@employees.org>
+F: src/plugins/npt66
+
cJSON
I: cjson
M: Ole Troan <ot@cisco.com>
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 178a230f54c..1a0669cb4e7 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -762,6 +762,8 @@ nodaemon
noevaluate
nonaddress
nosyslog
+npt
+npt66
ns
nsess
nsh
diff --git a/src/plugins/npt66/CMakeLists.txt b/src/plugins/npt66/CMakeLists.txt
new file mode 100644
index 00000000000..aee784d96f0
--- /dev/null
+++ b/src/plugins/npt66/CMakeLists.txt
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright(c) 2023 Cisco Systems, Inc.
+
+add_vpp_plugin(npt66
+ SOURCES
+ npt66.c
+ npt66_api.c
+ npt66_cli.c
+ npt66_node.c
+
+
+ MULTIARCH_SOURCES
+ npt66_node.c
+
+ API_FILES
+ npt66.api
+)
diff --git a/src/plugins/npt66/FEATURE.yaml b/src/plugins/npt66/FEATURE.yaml
new file mode 100644
index 00000000000..8874ae22017
--- /dev/null
+++ b/src/plugins/npt66/FEATURE.yaml
@@ -0,0 +1,16 @@
+---
+name: NPTv6
+maintainer: Ole Troan <otroan@employees.org>
+features:
+ - NPTv6
+
+description: "This plugin implements NPTv6 as described in RFC6296.
+ It supports arbitrary prefix lengths. And performs an
+ algorithmic mapping between internal and external IPv6 prefixes.
+ The mapping is checksum neutral.
+ The implementation is currently limited to a single statically configured binding
+ per interface.
+ A typical IPv6 CE use case, the external prefix would be learnt via DHCP PD
+ "
+state: development
+properties: [API, CLI, MULTITHREAD]
diff --git a/src/plugins/npt66/npt66.api b/src/plugins/npt66/npt66.api
new file mode 100644
index 00000000000..01ce7759799
--- /dev/null
+++ b/src/plugins/npt66/npt66.api
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright(c) 2023 Cisco Systems, Inc.
+
+option version = "0.0.1";
+
+import "vnet/interface_types.api";
+import "vnet/ip/ip_types.api";
+
+autoendian autoreply define npt66_binding_add_del
+{
+ u32 client_index;
+ u32 context;
+
+ bool is_add;
+ vl_api_interface_index_t sw_if_index;
+ vl_api_ip6_prefix_t internal;
+ vl_api_ip6_prefix_t external;
+};
diff --git a/src/plugins/npt66/npt66.c b/src/plugins/npt66/npt66.c
new file mode 100644
index 00000000000..e3cbbbd1a7b
--- /dev/null
+++ b/src/plugins/npt66/npt66.c
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright(c) 2023 Cisco Systems, Inc.
+
+/*
+ * npt66.c: NPT66 plugin
+ * An implementation of Network Prefix Translation for IPv6-to-IPv6 (NPTv6) as
+ * specified in RFC6296.
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <vlib/vlib.h>
+#include <vnet/feature/feature.h>
+#include <vppinfra/pool.h>
+#include "npt66.h"
+
+static int
+npt66_feature_enable_disable (u32 sw_if_index, bool is_add)
+{
+ if (vnet_feature_enable_disable ("ip6-unicast", "npt66-input", sw_if_index,
+ is_add, 0, 0) != 0)
+ return -1;
+ if (vnet_feature_enable_disable ("ip6-output", "npt66-output", sw_if_index,
+ is_add, 0, 0) != 0)
+ return -1;
+ return 0;
+}
+
+static void
+ipv6_prefix_zero (ip6_address_t *address, int prefix_len)
+{
+ int byte_index = prefix_len / 8;
+ int bit_offset = prefix_len % 8;
+ uint8_t mask = (1 << (8 - bit_offset)) - 1;
+ if (byte_index < 16)
+ {
+ address->as_u8[byte_index] &= mask;
+ for (int i = byte_index + 1; i < 16; i++)
+ {
+ address->as_u8[i] = 0;
+ }
+ }
+}
+
+int
+npt66_binding_add_del (u32 sw_if_index, ip6_address_t *internal,
+ int internal_plen, ip6_address_t *external,
+ int external_plen, bool is_add)
+{
+ npt66_main_t *nm = &npt66_main;
+
+ if (is_add)
+ {
+
+ /* Ensure prefix lengths are less than or equal to a /64 */
+ if (internal_plen > 64 || external_plen > 64)
+ return VNET_API_ERROR_INVALID_VALUE;
+
+ /* Create a binding entry */
+ npt66_binding_t *b;
+ pool_get_zero (nm->bindings, b);
+ b->internal = *internal;
+ b->internal_plen = internal_plen;
+ b->external = *external;
+ b->external_plen = external_plen;
+ b->sw_if_index = sw_if_index;
+
+ ipv6_prefix_zero (&b->internal, internal_plen);
+ ipv6_prefix_zero (&b->external, external_plen);
+ vec_validate_init_empty (nm->interface_by_sw_if_index, sw_if_index, ~0);
+ nm->interface_by_sw_if_index[sw_if_index] = b - nm->bindings;
+
+ uword delta = 0;
+ delta = ip_csum_add_even (delta, b->external.as_u64[0]);
+ delta = ip_csum_add_even (delta, b->external.as_u64[1]);
+ delta = ip_csum_sub_even (delta, b->internal.as_u64[0]);
+ delta = ip_csum_sub_even (delta, b->internal.as_u64[1]);
+ delta = ip_csum_fold (delta);
+ b->delta = delta;
+ }
+ else
+ {
+ /* Delete a binding entry */
+ npt66_binding_t *b = npt66_interface_by_sw_if_index (sw_if_index);
+ if (!b)
+ return VNET_API_ERROR_NO_SUCH_ENTRY;
+ nm->interface_by_sw_if_index[sw_if_index] = ~0;
+ pool_put (nm->bindings, b);
+ }
+
+ /* Enable feature on interface */
+ int rv = npt66_feature_enable_disable (sw_if_index, is_add);
+
+ return rv;
+}
+
+/*
+ * Do a lookup in the interface vector (interface_by_sw_if_index)
+ * and return pool entry.
+ */
+npt66_binding_t *
+npt66_interface_by_sw_if_index (u32 sw_if_index)
+{
+ npt66_main_t *nm = &npt66_main;
+
+ if (!nm->interface_by_sw_if_index ||
+ sw_if_index > (vec_len (nm->interface_by_sw_if_index) - 1))
+ return 0;
+ u32 index = nm->interface_by_sw_if_index[sw_if_index];
+ if (index == ~0)
+ return 0;
+ if (pool_is_free_index (nm->bindings, index))
+ return 0;
+ return pool_elt_at_index (nm->bindings, index);
+}
diff --git a/src/plugins/npt66/npt66.h b/src/plugins/npt66/npt66.h
new file mode 100644
index 00000000000..428dadb1672
--- /dev/null
+++ b/src/plugins/npt66/npt66.h
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright(c) 2023 Cisco Systems, Inc.
+
+#include <vlib/vlib.h>
+#include <vnet/ip/ip6_packet.h>
+
+typedef struct
+{
+ u32 sw_if_index;
+ ip6_address_t internal;
+ ip6_address_t external;
+ u8 internal_plen;
+ u8 external_plen;
+ uword delta;
+} npt66_binding_t;
+typedef struct
+{
+ u32 *interface_by_sw_if_index;
+ npt66_binding_t *bindings;
+ u16 msg_id_base;
+} npt66_main_t;
+
+extern npt66_main_t npt66_main;
+
+int npt66_binding_add_del (u32 sw_if_index, ip6_address_t *internal,
+ int internal_plen, ip6_address_t *external,
+ int external_plen, bool is_add);
+npt66_binding_t *npt66_interface_by_sw_if_index (u32 sw_if_index);
diff --git a/src/plugins/npt66/npt66_api.c b/src/plugins/npt66/npt66_api.c
new file mode 100644
index 00000000000..91eb73c1e45
--- /dev/null
+++ b/src/plugins/npt66/npt66_api.c
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright(c) 2023 Cisco Systems, Inc.
+
+#include <stdbool.h>
+#include <npt66/npt66.h>
+#include <vnet/vnet.h>
+#include <npt66/npt66.api_enum.h>
+#include <npt66/npt66.api_types.h>
+#include <vlibmemory/api.h>
+#include <vnet/ip/ip.h>
+#include <vnet/ip/ip_types_api.h>
+#include <vpp/app/version.h>
+
+npt66_main_t npt66_main;
+
+/*
+ * This file contains the API handlers for the pnat.api
+ */
+
+#define REPLY_MSG_ID_BASE npt66_main.msg_id_base
+#include <vlibapi/api_helper_macros.h>
+
+static void
+vl_api_npt66_binding_add_del_t_handler (vl_api_npt66_binding_add_del_t *mp)
+{
+ vl_api_npt66_binding_add_del_reply_t *rmp;
+ int rv;
+ clib_warning ("Interface index: %d", mp->sw_if_index);
+ VALIDATE_SW_IF_INDEX_END (mp);
+
+ rv = npt66_binding_add_del (
+ mp->sw_if_index, (ip6_address_t *) &mp->internal.address, mp->internal.len,
+ (ip6_address_t *) &mp->external.address, mp->external.len, mp->is_add);
+
+bad_sw_if_index:
+ REPLY_MACRO_END (VL_API_NPT66_BINDING_ADD_DEL_REPLY);
+}
+
+/* API definitions */
+#include <vnet/format_fns.h>
+#include <npt66/npt66.api.c>
+
+/* Set up the API message handling tables */
+clib_error_t *
+npt66_plugin_api_hookup (vlib_main_t *vm)
+{
+ npt66_main_t *nm = &npt66_main;
+
+ nm->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 = VPP_BUILD_VER,
+ .description = "NPTv6",
+};
+
+clib_error_t *
+npt66_init (vlib_main_t *vm)
+{
+ npt66_main_t *nm = &npt66_main;
+ memset (nm, 0, sizeof (*nm));
+
+ return npt66_plugin_api_hookup (vm);
+}
+
+VLIB_INIT_FUNCTION (npt66_init);
diff --git a/src/plugins/npt66/npt66_cli.c b/src/plugins/npt66/npt66_cli.c
new file mode 100644
index 00000000000..268bca264b3
--- /dev/null
+++ b/src/plugins/npt66/npt66_cli.c
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright(c) 2023 Cisco Systems, Inc.
+
+#include <stdbool.h>
+#include <vlib/vlib.h>
+#include <vnet/feature/feature.h>
+#include <vnet/ip/ip.h>
+#include <vppinfra/clib_error.h>
+#include "npt66.h"
+
+static clib_error_t *
+set_npt66_binding_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 internal_set = false, external_set = false;
+ bool add = true;
+ u32 sw_if_index = ~0;
+ ip6_address_t internal, external;
+ int internal_plen = 0, external_plen = 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, "internal %U/%d", unformat_ip6_address,
+ &internal, &internal_plen))
+ internal_set = true;
+ else if (unformat (line_input, "external %U/%d", unformat_ip6_address,
+ &external, &external_plen))
+ external_set = true;
+ else if (unformat (line_input, "interface %U",
+ unformat_vnet_sw_interface, vnet_get_main (),
+ &sw_if_index))
+ ;
+ 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 (!internal_set)
+ {
+ error = clib_error_return (0, "missing parameter: internal `%U'",
+ format_unformat_error, line_input);
+ goto done;
+ }
+ if (!external_set)
+ {
+ error = clib_error_return (0, "missing parameter: external `%U'",
+ format_unformat_error, line_input);
+ goto done;
+ }
+
+ int rv = npt66_binding_add_del (sw_if_index, &internal, internal_plen,
+ &external, external_plen, add);
+ if (rv)
+ {
+ error = clib_error_return (0, "Adding binding failed %d", rv);
+ goto done;
+ }
+
+done:
+ unformat_free (line_input);
+
+ return error;
+}
+
+VLIB_CLI_COMMAND (set_npt66_binding_command, static) = {
+ .path = "set npt66 binding",
+ .short_help = "set npt66 binding interface <name> internal <pfx> "
+ "external <pfx> [del]",
+ .function = set_npt66_binding_command_fn,
+};
diff --git a/src/plugins/npt66/npt66_node.c b/src/plugins/npt66/npt66_node.c
new file mode 100644
index 00000000000..95fe8594dbb
--- /dev/null
+++ b/src/plugins/npt66/npt66_node.c
@@ -0,0 +1,289 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright(c) 2023 Cisco Systems, Inc.
+
+// This file contains the implementation of the NPT66 node.
+// RFC6296: IPv6-to-IPv6 Network Prefix Translation (NPTv6)
+
+#include <vnet/ip/ip.h>
+#include <vnet/ip/ip6.h>
+#include <vnet/ip/ip6_packet.h>
+
+#include <npt66/npt66.h>
+
+typedef struct
+{
+ u32 pool_index;
+ ip6_address_t internal;
+ ip6_address_t external;
+} npt66_trace_t;
+
+static inline u8 *
+format_npt66_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 *);
+ npt66_trace_t *t = va_arg (*args, npt66_trace_t *);
+
+ if (t->pool_index != ~0)
+ s = format (s, "npt66: index %d internal: %U external: %U\n",
+ t->pool_index, format_ip6_address, &t->internal,
+ format_ip6_address, &t->external);
+ else
+ s = format (s, "npt66: index %d (binding not found)\n", t->pool_index);
+ return s;
+}
+
+/* NPT66 next-nodes */
+typedef enum
+{
+ NPT66_NEXT_DROP,
+ NPT66_N_NEXT
+} npt66_next_t;
+
+static ip6_address_t
+ip6_prefix_copy (ip6_address_t dest, ip6_address_t src, int plen)
+{
+ int bytes_to_copy = plen / 8;
+ int residual_bits = plen % 8;
+
+ // Copy full bytes
+ for (int i = 0; i < bytes_to_copy; i++)
+ {
+ dest.as_u8[i] = src.as_u8[i];
+ }
+
+ // Handle the residual bits, if any
+ if (residual_bits)
+ {
+ uint8_t mask = 0xFF << (8 - residual_bits);
+ dest.as_u8[bytes_to_copy] = (dest.as_u8[bytes_to_copy] & ~mask) |
+ (src.as_u8[bytes_to_copy] & mask);
+ }
+ return dest;
+}
+static int
+ip6_prefix_cmp (ip6_address_t a, ip6_address_t b, int plen)
+{
+ int bytes_to_compare = plen / 8;
+ int residual_bits = plen % 8;
+
+ // Compare full bytes
+ for (int i = 0; i < bytes_to_compare; i++)
+ {
+ if (a.as_u8[i] != b.as_u8[i])
+ {
+ return 0; // prefixes are not identical
+ }
+ }
+
+ // Compare the residual bits, if any
+ if (residual_bits)
+ {
+ uint8_t mask = 0xFF << (8 - residual_bits);
+ if ((a.as_u8[bytes_to_compare] & mask) !=
+ (b.as_u8[bytes_to_compare] & mask))
+ {
+ return 0; // prefixes are not identical
+ }
+ }
+ return 1; // prefixes are identical
+}
+
+static int
+npt66_adjust_checksum (int plen, bool add, ip_csum_t delta,
+ ip6_address_t *address)
+{
+ if (plen <= 48)
+ {
+ // TODO: Check for 0xFFFF
+ if (address->as_u16[3] == 0xffff)
+ return -1;
+ address->as_u16[3] = add ? ip_csum_add_even (address->as_u16[3], delta) :
+ ip_csum_sub_even (address->as_u16[3], delta);
+ }
+ else
+ {
+ /* For prefixes longer than 48 find a 16-bit word in the interface id */
+ for (int i = 4; i < 8; i++)
+ {
+ if (address->as_u16[i] == 0xffff)
+ continue;
+ address->as_u16[i] = add ?
+ ip_csum_add_even (address->as_u16[i], delta) :
+ ip_csum_sub_even (address->as_u16[i], delta);
+ break;
+ }
+ }
+ return 0;
+}
+
+static int
+npt66_translate (ip6_header_t *ip, npt66_binding_t *binding, int dir)
+{
+ int rv = 0;
+ clib_warning ("npt66_translate: before: %U", format_ip6_header, ip, 40);
+ if (dir == VLIB_TX)
+ {
+ if (!ip6_prefix_cmp (ip->src_address, binding->internal,
+ binding->internal_plen))
+ {
+ clib_warning ("npt66_translate: src address is not internal");
+ goto done;
+ }
+ ip->src_address = ip6_prefix_copy (ip->src_address, binding->external,
+ binding->external_plen);
+ /* Checksum neutrality */
+ rv = npt66_adjust_checksum (binding->internal_plen, false,
+ binding->delta, &ip->src_address);
+ }
+ else
+ {
+ if (!ip6_prefix_cmp (ip->dst_address, binding->external,
+ binding->external_plen))
+ {
+ clib_warning ("npt66_translate: dst address is not external");
+ goto done;
+ }
+ ip->dst_address = ip6_prefix_copy (ip->dst_address, binding->internal,
+ binding->internal_plen);
+ rv = npt66_adjust_checksum (binding->internal_plen, true, binding->delta,
+ &ip->src_address);
+ }
+ clib_warning ("npt66_translate: after: %U", format_ip6_header, ip, 40);
+done:
+ return rv;
+}
+
+/*
+ * 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)
+ */
+// TODO: Make use of SVR configurable
+static_always_inline uword
+npt66_node_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
+ vlib_frame_t *frame, int dir)
+{
+ npt66_main_t *nm = &npt66_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;
+ ip6_header_t *ip;
+
+ from = vlib_frame_vector_args (frame);
+ n_left_from = frame->n_vectors;
+ vlib_get_buffers (vm, from, b, n_left_from);
+ npt66_binding_t *binding;
+
+ /* Stage 1: build vector of flow hash (based on lookup mask) */
+ while (n_left_from > 0)
+ {
+ clib_warning ("DIRECTION: %u", dir);
+ u32 sw_if_index = vnet_buffer (b[0])->sw_if_index[dir];
+ u32 iph_offset =
+ dir == VLIB_TX ? vnet_buffer (b[0])->ip.save_rewrite_length : 0;
+ ip = (ip6_header_t *) (vlib_buffer_get_current (b[0]) + iph_offset);
+ binding = npt66_interface_by_sw_if_index (sw_if_index);
+ ASSERT (binding);
+ *pi = binding - nm->bindings;
+
+ /* By default pass packet to next node in the feature chain */
+ vnet_feature_next_u16 (next, b[0]);
+
+ int rv = npt66_translate (ip, binding, dir);
+ if (rv < 0)
+ {
+ clib_warning ("npt66_translate failed");
+ *next = NPT66_NEXT_DROP;
+ }
+
+ /*next: */
+ next += 1;
+ 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)
+ {
+ npt66_trace_t *t = vlib_add_trace (vm, node, b[0], sizeof (*t));
+ if (*pi != ~0)
+ {
+ if (!pool_is_free_index (nm->bindings, *pi))
+ {
+ npt66_binding_t *tr =
+ pool_elt_at_index (nm->bindings, *pi);
+ t->internal = tr->internal;
+ t->external = tr->external;
+ }
+ }
+ 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;
+}
+
+VLIB_NODE_FN (npt66_input_node)
+(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame)
+{
+ return npt66_node_inline (vm, node, frame, VLIB_RX);
+}
+VLIB_NODE_FN (npt66_output_node)
+(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame)
+{
+ return npt66_node_inline (vm, node, frame, VLIB_TX);
+}
+
+VLIB_REGISTER_NODE(npt66_input_node) = {
+ .name = "npt66-input",
+ .vector_size = sizeof(u32),
+ .format_trace = format_npt66_trace,
+ .type = VLIB_NODE_TYPE_INTERNAL,
+ // .n_errors = NPT66_N_ERROR,
+ // .error_counters = npt66_error_counters,
+ .n_next_nodes = NPT66_N_NEXT,
+ .next_nodes =
+ {
+ [NPT66_NEXT_DROP] = "error-drop",
+ },
+};
+
+VLIB_REGISTER_NODE (npt66_output_node) = {
+ .name = "npt66-output",
+ .vector_size = sizeof (u32),
+ .format_trace = format_npt66_trace,
+ .type = VLIB_NODE_TYPE_INTERNAL,
+ // .n_errors = npt66_N_ERROR,
+ // .error_counters = npt66_error_counters,
+ .sibling_of = "npt66-input",
+};
+
+/* Hook up features */
+VNET_FEATURE_INIT (npt66_input, static) = {
+ .arc_name = "ip6-unicast",
+ .node_name = "npt66-input",
+ .runs_after = VNET_FEATURES ("ip4-sv-reassembly-feature"),
+};
+VNET_FEATURE_INIT (npt66_output, static) = {
+ .arc_name = "ip6-output",
+ .node_name = "npt66-output",
+ .runs_after = VNET_FEATURES ("ip4-sv-reassembly-output-feature"),
+};
diff --git a/test/test_npt66.py b/test/test_npt66.py
new file mode 100644
index 00000000000..5173c62d44f
--- /dev/null
+++ b/test/test_npt66.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+
+import unittest
+import ipaddress
+from framework import VppTestCase, VppTestRunner
+
+from scapy.layers.inet6 import IPv6, ICMPv6EchoRequest
+from scapy.layers.l2 import Ether
+from scapy.packet import Raw
+
+
+class TestNPT66(VppTestCase):
+ """NPTv6 Test Case"""
+
+ def setUp(self):
+ super(TestNPT66, self).setUp()
+
+ # create 2 pg interfaces
+ self.create_pg_interfaces(range(2))
+
+ for i in self.pg_interfaces:
+ i.admin_up()
+ i.config_ip6()
+ i.resolve_ndp()
+
+ def tearDown(self):
+ for i in self.pg_interfaces:
+ i.unconfig_ip6()
+ i.admin_down()
+ super(TestNPT66, self).tearDown()
+
+ def send_and_verify(self, in2out, internal, external):
+ if in2out:
+ sendif = self.pg0
+ recvif = self.pg1
+ local_mac = self.pg0.local_mac
+ remote_mac = self.pg0.remote_mac
+ src = ipaddress.ip_interface(internal).ip + 1
+ dst = self.pg1.remote_ip6
+ else:
+ sendif = self.pg1
+ recvif = self.pg0
+ local_mac = self.pg1.local_mac
+ remote_mac = self.pg1.remote_mac
+ src = self.pg1.remote_ip6
+ dst = ipaddress.ip_interface(external).ip + 1
+
+ p = (
+ Ether(dst=local_mac, src=remote_mac)
+ / IPv6(src=src, dst=dst)
+ / ICMPv6EchoRequest()
+ )
+ rxs = self.send_and_expect(sendif, p, recvif)
+ for rx in rxs:
+ rx.show2()
+ original_cksum = rx[ICMPv6EchoRequest].cksum
+ del rx[ICMPv6EchoRequest].cksum
+ rx = rx.__class__(bytes(rx))
+ self.assertEqual(original_cksum, rx[ICMPv6EchoRequest].cksum)
+
+ def do_test(self, internal, external):
+ self.vapi.npt66_binding_add_del(
+ sw_if_index=self.pg1.sw_if_index,
+ internal=internal,
+ external=external,
+ is_add=True,
+ )
+ self.vapi.cli(f"ip route add {internal} via {self.pg0.remote_ip6}")
+
+ self.send_and_verify(True, internal, external)
+ self.send_and_verify(False, internal, external)
+
+ self.vapi.npt66_binding_add_del(
+ sw_if_index=self.pg1.sw_if_index,
+ internal=internal,
+ external=external,
+ is_add=False,
+ )
+
+ def test_npt66_simple(self):
+ """Send and receive a packet through NPT66"""
+
+ self.do_test("fc00:1::/48", "2001:db8:1::/48")
+ self.do_test("fc00:1234::/32", "2001:db8:1::/32")
+ self.do_test("fc00:1234::/63", "2001:db8:1::/56")
+
+
+if __name__ == "__main__":
+ unittest.main(testRunner=VppTestRunner)