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>
This commit is contained in:
Ole Troan
2023-08-17 13:36:08 +02:00
parent ecb62d2e5d
commit 6ee3aa41c3
11 changed files with 739 additions and 0 deletions

View 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>

View File

@@ -762,6 +762,8 @@ nodaemon
noevaluate
nonaddress
nosyslog
npt
npt66
ns
nsess
nsh

View File

@@ -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
)

View File

@@ -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]

View File

@@ -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;
};

116
src/plugins/npt66/npt66.c Normal file
View File

@@ -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);
}

28
src/plugins/npt66/npt66.h Normal file
View File

@@ -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);

View File

@@ -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);

View File

@@ -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,
};

View File

@@ -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"),
};

89
test/test_npt66.py Normal file
View File

@@ -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)