public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
From: Jon Maloy <jmaloy@redhat.com>
To: sbrivio@redhat.com, dgibson@redhat.com,
	david@gibson.dropbear.id.au, jmaloy@redhat.com,
	passt-dev@passt.top
Subject: [PATCH v6 13/13] ndp: Support advertising multiple prefixes in Router Advertisements
Date: Sat, 21 Mar 2026 20:43:33 -0400	[thread overview]
Message-ID: <20260322004333.365713-14-jmaloy@redhat.com> (raw)
In-Reply-To: <20260322004333.365713-1-jmaloy@redhat.com>

We extend NDP to advertise all suitable IPv6 prefixes in Router
Advertisements, per RFC 4861. Observed and link-local addresses,
plus addresses with a prefix length != 64, are excluded.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>

---
v6: Adapted to previous changes in series
---
 conf.c    |  19 ++++++---
 fwd.c     |   3 ++
 migrate.c |   5 +++
 ndp.c     | 121 ++++++++++++++++++++++++++++++++++++------------------
 passt.h   |   1 +
 5 files changed, 103 insertions(+), 46 deletions(-)

diff --git a/conf.c b/conf.c
index de2fb7c..b8dd40a 100644
--- a/conf.c
+++ b/conf.c
@@ -1213,7 +1213,7 @@ static void conf_print(const struct ctx *c)
 	}
 
 	if (c->ifi6) {
-		bool has_dhcpv6 = false;
+		bool has_ndp = false, has_dhcpv6 = false;
 		const char *head;
 
 		if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback))
@@ -1223,18 +1223,25 @@ static void conf_print(const struct ctx *c)
 
 		/* Check what we have to advertise */
 		for_each_addr(a, c, AF_INET6) {
+			if (a->flags & CONF_ADDR_NDP)
+				has_ndp = true;
 			if (a->flags & CONF_ADDR_DHCPV6)
 				has_dhcpv6 = true;
 		}
 
-		if (c->no_ndp && !has_dhcpv6)
+		if (!has_ndp && !has_dhcpv6)
 			goto dns6;
 
-		a = fwd_get_addr(c, AF_INET6, 0, CONF_ADDR_LINKLOCAL);
-		if (!c->no_ndp && a) {
+		if (has_ndp) {
 			info("NDP:");
-			inany_ntop(&a->addr, buf, sizeof(buf));
-			info("    assign: %s", buf);
+			head = "assign";
+			for_each_addr(a, c, AF_INET6) {
+				if (!(a->flags & CONF_ADDR_NDP))
+					continue;
+				inany_ntop(&a->addr, buf, sizeof(buf));
+				info("    %s: %s/%d", head, buf, a->prefix_len);
+				head = "      ";
+			}
 		}
 
 		if (has_dhcpv6) {
diff --git a/fwd.c b/fwd.c
index f867398..fe6e9d4 100644
--- a/fwd.c
+++ b/fwd.c
@@ -305,6 +305,9 @@ void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
 			/* DHCPv6 for IPv6 */
 			if (!c->no_dhcpv6)
 				flags |= CONF_ADDR_DHCPV6;
+			/* NDP/RA only if /64 prefix, as required for SLAAC */
+			if (!c->no_ndp && prefix_len == 64)
+				flags |= CONF_ADDR_NDP;
 		}
 	}
 
