Merge pull request #18626 from donaldsharp/RA_me
Some checks are pending
build-test / Build the x86 ubuntu 22.04 docker image (push) Waiting to run
build-test / Test ubuntu x86 docker image (push) Blocked by required conditions
build-test / Build the ARM ubuntu 22.04 docker image (push) Waiting to run
build-test / Test ubuntu ARM docker image (push) Blocked by required conditions

Implement RFC8781 (NAT64 prefix in RA's)
This commit is contained in:
Russ White 2025-04-21 17:32:58 -04:00 committed by GitHub
commit 87923c8cb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 517 additions and 0 deletions

View file

@ -204,6 +204,23 @@ Router Advertisement
Default: do not emit DNSSL option
.. clicmd:: ipv6 nd nat64 [X:X::X:X/M] [lifetime (0-65535)]
Include in RAs an advertisement of the NAT64 prefix in use (RFC8781).
(May be configured multiple times for multiple prefixes.)
If no prefix is given when configuring, the NAT64 default prefix of
64:ff9b::/96 is substituted. Only prefixes with a prefix length of /96,
/64, /56, /48, /40 or /32 can be encoded into advertisements.
Lifetime is specified in seconds and defaults to ``3 * ra-interval``. If
no value is configured, this is adjusted when ``ra-interval`` is changed.
A lifetime of 0 seconds is used to signal NAT64 prefixes that are no longer
valid. Note that this is rounded up to multiples of 8 seconds as a
limitation of the option encoding.
Default: don't advertise any NAT64 prefix.
Router Advertisement Configuration Example
==========================================
A small example:

View file

@ -0,0 +1,19 @@
log timestamp precision 6
log file frr.log
interface r1-eth0
ip address 1.1.1.1/24
ipv6 address 2001:1111::1/64
ipv6 nd nat64
no ipv6 nd suppress-ra
ipv6 nd ra-interval 3
exit
interface r1-eth1
ip address 2.2.2.1/24
ipv6 address 2002:2222::1/64
ipv6 nd nat64 64:ff9b::3/64 lifetime 15
no ipv6 nd suppress-ra
ipv6 nd ra-interval 3
exit

View file

View file

@ -0,0 +1,67 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: ISC
# Copyright (c) 2025 by David Lamparter for NetDEF, Inc.
# minimal tool to receive IPv6 RAs and check for PREF64 option
# usage:
# python3 rx_ipv6_ra_8781.py eth123 64:ff9b::/64 1200 5
# LIFETIME (first number) is the value expected in the packet
# TIMEOUT (second number) is how long to wait for a matching packet
import sys
import socket
import time
from scapy.config import conf
from scapy.layers.inet6 import (
IPv6,
ICMPv6ND_RA,
ICMPv6NDOptPREF64,
)
assert len(sys.argv) == 5, "required arguments: IFNAME PREF64 LIFETIME TIMEOUT"
sock = conf.L2socket(iface=sys.argv[1])
pref64str, masklenstr = sys.argv[2].split("/")
pref64 = socket.inet_pton(socket.AF_INET6, pref64str)
masklen = int(masklenstr)
# lifetime is 13 bits with a *8
lifetime = int(sys.argv[3]) // 8
timeout = float(sys.argv[4])
deadline = time.time() + timeout
while time.time() < deadline:
pkts = sock.sniff(timeout=min(deadline - time.time(), 0.25))
for pkt in pkts:
ra = pkt.getlayer(ICMPv6ND_RA)
if not ra:
continue
ra.show()
pl = ra.payload
while pl:
cur, pl = pl, pl.payload
if isinstance(cur, ICMPv6NDOptPREF64):
cmp_pref64 = socket.inet_pton(socket.AF_INET6, cur.prefix)
cmp_masklen = int(cur.sprintf("%plc%")[1:])
if pref64 != cmp_pref64:
print(f"prefix mismatch {pref64!r} != {cmp_pref64!r}")
continue
if masklen != cmp_masklen:
print(f"prefixlen mismatch {masklen!r} != {cmp_masklen!r}")
continue
if lifetime != cur.scaledlifetime:
print(
f"lifetime mismatch {lifetime*8!r} != {cur.scaledlifetime*8!r}"
)
continue
print("MATCH - exiting successfully")
sys.exit(0)
print("no matching packet received - exiting with failure")
sys.exit(1)

View file

@ -0,0 +1,57 @@
#!/usr/bin/env python
# -*- coding: utf-8 eval: (blacken-mode 1) -*-
# SPDX-License-Identifier: ISC
#
# Copyright (c) 2025 Nvidia Inc.
# Donald Sharp
#
"""
Test zebra ipv6 nd nat64 advertisement
Requires scapy 2.6.1 or greater
"""
import os
import pytest
import json
from lib.topogen import Topogen
from lib.topolog import logger
CWD = os.path.dirname(os.path.realpath(__file__))
pytestmark = [pytest.mark.mgmtd]
@pytest.fixture(scope="module")
def tgen(request):
"Setup/Teardown the environment and provide tgen argument to tests"
topodef = {"s1": ("r1", "r2"), "s2": ("r1", "r2")}
tgen = Topogen(topodef, request.module.__name__)
tgen.start_topology()
router_list = tgen.routers()
for rname, router in router_list.items():
router.load_frr_config("frr.conf")
tgen.start_router()
yield tgen
tgen.stop_topology()
def test_zebra_rapref64_sent(tgen):
if tgen.routers_have_failure():
pytest.skip(tgen.errors)
r2 = tgen.gears["r2"]
r2.cmd_raises("{}/rx_ipv6_ra_8781.py r2-eth0 64:ff9b::/96 16 10".format(CWD))
r2.cmd_raises("{}/rx_ipv6_ra_8781.py r2-eth1 64:ff9b::/64 16 10".format(CWD))
if __name__ == "__main__":
# To suppress tracebacks, either use the following pytest call or add "--tb=no" to cli
# retval = pytest.main(["-s", "--tb=no"])
retval = pytest.main(["-s"])
sys.exit(retval)

View file

@ -195,6 +195,16 @@ module frr-zebra {
"Type for VTEP flood type.";
}
/* PREF64 only accepts specific prefix lengths */
typedef pref64-prefix {
type inet:ipv6-prefix {
/* rely on inet:ipv6-prefix enforcing validity already */
pattern '.*/(96|64|56|48|40|32)';
}
description
"An IPv6 prefix suitable for PREF64 announcement";
}
/*
* Common route data, shared by v4 and v6 routes.
*/
@ -2829,6 +2839,31 @@ module frr-zebra {
}
}
}
container pref64 {
description
"A list of NAT64 prefixes that are placed in the PREF64 option in
Router Advertisement messages sent from the interface.";
reference
"RFC 8781: Discovering PREF64 in Router Advertisements";
list pref64-prefix {
key "prefix";
description
"NAT64 prefix details";
leaf prefix {
type pref64-prefix;
description
"NAT64 prefix, typically 64:ff9b::/96";
}
leaf lifetime {
type uint16;
units "seconds";
description
"Lifetime of the NAT64 prefix to be placed in RAs.
If omitted, the lifetime will be calculated automatically as
MaxRtrAdvInterval * 3";
}
}
}
}
leaf ptm-enable {
if-feature ptm-bfd;

View file

@ -95,6 +95,14 @@ DECLARE_RBTREE_UNIQ(rtadv_prefixes, struct rtadv_prefix, item,
DEFINE_MTYPE_STATIC(ZEBRA, RTADV_RDNSS, "Router Advertisement RDNSS");
DEFINE_MTYPE_STATIC(ZEBRA, RTADV_DNSSL, "Router Advertisement DNSSL");
DEFINE_MTYPE_STATIC(ZEBRA, RTADV_PREF64, "Router Advertisement NAT64 Prefix");
static int pref64_cmp(const struct pref64_adv *a, const struct pref64_adv *b)
{
return prefix_cmp(&a->p, &b->p);
}
DECLARE_SORTLIST_UNIQ(pref64_advs, struct pref64_adv, itm, pref64_cmp);
/* Order is intentional. Matches RFC4191. This array is also used for
command matching, so only modify with care. */
@ -110,6 +118,29 @@ enum rtadv_event {
RTADV_READ
};
#define PREF64_INVALID_PREFIXLEN 0xff
/* RFC8781 NAT64 prefix can encode /96, /64, /56, /48, /40 and /32 only. */
static uint8_t pref64_get_plc(const struct prefix_ipv6 *p)
{
switch (p->prefixlen) {
case 96:
return 0;
case 64:
return 1;
case 56:
return 2;
case 48:
return 3;
case 40:
return 4;
case 32:
return 5;
default:
return PREF64_INVALID_PREFIXLEN;
}
}
static void rtadv_event(struct zebra_vrf *, enum rtadv_event, int);
static int if_join_all_router(int, struct interface *);
@ -444,6 +475,50 @@ static void rtadv_send_packet(int sock, struct interface *ifp,
buf[len++] = '\0';
}
struct pref64_adv *pref64_adv;
frr_each (pref64_advs, zif->rtadv.pref64_advs, pref64_adv) {
struct nd_opt_pref64__frr *opt;
size_t opt_len = sizeof(*opt);
uint16_t lifetime_plc;
if (len + opt_len > max_len) {
zlog_warn("%s(%u): Tx RA: NAT64 option would exceed MTU, omitting it",
ifp->name, ifp->ifindex);
goto no_more_opts;
}
if (pref64_adv->lifetime == PREF64_LIFETIME_AUTO) {
/* starting in msec, so won't fit in 16bit */
unsigned lifetime;
lifetime = zif->rtadv.MaxRtrAdvInterval * 3;
lifetime += 999;
lifetime /= 1000;
if (lifetime > 65535)
lifetime = 65535;
lifetime_plc = lifetime;
} else
lifetime_plc = pref64_adv->lifetime;
/* rounding up to 8 sec, cap at 16 bits, and clear PLC */
lifetime_plc = MIN(lifetime_plc + 0x7, 0xffffU) & ~0x7U;
lifetime_plc |= pref64_get_plc(&pref64_adv->p);
opt = (struct nd_opt_pref64__frr *)(buf + len);
memset(opt, 0, opt_len);
opt->nd_opt_pref64_type = ND_OPT_PREF64;
opt->nd_opt_pref64_len = opt_len / 8;
opt->nd_opt_pref64_lifetime_plc = htons(lifetime_plc);
memcpy(opt->nd_opt_pref64_prefix, &pref64_adv->p.prefix,
sizeof(opt->nd_opt_pref64_prefix));
len += opt_len;
}
no_more_opts:
msg.msg_name = (void *)&addr;
@ -1723,6 +1798,41 @@ int rtadv_dnssl_encode(uint8_t *out, const char *in)
return outp;
}
struct pref64_adv *rtadv_pref64_set(struct zebra_if *zif, struct prefix_ipv6 *p, uint32_t lifetime)
{
struct pref64_adv *item, dummy = {};
prefix_copy(&dummy.p, p);
apply_mask_ipv6(&dummy.p);
item = pref64_advs_find(zif->rtadv.pref64_advs, &dummy);
if (!item) {
item = XCALLOC(MTYPE_RTADV_PREF64, sizeof(*item));
prefix_copy(&item->p, &dummy.p);
pref64_advs_add(zif->rtadv.pref64_advs, item);
}
item->lifetime = lifetime;
return item;
}
static void rtadv_pref64_free(struct pref64_adv *item)
{
XFREE(MTYPE_RTADV_PREF64, item);
}
void rtadv_pref64_update(struct zebra_if *zif, struct pref64_adv *item, uint32_t lifetime)
{
item->lifetime = lifetime;
}
void rtadv_pref64_reset(struct zebra_if *zif, struct pref64_adv *item)
{
pref64_advs_del(zif->rtadv.pref64_advs, item);
rtadv_pref64_free(item);
}
/* Dump interface ND information to vty. */
static int nd_dump_vty(struct vty *vty, json_object *json_if, struct interface *ifp)
{
@ -1956,12 +2066,16 @@ void rtadv_if_fini(struct zebra_if *zif)
{
struct rtadvconf *rtadv;
struct rtadv_prefix *rp;
struct pref64_adv *pref64_adv;
rtadv = &zif->rtadv;
while ((rp = rtadv_prefixes_pop(rtadv->prefixes)))
rtadv_prefix_free(rp);
while ((pref64_adv = pref64_advs_pop(rtadv->pref64_advs)))
rtadv_pref64_free(pref64_adv);
list_delete(&rtadv->AdvRDNSSList);
list_delete(&rtadv->AdvDNSSLList);
}

View file

@ -35,6 +35,7 @@ struct rtadv {
};
PREDECL_RBTREE_UNIQ(rtadv_prefixes);
PREDECL_SORTLIST_UNIQ(pref64_advs);
/* Router advertisement parameter. From RFC4861, RFC6275 and RFC4191. */
struct rtadvconf {
@ -189,6 +190,9 @@ struct rtadvconf {
*/
struct list *AdvDNSSLList;
/* NAT64 prefix advertisements [RFC8781] */
struct pref64_advs_head pref64_advs[1];
/*
* rfc4861 states RAs must be sent at least 3 seconds apart.
* We allow faster retransmits to speed up convergence but can
@ -333,6 +337,9 @@ struct nd_opt_homeagent_info { /* Home Agent info */
#ifndef ND_OPT_DNSSL
#define ND_OPT_DNSSL 31
#endif
#ifndef ND_OPT_PREF64
#define ND_OPT_PREF64 38
#endif
#ifndef HAVE_STRUCT_ND_OPT_RDNSS
struct nd_opt_rdnss { /* Recursive DNS server option [RFC8106 5.1] */
@ -358,6 +365,27 @@ struct nd_opt_dnssl { /* DNS search list option [RFC8106 5.2] */
} __attribute__((__packed__));
#endif
/* not in a system header (yet?)
* => added "__frr" to avoid future conflicts
*/
struct nd_opt_pref64__frr {
uint8_t nd_opt_pref64_type;
uint8_t nd_opt_pref64_len;
uint16_t nd_opt_pref64_lifetime_plc;
uint8_t nd_opt_pref64_prefix[12]; /* highest 96 bits only */
} __attribute__((__packed__));
#define PREF64_LIFETIME_AUTO UINT32_MAX
#define PREF64_DFLT_PREFIX "64:ff9b::/96"
struct pref64_adv {
struct pref64_advs_item itm;
struct prefix_ipv6 p;
uint32_t lifetime;
};
/*
* ipv6 nd prefixes can be manually defined, derived from the kernel interface
* configs or both. If both, manual flag/timer settings are used.
@ -405,6 +433,26 @@ struct rtadv_dnssl *rtadv_dnssl_set(struct zebra_if *zif,
void rtadv_dnssl_reset(struct zebra_if *zif, struct rtadv_dnssl *p);
int rtadv_dnssl_encode(uint8_t *out, const char *in);
/* lifetime: 0-65535 or PREF64_LIFETIME_AUTO */
static inline bool rtadv_pref64_valid_prefix(const struct prefix_ipv6 *p)
{
switch (p->prefixlen) {
case 96:
case 64:
case 56:
case 48:
case 40:
case 32:
return true;
default:
return false;
}
}
struct pref64_adv *rtadv_pref64_set(struct zebra_if *zif, struct prefix_ipv6 *p, uint32_t lifetime);
void rtadv_pref64_update(struct zebra_if *zif, struct pref64_adv *item, uint32_t lifetime);
void rtadv_pref64_reset(struct zebra_if *zif, struct pref64_adv *item);
void ipv6_nd_suppress_ra_set(struct interface *ifp,
enum ipv6_nd_suppress_ra_status status);
void ipv6_nd_interval_set(struct interface *ifp, uint32_t interval);

View file

@ -9,6 +9,7 @@
#include "northbound_cli.h"
#include "vrf.h"
#include "zebra/rtadv.h"
#include "zebra_cli.h"
#include "zebra/zebra_cli_clippy.c"
@ -1823,6 +1824,58 @@ lib_interface_zebra_ipv6_router_advertisements_dnssl_dnssl_domain_cli_write(
vty_out(vty, "\n");
}
DEFPY_YANG(
ipv6_nd_pref64,
ipv6_nd_pref64_cmd,
"[no] ipv6 nd nat64 [X:X::X:X/M]$prefix [lifetime <(0-65535)|auto>]",
NO_STR
"Interface IPv6 config commands\n"
"Neighbor discovery\n"
"NAT64 prefix advertisement (RFC8781)\n"
"NAT64 prefix to advertise (default: 64:ff9b::/96)\n"
"Specify validity lifetime\n"
"Valid lifetime in seconds\n"
"Calculate lifetime automatically\n")
{
if (!prefix_str)
prefix_str = PREF64_DFLT_PREFIX;
else if (!rtadv_pref64_valid_prefix(prefix)) {
vty_out(vty,
"Invalid NAT64 prefix length - must be /96, /64, /56, /48, /40 or /32\n");
return CMD_WARNING_CONFIG_FAILED;
}
if (!no) {
nb_cli_enqueue_change(vty, ".", NB_OP_CREATE, NULL);
if (lifetime_str && strcmp(lifetime_str, "auto")) {
nb_cli_enqueue_change(vty, "./lifetime", NB_OP_MODIFY, lifetime_str);
} else {
nb_cli_enqueue_change(vty, "./lifetime", NB_OP_DESTROY, NULL);
}
} else {
nb_cli_enqueue_change(vty, ".", NB_OP_DESTROY, NULL);
}
return nb_cli_apply_changes(vty,
"./frr-zebra:zebra/ipv6-router-advertisements/pref64/pref64-prefix[prefix='%s']",
prefix_str);
}
static void lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_cli_write(
struct vty *vty, const struct lyd_node *dnode, bool show_defaults)
{
const char *prefix = yang_dnode_get_string(dnode, "prefix");
vty_out(vty, " ipv6 nd nat64 %s", prefix);
if (yang_dnode_exists(dnode, "lifetime")) {
uint16_t lifetime = yang_dnode_get_uint16(dnode, "lifetime");
vty_out(vty, " %u", lifetime);
}
vty_out(vty, "\n");
}
#endif /* HAVE_RTADV */
#if HAVE_BFDD == 0
@ -2874,6 +2927,10 @@ const struct frr_yang_module_info frr_zebra_cli_info = {
.xpath = "/frr-interface:lib/interface/frr-zebra:zebra/ipv6-router-advertisements/rdnss/rdnss-address",
.cbs.cli_show = lib_interface_zebra_ipv6_router_advertisements_rdnss_rdnss_address_cli_write,
},
{
.xpath = "/frr-interface:lib/interface/frr-zebra:zebra/ipv6-router-advertisements/pref64/pref64-prefix",
.cbs.cli_show = lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_cli_write,
},
#endif /* defined(HAVE_RTADV) */
#if HAVE_BFDD == 0
{
@ -2989,6 +3046,7 @@ void zebra_cli_init(void)
install_element(INTERFACE_NODE, &ipv6_nd_mtu_cmd);
install_element(INTERFACE_NODE, &ipv6_nd_rdnss_cmd);
install_element(INTERFACE_NODE, &ipv6_nd_dnssl_cmd);
install_element(INTERFACE_NODE, &ipv6_nd_pref64_cmd);
#endif
#if HAVE_BFDD == 0
install_element(INTERFACE_NODE, &zebra_ptm_enable_if_cmd);

View file

@ -763,6 +763,20 @@ const struct frr_yang_module_info frr_zebra_info = {
.destroy = lib_interface_zebra_ipv6_router_advertisements_rdnss_rdnss_address_lifetime_destroy,
}
},
{
.xpath = "/frr-interface:lib/interface/frr-zebra:zebra/ipv6-router-advertisements/pref64/pref64-prefix",
.cbs = {
.create = lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_create,
.destroy = lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_destroy,
}
},
{
.xpath = "/frr-interface:lib/interface/frr-zebra:zebra/ipv6-router-advertisements/pref64/pref64-prefix/lifetime",
.cbs = {
.modify = lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_lifetime_modify,
.destroy = lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_lifetime_destroy,
}
},
#endif /* defined(HAVE_RTADV) */
#if HAVE_BFDD == 0
{

View file

@ -269,6 +269,14 @@ int lib_interface_zebra_ipv6_router_advertisements_dnssl_dnssl_domain_lifetime_m
struct nb_cb_modify_args *args);
int lib_interface_zebra_ipv6_router_advertisements_dnssl_dnssl_domain_lifetime_destroy(
struct nb_cb_destroy_args *args);
int lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_create(
struct nb_cb_create_args *args);
int lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_destroy(
struct nb_cb_destroy_args *args);
int lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_lifetime_modify(
struct nb_cb_modify_args *args);
int lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_lifetime_destroy(
struct nb_cb_destroy_args *args);
#endif /* defined(HAVE_RTADV) */
#if HAVE_BFDD == 0
int lib_interface_zebra_ptm_enable_modify(struct nb_cb_modify_args *args);

View file

@ -14,6 +14,7 @@
#include "libfrr.h"
#include "lib/command.h"
#include "lib/routemap.h"
#include "zebra/rtadv.h"
#include "zebra/zebra_nb.h"
#include "zebra/rib.h"
#include "zebra_nb.h"
@ -3248,6 +3249,85 @@ int lib_interface_zebra_ipv6_router_advertisements_dnssl_dnssl_domain_lifetime_d
return NB_OK;
}
/*
* XPath: /frr-interface:lib/interface/frr-zebra:zebra/ipv6-router-advertisements/pref64/pref64-prefix
*/
int lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_create(
struct nb_cb_create_args *args)
{
struct interface *ifp;
struct pref64_adv *entry;
struct prefix_ipv6 p;
uint32_t lifetime = PREF64_LIFETIME_AUTO;
if (args->event != NB_EV_APPLY)
return NB_OK;
ifp = nb_running_get_entry(args->dnode, NULL, true);
yang_dnode_get_ipv6p(&p, args->dnode, "prefix");
if (yang_dnode_exists(args->dnode, "lifetime"))
lifetime = yang_dnode_get_uint16(args->dnode, "lifetime");
entry = rtadv_pref64_set(ifp->info, &p, lifetime);
nb_running_set_entry(args->dnode, entry);
return NB_OK;
}
int lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_destroy(
struct nb_cb_destroy_args *args)
{
struct interface *ifp;
struct pref64_adv *entry;
if (args->event != NB_EV_APPLY)
return NB_OK;
entry = nb_running_unset_entry(args->dnode);
ifp = nb_running_get_entry(args->dnode, NULL, true);
rtadv_pref64_reset(ifp->info, entry);
return NB_OK;
}
/*
* XPath: /frr-interface:lib/interface/frr-zebra:zebra/ipv6-router-advertisements/rdnss/rdnss-address/lifetime
*/
int lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_lifetime_modify(
struct nb_cb_modify_args *args)
{
struct interface *ifp;
struct pref64_adv *entry;
if (args->event != NB_EV_APPLY)
return NB_OK;
entry = nb_running_get_entry(args->dnode, NULL, true);
ifp = nb_running_get_entry(lyd_parent(lyd_parent(args->dnode)), NULL, true);
rtadv_pref64_update(ifp->info, entry, yang_dnode_get_uint16(args->dnode, NULL));
return NB_OK;
}
int lib_interface_zebra_ipv6_router_advertisements_pref64_pref64_prefix_lifetime_destroy(
struct nb_cb_destroy_args *args)
{
struct interface *ifp;
struct pref64_adv *entry;
if (args->event != NB_EV_APPLY)
return NB_OK;
entry = nb_running_get_entry(args->dnode, NULL, true);
ifp = nb_running_get_entry(lyd_parent(lyd_parent(args->dnode)), NULL, true);
rtadv_pref64_update(ifp->info, entry, PREF64_LIFETIME_AUTO);
return NB_OK;
}
#endif /* defined(HAVE_RTADV) */
#if HAVE_BFDD == 0