npt66: network prefix translation for ipv6 93/39393/5
authorOle Troan <otroan@employees.org>
Thu, 17 Aug 2023 11:36:08 +0000 (13:36 +0200)
committerOle Troan <otroan@employees.org>
Fri, 25 Aug 2023 07:15:32 +0000 (09:15 +0200)
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>
MAINTAINERS
docs/spelling_wordlist.txt
src/plugins/npt66/CMakeLists.txt [new file with mode: 0644]
src/plugins/npt66/FEATURE.yaml [new file with mode: 0644]
src/plugins/npt66/npt66.api [new file with mode: 0644]
src/plugins/npt66/npt66.c [new file with mode: 0644]
src/plugins/npt66/npt66.h [new file with mode: 0644]
src/plugins/npt66/npt66_api.c [new file with mode: 0644]
src/plugins/npt66/npt66_cli.c [new file with mode: 0644]
src/plugins/npt66/npt66_node.c [new file with mode: 0644]
test/test_npt66.py [new file with mode: 0644]

index 84894a3..6e9b535 100644 (file)
@@ -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>
index 178a230..1a0669c 100644 (file)
@@ -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 (file)
index 0000000..aee784d
--- /dev/null
@@ -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 (file)
index 0000000..8874ae2
--- /dev/null
@@ -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 (file)
index 0000000..01ce775
--- /dev/null
@@ -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 (file)
index 0000000..e3cbbbd
--- /dev/null
@@ -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 (file)
index 0000000..428dadb
--- /dev/null
@@ -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 (file)
index 0000000..91eb73c
--- /dev/null
@@ -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 (file)
index 0000000..268bca2
--- /dev/null
@@ -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 (file)
index 0000000..95fe859
--- /dev/null
@@ -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 (file)
index 0000000..5173c62
--- /dev/null
@@ -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)