diff --git a/migrate.c b/migrate.c
index 105f624..98c2d7e 100644
--- a/migrate.c
+++ b/migrate.c
@@ -54,6 +54,7 @@ struct migrate_seen_addrs_v2 {
 #define MIGRATE_ADDR_OBSERVED	BIT(3)
 #define MIGRATE_ADDR_DHCP	BIT(4)
 #define MIGRATE_ADDR_DHCPV6	BIT(5)
+#define MIGRATE_ADDR_NDP	BIT(6)
 
 /**
  * struct migrate_addr_v3 - Wire format for a single address entry
@@ -89,6 +90,8 @@ static uint8_t flags_to_wire(uint8_t flags)
 		wire |= MIGRATE_ADDR_DHCP;
 	if (flags & CONF_ADDR_DHCPV6)
 		wire |= MIGRATE_ADDR_DHCPV6;
+	if (flags & CONF_ADDR_NDP)
+		wire |= MIGRATE_ADDR_NDP;
 
 	return wire;
 }
@@ -115,6 +118,8 @@ static uint8_t flags_from_wire(uint8_t wire)
 		flags |= CONF_ADDR_DHCP;
 	if (wire & MIGRATE_ADDR_DHCPV6)
 		flags |= CONF_ADDR_DHCPV6;
+	if (wire & MIGRATE_ADDR_NDP)
+		flags |= CONF_ADDR_NDP;
 
 	return flags;
 }
diff --git a/ndp.c b/ndp.c
index 3750fc5..f47286e 100644
--- a/ndp.c
+++ b/ndp.c
@@ -32,6 +32,8 @@
 #include "passt.h"
 #include "tap.h"
 #include "log.h"
+#include "fwd.h"
+#include "conf.h"
 
 #define	RT_LIFETIME	65535
 
@@ -82,7 +84,7 @@ struct ndp_na {
 } __attribute__((packed));
 
 /**
- * struct opt_prefix_info - Prefix Information option
+ * struct opt_prefix_info - Prefix Information option header
  * @header:		Option header
  * @prefix_len:		The number of leading bits in the Prefix that are valid
  * @prefix_flags:	Flags associated with the prefix
@@ -99,6 +101,16 @@ struct opt_prefix_info {
 	uint32_t reserved;
 } __attribute__((packed));
 
+/**
+ * struct ndp_prefix - Complete Prefix Information option with prefix
+ * @info:		Prefix Information option header
+ * @prefix:		IPv6 prefix
+ */
+struct ndp_prefix {
+	struct opt_prefix_info info;
+	struct in6_addr prefix;
+} __attribute__((__packed__));
+
 /**
  * struct opt_mtu - Maximum transmission unit (MTU) option
  * @header:		Option header
@@ -140,27 +152,23 @@ struct opt_dnssl {
 } __attribute__((packed));
 
 /**
- * struct ndp_ra - NDP Router Advertisement (RA) message
+ * struct ndp_ra_hdr - NDP Router Advertisement fixed header
  * @ih:			ICMPv6 header
  * @reachable:		Reachability time, after confirmation (ms)
  * @retrans:		Time between retransmitted NS messages (ms)
- * @prefix_info:	Prefix Information option
- * @prefix:		IPv6 prefix
- * @mtu:		MTU option
- * @source_ll:		Target link-layer address
- * @var:		Variable fields
  */
-struct ndp_ra {
+struct ndp_ra_hdr {
 	struct icmp6hdr ih;
 	uint32_t reachable;
 	uint32_t retrans;
-	struct opt_prefix_info prefix_info;
-	struct in6_addr prefix;
-	struct opt_l2_addr source_ll;
+} __attribute__((__packed__));
 
-	unsigned char var[sizeof(struct opt_mtu) + sizeof(struct opt_rdnss) +
-			  sizeof(struct opt_dnssl)];
-} __attribute__((packed, aligned(__alignof__(struct in6_addr))));
+/* Maximum RA message size: hdr + prefixes + source_ll + mtu + rdnss + dnssl */
+#define NDP_RA_MAX_SIZE	(sizeof(struct ndp_ra_hdr) + \
+			 MAX_GUEST_ADDRS * sizeof(struct ndp_prefix) + \
+			 sizeof(struct opt_l2_addr) + \
+			 sizeof(struct opt_mtu) + sizeof(struct opt_rdnss) + \
+			 sizeof(struct opt_dnssl))
 
 /**
  * struct ndp_ns - NDP Neighbor Solicitation (NS) message
@@ -231,6 +239,42 @@ void ndp_unsolicited_na(const struct ctx *c, const struct in6_addr *addr)
 		ndp_na(c, &in6addr_ll_all_nodes, addr);
 }
 
+/**
+ * ndp_prefix_fill() - Fill prefix options for all suitable addresses
+ * @c:		Execution context
+ * @buf:	Buffer to write prefix options into
+ *
+ * Fills buffer with Prefix Information options for all non-linklocal,
+ * non-observed addresses with prefix_len == 64 (required for SLAAC).
+ *
+ * Return: number of bytes written
+ */
+static size_t ndp_prefix_fill(const struct ctx *c, unsigned char *buf)
+{
+	const struct guest_addr *a;
+	struct ndp_prefix *p;
+	size_t offset = 0;
+
+	for_each_addr(a, c, AF_INET6) {
+		if (!(a->flags & CONF_ADDR_NDP))
+			continue;
+
+		p = (struct ndp_prefix *)(buf + offset);
+		p->info.header.type = OPT_PREFIX_INFO;
+		p->info.header.len = 4;  /* 4 * 8 = 32 bytes */
+		p->info.prefix_len = 64;
+		p->info.prefix_flags = 0xc0;  /* L, A flags */
+		p->info.valid_lifetime = ~0U;
+		p->info.pref_lifetime = ~0U;
+		p->info.reserved = 0;
+		p->prefix = a->addr.a6;
+
+		offset += sizeof(struct ndp_prefix);
+	}
+
+	return offset;
+}
+
 /**
  * ndp_ra() - Send an NDP Router Advertisement (RA) message
  * @c:		Execution context
@@ -238,7 +282,15 @@ void ndp_unsolicited_na(const struct ctx *c, const struct in6_addr *addr)
  */
 static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
 {
-	struct ndp_ra ra = {
+	unsigned char buf[NDP_RA_MAX_SIZE]
+		__attribute__((__aligned__(__alignof__(struct in6_addr))));
+	struct ndp_ra_hdr *hdr = (struct ndp_ra_hdr *)buf;
+	struct opt_l2_addr *source_ll;
+	unsigned char *ptr;
+	size_t prefix_len;
+
+	/* Build RA header */
+	*hdr = (struct ndp_ra_hdr){
 		.ih = {
 			.icmp6_type		= RA,
 			.icmp6_code		= 0,
@@ -247,31 +299,22 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
 			.icmp6_rt_lifetime	= htons_constant(RT_LIFETIME),
 			.icmp6_addrconf_managed	= 1,
 		},
-		.prefix_info = {
-			.header = {
-				.type		= OPT_PREFIX_INFO,
-				.len		= 4,
-			},
-			.prefix_len		= 64,
-			.prefix_flags		= 0xc0,	/* prefix flags: L, A */
-			.valid_lifetime		= ~0U,
-			.pref_lifetime		= ~0U,
-		},
-		.source_ll = {
-			.header = {
-				.type		= OPT_SRC_L2_ADDR,
-				.len		= 1,
-			},
-		},
 	};
-	const struct guest_addr *a = fwd_get_addr(c, AF_INET6, 0, 0);
-	unsigned char *ptr = NULL;
-
-	ASSERT(a);
 
-	ra.prefix = a->addr.a6;
+	/* Fill prefix options */
+	prefix_len = ndp_prefix_fill(c, (unsigned char *)(hdr + 1));
+	if (prefix_len == 0) {
+		/* No suitable prefixes to advertise */
+		return;
+	}
 
-	ptr = &ra.var[0];
+	/* Add source link-layer address option */
+	ptr = (unsigned char *)(hdr + 1) + prefix_len;
+	source_ll = (struct opt_l2_addr *)ptr;
+	source_ll->header.type = OPT_SRC_L2_ADDR;
+	source_ll->header.len = 1;
+	memcpy(source_ll->mac, c->our_tap_mac, ETH_ALEN);
+	ptr += sizeof(struct opt_l2_addr);
 
 	if (c->mtu) {
 		struct opt_mtu *mtu = (struct opt_mtu *)ptr;
@@ -345,10 +388,8 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
 		}
 	}
 
-	memcpy(&ra.source_ll.mac, c->our_tap_mac, ETH_ALEN);
-
 	/* NOLINTNEXTLINE(clang-analyzer-security.PointerSub) */
-	ndp_send(c, dst, &ra, ptr - (unsigned char *)&ra);
+	ndp_send(c, dst, buf, ptr - buf);
 }
 
 /**
diff --git a/passt.h b/passt.h
index c4c1f04..c6f4406 100644
--- a/passt.h
+++ b/passt.h
@@ -77,6 +77,7 @@ enum passt_modes {
 #define CONF_ADDR_OBSERVED	BIT(3)		/* Seen in guest traffic */
 #define CONF_ADDR_DHCP		BIT(4)		/* Advertise via DHCP (IPv4) */
 #define CONF_ADDR_DHCPV6	BIT(5)		/* Advertise via DHCPv6 (IPv6) */
+#define CONF_ADDR_NDP		BIT(6)		/* Advertise via NDP/RA (IPv6, /64) */
 
 /**
  * struct guest_addr - Unified IPv4/IPv6 address entry
-- 
2.52.0


      parent reply	other threads:[~2026-03-22  0:43 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
2026-03-22  0:43 ` [PATCH v6 01/13] conf: use a single buffer for print formatting in conf_print() Jon Maloy
2026-03-22  0:43 ` [PATCH v6 02/13] ip: Introduce unified multi-address data structures Jon Maloy
2026-03-22  0:43 ` [PATCH v6 03/13] fwd: Unify guest accessibility checks with unified address array Jon Maloy
2026-03-22  0:43 ` [PATCH v6 04/13] arp: Check all configured addresses in ARP filtering Jon Maloy
2026-03-22  0:43 ` [PATCH v6 05/13] conf: Allow multiple -a/--address options per address family Jon Maloy
2026-03-22  0:43 ` [PATCH v6 06/13] netlink, conf: Read all addresses from template interface at startup Jon Maloy
2026-03-22  0:43 ` [PATCH v6 07/13] ip: refactor function pasta_ns_conf() Jon Maloy
2026-03-22  0:43 ` [PATCH v6 08/13] ip: Track observed guest IPv4 addresses in unified address array Jon Maloy
2026-03-22  0:43 ` [PATCH v6 09/13] ip: Track observed guest IPv6 " Jon Maloy
2026-03-22  0:43 ` [PATCH v6 10/13] migrate: Update protocol to v3 for multi-address support Jon Maloy
2026-03-22  0:43 ` [PATCH v6 11/13] dhcp: Select address for DHCP distribution Jon Maloy
2026-03-22  0:43 ` [PATCH v6 12/13] dhcpv6: Select addresses for DHCPv6 distribution Jon Maloy
2026-03-22  0:43 ` Jon Maloy [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260322004333.365713-14-jmaloy@redhat.com \
    --to=jmaloy@redhat.com \
    --cc=david@gibson.dropbear.id.au \
    --cc=dgibson@redhat.com \
    --cc=passt-dev@passt.top \
    --cc=sbrivio@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://passt.top/passt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).