public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
* [PATCH v6 00/13] Introduce multiple addresses and late binding
@ 2026-03-22  0:43 Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 01/13] conf: use a single buffer for print formatting in conf_print() Jon Maloy
                   ` (12 more replies)
  0 siblings, 13 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev


This series adds handling of multiple addresses into a unified address
array, so that a guest can see the same addresses on his own interface.

o All addresses are stored as union inany_addr
o User configured addresses are marked with a USER flag.
o Host provided addresses are marked with a HOST flag.
o Link local addresses are also marked with a LINKLOCAL flag.
o Addresses the guest is actually using are marked with an OBSERVED flag.
o Addresses eligible for DHCP assignments are marked with an DHCP flag.
o Addresses eligible for DHCPv6 advertisement are marked with an DHCPV6 flag.
o Addresses eligible for NDP advertisement are marked with an NDP flag.

v2:
  - Added the earlier standalone CIDR commit to the head of the series.
  - Replaced the guest namespace interface subscriptions with just
    an address observation feature, so that it works with both PASTA
    and PASST.
  - Unified 'no_copy_addrs' and 'copy_addrs' code paths, as suggested
    by David G.
  - Multiple other changes, also based on feedback from David.
  - Removed the host interface subscription patches, -for now.
    I intend to re-add them once this series is applied.
  - Outstanding question: When do we add an IPv4 link local address
    to the guest? Only in local/opaque mode? Only when
    explicitly requested? Always?

v3:
  - Unified the IPv4 and IPv6 arrays into one array
  - Changed prefix_len to always be in IPv6/IpV4 mapped format
  - Updated migration protocol to v3, handling multiple addresses
  - Many other smaller changes, based on feedback from the PASST team   

v4:
  - Numerous changes based on feedback
  - Added several new commits, mostly broken
    out of the pre-existing ones.

v5: - Re-introduced multiple OBSERVED addresses. This actually
      turned out to be cleaner and with more predictable behaviour
      than allowing only one.
    - Included the DHCP and NDP patches from previous versions,
      improved and updated according to feedback from the team.
    - Likewise re-included the host-side netlink commit to support
      late binding.

v6: - Skipped late binding commit for now.
    - Added commit for using a single print buffer in conf_print
    - Added commit for reading and adding all addresses from
      template interface.
    - Added commit for refactoring pasta_ns_conf().
    - Added separate address flags for DHCP, DHCPv6, and NDP,
      so that those are easy to recognize for their respective
      functions.
    - Split DHCP and DHCPv6 address selection into separate commits.
    - Updated migration protocol to v3 for multi-address support.
    - Numerous other smaller changes, both after feedback from
      David G. and issues I have identified myself.

Jon Maloy (13):
  conf: use a single buffer for print formatting in conf_print()
  ip: Introduce unified multi-address data structures
  fwd: Unify guest accessibility checks with unified address array
  arp: Check all configured addresses in ARP filtering
  conf: Allow multiple -a/--address options per address family
  netlink, conf: Read all addresses from template interface at startup
  ip: refactor function pasta_ns_conf()
  ip: Track observed guest IPv4 addresses in unified address array
  ip: Track observed guest IPv6 addresses in unified address array
  migrate: Update protocol to v3 for multi-address support
  dhcp: Select address for DHCP distribution
  dhcpv6: Select addresses for DHCPv6 distribution
  ndp: Support advertising multiple prefixes in Router Advertisements


 arp.c     |  17 +++-
 conf.c    | 225 ++++++++++++++++++++++------------------
 dhcp.c    |  28 +++--
 dhcp.h    |   2 +-
 dhcpv6.c  | 120 +++++++++++++---------
 dhcpv6.h  |   2 +-
 fwd.c     | 300 ++++++++++++++++++++++++++++++++++++++++--------------
 fwd.h     |   8 ++
 igmp.c    |   1 +
 inany.h   |  13 +++
 ip.h      |   2 +
 migrate.c | 239 +++++++++++++++++++++++++++++++++++++++++--
 ndp.c     | 129 ++++++++++++++++-------
 netlink.c |  66 ++++++------
 netlink.h |   5 +-
 passt.h   |  93 ++++++++++++++---
 pasta.c   | 227 +++++++++++++++++++++--------------------
 tap.c     |  75 ++++++++++----
 18 files changed, 1093 insertions(+), 459 deletions(-)

-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 01/13] conf: use a single buffer for print formatting in conf_print()
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
@ 2026-03-22  0:43 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 02/13] ip: Introduce unified multi-address data structures Jon Maloy
                   ` (11 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

The function conf_print() uses three different buffers as target
for address print formatting. This is unnecessary, as a single
buffer of length INET6_ADDRSTRLEN has sufficient space for all
address types, IPv4, IPv6 and MAC. There is no risk for conflicts,
since all formatting is followed by an immediate info() printout.

To make our life easier in the following commits, we do this
simplification here.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
 conf.c | 35 ++++++++++++++++++-----------------
 1 file changed, 18 insertions(+), 17 deletions(-)

diff --git a/conf.c b/conf.c
index dafac46..9bcd9de 100644
--- a/conf.c
+++ b/conf.c
@@ -1136,11 +1136,12 @@ enum passt_modes conf_mode(int argc, char *argv[])
  */
 static void conf_print(const struct ctx *c)
 {
-	char buf4[INET_ADDRSTRLEN], buf6[INET6_ADDRSTRLEN];
-	char bufmac[ETH_ADDRSTRLEN], ifn[IFNAMSIZ];
+	char buf[INET6_ADDRSTRLEN];
 	int i;
 
 	if (c->ifi4 > 0 || c->ifi6 > 0) {
+		char ifn[IFNAMSIZ];
+
 		info("Template interface: %s%s%s%s%s",
 		     c->ifi4 > 0 ? if_indextoname(c->ifi4, ifn) : "",
 		     c->ifi4 > 0 ? " (IPv4)" : "",
@@ -1162,24 +1163,24 @@ static void conf_print(const struct ctx *c)
 	    !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_out)) {
 		info("Outbound address: %s%s%s",
 		     IN4_IS_ADDR_UNSPECIFIED(&c->ip4.addr_out) ? "" :
-		     inet_ntop(AF_INET, &c->ip4.addr_out, buf4, sizeof(buf4)),
+		     inet_ntop(AF_INET, &c->ip4.addr_out, buf, sizeof(buf)),
 		     (!IN4_IS_ADDR_UNSPECIFIED(&c->ip4.addr_out) &&
 		      !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_out)) ? ", " : "",
 		     IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_out) ? "" :
-		     inet_ntop(AF_INET6, &c->ip6.addr_out, buf6, sizeof(buf6)));
+		     inet_ntop(AF_INET6, &c->ip6.addr_out, buf, sizeof(buf)));
 	}
 
 	if (c->mode == MODE_PASTA && !c->splice_only)
 		info("Namespace interface: %s", c->pasta_ifn);
 
 	info("MAC:");
-	info("    host: %s", eth_ntop(c->our_tap_mac, bufmac, sizeof(bufmac)));
+	info("    host: %s", eth_ntop(c->our_tap_mac, buf, sizeof(buf)));
 
 	if (c->ifi4) {
 		if (!IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback))
 			info("    NAT to host 127.0.0.1: %s",
 			     inet_ntop(AF_INET, &c->ip4.map_host_loopback,
-				       buf4, sizeof(buf4)));
+				       buf, sizeof(buf)));
 
 		if (!c->no_dhcp) {
 			uint32_t mask;
@@ -1188,12 +1189,12 @@ static void conf_print(const struct ctx *c)
 
 			info("DHCP:");
 			info("    assign: %s",
-			     inet_ntop(AF_INET, &c->ip4.addr, buf4, sizeof(buf4)));
+			     inet_ntop(AF_INET, &c->ip4.addr, buf, sizeof(buf)));
 			info("    mask: %s",
-			     inet_ntop(AF_INET, &mask,        buf4, sizeof(buf4)));
+			     inet_ntop(AF_INET, &mask,        buf, sizeof(buf)));
 			info("    router: %s",
 			     inet_ntop(AF_INET, &c->ip4.guest_gw,
-				       buf4, sizeof(buf4)));
+				       buf, sizeof(buf)));
 		}
 
 		for (i = 0; i < ARRAY_SIZE(c->ip4.dns); i++) {
@@ -1201,8 +1202,8 @@ static void conf_print(const struct ctx *c)
 				break;
 			if (!i)
 				info("DNS:");
-			inet_ntop(AF_INET, &c->ip4.dns[i], buf4, sizeof(buf4));
-			info("    %s", buf4);
+			inet_ntop(AF_INET, &c->ip4.dns[i], buf, sizeof(buf));
+			info("    %s", buf);
 		}
 
 		for (i = 0; *c->dns_search[i].n; i++) {
@@ -1216,7 +1217,7 @@ static void conf_print(const struct ctx *c)
 		if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback))
 			info("    NAT to host ::1: %s",
 			     inet_ntop(AF_INET6, &c->ip6.map_host_loopback,
-				       buf6, sizeof(buf6)));
+				       buf, sizeof(buf)));
 
 		if (!c->no_ndp && !c->no_dhcpv6)
 			info("NDP/DHCPv6:");
@@ -1228,12 +1229,12 @@ static void conf_print(const struct ctx *c)
 			goto dns6;
 
 		info("    assign: %s",
-		     inet_ntop(AF_INET6, &c->ip6.addr, buf6, sizeof(buf6)));
+		     inet_ntop(AF_INET6, &c->ip6.addr, buf, sizeof(buf)));
 		info("    router: %s",
-		     inet_ntop(AF_INET6, &c->ip6.guest_gw, buf6, sizeof(buf6)));
+		     inet_ntop(AF_INET6, &c->ip6.guest_gw, buf, sizeof(buf)));
 		info("    our link-local: %s",
 		     inet_ntop(AF_INET6, &c->ip6.our_tap_ll,
-			       buf6, sizeof(buf6)));
+			       buf, sizeof(buf)));
 
 dns6:
 		for (i = 0; i < ARRAY_SIZE(c->ip6.dns); i++) {
@@ -1241,8 +1242,8 @@ dns6:
 			    break;
 			if (!i)
 				info("DNS:");
-			inet_ntop(AF_INET6, &c->ip6.dns[i], buf6, sizeof(buf6));
-			info("    %s", buf6);
+			inet_ntop(AF_INET6, &c->ip6.dns[i], buf, sizeof(buf));
+			info("    %s", buf);
 		}
 
 		for (i = 0; *c->dns_search[i].n; i++) {
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 02/13] ip: Introduce unified multi-address data structures
  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 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 03/13] fwd: Unify guest accessibility checks with unified address array Jon Maloy
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

As preparation for supporting multiple addresses per interface,
we replace the single addr/prefix_len fields with an array. The
array consists of a new struct inany_addr_entry containing an
address and prefix length, both in inany_addr format.

Despite some code refactoring, there are only two real functional
changes:
- The indicated IPv6 prefix length is now properly stored, instead
  of being ignored and overridden with the hardcoded value 64, as
  as has been the case until now.
- Since even IPv4 addresses now are stored in IPv6 format, we
  also store the corresponding prefix length in that format,
  i.e. using the range [96,128] instead of [0,32].

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

---
v2: -Using inany_addr instead of protocol specific addresses as
     entry address field.

v3: -Merging into one array, directly in struct ctx
    -Changed prefix_len and flags fields in struct inany_addr_entry
     to uint8_t, since that makes the struct directly migratable

v4: -Updated according to changes in previous commits
    -Updated according to feedback from David G.
    -Squashed IP4_MASK macro commit into this one

v6: -Renamed and moved some definitions
    -Introduced fwd_set_addr() and fwd_get_addr() already in this commit
    -Eliminated first_v4/v6() functions, replaced with fwd_get_addr()
    -Some other changes as suggested by David G.
    -I kept the flag CONF_ADDR_LINKLOCAL, since it will be
     needed later in an address selection function.
---
 arp.c    |  10 +++-
 conf.c   | 142 +++++++++++++++++++++++++++++--------------------------
 dhcp.c   |  13 +++--
 dhcpv6.c |  15 ++++--
 fwd.c    | 104 +++++++++++++++++++++++++++++++++-------
 fwd.h    |   4 ++
 igmp.c   |   1 +
 ip.h     |   2 +
 ndp.c    |  16 +++++--
 passt.h  |  82 ++++++++++++++++++++++++++++----
 pasta.c  |  21 ++++----
 tap.c    |   7 ++-
 12 files changed, 302 insertions(+), 115 deletions(-)

diff --git a/arp.c b/arp.c
index bb042e9..18ff5de 100644
--- a/arp.c
+++ b/arp.c
@@ -41,6 +41,8 @@
 static bool ignore_arp(const struct ctx *c,
 		       const struct arphdr *ah, const struct arpmsg *am)
 {
+	const struct guest_addr *a = fwd_get_addr(c, AF_INET, 0, 0);
+
 	if (ah->ar_hrd != htons(ARPHRD_ETHER)	||
 	    ah->ar_pro != htons(ETH_P_IP)	||
 	    ah->ar_hln != ETH_ALEN		||
@@ -54,7 +56,7 @@ static bool ignore_arp(const struct ctx *c,
 		return true;
 
 	/* Don't resolve the guest's assigned address, either. */
-	if (!memcmp(am->tip, &c->ip4.addr, sizeof(am->tip)))
+	if (a && !memcmp(am->tip, inany_v4(&a->addr), sizeof(am->tip)))
 		return true;
 
 	return false;
@@ -123,12 +125,16 @@ int arp(const struct ctx *c, struct iov_tail *data)
  */
 void arp_send_init_req(const struct ctx *c)
 {
+	const struct guest_addr *a = fwd_get_addr(c, AF_INET, 0, 0);
 	struct {
 		struct ethhdr eh;
 		struct arphdr ah;
 		struct arpmsg am;
 	} __attribute__((__packed__)) req;
 
+	if (!a)
+		return;
+
 	/* Ethernet header */
 	req.eh.h_proto = htons(ETH_P_ARP);
 	memcpy(req.eh.h_dest, MAC_BROADCAST, sizeof(req.eh.h_dest));
@@ -145,7 +151,7 @@ void arp_send_init_req(const struct ctx *c)
 	memcpy(req.am.sha,	c->our_tap_mac,		sizeof(req.am.sha));
 	memcpy(req.am.sip,	&c->ip4.our_tap_addr,	sizeof(req.am.sip));
 	memcpy(req.am.tha,	MAC_BROADCAST,		sizeof(req.am.tha));
-	memcpy(req.am.tip,	&c->ip4.addr,		sizeof(req.am.tip));
+	memcpy(req.am.tip,	inany_v4(&a->addr),	sizeof(req.am.tip));
 
 	debug("Sending initial ARP request for guest MAC address");
 	tap_send_single(c, &req, sizeof(req));
diff --git a/conf.c b/conf.c
index 9bcd9de..8f02494 100644
--- a/conf.c
+++ b/conf.c
@@ -728,13 +728,15 @@ static int conf_ip4_prefix(const char *arg)
 
 /**
  * conf_ip4() - Verify or detect IPv4 support, get relevant addresses
+ * @c:		Execution context
  * @ifi:	Host interface to attempt (0 to determine one)
- * @ip4:	IPv4 context (will be written)
  *
  * Return: interface index for IPv4, or 0 on failure.
  */
-static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
+static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
 {
+	struct ip4_ctx *ip4 = &c->ip4;
+
 	if (!ifi)
 		ifi = nl_get_ext_if(nl_sock, AF_INET);
 
@@ -753,60 +755,57 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
 		}
 	}
 
-	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr)) {
+	if (!fwd_get_addr(c, AF_INET, 0, 0)) {
+		struct in_addr addr;
+		int prefix_len;
 		int rc = nl_addr_get(nl_sock, ifi, AF_INET,
-				     &ip4->addr, &ip4->prefix_len, NULL);
+				     &addr, &prefix_len, NULL);
 		if (rc < 0) {
 			debug("Couldn't discover IPv4 address: %s",
 			      strerror_(-rc));
 			return 0;
 		}
-	}
+		if (IN4_IS_ADDR_UNSPECIFIED(&addr))
+			return 0;
 
-	if (!ip4->prefix_len) {
-		in_addr_t addr = ntohl(ip4->addr.s_addr);
-		if (IN_CLASSA(addr))
-			ip4->prefix_len = (32 - IN_CLASSA_NSHIFT);
-		else if (IN_CLASSB(addr))
-			ip4->prefix_len = (32 - IN_CLASSB_NSHIFT);
-		else if (IN_CLASSC(addr))
-			ip4->prefix_len = (32 - IN_CLASSC_NSHIFT);
-		else
-			ip4->prefix_len = 32;
+		fwd_set_addr(c, &inany_from_v4(addr), CONF_ADDR_HOST,
+			     prefix_len + 96);
+		ip4->addr_seen = addr;
 	}
 
-	ip4->addr_seen = ip4->addr;
-
 	ip4->our_tap_addr = ip4->guest_gw;
 
-	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr))
-		return 0;
-
 	return ifi;
 }
 
 /**
  * conf_ip4_local() - Configure IPv4 addresses and attributes for local mode
- * @ip4:	IPv4 context (will be written)
+ * @c:		Execution context (will be written)
  */
-static void conf_ip4_local(struct ip4_ctx *ip4)
+static void conf_ip4_local(struct ctx *c)
 {
-	ip4->addr_seen = ip4->addr = IP4_LL_GUEST_ADDR;
-	ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
-	ip4->prefix_len = IP4_LL_PREFIX_LEN;
+	struct ip4_ctx *ip4 = &c->ip4;
 
+	ip4->addr_seen = IP4_LL_GUEST_ADDR;
+	ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
 	ip4->no_copy_addrs = ip4->no_copy_routes = true;
+	fwd_set_addr(c, &inany_from_v4(IP4_LL_GUEST_ADDR),
+		     CONF_ADDR_HOST | CONF_ADDR_LINKLOCAL,
+		     IP4_LL_PREFIX_LEN + 96);
 }
 
 /**
  * conf_ip6() - Verify or detect IPv6 support, get relevant addresses
+ * @c:		Execution context
  * @ifi:	Host interface to attempt (0 to determine one)
- * @ip6:	IPv6 context (will be written)
  *
  * Return: interface index for IPv6, or 0 on failure.
  */
-static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
+static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
 {
+	const struct guest_addr *a = fwd_get_addr(c, AF_INET6, 0, 0);
+	struct ip6_ctx *ip6 = &c->ip6;
+	union inany_addr addr;
 	int prefix_len = 0;
 	int rc;
 
@@ -827,21 +826,24 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
 		}
 	}
 
-	rc = nl_addr_get(nl_sock, ifi, AF_INET6,
-			 IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ? &ip6->addr : NULL,
+	rc = nl_addr_get(nl_sock, ifi, AF_INET6, &addr.a6,
 			 &prefix_len, &ip6->our_tap_ll);
 	if (rc < 0) {
 		debug("Couldn't discover IPv6 address: %s", strerror_(-rc));
 		return 0;
 	}
 
-	ip6->addr_seen = ip6->addr;
+	if (!a) {
+		fwd_set_addr(c, &addr, CONF_ADDR_HOST, prefix_len);
+		ip6->addr_seen = addr.a6;
+	} else {
+		ip6->addr_seen = a->addr.a6;
+	}
 
 	if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
 		ip6->our_tap_ll = ip6->guest_gw;
 
-	if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ||
-	    IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
+	if (IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
 		return 0;
 
 	return ifi;
@@ -849,13 +851,13 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
 
 /**
  * conf_ip6_local() - Configure IPv6 addresses and attributes for local mode
- * @ip6:	IPv6 context (will be written)
+ * @c:		Execution context (will be written)
  */
-static void conf_ip6_local(struct ip6_ctx *ip6)
+static void conf_ip6_local(struct ctx *c)
 {
-	ip6->our_tap_ll = ip6->guest_gw = IP6_LL_GUEST_GW;
+	c->ip6.our_tap_ll = c->ip6.guest_gw = IP6_LL_GUEST_GW;
 
-	ip6->no_copy_addrs = ip6->no_copy_routes = true;
+	c->ip6.no_copy_addrs = c->ip6.no_copy_routes = true;
 }
 
 /**
@@ -1137,6 +1139,7 @@ enum passt_modes conf_mode(int argc, char *argv[])
 static void conf_print(const struct ctx *c)
 {
 	char buf[INET6_ADDRSTRLEN];
+	const struct guest_addr *a;
 	int i;
 
 	if (c->ifi4 > 0 || c->ifi6 > 0) {
@@ -1182,19 +1185,19 @@ static void conf_print(const struct ctx *c)
 			     inet_ntop(AF_INET, &c->ip4.map_host_loopback,
 				       buf, sizeof(buf)));
 
-		if (!c->no_dhcp) {
+		a = fwd_get_addr(c, AF_INET, 0, 0);
+		if (a && !c->no_dhcp) {
 			uint32_t mask;
 
-			mask = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+			mask = IN4_MASK(inany_prefix_len(a));
 
 			info("DHCP:");
-			info("    assign: %s",
-			     inet_ntop(AF_INET, &c->ip4.addr, buf, sizeof(buf)));
-			info("    mask: %s",
-			     inet_ntop(AF_INET, &mask,        buf, sizeof(buf)));
-			info("    router: %s",
-			     inet_ntop(AF_INET, &c->ip4.guest_gw,
-				       buf, sizeof(buf)));
+			inany_ntop(&a->addr, buf, sizeof(buf));
+			info("    assign: %s", buf);
+			inet_ntop(AF_INET, &mask, buf, sizeof(buf));
+			info("    mask: %s", buf);
+			inet_ntop(AF_INET, &c->ip4.guest_gw, buf, sizeof(buf));
+			info("    router: %s", buf);
 		}
 
 		for (i = 0; i < ARRAY_SIZE(c->ip4.dns); i++) {
@@ -1228,13 +1231,14 @@ static void conf_print(const struct ctx *c)
 		else
 			goto dns6;
 
-		info("    assign: %s",
-		     inet_ntop(AF_INET6, &c->ip6.addr, buf, sizeof(buf)));
-		info("    router: %s",
-		     inet_ntop(AF_INET6, &c->ip6.guest_gw, buf, sizeof(buf)));
-		info("    our link-local: %s",
-		     inet_ntop(AF_INET6, &c->ip6.our_tap_ll,
-			       buf, sizeof(buf)));
+		a = fwd_get_addr(c, AF_INET6, 0, CONF_ADDR_LINKLOCAL);
+		if (a)
+			inany_ntop(&a->addr, buf, sizeof(buf));
+		info("    assign: %s", !a ? "" : buf);
+		inet_ntop(AF_INET6, &c->ip6.guest_gw, buf, sizeof(buf));
+		info("    router: %s", buf);
+		inet_ntop(AF_INET6, &c->ip6.our_tap_ll, buf, sizeof(buf));
+		info("    our link-local: %s", buf);
 
 dns6:
 		for (i = 0; i < ARRAY_SIZE(c->ip6.dns); i++) {
@@ -1887,16 +1891,12 @@ void conf(struct ctx *c, int argc, char **argv)
 			    IN6_IS_ADDR_V4COMPAT(&addr.a6))
 				die("Invalid address: %s", optarg);
 
-			if (inany_v4(&addr)) {
-				c->ip4.addr = *inany_v4(&addr);
-				c->ip4.prefix_len = prefix_len - 96;
-				if (c->mode == MODE_PASTA)
-					c->ip4.no_copy_addrs = true;
-			} else {
-				c->ip6.addr = addr.a6;
-				if (c->mode == MODE_PASTA)
-					c->ip6.no_copy_addrs = true;
-			}
+			/* Legacy behaviour: replace existing address if any */
+			fwd_set_addr(c, &addr, CONF_ADDR_USER, prefix_len);
+			if (inany_v4(&addr))
+				c->ip4.no_copy_addrs = true;
+			else
+				c->ip6.no_copy_addrs = true;
 			break;
 		}
 		case 'n': {
@@ -1910,7 +1910,13 @@ void conf(struct ctx *c, int argc, char **argv)
 				die("Invalid prefix length: %s", optarg);
 
 			prefix_len_from_opt = plen + 96;
-			c->ip4.prefix_len = plen;
+
+			for (int i = 0; i < c->addr_count; i++) {
+				if (!inany_v4(&c->addrs[i].addr))
+					continue;
+				c->addrs[i].prefix_len = prefix_len_from_opt;
+				break;
+			}
 			break;
 		}
 		case 'M':
@@ -2104,9 +2110,9 @@ void conf(struct ctx *c, int argc, char **argv)
 
 	nl_sock_init(c, false);
 	if (!v6_only && !c->splice_only)
-		c->ifi4 = conf_ip4(ifi4, &c->ip4);
+		c->ifi4 = conf_ip4(c, ifi4);
 	if (!v4_only && !c->splice_only)
-		c->ifi6 = conf_ip6(ifi6, &c->ip6);
+		c->ifi6 = conf_ip6(c, ifi6);
 
 	if (c->ifi4 && c->mtu < IPV4_MIN_MTU) {
 		warn("MTU %"PRIu16" is too small for IPv4 (minimum %u)",
@@ -2129,7 +2135,7 @@ void conf(struct ctx *c, int argc, char **argv)
 	if (!c->ifi4 && !v6_only) {
 		if (!c->splice_only) {
 			info("IPv4: no external interface as template, use local mode");
-			conf_ip4_local(&c->ip4);
+			conf_ip4_local(c);
 		}
 		c->ifi4 = -1;
 	}
@@ -2137,7 +2143,7 @@ void conf(struct ctx *c, int argc, char **argv)
 	if (!c->ifi6 && !v4_only) {
 		if (!c->splice_only) {
 			info("IPv6: no external interface as template, use local mode");
-			conf_ip6_local(&c->ip6);
+			conf_ip6_local(c);
 		}
 		c->ifi6 = -1;
 	}
@@ -2202,7 +2208,7 @@ void conf(struct ctx *c, int argc, char **argv)
 	if (!c->ifi6) {
 		c->no_ndp = 1;
 		c->no_dhcpv6 = 1;
-	} else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
+	} else if (!fwd_get_addr(c, AF_INET6, 0, 0)) {
 		c->no_dhcpv6 = 1;
 	}
 
diff --git a/dhcp.c b/dhcp.c
index 1ff8cba..6d9def5 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -302,6 +302,7 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
  */
 int dhcp(const struct ctx *c, struct iov_tail *data)
 {
+	const struct guest_addr *a = fwd_get_addr(c, AF_INET, 0, 0);
 	char macstr[ETH_ADDRSTRLEN];
 	size_t mlen, dlen, opt_len;
 	struct in_addr mask, dst;
@@ -313,6 +314,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	const struct udphdr *uh;
 	struct msg m_storage;
 	struct msg const *m;
+	struct in_addr addr;
 	struct msg reply;
 	unsigned int i;
 
@@ -344,6 +346,9 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	    m->op != BOOTREQUEST)
 		return -1;
 
+	ASSERT(a);
+	addr = *inany_v4(&a->addr);
+
 	reply.op		= BOOTREPLY;
 	reply.htype		= m->htype;
 	reply.hlen		= m->hlen;
@@ -352,7 +357,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	reply.secs		= 0;
 	reply.flags		= m->flags;
 	reply.ciaddr		= m->ciaddr;
-	reply.yiaddr		= c->ip4.addr;
+	reply.yiaddr		= addr;
 	reply.siaddr		= 0;
 	reply.giaddr		= m->giaddr;
 	memcpy(&reply.chaddr,	m->chaddr,	sizeof(reply.chaddr));
@@ -404,7 +409,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 
 	info("    from %s", eth_ntop(m->chaddr, macstr, sizeof(macstr)));
 
-	mask.s_addr = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+	mask.s_addr = IN4_MASK(inany_prefix_len(a));
 	memcpy(opts[1].s,  &mask,                sizeof(mask));
 	memcpy(opts[3].s,  &c->ip4.guest_gw,     sizeof(c->ip4.guest_gw));
 	memcpy(opts[54].s, &c->ip4.our_tap_addr, sizeof(c->ip4.our_tap_addr));
@@ -412,7 +417,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	/* If the gateway is not on the assigned subnet, send an option 121
 	 * (Classless Static Routing) adding a dummy route to it.
 	 */
-	if ((c->ip4.addr.s_addr & mask.s_addr)
+	if ((addr.s_addr & mask.s_addr)
 	    != (c->ip4.guest_gw.s_addr & mask.s_addr)) {
 		/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
 		opts[121].slen = 14;
@@ -471,7 +476,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	if (m->flags & FLAG_BROADCAST)
 		dst = in4addr_broadcast;
 	else
-		dst = c->ip4.addr;
+		dst = addr;
 
 	tap_udp4_send(c, c->ip4.our_tap_addr, 67, dst, 68, &reply, dlen);
 
diff --git a/dhcpv6.c b/dhcpv6.c
index 97c04e2..3a007bf 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -318,7 +318,7 @@ static bool dhcpv6_opt(struct iov_tail *data, uint16_t type)
  *         false otherwise and @data is unmodified
  */
 static bool dhcpv6_ia_notonlink(struct iov_tail *data,
-				struct in6_addr *la)
+				const struct in6_addr *la)
 {
 	int ia_types[2] = { OPT_IA_NA, OPT_IA_TA };
 	struct opt_ia_addr opt_addr_storage;
@@ -566,6 +566,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	struct opt_hdr client_id_storage;
 	/* cppcheck-suppress [variableScope,unmatchedSuppression] */
 	struct opt_ia_na ia_storage;
+	const struct guest_addr *a;
 	const struct in6_addr *src;
 	struct msg_hdr mh_storage;
 	const struct msg_hdr *mh;
@@ -573,6 +574,8 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	const struct udphdr *uh;
 	size_t mlen, n;
 
+	a = fwd_get_addr(c, AF_INET6, 0, CONF_ADDR_LINKLOCAL);
+
 	uh = IOV_REMOVE_HEADER(data, uh_storage);
 	if (!uh)
 		return -1;
@@ -628,7 +631,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 		if (mh->type == TYPE_CONFIRM && server_id)
 			return -1;
 
-		if (dhcpv6_ia_notonlink(data, &c->ip6.addr)) {
+		if (a && dhcpv6_ia_notonlink(data, &a->addr.a6)) {
 
 			dhcpv6_send_ia_notonlink(c, data, &client_id_base,
 						 ntohs(client_id->l), mh->xid);
@@ -682,7 +685,8 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 
 	tap_udp6_send(c, src, 547, tap_ip6_daddr(c, src), 546,
 		      mh->xid, &resp, n);
-	c->ip6.addr_seen = c->ip6.addr;
+	if (a)
+		c->ip6.addr_seen = a->addr.a6;
 
 	return 1;
 }
@@ -693,6 +697,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
  */
 void dhcpv6_init(const struct ctx *c)
 {
+	const struct guest_addr *a;
 	time_t y2k = 946684800; /* Epoch to 2000-01-01T00:00:00Z, no mktime() */
 	uint32_t duid_time;
 
@@ -706,5 +711,7 @@ void dhcpv6_init(const struct ctx *c)
 	memcpy(resp_not_on_link.server_id.duid_lladdr,
 	       c->our_tap_mac, sizeof(c->our_tap_mac));
 
-	resp.ia_addr.addr	= c->ip6.addr;
+	a = fwd_get_addr(c, AF_INET6, 0, CONF_ADDR_LINKLOCAL);
+	if (a)
+		resp.ia_addr.addr = a->addr.a6;
 }
diff --git a/fwd.c b/fwd.c
index bedbf98..9101d6d 100644
--- a/fwd.c
+++ b/fwd.c
@@ -249,6 +249,64 @@ void fwd_neigh_table_init(const struct ctx *c)
 		fwd_neigh_table_update(c, &mga, c->our_tap_mac, true);
 }
 
+/**
+ * fwd_set_addr() - Add or update an address in the unified address array
+ * @c:		Execution context
+ * @addr:	Address to add (IPv4-mapped or IPv6)
+ * @flags:	CONF_ADDR_* flags for this address
+ * @prefix_len:	Prefix length in IPv6/mapped format, 0-128
+ *
+ * Find the first existing entry of the same address family and
+ * overwrite it, or create a new one if none exists
+ */
+void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
+		  uint8_t flags, int prefix_len)
+{
+	struct guest_addr *a;
+
+	for (int i = 0; i < c->addr_count; i++) {
+		a = &c->addrs[i];
+		if ((inany_v4(addr) && inany_v4(&a->addr)) ||
+		    (!inany_v4(addr) && !inany_v4(&a->addr)))
+			goto found;
+	}
+
+	if (c->addr_count >= MAX_GUEST_ADDRS)
+		return;
+
+	a = &c->addrs[c->addr_count++];
+
+found:
+	a->addr = *addr;
+	a->prefix_len = prefix_len;
+	a->flags = flags;
+}
+
+/**
+ * fwd_get_addr() - Get guest address entry matching criteria
+ * @c:		Execution context
+ * @af:		Address family (AF_INET, AF_INET6, or 0 for any)
+ * @incl:	Flags that must be present (any-match)
+ * @excl:	Flags that must not be present
+ *
+ * Return: first address entry matching criteria, or NULL
+ */
+const struct guest_addr *fwd_get_addr(const struct ctx *c, sa_family_t af,
+				      uint8_t incl, uint8_t excl)
+{
+	const struct guest_addr *a;
+
+	for_each_addr(a, c, af) {
+		if (incl && !(a->flags & incl))
+			continue;
+		if (a->flags & excl)
+			continue;
+		return a;
+	}
+
+	return NULL;
+}
+
 /** fwd_probe_ephemeral() - Determine what ports this host considers ephemeral
  *
  * Work out what ports the host thinks are emphemeral and record it for later
@@ -941,8 +999,10 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
  *         translation, false otherwise
  */
 static bool fwd_guest_accessible4(const struct ctx *c,
-				    const struct in_addr *addr)
+				  const struct in_addr *addr)
 {
+	const struct guest_addr *a = fwd_get_addr(c, AF_INET, 0, 0);
+
 	if (IN4_IS_ADDR_LOOPBACK(addr))
 		return false;
 
@@ -957,7 +1017,7 @@ static bool fwd_guest_accessible4(const struct ctx *c,
 	/* For IPv4, addr_seen is initialised to addr, so is always a valid
 	 * address
 	 */
-	if (IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr) ||
+	if ((a && IN4_ARE_ADDR_EQUAL(addr, inany_v4(&a->addr))) ||
 	    IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
 		return false;
 
@@ -975,10 +1035,12 @@ static bool fwd_guest_accessible4(const struct ctx *c,
 static bool fwd_guest_accessible6(const struct ctx *c,
 				  const struct in6_addr *addr)
 {
+	const struct guest_addr *a = fwd_get_addr(c, AF_INET6, 0, 0);
+
 	if (IN6_IS_ADDR_LOOPBACK(addr))
 		return false;
 
-	if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addr))
+	if (a && IN6_ARE_ADDR_EQUAL(addr, &a->addr.a6))
 		return false;
 
 	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
@@ -1023,14 +1085,19 @@ static bool fwd_guest_accessible(const struct ctx *c,
 static void nat_outbound(const struct ctx *c, const union inany_addr *addr,
 			 union inany_addr *translated)
 {
+	const struct guest_addr *ga;
+
 	if (inany_equals4(addr, &c->ip4.map_host_loopback))
 		*translated = inany_loopback4;
 	else if (inany_equals6(addr, &c->ip6.map_host_loopback))
 		*translated = inany_loopback6;
-	else if (inany_equals4(addr, &c->ip4.map_guest_addr))
-		*translated = inany_from_v4(c->ip4.addr);
-	else if (inany_equals6(addr, &c->ip6.map_guest_addr))
-		translated->a6 = c->ip6.addr;
+	else if (inany_equals4(addr, &c->ip4.map_guest_addr)) {
+		ga = fwd_get_addr(c, AF_INET, 0, 0);
+		*translated = ga ? ga->addr : inany_any4;
+	} else if (inany_equals6(addr, &c->ip6.map_guest_addr)) {
+		ga = fwd_get_addr(c, AF_INET6, 0, 0);
+		translated->a6 = ga ? ga->addr.a6 : in6addr_any;
+	}
 	else
 		*translated = *addr;
 }
@@ -1137,16 +1204,21 @@ bool nat_inbound(const struct ctx *c, const union inany_addr *addr,
 	} else if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback) &&
 		   inany_equals6(addr, &in6addr_loopback)) {
 		translated->a6 = c->ip6.map_host_loopback;
-	} else if (!IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_guest_addr) &&
-		   inany_equals4(addr, &c->ip4.addr)) {
-		*translated = inany_from_v4(c->ip4.map_guest_addr);
-	} else if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_guest_addr) &&
-		   inany_equals6(addr, &c->ip6.addr)) {
-		translated->a6 = c->ip6.map_guest_addr;
-	} else if (fwd_guest_accessible(c, addr)) {
-		*translated = *addr;
 	} else {
-		return false;
+		const struct guest_addr *ga4 = fwd_get_addr(c, AF_INET, 0, 0);
+		const struct guest_addr *ga6 = fwd_get_addr(c, AF_INET6, 0, 0);
+
+		if (!IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_guest_addr) &&
+		    ga4 && inany_equals(addr, &ga4->addr)) {
+			*translated = inany_from_v4(c->ip4.map_guest_addr);
+		} else if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_guest_addr) &&
+			   ga6 && inany_equals(addr, &ga6->addr)) {
+			translated->a6 = c->ip6.map_guest_addr;
+		} else if (fwd_guest_accessible(c, addr)) {
+			*translated = *addr;
+		} else {
+			return false;
+		}
 	}
 
 	return true;
diff --git a/fwd.h b/fwd.h
index 958eee2..c5a1068 100644
--- a/fwd.h
+++ b/fwd.h
@@ -23,6 +23,8 @@ struct flowside;
 
 void fwd_probe_ephemeral(void);
 bool fwd_port_is_ephemeral(in_port_t port);
+const struct guest_addr *fwd_get_addr(const struct ctx *c, sa_family_t af,
+				      uint8_t incl, uint8_t excl);
 
 /**
  * struct fwd_rule - Forwarding rule governing a range of ports
@@ -141,5 +143,7 @@ void fwd_neigh_table_free(const struct ctx *c,
 void fwd_neigh_mac_get(const struct ctx *c, const union inany_addr *addr,
 		       uint8_t *mac);
 void fwd_neigh_table_init(const struct ctx *c);
+void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
+		  uint8_t flags, int prefix_len);
 
 #endif /* FWD_H */
diff --git a/igmp.c b/igmp.c
index a3971fc..0e77584 100644
--- a/igmp.c
+++ b/igmp.c
@@ -13,4 +13,5 @@
  */
 
 /* TO BE IMPLEMENTED */
+/* cppcheck-suppress unusedFunction */
 __attribute__((__unused__)) static void unused(void) { }
diff --git a/ip.h b/ip.h
index d0de6c8..933d98c 100644
--- a/ip.h
+++ b/ip.h
@@ -19,6 +19,8 @@
 	(ntohl(((struct in_addr *)(a))->s_addr) >> IN_CLASSA_NSHIFT == IN_LOOPBACKNET)
 #define IN4_IS_ADDR_MULTICAST(a) \
 	(IN_MULTICAST(ntohl(((struct in_addr *)(a))->s_addr)))
+#define IN4_MASK(prefix) \
+	((prefix) <= 0 ? 0 : htonl(0xffffffff << (32 - (prefix))))
 #define IN4_ARE_ADDR_EQUAL(a, b) \
 	(((struct in_addr *)(a))->s_addr == ((struct in_addr *)b)->s_addr)
 #define IN4ADDR_LOOPBACK_INIT \
diff --git a/ndp.c b/ndp.c
index 1f2bcb0..3750fc5 100644
--- a/ndp.c
+++ b/ndp.c
@@ -257,7 +257,6 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
 			.valid_lifetime		= ~0U,
 			.pref_lifetime		= ~0U,
 		},
-		.prefix = c->ip6.addr,
 		.source_ll = {
 			.header = {
 				.type		= OPT_SRC_L2_ADDR,
@@ -265,8 +264,13 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
 			},
 		},
 	};
+	const struct guest_addr *a = fwd_get_addr(c, AF_INET6, 0, 0);
 	unsigned char *ptr = NULL;
 
+	ASSERT(a);
+
+	ra.prefix = a->addr.a6;
+
 	ptr = &ra.var[0];
 
 	if (c->mtu) {
@@ -460,6 +464,7 @@ first:
  */
 void ndp_send_init_req(const struct ctx *c)
 {
+	const struct guest_addr *a = fwd_get_addr(c, AF_INET6, 0, 0);
 	struct ndp_ns ns = {
 		.ih = {
 			.icmp6_type		= NS,
@@ -468,8 +473,13 @@ void ndp_send_init_req(const struct ctx *c)
 			.icmp6_solicited	= 0, /* Reserved */
 			.icmp6_override		= 0, /* Reserved */
 		},
-		.target_addr = c->ip6.addr
+		.target_addr = IN6ADDR_ANY_INIT
 	};
+
+	if (!a)
+		return;
+
+	ns.target_addr = a->addr.a6;
 	debug("Sending initial NDP NS request for guest MAC address");
-	ndp_send(c, &c->ip6.addr, &ns, sizeof(ns));
+	ndp_send(c, &a->addr.a6, &ns, sizeof(ns));
 }
diff --git a/passt.h b/passt.h
index b614bdf..6bca778 100644
--- a/passt.h
+++ b/passt.h
@@ -64,11 +64,31 @@ enum passt_modes {
 	MODE_VU,
 };
 
+/* Maximum number of addresses in context address array */
+#define MAX_GUEST_ADDRS		32
+
+/* Flags indicating origin and role of a guest address
+ * To be used in struct guest_addr
+ */
+#define CONF_ADDR_USER		BIT(0)		/* User set via -a */
+#define CONF_ADDR_HOST		BIT(1)		/* From host interface */
+#define CONF_ADDR_LINKLOCAL	BIT(2)		/* Link-local address */
+
+/**
+ * struct guest_addr - Unified IPv4/IPv6 address entry
+ * @addr:	IPv4 (as mapped) or IPv6 address
+ * @prefix_len:	Prefix length in IPv6/IPv4-mapped [0,128]/[96,128] format
+ * @flags:	CONF_ADDR_* flags
+ */
+struct guest_addr {
+	union inany_addr addr;
+	uint8_t prefix_len;
+	uint8_t flags;
+};
+
 /**
  * struct ip4_ctx - IPv4 execution context
- * @addr:		IPv4 address assigned to guest
  * @addr_seen:		Latest IPv4 address seen as source from tap
- * @prefixlen:		IPv4 prefix length (netmask)
  * @guest_gw:		IPv4 gateway as seen by the guest
  * @map_host_loopback:	Outbound connections to this address are NATted to the
  *                      host's 127.0.0.1
@@ -84,10 +104,7 @@ enum passt_modes {
  * @no_copy_addrs:	Don't copy all addresses when configuring namespace
  */
 struct ip4_ctx {
-	/* PIF_TAP addresses */
-	struct in_addr addr;
 	struct in_addr addr_seen;
-	int prefix_len;
 	struct in_addr guest_gw;
 	struct in_addr map_host_loopback;
 	struct in_addr map_guest_addr;
@@ -107,7 +124,6 @@ struct ip4_ctx {
 
 /**
  * struct ip6_ctx - IPv6 execution context
- * @addr:		IPv6 address assigned to guest
  * @addr_seen:		Latest IPv6 global/site address seen as source from tap
  * @addr_ll_seen:	Latest IPv6 link-local address seen as source from tap
  * @guest_gw:		IPv6 gateway as seen by the guest
@@ -125,8 +141,6 @@ struct ip4_ctx {
  * @no_copy_addrs:	Don't copy all addresses when configuring namespace
  */
 struct ip6_ctx {
-	/* PIF_TAP addresses */
-	struct in6_addr addr;
 	struct in6_addr addr_seen;
 	struct in6_addr addr_ll_seen;
 	struct in6_addr guest_gw;
@@ -181,6 +195,8 @@ struct ip6_ctx {
  * @fqdn:		Guest FQDN
  * @ifi6:		Template interface for IPv6, -1: none, 0: IPv6 disabled
  * @ip6:		IPv6 configuration
+ * @addrs:		Unified address array for both IPv4 (mapped) and IPv6
+ * @addr_count:		Number of active entries in @addrs array
  * @pasta_ifn:		Name of namespace interface for pasta
  * @pasta_ifi:		Index of namespace interface for pasta
  * @pasta_conf_ns:	Configure namespace after creating it
@@ -260,6 +276,9 @@ struct ctx {
 	int ifi6;
 	struct ip6_ctx ip6;
 
+	struct guest_addr addrs[MAX_GUEST_ADDRS];
+	int addr_count;
+
 	char pasta_ifn[IF_NAMESIZE];
 	unsigned int pasta_ifi;
 	int pasta_conf_ns;
@@ -301,6 +320,53 @@ struct ctx {
 	bool migrate_exit;
 };
 
+/**
+ * inany_prefix_len() - Get address-family-appropriate prefix length
+ * @a:		Address entry
+ *
+ * Return: prefix length in native format (0-32 for IPv4, 0-128 for IPv6)
+ */
+static inline int inany_prefix_len(const struct guest_addr *a)
+{
+	if (inany_v4(&a->addr))
+		return a->prefix_len - 96;
+	return a->prefix_len;
+}
+
+/**
+ * _next_addr_idx() - Find next address index matching family filter
+ * @c:		Pointer to struct ctx
+ * @i:		Starting index
+ * @af:		Address family filter: AF_INET, AF_INET6, or 0 for all
+ *
+ * Return: next matching index, or addr_count if none found
+ */
+static inline int _next_addr_idx(const struct ctx *c, int i, sa_family_t af)
+{
+	for (; i < c->addr_count; i++) {
+		sa_family_t entry_af;
+
+		entry_af = inany_v4(&c->addrs[i].addr) ? AF_INET : AF_INET6;
+
+		if (af == AF_UNSPEC || af == entry_af)
+			return i;
+	}
+	return i;
+}
+
+/**
+ * for_each_addr() - Iterate over addresses in unified array
+ * @a:		Pointer variable for current entry (struct guest_addr *)
+ * @c:		Pointer to struct ctx
+ * @af:		Address family filter: AF_INET, AF_INET6, or 0 for all
+ *
+ * Note: @_i is the internal loop counter, uses _next_addr_idx() helper
+ */
+#define for_each_addr(a, c, af)						\
+	for (int _i = _next_addr_idx((c), 0, (af));			\
+	     _i < (c)->addr_count && ((a) = &(c)->addrs[_i], true);	\
+	     _i = _next_addr_idx((c), _i + 1, (af)))
+
 void proto_update_l2_buf(const unsigned char *eth_d);
 
 #endif /* PASST_H */
diff --git a/pasta.c b/pasta.c
index bab945f..e88a893 100644
--- a/pasta.c
+++ b/pasta.c
@@ -330,6 +330,7 @@ void pasta_ns_conf(struct ctx *c)
 
 	if (c->pasta_conf_ns) {
 		unsigned int flags = IFF_UP;
+		const struct guest_addr *a;
 
 		if (c->mtu)
 			nl_link_set_mtu(nl_sock_ns, c->pasta_ifi, c->mtu);
@@ -341,10 +342,12 @@ void pasta_ns_conf(struct ctx *c)
 
 		if (c->ifi4) {
 			if (c->ip4.no_copy_addrs) {
-				rc = nl_addr_set(nl_sock_ns, c->pasta_ifi,
-						 AF_INET,
-						 &c->ip4.addr,
-						 c->ip4.prefix_len);
+				a = fwd_get_addr(c, AF_INET, 0, 0);
+				if (a)
+					rc = nl_addr_set(nl_sock_ns,
+							 c->pasta_ifi, AF_INET,
+							 inany_v4(&a->addr),
+							 a->prefix_len - 96);
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi4,
 						 nl_sock_ns, c->pasta_ifi,
@@ -397,11 +400,13 @@ ipv4_done:
 					  0, IFF_NOARP);
 
 			if (c->ip6.no_copy_addrs) {
-				if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
+				a = fwd_get_addr(c, AF_INET6, 0, 0);
+				if (a)
 					rc = nl_addr_set(nl_sock_ns,
-							 c->pasta_ifi, AF_INET6,
-							 &c->ip6.addr, 64);
-				}
+							 c->pasta_ifi,
+							 AF_INET6,
+							 &a->addr.a6,
+							 a->prefix_len);
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi6,
 						 nl_sock_ns, c->pasta_ifi,
diff --git a/tap.c b/tap.c
index eaa6111..78a494a 100644
--- a/tap.c
+++ b/tap.c
@@ -951,8 +951,11 @@ resume:
 				c->ip6.addr_seen = *saddr;
 			}
 
-			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
-				c->ip6.addr = *saddr;
+			if (!fwd_get_addr(c, AF_INET6, 0, 0)) {
+				union inany_addr addr = { .a6 = *saddr };
+
+				fwd_set_addr(c, &addr, CONF_ADDR_LINKLOCAL, 64);
+			}
 		} else if (!IN6_IS_ADDR_UNSPECIFIED(saddr)){
 			c->ip6.addr_seen = *saddr;
 		}
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 03/13] fwd: Unify guest accessibility checks with unified address array
  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 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 04/13] arp: Check all configured addresses in ARP filtering Jon Maloy
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

We replace the fwd_guest_accessible4() and fwd_guest_accessible6()
functions with a unified fwd_guest_accessible() function that handles
both address families. With the unified address array, we can check
all configured addresses in a single pass using for_each_addr() with
family filter AF_UNSPEC.

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

---
v6: -Some fixes based on feedback from David Gibson
---
 fwd.c | 69 ++++++++++++++---------------------------------------------
 1 file changed, 16 insertions(+), 53 deletions(-)

diff --git a/fwd.c b/fwd.c
index 9101d6d..695b5a5 100644
--- a/fwd.c
+++ b/fwd.c
@@ -991,19 +991,19 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
 }
 
 /**
- * fwd_guest_accessible4() - Is IPv4 address guest-accessible
+ * fwd_guest_accessible() - Is address guest-accessible
  * @c:		Execution context
- * @addr:	Host visible IPv4 address
+ * @addr:	Host visible address (IPv4 or IPv6)
  *
  * Return: true if @addr on the host is accessible to the guest without
  *         translation, false otherwise
  */
-static bool fwd_guest_accessible4(const struct ctx *c,
-				  const struct in_addr *addr)
+static bool fwd_guest_accessible(const struct ctx *c,
+				 const union inany_addr *addr)
 {
-	const struct guest_addr *a = fwd_get_addr(c, AF_INET, 0, 0);
+	const struct guest_addr *a;
 
-	if (IN4_IS_ADDR_LOOPBACK(addr))
+	if (inany_is_loopback(addr))
 		return false;
 
 	/* In socket interfaces 0.0.0.0 generally means "any" or unspecified,
@@ -1011,36 +1011,18 @@ static bool fwd_guest_accessible4(const struct ctx *c,
 	 * that has a different meaning for host and guest, we can't let it
 	 * through untranslated.
 	 */
-	if (IN4_IS_ADDR_UNSPECIFIED(addr))
+	if (inany_is_unspecified(addr))
 		return false;
 
-	/* For IPv4, addr_seen is initialised to addr, so is always a valid
-	 * address
-	 */
-	if ((a && IN4_ARE_ADDR_EQUAL(addr, inany_v4(&a->addr))) ||
-	    IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
-		return false;
-
-	return true;
-}
-
-/**
- * fwd_guest_accessible6() - Is IPv6 address guest-accessible
- * @c:		Execution context
- * @addr:	Host visible IPv6 address
- *
- * Return: true if @addr on the host is accessible to the guest without
- *         translation, false otherwise
- */
-static bool fwd_guest_accessible6(const struct ctx *c,
-				  const struct in6_addr *addr)
-{
-	const struct guest_addr *a = fwd_get_addr(c, AF_INET6, 0, 0);
-
-	if (IN6_IS_ADDR_LOOPBACK(addr))
-		return false;
+	/* Check against all configured guest addresses */
+	for_each_addr(a, c, AF_UNSPEC)
+		if (inany_equals(addr, &a->addr))
+			return false;
 
-	if (a && IN6_ARE_ADDR_EQUAL(addr, &a->addr.a6))
+	/* Also check addr_seen: it tracks the address the guest is actually
+	 * using, which may differ from configured addresses.
+	 */
+	if (inany_equals4(addr, &c->ip4.addr_seen))
 		return false;
 
 	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
@@ -1048,31 +1030,12 @@ static bool fwd_guest_accessible6(const struct ctx *c,
 	 * if it has been set to a real address.
 	 */
 	if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen) &&
-	    IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addr_seen))
+	    inany_equals6(addr, &c->ip6.addr_seen))
 		return false;
 
 	return true;
 }
 
-/**
- * fwd_guest_accessible() - Is IPv[46] address guest-accessible
- * @c:		Execution context
- * @addr:	Host visible IPv[46] address
- *
- * Return: true if @addr on the host is accessible to the guest without
- *         translation, false otherwise
- */
-static bool fwd_guest_accessible(const struct ctx *c,
-				 const union inany_addr *addr)
-{
-	const struct in_addr *a4 = inany_v4(addr);
-
-	if (a4)
-		return fwd_guest_accessible4(c, a4);
-
-	return fwd_guest_accessible6(c, &addr->a6);
-}
-
 /**
  * nat_outbound() - Apply address translation for outbound (TAP to HOST)
  * @c:		Execution context
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 04/13] arp: Check all configured addresses in ARP filtering
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (2 preceding siblings ...)
  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 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 05/13] conf: Allow multiple -a/--address options per address family Jon Maloy
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

As a preparation for handling multiple addresses, we update ignore_arp()
to check against all addresses in the unified addrs[] array using the
for_each_addr() macro.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
Reviewed-by: David Gibson <david@gibson.dropbear.id.au>

---
v3: -Adapted to single-array changes earlier in this series
v6: -Made loop in ignore_arp() a little more palatable,
     but not entirely as suggested by David.
---
 arp.c | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/arp.c b/arp.c
index 18ff5de..6614804 100644
--- a/arp.c
+++ b/arp.c
@@ -41,7 +41,8 @@
 static bool ignore_arp(const struct ctx *c,
 		       const struct arphdr *ah, const struct arpmsg *am)
 {
-	const struct guest_addr *a = fwd_get_addr(c, AF_INET, 0, 0);
+	const struct guest_addr *a;
+	union inany_addr addr;
 
 	if (ah->ar_hrd != htons(ARPHRD_ETHER)	||
 	    ah->ar_pro != htons(ETH_P_IP)	||
@@ -55,9 +56,11 @@ static bool ignore_arp(const struct ctx *c,
 	    !memcmp(am->sip, am->tip, sizeof(am->sip)))
 		return true;
 
-	/* Don't resolve the guest's assigned address, either. */
-	if (a && !memcmp(am->tip, inany_v4(&a->addr), sizeof(am->tip)))
-		return true;
+	/* Don't resolve any of the guest's addresses */
+	inany_from_af(&addr, AF_INET, am->tip);
+	for_each_addr(a, c, AF_INET)
+		if (inany_equals(&addr, &a->addr))
+			return true;
 
 	return false;
 }
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 05/13] conf: Allow multiple -a/--address options per address family
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (3 preceding siblings ...)
  2026-03-22  0:43 ` [PATCH v6 04/13] arp: Check all configured addresses in ARP filtering Jon Maloy
@ 2026-03-22  0:43 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 06/13] netlink, conf: Read all addresses from template interface at startup Jon Maloy
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

Allow specifying multiple addresses per family with -a/--address.
The first address of each family is used for DHCP/DHCPv6 assignment.

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

---
v2: - Adapted to previous code changes
v3: - Adapted to single-array strategy
    - Changes according to feedback from S. Brivio and G Gibson.
v4: - Stripped down and adapted after feedback from David G.
v6: - Adapted to previous changes in series
    - Removed the "one address" limitation for -n option
---
 conf.c  |  7 ++++---
 fwd.c   |  7 ++-----
 pasta.c | 17 ++++++++++-------
 3 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/conf.c b/conf.c
index 8f02494..95f09cb 100644
--- a/conf.c
+++ b/conf.c
@@ -935,9 +935,11 @@ static void usage(const char *name, FILE *f, int status)
 		"    default: 65520: maximum 802.3 MTU minus 802.3 header\n"
 		"                    length, rounded to 32 bits (IPv4 words)\n"
 		"  -a, --address ADDR	Assign IPv4 or IPv6 address ADDR[/PREFIXLEN]\n"
-		"    can be specified zero to two times (for IPv4 and IPv6)\n"
+		"    can be specified up to a maximum of %d times\n"
 		"    default: use addresses from interface with default route\n"
-		"  -n, --netmask MASK	Assign IPv4 MASK, dot-decimal or bits\n"
+		"  -n, --netmask MASK	Assign IPv4 MASK, dot-decimal or bits\n",
+		MAX_GUEST_ADDRS);
+	FPRINTF(f,
 		"    default: netmask from matching address on the host\n"
 		"  -M, --mac-addr ADDR	Use source MAC address ADDR\n"
 		"    default: 9a:55:9a:55:9a:55 (locally administered)\n"
@@ -1891,7 +1893,6 @@ void conf(struct ctx *c, int argc, char **argv)
 			    IN6_IS_ADDR_V4COMPAT(&addr.a6))
 				die("Invalid address: %s", optarg);
 
-			/* Legacy behaviour: replace existing address if any */
 			fwd_set_addr(c, &addr, CONF_ADDR_USER, prefix_len);
 			if (inany_v4(&addr))
 				c->ip4.no_copy_addrs = true;
diff --git a/fwd.c b/fwd.c
index 695b5a5..2853c0e 100644
--- a/fwd.c
+++ b/fwd.c
@@ -250,14 +250,12 @@ void fwd_neigh_table_init(const struct ctx *c)
 }
 
 /**
- * fwd_set_addr() - Add or update an address in the unified address array
+ * fwd_set_addr() - Update address entry, adding one if needed
  * @c:		Execution context
  * @addr:	Address to add (IPv4-mapped or IPv6)
  * @flags:	CONF_ADDR_* flags for this address
  * @prefix_len:	Prefix length in IPv6/mapped format, 0-128
  *
- * Find the first existing entry of the same address family and
- * overwrite it, or create a new one if none exists
  */
 void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
 		  uint8_t flags, int prefix_len)
@@ -266,8 +264,7 @@ void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
 
 	for (int i = 0; i < c->addr_count; i++) {
 		a = &c->addrs[i];
-		if ((inany_v4(addr) && inany_v4(&a->addr)) ||
-		    (!inany_v4(addr) && !inany_v4(&a->addr)))
+		if (inany_equals(addr, &a->addr))
 			goto found;
 	}
 
diff --git a/pasta.c b/pasta.c
index e88a893..6307c65 100644
--- a/pasta.c
+++ b/pasta.c
@@ -342,12 +342,14 @@ void pasta_ns_conf(struct ctx *c)
 
 		if (c->ifi4) {
 			if (c->ip4.no_copy_addrs) {
-				a = fwd_get_addr(c, AF_INET, 0, 0);
-				if (a)
+				for_each_addr(a, c, AF_INET) {
 					rc = nl_addr_set(nl_sock_ns,
 							 c->pasta_ifi, AF_INET,
 							 inany_v4(&a->addr),
-							 a->prefix_len - 96);
+							 inany_prefix_len(a));
+					if (rc < 0)
+						break;
+				}
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi4,
 						 nl_sock_ns, c->pasta_ifi,
@@ -400,13 +402,14 @@ ipv4_done:
 					  0, IFF_NOARP);
 
 			if (c->ip6.no_copy_addrs) {
-				a = fwd_get_addr(c, AF_INET6, 0, 0);
-				if (a)
+				for_each_addr(a, c, AF_INET6) {
 					rc = nl_addr_set(nl_sock_ns,
 							 c->pasta_ifi,
-							 AF_INET6,
-							 &a->addr.a6,
+							 AF_INET6, &a->addr.a6,
 							 a->prefix_len);
+					if (rc < 0)
+						break;
+				}
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi6,
 						 nl_sock_ns, c->pasta_ifi,
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 06/13] netlink, conf: Read all addresses from template interface at startup
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (4 preceding siblings ...)
  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 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 07/13] ip: refactor function pasta_ns_conf() Jon Maloy
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

Add nl_addr_get_all() to read all addresses from the template interface
into c->addrs[] array, rather than just selecting the "best" one.

This allows multi-address configurations where the template interface
has multiple IPv4 or IPv6 addresses assigned to it, all of which
will now be copied to the guest namespace when using --config-net.

For IPv6, the function also captures the link-local address into
c->ip6.our_tap_ll as a side effect.

Update conf_ip4() and conf_ip6() to use nl_addr_get_all() when no
user-specified addresses are present.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
 conf.c    | 51 ++++++++++++++++++++++++++++-----------------------
 netlink.c | 53 +++++++++++++++++++++++++++++------------------------
 netlink.h |  3 +--
 3 files changed, 58 insertions(+), 49 deletions(-)

diff --git a/conf.c b/conf.c
index 95f09cb..70435d3 100644
--- a/conf.c
+++ b/conf.c
@@ -736,6 +736,7 @@ static int conf_ip4_prefix(const char *arg)
 static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
 {
 	struct ip4_ctx *ip4 = &c->ip4;
+	const struct guest_addr *a;
 
 	if (!ifi)
 		ifi = nl_get_ext_if(nl_sock, AF_INET);
@@ -755,24 +756,24 @@ static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
 		}
 	}
 
-	if (!fwd_get_addr(c, AF_INET, 0, 0)) {
-		struct in_addr addr;
-		int prefix_len;
-		int rc = nl_addr_get(nl_sock, ifi, AF_INET,
-				     &addr, &prefix_len, NULL);
+	a = fwd_get_addr(c, AF_INET, CONF_ADDR_USER, 0);
+	if (!a) {
+		int rc = nl_addr_get_all(c, nl_sock, ifi, AF_INET);
+
 		if (rc < 0) {
-			debug("Couldn't discover IPv4 address: %s",
+			debug("Couldn't discover IPv4 addresses: %s",
 			      strerror_(-rc));
 			return 0;
 		}
-		if (IN4_IS_ADDR_UNSPECIFIED(&addr))
+		if (!rc || !fwd_get_addr(c, AF_INET, 0, 0))
 			return 0;
 
-		fwd_set_addr(c, &inany_from_v4(addr), CONF_ADDR_HOST,
-			     prefix_len + 96);
-		ip4->addr_seen = addr;
+		a = fwd_get_addr(c, AF_INET, CONF_ADDR_HOST, 0);
 	}
 
+	if (a)
+		ip4->addr_seen = *inany_v4(&a->addr);
+
 	ip4->our_tap_addr = ip4->guest_gw;
 
 	return ifi;
@@ -803,10 +804,8 @@ static void conf_ip4_local(struct ctx *c)
  */
 static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
 {
-	const struct guest_addr *a = fwd_get_addr(c, AF_INET6, 0, 0);
 	struct ip6_ctx *ip6 = &c->ip6;
-	union inany_addr addr;
-	int prefix_len = 0;
+	const struct guest_addr *a;
 	int rc;
 
 	if (!ifi)
@@ -826,20 +825,26 @@ static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
 		}
 	}
 
-	rc = nl_addr_get(nl_sock, ifi, AF_INET6, &addr.a6,
-			 &prefix_len, &ip6->our_tap_ll);
-	if (rc < 0) {
-		debug("Couldn't discover IPv6 address: %s", strerror_(-rc));
-		return 0;
-	}
-
+	a = fwd_get_addr(c, AF_INET6, CONF_ADDR_USER, 0);
 	if (!a) {
-		fwd_set_addr(c, &addr, CONF_ADDR_HOST, prefix_len);
-		ip6->addr_seen = addr.a6;
+		rc = nl_addr_get_all(c, nl_sock, ifi, AF_INET6);
+		if (rc < 0) {
+			debug("Couldn't discover IPv6 addresses: %s",
+			      strerror_(-rc));
+			return 0;
+		}
+		a = fwd_get_addr(c, AF_INET6, CONF_ADDR_HOST, 0);
 	} else {
-		ip6->addr_seen = a->addr.a6;
+		rc = nl_addr_get_ll(nl_sock, ifi, &ip6->our_tap_ll);
+		if (rc < 0) {
+			debug("Couldn't get link-local address: %s",
+			      strerror_(-rc));
+		}
 	}
 
+	if (a)
+		ip6->addr_seen = a->addr.a6;
+
 	if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
 		ip6->our_tap_ll = ip6->guest_gw;
 
diff --git a/netlink.c b/netlink.c
index e07b47f..2727eec 100644
--- a/netlink.c
+++ b/netlink.c
@@ -747,20 +747,20 @@ int nl_addr_set_ll_nodad(int s, unsigned int ifi)
 }
 
 /**
- * nl_addr_get() - Get most specific global address, given interface and family
+ * nl_addr_get_all() - Get all addresses for a given interface into ctx
+ * @c:		Execution context
  * @s:		Netlink socket
- * @ifi:	Interface index in outer network namespace
- * @af:		Address family
- * @addr:	Global address to fill
- * @prefix_len:	Mask or prefix length, to fill
- * @addr_l:	Link-scoped address to fill (for IPv6)
+ * @ifi:	Interface index
+ * @af:		Address family (AF_INET or AF_INET6)
  *
- * Return: 0 on success, negative error code on failure
+ * Populates c->addrs[] with all non-deprecated addresses from the interface.
+ * For IPv6, also captures link-local address in c->ip6.our_tap_ll.
+ * Skips link-local addresses for the main array (kernel auto-configures them).
+ *
+ * Return: number of addresses added, or negative error code on failure
  */
-int nl_addr_get(int s, unsigned int ifi, sa_family_t af,
-		void *addr, int *prefix_len, void *addr_l)
+int nl_addr_get_all(struct ctx *c, int s, unsigned int ifi, sa_family_t af)
 {
-	uint8_t prefix_max = 0, prefix_max_ll = 0;
 	struct req_t {
 		struct nlmsghdr nlh;
 		struct ifaddrmsg ifa;
@@ -771,6 +771,7 @@ int nl_addr_get(int s, unsigned int ifi, sa_family_t af,
 	struct nlmsghdr *nh;
 	char buf[NLBUFSIZ];
 	ssize_t status;
+	int count = 0;
 	uint32_t seq;
 
 	seq = nl_send(s, &req, RTM_GETADDR, NLM_F_DUMP, sizeof(req));
@@ -784,27 +785,31 @@ int nl_addr_get(int s, unsigned int ifi, sa_family_t af,
 
 		for (rta = IFA_RTA(ifa), na = IFA_PAYLOAD(nh); RTA_OK(rta, na);
 		     rta = RTA_NEXT(rta, na)) {
-			if ((af == AF_INET  && rta->rta_type != IFA_LOCAL) ||
-			    (af == AF_INET6 && rta->rta_type != IFA_ADDRESS))
-				continue;
+			union inany_addr addr;
+			int prefix_len;
 
-			if (ifa->ifa_prefixlen > prefix_max && addr &&
-			    (af == AF_INET || ifa->ifa_scope < RT_SCOPE_LINK)) {
-				memcpy(addr, RTA_DATA(rta), RTA_PAYLOAD(rta));
+			if (af == AF_INET && rta->rta_type != IFA_LOCAL)
+				continue;
+			if (af == AF_INET6 && rta->rta_type != IFA_ADDRESS)
+				continue;
 
-				prefix_max = *prefix_len = ifa->ifa_prefixlen;
+			if (af == AF_INET6 &&
+			    ifa->ifa_scope == RT_SCOPE_LINK) {
+				memcpy(&c->ip6.our_tap_ll, RTA_DATA(rta),
+				       sizeof(c->ip6.our_tap_ll));
+				continue;
 			}
 
-			if (addr_l &&
-			    af == AF_INET6 && ifa->ifa_scope == RT_SCOPE_LINK &&
-			    ifa->ifa_prefixlen > prefix_max_ll) {
-				memcpy(addr_l, RTA_DATA(rta), RTA_PAYLOAD(rta));
+			inany_from_af(&addr, af, RTA_DATA(rta));
+			prefix_len = ifa->ifa_prefixlen +
+				     (af == AF_INET ? 96 : 0);
 
-				prefix_max_ll = ifa->ifa_prefixlen;
-			}
+			fwd_set_addr(c, &addr, CONF_ADDR_HOST, prefix_len);
+			count++;
 		}
 	}
-	return status;
+
+	return status < 0 ? status : count;
 }
 
 /**
diff --git a/netlink.h b/netlink.h
index b22f485..3af6d58 100644
--- a/netlink.h
+++ b/netlink.h
@@ -19,8 +19,7 @@ int nl_route_get_def(int s, unsigned int ifi, sa_family_t af, void *gw);
 int nl_route_set_def(int s, unsigned int ifi, sa_family_t af, const void *gw);
 int nl_route_dup(int s_src, unsigned int ifi_src,
 		 int s_dst, unsigned int ifi_dst, sa_family_t af);
-int nl_addr_get(int s, unsigned int ifi, sa_family_t af,
-		void *addr, int *prefix_len, void *addr_l);
+int nl_addr_get_all(struct ctx *c, int s, unsigned int ifi, sa_family_t af);
 bool nl_neigh_mac_get(int s, const union inany_addr *addr, int ifi,
 		      unsigned char *mac);
 int nl_addr_set(int s, unsigned int ifi, sa_family_t af,
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 07/13] ip: refactor function pasta_ns_conf()
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (5 preceding siblings ...)
  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 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 08/13] ip: Track observed guest IPv4 addresses in unified address array Jon Maloy
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

After the previous changes in this series it becomes possible
to simplify the pasta_ns_conf() function.

We extract address and route configuration into helper functions
pasta_conf_addrs() and pasta_conf_routes(), reducing nesting
and improving readability.

To allow pasta_conf_addrs() to handle both address families
uniformly, we change nl_addr_set() to take a union inany_addr pointer
instead of void pointer, moving the address family handling into
the function itself.

We also fix a bug where the IPv6 code path incorrectly wrote to
req.set.a4.rta_l.rta_type instead of req.set.a6.rta_l.rta_type.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
 netlink.c |  13 +--
 netlink.h |   2 +-
 pasta.c   | 232 +++++++++++++++++++++++++++---------------------------
 3 files changed, 124 insertions(+), 123 deletions(-)

diff --git a/netlink.c b/netlink.c
index 2727eec..fcdc983 100644
--- a/netlink.c
+++ b/netlink.c
@@ -870,7 +870,7 @@ int nl_addr_get_ll(int s, unsigned int ifi, struct in6_addr *addr)
  * Return: 0 on success, negative error code on failure
  */
 int nl_addr_set(int s, unsigned int ifi, sa_family_t af,
-		const void *addr, int prefix_len)
+		const union inany_addr *addr, int prefix_len)
 {
 	struct req_t {
 		struct nlmsghdr nlh;
@@ -905,21 +905,22 @@ int nl_addr_set(int s, unsigned int ifi, sa_family_t af,
 
 		len = offsetof(struct req_t, set.a6) + sizeof(req.set.a6);
 
-		memcpy(&req.set.a6.l, addr, sizeof(req.set.a6.l));
+		memcpy(&req.set.a6.l, &addr->a6, sizeof(req.set.a6.l));
 		req.set.a6.rta_l.rta_len = rta_len;
-		req.set.a4.rta_l.rta_type = IFA_LOCAL;
-		memcpy(&req.set.a6.a, addr, sizeof(req.set.a6.a));
+		req.set.a6.rta_l.rta_type = IFA_LOCAL;
+		memcpy(&req.set.a6.a, &addr->a6, sizeof(req.set.a6.a));
 		req.set.a6.rta_a.rta_len = rta_len;
 		req.set.a6.rta_a.rta_type = IFA_ADDRESS;
 	} else {
+		const struct in_addr *v4 = inany_v4(addr);
 		size_t rta_len = RTA_LENGTH(sizeof(req.set.a4.l));
 
 		len = offsetof(struct req_t, set.a4) + sizeof(req.set.a4);
 
-		memcpy(&req.set.a4.l, addr, sizeof(req.set.a4.l));
+		memcpy(&req.set.a4.l, v4, sizeof(req.set.a4.l));
 		req.set.a4.rta_l.rta_len = rta_len;
 		req.set.a4.rta_l.rta_type = IFA_LOCAL;
-		memcpy(&req.set.a4.a, addr, sizeof(req.set.a4.a));
+		memcpy(&req.set.a4.a, v4, sizeof(req.set.a4.a));
 		req.set.a4.rta_a.rta_len = rta_len;
 		req.set.a4.rta_a.rta_type = IFA_ADDRESS;
 	}
diff --git a/netlink.h b/netlink.h
index 3af6d58..26f5ef7 100644
--- a/netlink.h
+++ b/netlink.h
@@ -23,7 +23,7 @@ int nl_addr_get_all(struct ctx *c, int s, unsigned int ifi, sa_family_t af);
 bool nl_neigh_mac_get(int s, const union inany_addr *addr, int ifi,
 		      unsigned char *mac);
 int nl_addr_set(int s, unsigned int ifi, sa_family_t af,
-		const void *addr, int prefix_len);
+		const union inany_addr *addr, int prefix_len);
 int nl_addr_get_ll(int s, unsigned int ifi, struct in6_addr *addr);
 int nl_addr_set_ll_nodad(int s, unsigned int ifi);
 int nl_addr_dup(int s_src, unsigned int ifi_src,
diff --git a/pasta.c b/pasta.c
index 6307c65..b8d7cf4 100644
--- a/pasta.c
+++ b/pasta.c
@@ -46,6 +46,7 @@
 
 #include "util.h"
 #include "passt.h"
+#include "conf.h"
 #include "isolation.h"
 #include "netlink.h"
 #include "log.h"
@@ -303,13 +304,73 @@ void pasta_start_ns(struct ctx *c, uid_t uid, gid_t gid,
 		die_perror("Failed to join network namespace");
 }
 
+/**
+ * pasta_conf_addrs() - Configure addresses for one address family in namespace
+ * @c:		Execution context
+ * @af:		Address family (AF_INET or AF_INET6)
+ * @ifi:	Host interface index for this address family
+ * @no_copy:	If true, set addresses from c->addrs; if false, copy from host
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+static int pasta_conf_addrs(struct ctx *c, sa_family_t af,
+			    int ifi, bool no_copy)
+{
+	const struct guest_addr *a;
+
+	if (!ifi)
+		return 0;
+
+	if (!no_copy)
+		return nl_addr_dup(nl_sock, ifi, nl_sock_ns, c->pasta_ifi, af);
+
+	for_each_addr(a, c, af) {
+		int rc;
+
+		/* Skip link-local, kernel auto-configures */
+		if (a->flags & CONF_ADDR_LINKLOCAL)
+			continue;
+
+		rc = nl_addr_set(nl_sock_ns, c->pasta_ifi, af, &a->addr,
+				 inany_prefix_len(a));
+		if (rc < 0)
+			return rc;
+	}
+	return 0;
+}
+
+/**
+ * pasta_conf_routes() - Configure routes for one address family in namespace
+ * @c:		Execution context
+ * @af:		Address family (AF_INET or AF_INET6)
+ * @ifi:	Host interface index for this address family
+ * @no_copy:	If true, set default route; if false, copy routes from host
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+static int pasta_conf_routes(struct ctx *c, sa_family_t af, int ifi,
+			     bool no_copy)
+{
+	const void *gw = (af == AF_INET) ?
+		(const void *)&c->ip4.guest_gw : (const void *)&c->ip6.guest_gw;
+
+	if (!ifi)
+		return 0;
+
+	if (no_copy)
+		return nl_route_set_def(nl_sock_ns, c->pasta_ifi, af, gw);
+
+	return nl_route_dup(nl_sock, ifi, nl_sock_ns, c->pasta_ifi, af);
+}
+
 /**
  * pasta_ns_conf() - Set up loopback and tap interfaces in namespace as needed
  * @c:		Execution context
  */
 void pasta_ns_conf(struct ctx *c)
 {
-	int rc = 0;
+	unsigned int flags = IFF_UP;
+	int rc;
 
 	rc = nl_link_set_flags(nl_sock_ns, 1 /* lo */, IFF_UP, IFF_UP);
 	if (rc < 0)
@@ -328,123 +389,62 @@ void pasta_ns_conf(struct ctx *c)
 		die("Couldn't set MAC address in namespace: %s",
 		    strerror_(-rc));
 
-	if (c->pasta_conf_ns) {
-		unsigned int flags = IFF_UP;
-		const struct guest_addr *a;
-
-		if (c->mtu)
-			nl_link_set_mtu(nl_sock_ns, c->pasta_ifi, c->mtu);
-
-		if (c->ifi6) /* Avoid duplicate address detection on link up */
-			flags |= IFF_NOARP;
-
-		nl_link_set_flags(nl_sock_ns, c->pasta_ifi, flags, flags);
-
-		if (c->ifi4) {
-			if (c->ip4.no_copy_addrs) {
-				for_each_addr(a, c, AF_INET) {
-					rc = nl_addr_set(nl_sock_ns,
-							 c->pasta_ifi, AF_INET,
-							 inany_v4(&a->addr),
-							 inany_prefix_len(a));
-					if (rc < 0)
-						break;
-				}
-			} else {
-				rc = nl_addr_dup(nl_sock, c->ifi4,
-						 nl_sock_ns, c->pasta_ifi,
-						 AF_INET);
-			}
-
-			if (c->ifi4 == -1 && rc == -ENOTSUP) {
-				warn("IPv4 not supported, disabling");
-				c->ifi4 = 0;
-				goto ipv4_done;
-			}
-
-			if (rc < 0) {
-				die("Couldn't set IPv4 address(es) in namespace: %s",
-				    strerror_(-rc));
-			}
-
-			if (c->ip4.no_copy_routes) {
-				rc = nl_route_set_def(nl_sock_ns, c->pasta_ifi,
-						      AF_INET,
-						      &c->ip4.guest_gw);
-			} else {
-				rc = nl_route_dup(nl_sock, c->ifi4, nl_sock_ns,
-						  c->pasta_ifi, AF_INET);
-			}
-
-			if (rc < 0) {
-				die("Couldn't set IPv4 route(s) in guest: %s",
-				    strerror_(-rc));
-			}
-		}
-ipv4_done:
-
-		if (c->ifi6) {
-			rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi,
-					    &c->ip6.addr_ll_seen);
-			if (rc < 0) {
-				warn("Can't get LL address from namespace: %s",
-				    strerror_(-rc));
-			}
-
-			rc = nl_addr_set_ll_nodad(nl_sock_ns, c->pasta_ifi);
-			if (rc < 0) {
-				warn("Can't set nodad for LL in namespace: %s",
-				    strerror_(-rc));
-			}
-
-			/* We dodged DAD: re-enable neighbour solicitations */
-			nl_link_set_flags(nl_sock_ns, c->pasta_ifi,
-					  0, IFF_NOARP);
-
-			if (c->ip6.no_copy_addrs) {
-				for_each_addr(a, c, AF_INET6) {
-					rc = nl_addr_set(nl_sock_ns,
-							 c->pasta_ifi,
-							 AF_INET6, &a->addr.a6,
-							 a->prefix_len);
-					if (rc < 0)
-						break;
-				}
-			} else {
-				rc = nl_addr_dup(nl_sock, c->ifi6,
-						 nl_sock_ns, c->pasta_ifi,
-						 AF_INET6);
-			}
-
-			if (rc < 0) {
-				die("Couldn't set IPv6 address(es) in namespace: %s",
-				    strerror_(-rc));
-			}
-
-			if (c->ip6.no_copy_routes) {
-				rc = nl_route_set_def(nl_sock_ns, c->pasta_ifi,
-						      AF_INET6,
-						      &c->ip6.guest_gw);
-			} else {
-				rc = nl_route_dup(nl_sock, c->ifi6,
-						  nl_sock_ns, c->pasta_ifi,
-						  AF_INET6);
-			}
-
-			if (c->ifi6 == -1 && rc == -ENOTSUP) {
-				warn("IPv6 not supported, disabling");
-				c->ifi6 = 0;
-				goto ipv6_done;
-			}
-
-			if (rc < 0) {
-				die("Couldn't set IPv6 route(s) in guest: %s",
-				    strerror_(-rc));
-			}
-		}
+	if (!c->pasta_conf_ns)
+		goto done;
+
+	if (c->mtu)
+		nl_link_set_mtu(nl_sock_ns, c->pasta_ifi, c->mtu);
+
+	if (c->ifi6) /* Avoid duplicate address detection on link up */
+		flags |= IFF_NOARP;
+
+	nl_link_set_flags(nl_sock_ns, c->pasta_ifi, flags, flags);
+
+	/* IPv4 configuration */
+	rc = pasta_conf_addrs(c, AF_INET, c->ifi4, c->ip4.no_copy_addrs);
+	if (c->ifi4 == -1 && rc == -ENOTSUP) {
+		warn("IPv4 not supported, disabling");
+		c->ifi4 = 0;
+	} else if (rc < 0) {
+		die("Couldn't set IPv4 address(es): %s", strerror_(-rc));
+	} else if (c->ifi4) {
+		rc = pasta_conf_routes(c, AF_INET, c->ifi4,
+				       c->ip4.no_copy_routes);
+		if (rc < 0)
+			die("Couldn't set IPv4 route(s): %s", strerror_(-rc));
+	}
+
+	if (!c->ifi6)
+		goto done;
+
+	rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi,
+			    &c->ip6.addr_ll_seen);
+	if (rc < 0)
+		warn("Can't get LL address from namespace: %s",
+		     strerror_(-rc));
+
+	rc = nl_addr_set_ll_nodad(nl_sock_ns, c->pasta_ifi);
+	if (rc < 0)
+		warn("Can't set nodad for LL in namespace: %s",
+		     strerror_(-rc));
+
+	/* We dodged DAD: re-enable neighbour solicitations */
+	nl_link_set_flags(nl_sock_ns, c->pasta_ifi, 0, IFF_NOARP);
+
+	rc = pasta_conf_addrs(c, AF_INET6, c->ifi6, c->ip6.no_copy_addrs);
+	if (c->ifi6 == -1 && rc == -ENOTSUP) {
+		warn("IPv6 not supported, disabling");
+		c->ifi6 = 0;
+	} else if (rc < 0) {
+		die("Couldn't set IPv6 address(es): %s", strerror_(-rc));
+	} else {
+		rc = pasta_conf_routes(c, AF_INET6, c->ifi6,
+				       c->ip6.no_copy_routes);
+		if (rc < 0)
+			die("Couldn't set IPv6 route(s): %s", strerror_(-rc));
 	}
-ipv6_done:
 
+done:
 	proto_update_l2_buf(c->guest_mac);
 }
 
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 08/13] ip: Track observed guest IPv4 addresses in unified address array
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (6 preceding siblings ...)
  2026-03-22  0:43 ` [PATCH v6 07/13] ip: refactor function pasta_ns_conf() Jon Maloy
@ 2026-03-22  0:43 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 09/13] ip: Track observed guest IPv6 " Jon Maloy
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

We remove the addr_seen field in struct ip4_ctx and replace it by
setting a new CONF_ADDR_OBSERVED flag in the corresponding entry
in the unified address array.

The observed IPv4 address is always added at or moved to position 0,
increasing chances for a fast lookup.

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

---
v4: - Removed migration protocol update, to be added in later commit
    - Allow only one OBSERVED address at a time
    - Some other changes based on feedback from David G
v5: - Allowing multiple observed IPv4 addresses
v6: - Refactored fwd_set_addr(), notably:
      o Limited number of allowed observed addresses to four per protocol
      o I kept the memmove() calls, since I find no more elegant way to
        do this. Performance cost should be minimal, since these parts
        of the code will execute only very exceptionally. Note that
        removing the 'oldest' entry implicitly means removing the least
        used one, since the latter will migrate to the highest position
        after a few iterations of remove/add.
      o Also kept the prefix_len update. Not sure about this, but I
        cannot see how the current approach can cause any harm.
    - Other changes suggested by David G, notably reversing some
      residues after an accidental merge/re-split with the next
      commit.
---
 conf.c    |   4 --
 fwd.c     | 132 +++++++++++++++++++++++++++++++++++++++++++++---------
 fwd.h     |   4 ++
 inany.h   |  10 +++++
 migrate.c |  17 ++++++-
 passt.h   |   6 +--
 tap.c     |  20 +++++++--
 7 files changed, 160 insertions(+), 33 deletions(-)

diff --git a/conf.c b/conf.c
index 70435d3..1c9f07c 100644
--- a/conf.c
+++ b/conf.c
@@ -771,9 +771,6 @@ static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
 		a = fwd_get_addr(c, AF_INET, CONF_ADDR_HOST, 0);
 	}
 
-	if (a)
-		ip4->addr_seen = *inany_v4(&a->addr);
-
 	ip4->our_tap_addr = ip4->guest_gw;
 
 	return ifi;
@@ -787,7 +784,6 @@ static void conf_ip4_local(struct ctx *c)
 {
 	struct ip4_ctx *ip4 = &c->ip4;
 
-	ip4->addr_seen = IP4_LL_GUEST_ADDR;
 	ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
 	ip4->no_copy_addrs = ip4->no_copy_routes = true;
 	fwd_set_addr(c, &inany_from_v4(IP4_LL_GUEST_ADDR),
diff --git a/fwd.c b/fwd.c
index 2853c0e..28a721e 100644
--- a/fwd.c
+++ b/fwd.c
@@ -28,6 +28,7 @@
 #include "inany.h"
 #include "fwd.h"
 #include "passt.h"
+#include "conf.h"
 #include "lineread.h"
 #include "flow_table.h"
 #include "netlink.h"
@@ -252,7 +253,7 @@ void fwd_neigh_table_init(const struct ctx *c)
 /**
  * fwd_set_addr() - Update address entry, adding one if needed
  * @c:		Execution context
- * @addr:	Address to add (IPv4-mapped or IPv6)
+ * @addr:	Address to add/alter
  * @flags:	CONF_ADDR_* flags for this address
  * @prefix_len:	Prefix length in IPv6/mapped format, 0-128
  *
@@ -260,23 +261,69 @@ void fwd_neigh_table_init(const struct ctx *c)
 void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
 		  uint8_t flags, int prefix_len)
 {
-	struct guest_addr *a;
+	struct guest_addr *a, *arr = &c->addrs[0], *rm = NULL;
+	int count = c->addr_count;
+	int af_cnt = 0;
 
-	for (int i = 0; i < c->addr_count; i++) {
-		a = &c->addrs[i];
-		if (inany_equals(addr, &a->addr))
-			goto found;
+	for_each_addr(a, c, inany_af(addr)) {
+		if (!inany_equals(&a->addr, addr))
+			continue;
+
+		/* Update prefix_len if provided and applicable */
+		if (prefix_len && !(a->flags & CONF_ADDR_USER))
+			a->prefix_len = prefix_len;
+
+		/* Nothing more to change */
+		if ((a->flags & flags) == flags)
+			return;
+
+		if (!(flags & CONF_ADDR_OBSERVED)) {
+			a->flags |= flags;
+			return;
+		}
+
+		/* Observed address moves to position 0: remove, re-add later */
+		flags |= a->flags;
+		prefix_len = a->prefix_len;
+		memmove(a, a + 1, (&arr[count] - (a + 1)) * sizeof(*a));
+		c->addr_count = --count;
+		break;
 	}
 
-	if (c->addr_count >= MAX_GUEST_ADDRS)
+	if (count >= MAX_GUEST_ADDRS) {
+		debug("Address table full, can't add address");
 		return;
+	}
 
-	a = &c->addrs[c->addr_count++];
-
-found:
+	/* Add to head or tail, depending on flag */
+	if (flags & CONF_ADDR_OBSERVED) {
+		a = &arr[0];
+		memmove(&arr[1], a, count * sizeof(*a));
+	} else {
+		a = &arr[count];
+	}
+	c->addr_count = ++count;
 	a->addr = *addr;
 	a->prefix_len = prefix_len;
 	a->flags = flags;
+
+	if (!(flags & CONF_ADDR_OBSERVED))
+		return;
+
+	/* Remove oldest observed address of this protocol if too many */
+	for (int i = count - 1; i >= 0; i--) {
+		a = &arr[i];
+		if ((!(a->flags & CONF_ADDR_OBSERVED)) ||
+		    (inany_af(&a->addr)) != inany_af(addr))
+			continue;
+		if (!rm)
+			rm = a;
+		af_cnt++;
+	}
+	if (af_cnt > MAX_OBSERVED_ADDRS) {
+		memmove(rm, rm + 1, (&arr[count] - (rm + 1)) * sizeof(*rm));
+		c->addr_count--;
+	}
 }
 
 /**
@@ -987,6 +1034,38 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
 		((ini->oport == 53) || (ini->oport == 853));
 }
 
+/**
+ * fwd_select_addr() - Select address with priority-based search
+ * @c:		Execution context
+ * @af:		Address family (AF_INET or AF_INET6)
+ * @primary:	Primary flags to match (or 0 to skip)
+ * @secondary:	Secondary flags to match (or 0 to skip)
+ * @skip:	Flags to exclude from search
+ *
+ * Search for address entries in priority order.
+ *
+ * Return: pointer to selected address entry, or NULL if none found
+ */
+const struct guest_addr *fwd_select_addr(const struct ctx *c, int af,
+					 int primary, int secondary, int skip)
+{
+	const struct guest_addr *a;
+
+	if (primary) {
+		a = fwd_get_addr(c, af, primary, skip);
+		if (a)
+			return a;
+	}
+
+	if (secondary) {
+		a = fwd_get_addr(c, af, secondary, skip);
+		if (a)
+			return a;
+	}
+
+	return NULL;
+}
+
 /**
  * fwd_guest_accessible() - Is address guest-accessible
  * @c:		Execution context
@@ -1011,17 +1090,11 @@ static bool fwd_guest_accessible(const struct ctx *c,
 	if (inany_is_unspecified(addr))
 		return false;
 
-	/* Check against all configured guest addresses */
+	/* Check against all known guest addresses */
 	for_each_addr(a, c, AF_UNSPEC)
 		if (inany_equals(addr, &a->addr))
 			return false;
 
-	/* Also check addr_seen: it tracks the address the guest is actually
-	 * using, which may differ from configured addresses.
-	 */
-	if (inany_equals4(addr, &c->ip4.addr_seen))
-		return false;
-
 	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
 	 * address the guest will take until we see it.  Only check against it
 	 * if it has been set to a real address.
@@ -1216,10 +1289,20 @@ uint8_t fwd_nat_from_host(const struct ctx *c,
 		 * match.
 		 */
 		if (inany_v4(&ini->eaddr)) {
-			if (c->host_lo_to_ns_lo)
+			if (c->host_lo_to_ns_lo) {
 				tgt->eaddr = inany_loopback4;
-			else
-				tgt->eaddr = inany_from_v4(c->ip4.addr_seen);
+			} else {
+				const struct guest_addr *a;
+
+				a = fwd_select_addr(c, AF_INET,
+						    CONF_ADDR_OBSERVED,
+						    CONF_ADDR_USER |
+						    CONF_ADDR_HOST, 0);
+				if (!a)
+					return PIF_NONE;
+
+				tgt->eaddr = a->addr;
+			}
 			tgt->oaddr = inany_any4;
 		} else {
 			if (c->host_lo_to_ns_lo)
@@ -1254,7 +1337,14 @@ uint8_t fwd_nat_from_host(const struct ctx *c,
 	tgt->oport = ini->eport;
 
 	if (inany_v4(&tgt->oaddr)) {
-		tgt->eaddr = inany_from_v4(c->ip4.addr_seen);
+		const struct guest_addr *a;
+
+		a = fwd_select_addr(c, AF_INET, CONF_ADDR_OBSERVED,
+				    CONF_ADDR_USER | CONF_ADDR_HOST, 0);
+		if (!a)
+			return PIF_NONE;
+
+		tgt->eaddr = a->addr;
 	} else {
 		if (inany_is_linklocal6(&tgt->oaddr))
 			tgt->eaddr.a6 = c->ip6.addr_ll_seen;
diff --git a/fwd.h b/fwd.h
index c5a1068..9893856 100644
--- a/fwd.h
+++ b/fwd.h
@@ -25,6 +25,10 @@ void fwd_probe_ephemeral(void);
 bool fwd_port_is_ephemeral(in_port_t port);
 const struct guest_addr *fwd_get_addr(const struct ctx *c, sa_family_t af,
 				      uint8_t incl, uint8_t excl);
+const struct guest_addr *fwd_select_addr(const struct ctx *c, int af,
+					 int primary, int secondary, int skip);
+void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
+		  uint8_t flags, int prefix_len);
 
 /**
  * struct fwd_rule - Forwarding rule governing a range of ports
diff --git a/inany.h b/inany.h
index 9891ed6..7b23cb0 100644
--- a/inany.h
+++ b/inany.h
@@ -102,6 +102,16 @@ static inline struct in_addr *inany_v4(const union inany_addr *addr)
 	return (struct in_addr *)&addr->v4mapped.a4;
 }
 
+/** inany_af - Get address family of IPv[46] address
+ * @addr:	IPv4 or IPv6 address
+ *
+ * Return: AF_INET for IPv4, AF_INET6 for IPv6
+ */
+static inline sa_family_t inany_af(const union inany_addr *addr)
+{
+	return inany_v4(addr) ? AF_INET : AF_INET6;
+}
+
 /** inany_default_prefix_len() - Get default prefix length for address
  * @addr:	IPv4 or iPv6 address
  *
diff --git a/migrate.c b/migrate.c
index 1e8858a..1e02720 100644
--- a/migrate.c
+++ b/migrate.c
@@ -18,6 +18,8 @@
 #include "util.h"
 #include "ip.h"
 #include "passt.h"
+#include "conf.h"
+#include "fwd.h"
 #include "inany.h"
 #include "flow.h"
 #include "flow_table.h"
@@ -57,11 +59,18 @@ static int seen_addrs_source_v2(struct ctx *c,
 	struct migrate_seen_addrs_v2 addrs = {
 		.addr6 = c->ip6.addr_seen,
 		.addr6_ll = c->ip6.addr_ll_seen,
-		.addr4 = c->ip4.addr_seen,
 	};
+	const struct guest_addr *a;
 
 	(void)stage;
 
+	/* IPv4 observed address, with fallback to configured address */
+	a = fwd_select_addr(c, AF_INET, CONF_ADDR_OBSERVED,
+			    CONF_ADDR_USER | CONF_ADDR_HOST,
+			    CONF_ADDR_LINKLOCAL);
+	if (a)
+		addrs.addr4 = *inany_v4(&a->addr);
+
 	memcpy(addrs.mac, c->guest_mac, sizeof(addrs.mac));
 
 	if (write_all_buf(fd, &addrs, sizeof(addrs)))
@@ -90,7 +99,11 @@ static int seen_addrs_target_v2(struct ctx *c,
 
 	c->ip6.addr_seen = addrs.addr6;
 	c->ip6.addr_ll_seen = addrs.addr6_ll;
-	c->ip4.addr_seen = addrs.addr4;
+
+	if (addrs.addr4.s_addr)
+		fwd_set_addr(c, &inany_from_v4(addrs.addr4),
+			     CONF_ADDR_OBSERVED, 0);
+
 	memcpy(c->guest_mac, addrs.mac, sizeof(c->guest_mac));
 
 	return 0;
diff --git a/passt.h b/passt.h
index 6bca778..5452225 100644
--- a/passt.h
+++ b/passt.h
@@ -64,8 +64,9 @@ enum passt_modes {
 	MODE_VU,
 };
 
-/* Maximum number of addresses in context address array */
+/* Limits on number of addresses in context address array */
 #define MAX_GUEST_ADDRS		32
+#define MAX_OBSERVED_ADDRS	4
 
 /* Flags indicating origin and role of a guest address
  * To be used in struct guest_addr
@@ -73,6 +74,7 @@ enum passt_modes {
 #define CONF_ADDR_USER		BIT(0)		/* User set via -a */
 #define CONF_ADDR_HOST		BIT(1)		/* From host interface */
 #define CONF_ADDR_LINKLOCAL	BIT(2)		/* Link-local address */
+#define CONF_ADDR_OBSERVED	BIT(3)		/* Seen in guest traffic */
 
 /**
  * struct guest_addr - Unified IPv4/IPv6 address entry
@@ -88,7 +90,6 @@ struct guest_addr {
 
 /**
  * struct ip4_ctx - IPv4 execution context
- * @addr_seen:		Latest IPv4 address seen as source from tap
  * @guest_gw:		IPv4 gateway as seen by the guest
  * @map_host_loopback:	Outbound connections to this address are NATted to the
  *                      host's 127.0.0.1
@@ -104,7 +105,6 @@ struct guest_addr {
  * @no_copy_addrs:	Don't copy all addresses when configuring namespace
  */
 struct ip4_ctx {
-	struct in_addr addr_seen;
 	struct in_addr guest_gw;
 	struct in_addr map_host_loopback;
 	struct in_addr map_guest_addr;
diff --git a/tap.c b/tap.c
index 78a494a..c75a4df 100644
--- a/tap.c
+++ b/tap.c
@@ -47,6 +47,7 @@
 #include "ip.h"
 #include "iov.h"
 #include "passt.h"
+#include "fwd.h"
 #include "arp.h"
 #include "dhcp.h"
 #include "ndp.h"
@@ -161,6 +162,17 @@ void tap_send_single(const struct ctx *c, const void *data, size_t l2len)
 	}
 }
 
+/**
+ * tap_check_src_addr4() - Note an IPv4 address seen in guest traffic
+ * @c:		Execution context
+ * @addr:	IPv4 address seen as source from guest
+ */
+static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
+{
+	if (addr->s_addr)
+		fwd_set_addr(c, &inany_from_v4(*addr), CONF_ADDR_OBSERVED, 0);
+}
+
 /**
  * tap_ip6_daddr() - Normal IPv6 destination address for inbound packets
  * @c:		Execution context
@@ -723,6 +735,7 @@ static int tap4_handler(struct ctx *c, const struct pool *in,
 resume:
 	for (seq_count = 0, seq = NULL; i < in->count; i++) {
 		size_t l3len, hlen, l4len;
+		const struct in_addr *ia;
 		struct ethhdr eh_storage;
 		struct iphdr iph_storage;
 		struct udphdr uh_storage;
@@ -771,9 +784,10 @@ resume:
 			continue;
 		}
 
-		if (iph->saddr && c->ip4.addr_seen.s_addr != iph->saddr)
-			c->ip4.addr_seen.s_addr = iph->saddr;
-
+		if (iph->saddr) {
+			ia = (const struct in_addr *) &iph->saddr;
+			tap_check_src_addr4(c, ia);
+		}
 		if (!iov_drop_header(&data, hlen))
 			continue;
 		if (iov_tail_size(&data) != l4len)
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 09/13] ip: Track observed guest IPv6 addresses in unified address array
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (7 preceding siblings ...)
  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 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 10/13] migrate: Update protocol to v3 for multi-address support Jon Maloy
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

We remove the addr_seen and addr_ll_seen fields in struct ip6_ctx
and replace them by setting CONF_ADDR_OBSERVED and CONF_ADDR_LINKLOCAL
flags in the corresponding entry in the unified address array.

The observed IPv6 address is always added/moved to position 0
in the array, improving chances for fast lookup.

This completes the unification of address storage for both IPv4 and
IPv6, enabling future support for multiple guest addresses per family.

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

---
v5: - Made to use same algorithm and function as IPv4 for inserting
      observed into the array.

v6: - Re-introduced code that by accident had been moved to the
      previous commit.
    - Some fixes based on feedback from David G.
---
 conf.c    |  6 ------
 dhcpv6.c  | 20 +++++++++++--------
 dhcpv6.h  |  2 +-
 fwd.c     | 38 ++++++++++++++++++++++--------------
 inany.h   |  3 +++
 migrate.c | 33 +++++++++++++++++++++++--------
 passt.h   |  4 ----
 pasta.c   |  7 +++++--
 tap.c     | 58 ++++++++++++++++++++++++++++++++++++-------------------
 9 files changed, 107 insertions(+), 64 deletions(-)

diff --git a/conf.c b/conf.c
index 1c9f07c..320a9e4 100644
--- a/conf.c
+++ b/conf.c
@@ -767,8 +767,6 @@ static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
 		}
 		if (!rc || !fwd_get_addr(c, AF_INET, 0, 0))
 			return 0;
-
-		a = fwd_get_addr(c, AF_INET, CONF_ADDR_HOST, 0);
 	}
 
 	ip4->our_tap_addr = ip4->guest_gw;
@@ -829,7 +827,6 @@ static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
 			      strerror_(-rc));
 			return 0;
 		}
-		a = fwd_get_addr(c, AF_INET6, CONF_ADDR_HOST, 0);
 	} else {
 		rc = nl_addr_get_ll(nl_sock, ifi, &ip6->our_tap_ll);
 		if (rc < 0) {
@@ -838,9 +835,6 @@ static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
 		}
 	}
 
-	if (a)
-		ip6->addr_seen = a->addr.a6;
-
 	if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
 		ip6->our_tap_ll = ip6->guest_gw;
 
diff --git a/dhcpv6.c b/dhcpv6.c
index 3a007bf..313c243 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -382,8 +382,12 @@ static void dhcpv6_send_ia_notonlink(struct ctx *c,
 {
 	const struct in6_addr *src = &c->ip6.our_tap_ll;
 	struct opt_hdr *ia = (struct opt_hdr *)resp_not_on_link.var;
+	const struct in6_addr *dst = tap_ip6_daddr(c, src);
 	size_t n;
 
+	if (!dst)
+		return;
+
 	info("DHCPv6: received CONFIRM with inappropriate IA,"
 	     " sending NotOnLink status in REPLY");
 
@@ -405,7 +409,7 @@ static void dhcpv6_send_ia_notonlink(struct ctx *c,
 
 	resp_not_on_link.hdr.xid = xid;
 
-	tap_udp6_send(c, src, 547, tap_ip6_daddr(c, src), 546,
+	tap_udp6_send(c, src, 547, dst, 546,
 		      xid, &resp_not_on_link, n);
 }
 
@@ -549,7 +553,7 @@ static size_t dhcpv6_client_fqdn_fill(const struct iov_tail *data,
  * Return: 0 if it's not a DHCPv6 message, 1 if handled, -1 on failure
  */
 int dhcpv6(struct ctx *c, struct iov_tail *data,
-	   const struct in6_addr *saddr, const struct in6_addr *daddr)
+	   const struct in6_addr *daddr)
 {
 	const struct opt_server_id *server_id = NULL;
 	const struct opt_hdr *client_id = NULL;
@@ -565,9 +569,9 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	/* cppcheck-suppress [variableScope,unmatchedSuppression] */
 	struct opt_hdr client_id_storage;
 	/* cppcheck-suppress [variableScope,unmatchedSuppression] */
+	const struct in6_addr *src, *dst;
 	struct opt_ia_na ia_storage;
 	const struct guest_addr *a;
-	const struct in6_addr *src;
 	struct msg_hdr mh_storage;
 	const struct msg_hdr *mh;
 	struct udphdr uh_storage;
@@ -593,9 +597,12 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	if (mlen + sizeof(*uh) != ntohs(uh->len) || mlen < sizeof(*mh))
 		return -1;
 
-	c->ip6.addr_ll_seen = *saddr;
+	/* Guest LL address already recorded by tap_check_src_addr6() */
 
 	src = &c->ip6.our_tap_ll;
+	dst = tap_ip6_daddr(c, src);
+	if (!dst)
+		return -1;
 
 	mh = IOV_REMOVE_HEADER(data, mh_storage);
 	if (!mh)
@@ -683,10 +690,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 
 	resp.hdr.xid = mh->xid;
 
-	tap_udp6_send(c, src, 547, tap_ip6_daddr(c, src), 546,
-		      mh->xid, &resp, n);
-	if (a)
-		c->ip6.addr_seen = a->addr.a6;
+	tap_udp6_send(c, src, 547, dst, 546, mh->xid, &resp, n);
 
 	return 1;
 }
diff --git a/dhcpv6.h b/dhcpv6.h
index c706dfd..eda133f 100644
--- a/dhcpv6.h
+++ b/dhcpv6.h
@@ -7,7 +7,7 @@
 #define DHCPV6_H
 
 int dhcpv6(struct ctx *c, struct iov_tail *data,
-	   struct in6_addr *saddr, struct in6_addr *daddr);
+	   const struct in6_addr *daddr);
 void dhcpv6_init(const struct ctx *c);
 
 #endif /* DHCPV6_H */
diff --git a/fwd.c b/fwd.c
index 28a721e..b3f5dc0 100644
--- a/fwd.c
+++ b/fwd.c
@@ -1095,14 +1095,6 @@ static bool fwd_guest_accessible(const struct ctx *c,
 		if (inany_equals(addr, &a->addr))
 			return false;
 
-	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
-	 * address the guest will take until we see it.  Only check against it
-	 * if it has been set to a real address.
-	 */
-	if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen) &&
-	    inany_equals6(addr, &c->ip6.addr_seen))
-		return false;
-
 	return true;
 }
 
@@ -1305,10 +1297,20 @@ uint8_t fwd_nat_from_host(const struct ctx *c,
 			}
 			tgt->oaddr = inany_any4;
 		} else {
-			if (c->host_lo_to_ns_lo)
+			if (c->host_lo_to_ns_lo) {
 				tgt->eaddr = inany_loopback6;
-			else
-				tgt->eaddr.a6 = c->ip6.addr_seen;
+			} else {
+				const struct guest_addr *a;
+
+				a = fwd_select_addr(c, AF_INET6,
+						    CONF_ADDR_OBSERVED,
+						    CONF_ADDR_USER |
+						    CONF_ADDR_HOST,
+						    CONF_ADDR_LINKLOCAL);
+				if (!a)
+					return PIF_NONE;
+				tgt->eaddr = a->addr;
+			}
 			tgt->oaddr = inany_any6;
 		}
 
@@ -1346,10 +1348,16 @@ uint8_t fwd_nat_from_host(const struct ctx *c,
 
 		tgt->eaddr = a->addr;
 	} else {
-		if (inany_is_linklocal6(&tgt->oaddr))
-			tgt->eaddr.a6 = c->ip6.addr_ll_seen;
-		else
-			tgt->eaddr.a6 = c->ip6.addr_seen;
+		bool linklocal = inany_is_linklocal6(&tgt->oaddr);
+		int excl = linklocal ? 0 : CONF_ADDR_LINKLOCAL;
+		const struct guest_addr *a;
+
+		a = fwd_select_addr(c, AF_INET6, CONF_ADDR_OBSERVED,
+				    CONF_ADDR_USER | CONF_ADDR_HOST, excl);
+		if (!a)
+			return PIF_NONE;
+
+		tgt->eaddr = a->addr;
 	}
 
 	return PIF_TAP;
diff --git a/inany.h b/inany.h
index 7b23cb0..82dd102 100644
--- a/inany.h
+++ b/inany.h
@@ -60,6 +60,9 @@ extern const union inany_addr inany_any4;
 #define inany_from_v4(a4)	\
 	((union inany_addr)INANY_INIT4((a4)))
 
+#define inany_from_v6(v6)	\
+	((union inany_addr){ .a6 = (v6) })
+
 /** union sockaddr_inany - Either a sockaddr_in or a sockaddr_in6
  * @sa_family:	Address family, AF_INET or AF_INET6
  * @sa:		Plain struct sockaddr (useful to avoid casts)
diff --git a/migrate.c b/migrate.c
index 1e02720..a92301b 100644
--- a/migrate.c
+++ b/migrate.c
@@ -56,21 +56,30 @@ struct migrate_seen_addrs_v2 {
 static int seen_addrs_source_v2(struct ctx *c,
 				const struct migrate_stage *stage, int fd)
 {
-	struct migrate_seen_addrs_v2 addrs = {
-		.addr6 = c->ip6.addr_seen,
-		.addr6_ll = c->ip6.addr_ll_seen,
-	};
+	struct migrate_seen_addrs_v2 addrs = { 0 };
 	const struct guest_addr *a;
 
 	(void)stage;
 
-	/* IPv4 observed address, with fallback to configured address */
+	/* IPv4 observed address, with fallback to any other non-LL address */
 	a = fwd_select_addr(c, AF_INET, CONF_ADDR_OBSERVED,
 			    CONF_ADDR_USER | CONF_ADDR_HOST,
 			    CONF_ADDR_LINKLOCAL);
 	if (a)
 		addrs.addr4 = *inany_v4(&a->addr);
 
+	/* IPv6 observed address, with fallback to any other non-LL address */
+	a = fwd_select_addr(c, AF_INET6, CONF_ADDR_OBSERVED,
+			    CONF_ADDR_USER | CONF_ADDR_HOST,
+			    CONF_ADDR_LINKLOCAL);
+	if (a)
+		addrs.addr6 = a->addr.a6;
+
+	/* IPv6 link-local address */
+	a = fwd_get_addr(c, AF_INET6, CONF_ADDR_LINKLOCAL, 0);
+	if (a)
+		addrs.addr6_ll = a->addr.a6;
+
 	memcpy(addrs.mac, c->guest_mac, sizeof(addrs.mac));
 
 	if (write_all_buf(fd, &addrs, sizeof(addrs)))
@@ -91,19 +100,27 @@ static int seen_addrs_target_v2(struct ctx *c,
 				const struct migrate_stage *stage, int fd)
 {
 	struct migrate_seen_addrs_v2 addrs;
+	struct in6_addr addr6, addr6_ll;
 
 	(void)stage;
 
 	if (read_all_buf(fd, &addrs, sizeof(addrs)))
 		return errno;
 
-	c->ip6.addr_seen = addrs.addr6;
-	c->ip6.addr_ll_seen = addrs.addr6_ll;
-
 	if (addrs.addr4.s_addr)
 		fwd_set_addr(c, &inany_from_v4(addrs.addr4),
 			     CONF_ADDR_OBSERVED, 0);
 
+	addr6 = addrs.addr6;
+	if (!IN6_IS_ADDR_UNSPECIFIED(&addr6))
+		fwd_set_addr(c, &inany_from_v6(addr6),
+			     CONF_ADDR_OBSERVED, 0);
+
+	addr6_ll = addrs.addr6_ll;
+	if (!IN6_IS_ADDR_UNSPECIFIED(&addr6_ll))
+		fwd_set_addr(c, &inany_from_v6(addr6_ll),
+			     CONF_ADDR_OBSERVED | CONF_ADDR_LINKLOCAL, 0);
+
 	memcpy(c->guest_mac, addrs.mac, sizeof(c->guest_mac));
 
 	return 0;
diff --git a/passt.h b/passt.h
index 5452225..db2f10d 100644
--- a/passt.h
+++ b/passt.h
@@ -124,8 +124,6 @@ struct ip4_ctx {
 
 /**
  * struct ip6_ctx - IPv6 execution context
- * @addr_seen:		Latest IPv6 global/site address seen as source from tap
- * @addr_ll_seen:	Latest IPv6 link-local address seen as source from tap
  * @guest_gw:		IPv6 gateway as seen by the guest
  * @map_host_loopback:	Outbound connections to this address are NATted to the
  *                      host's [::1]
@@ -141,8 +139,6 @@ struct ip4_ctx {
  * @no_copy_addrs:	Don't copy all addresses when configuring namespace
  */
 struct ip6_ctx {
-	struct in6_addr addr_seen;
-	struct in6_addr addr_ll_seen;
 	struct in6_addr guest_gw;
 	struct in6_addr map_host_loopback;
 	struct in6_addr map_guest_addr;
diff --git a/pasta.c b/pasta.c
index b8d7cf4..fcb169a 100644
--- a/pasta.c
+++ b/pasta.c
@@ -370,6 +370,7 @@ static int pasta_conf_routes(struct ctx *c, sa_family_t af, int ifi,
 void pasta_ns_conf(struct ctx *c)
 {
 	unsigned int flags = IFF_UP;
+	struct in6_addr addr_ll;
 	int rc;
 
 	rc = nl_link_set_flags(nl_sock_ns, 1 /* lo */, IFF_UP, IFF_UP);
@@ -417,11 +418,13 @@ void pasta_ns_conf(struct ctx *c)
 	if (!c->ifi6)
 		goto done;
 
-	rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi,
-			    &c->ip6.addr_ll_seen);
+	rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi, &addr_ll);
 	if (rc < 0)
 		warn("Can't get LL address from namespace: %s",
 		     strerror_(-rc));
+	else
+		fwd_set_addr(c, &inany_from_v6(addr_ll),
+			     CONF_ADDR_LINKLOCAL | CONF_ADDR_HOST, 0);
 
 	rc = nl_addr_set_ll_nodad(nl_sock_ns, c->pasta_ifi);
 	if (rc < 0)
diff --git a/tap.c b/tap.c
index c75a4df..07f92bb 100644
--- a/tap.c
+++ b/tap.c
@@ -173,19 +173,51 @@ static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
 		fwd_set_addr(c, &inany_from_v4(*addr), CONF_ADDR_OBSERVED, 0);
 }
 
+/**
+ * tap_check_src_addr6() - Note an IPv6 address seen in guest traffic
+ * @c:		Execution context
+ * @addr:	IPv6 address seen as source from guest
+ */
+static void tap_check_src_addr6(struct ctx *c, const struct in6_addr *addr)
+{
+	uint8_t flags = CONF_ADDR_OBSERVED;
+
+	if (IN6_IS_ADDR_UNSPECIFIED(addr))
+		return;
+
+	if (IN6_IS_ADDR_LINKLOCAL(addr))
+		flags |= CONF_ADDR_LINKLOCAL;
+
+	fwd_set_addr(c, &inany_from_v6(*addr), flags, 0);
+}
+
 /**
  * tap_ip6_daddr() - Normal IPv6 destination address for inbound packets
  * @c:		Execution context
  * @src:	Source address
  *
- * Return: pointer to IPv6 address
+ * Return: pointer to IPv6 address, NULL if no suitable address found
  */
 const struct in6_addr *tap_ip6_daddr(const struct ctx *c,
 				     const struct in6_addr *src)
 {
-	if (IN6_IS_ADDR_LINKLOCAL(src))
-		return &c->ip6.addr_ll_seen;
-	return &c->ip6.addr_seen;
+	const struct guest_addr *a;
+
+	if (IN6_IS_ADDR_LINKLOCAL(src)) {
+		/* Link-local: first LL address in array */
+		a = fwd_get_addr(c, AF_INET6, CONF_ADDR_LINKLOCAL, 0);
+	} else {
+		/* Global: observed non-LL first, then any non-LL */
+		a = fwd_select_addr(c, AF_INET6, CONF_ADDR_OBSERVED,
+				    CONF_ADDR_USER | CONF_ADDR_HOST,
+				    CONF_ADDR_LINKLOCAL);
+	}
+
+	if (a)
+		return &a->addr.a6;
+
+	debug("No suitable IPv6 guest address found");
+	return NULL;
 }
 
 /**
@@ -958,21 +990,7 @@ resume:
 			continue;
 		}
 
-		if (IN6_IS_ADDR_LINKLOCAL(saddr)) {
-			c->ip6.addr_ll_seen = *saddr;
-
-			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen)) {
-				c->ip6.addr_seen = *saddr;
-			}
-
-			if (!fwd_get_addr(c, AF_INET6, 0, 0)) {
-				union inany_addr addr = { .a6 = *saddr };
-
-				fwd_set_addr(c, &addr, CONF_ADDR_LINKLOCAL, 64);
-			}
-		} else if (!IN6_IS_ADDR_UNSPECIFIED(saddr)){
-			c->ip6.addr_seen = *saddr;
-		}
+		tap_check_src_addr6(c, saddr);
 
 		if (proto == IPPROTO_ICMPV6) {
 			struct iov_tail ndp_data;
@@ -1003,7 +1021,7 @@ resume:
 		if (proto == IPPROTO_UDP) {
 			struct iov_tail uh_data = data;
 
-			if (dhcpv6(c, &uh_data, saddr, daddr))
+			if (dhcpv6(c, &uh_data, daddr))
 				continue;
 		}
 
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 10/13] migrate: Update protocol to v3 for multi-address support
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (8 preceding siblings ...)
  2026-03-22  0:43 ` [PATCH v6 09/13] ip: Track observed guest IPv6 " Jon Maloy
@ 2026-03-22  0:43 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 11/13] dhcp: Select address for DHCP distribution Jon Maloy
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

We update the migration protocol to version 3 to support distributing
multiple addresses from the unified address array. The new protocol
migrates all address entries in the array, along with their prefix
lengths and flags, and leaves it to the receiver to filter which
ones he wants to apply.

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

---
v4: - Broke out as separate commit
    - Made number of transferable addresses variable

v6: - Separated internal and wire transfer format
---
 migrate.c | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 178 insertions(+)

diff --git a/migrate.c b/migrate.c
index a92301b..7b9a2f6 100644
--- a/migrate.c
+++ b/migrate.c
@@ -44,6 +44,71 @@ struct migrate_seen_addrs_v2 {
 	unsigned char mac[ETH_ALEN];
 } __attribute__((packed));
 
+/**
+ * Wire format flags for address migration (v3)
+ * These are stable values - do not change existing assignments
+ */
+#define MIGRATE_ADDR_USER	BIT(0)
+#define MIGRATE_ADDR_HOST	BIT(1)
+#define MIGRATE_ADDR_LINKLOCAL	BIT(2)
+#define MIGRATE_ADDR_OBSERVED	BIT(3)
+
+/**
+ * struct migrate_addr_v3 - Wire format for a single address entry
+ * @addr:	IPv6 or IPv4-mapped address (16 bytes)
+ * @prefix_len:	Prefix length
+ * @flags:	MIGRATE_ADDR_* flags (wire format)
+ */
+struct migrate_addr_v3 {
+	struct in6_addr addr;
+	uint8_t prefix_len;
+	uint8_t flags;
+} __attribute__((__packed__));
+
+/**
+ * flags_to_wire() - Convert internal flags to stable wire format
+ * @flags:	Internal CONF_ADDR_* flags
+ *
+ * Return: Wire format MIGRATE_ADDR_* flags
+ */
+static uint8_t flags_to_wire(uint8_t flags)
+{
+	uint8_t wire = 0;
+
+	if (flags & CONF_ADDR_USER)
+		wire |= MIGRATE_ADDR_USER;
+	if (flags & CONF_ADDR_HOST)
+		wire |= MIGRATE_ADDR_HOST;
+	if (flags & CONF_ADDR_LINKLOCAL)
+		wire |= MIGRATE_ADDR_LINKLOCAL;
+	if (flags & CONF_ADDR_OBSERVED)
+		wire |= MIGRATE_ADDR_OBSERVED;
+
+	return wire;
+}
+
+/**
+ * flags_from_wire() - Convert wire format flags to internal format
+ * @wire:	Wire format MIGRATE_ADDR_* flags
+ *
+ * Return: Internal CONF_ADDR_* flags
+ */
+static uint8_t flags_from_wire(uint8_t wire)
+{
+	uint8_t flags = 0;
+
+	if (wire & MIGRATE_ADDR_USER)
+		flags |= CONF_ADDR_USER;
+	if (wire & MIGRATE_ADDR_HOST)
+		flags |= CONF_ADDR_HOST;
+	if (wire & MIGRATE_ADDR_LINKLOCAL)
+		flags |= CONF_ADDR_LINKLOCAL;
+	if (wire & MIGRATE_ADDR_OBSERVED)
+		flags |= CONF_ADDR_OBSERVED;
+
+	return flags;
+}
+
 /**
  * seen_addrs_source_v2() - Copy and send guest observed addresses from source
  * @c:		Execution context
@@ -126,6 +191,98 @@ static int seen_addrs_target_v2(struct ctx *c,
 	return 0;
 }
 
+/**
+ * addrs_source_v3() - Send all addresses with flags from source
+ * @c:		Execution context
+ * @stage:	Migration stage, unused
+ * @fd:		File descriptor for state transfer
+ *
+ * Send all address entries using a stable wire format. Each field is
+ * serialized explicitly to avoid coupling the wire format to internal
+ * structure layout or flag bit assignments.
+ *
+ * Return: 0 on success, positive error code on failure
+ */
+/* cppcheck-suppress [constParameterCallback, unmatchedSuppression] */
+static int addrs_source_v3(struct ctx *c,
+			   const struct migrate_stage *stage, int fd)
+{
+	uint8_t addr_count = c->addr_count;
+	const struct guest_addr *a;
+
+	(void)stage;
+
+	/* Send count first */
+	if (write_all_buf(fd, &addr_count, sizeof(addr_count)))
+		return errno;
+
+	/* Send each address in stable wire format */
+	for_each_addr(a, c, 0) {
+		struct migrate_addr_v3 wire = {
+			.addr = a->addr.a6,
+			.prefix_len = a->prefix_len,
+			.flags = flags_to_wire(a->flags),
+		};
+
+		if (write_all_buf(fd, &wire, sizeof(wire)))
+			return errno;
+	}
+
+	/* Send MAC */
+	if (write_all_buf(fd, c->guest_mac, ETH_ALEN))
+		return errno;
+
+	return 0;
+}
+
+/**
+ * addrs_target_v3() - Receive addresses on target
+ * @c:		Execution context
+ * @stage:	Migration stage, unused
+ * @fd:		File descriptor for state transfer
+ *
+ * Receive address entries from the stable wire format and merge only
+ * observed addresses into local array. Source sends all addresses for
+ * forward compatibility, but target only applies those marked as observed.
+ *
+ * Return: 0 on success, positive error code on failure
+ */
+static int addrs_target_v3(struct ctx *c,
+			   const struct migrate_stage *stage, int fd)
+{
+	uint8_t addr_count, i;
+
+	(void)stage;
+
+	if (read_all_buf(fd, &addr_count, sizeof(addr_count)))
+		return errno;
+
+	if (addr_count > MAX_GUEST_ADDRS)
+		addr_count = MAX_GUEST_ADDRS;
+
+	/* Read each address from stable wire format */
+	for (i = 0; i < addr_count; i++) {
+		struct migrate_addr_v3 wire;
+		struct guest_addr addr;
+
+		if (read_all_buf(fd, &wire, sizeof(wire)))
+			return errno;
+
+		addr.addr.a6 = wire.addr;
+		addr.prefix_len = wire.prefix_len;
+		addr.flags = flags_from_wire(wire.flags);
+
+		if (addr.flags & CONF_ADDR_OBSERVED)
+			fwd_set_addr(c, &addr.addr, addr.flags,
+				     addr.prefix_len);
+	}
+
+	if (read_all_buf(fd, c->guest_mac, ETH_ALEN))
+		return errno;
+
+	return 0;
+}
+
 /* Stages for version 2 */
 static const struct migrate_stage stages_v2[] = {
 	{
@@ -146,8 +303,29 @@ static const struct migrate_stage stages_v2[] = {
 	{ 0 },
 };
 
+/* Stages for version 3 (multiple observed IPv4 addresses) */
+static const struct migrate_stage stages_v3[] = {
+	{
+		.name = "addresses",
+		.source = addrs_source_v3,
+		.target = addrs_target_v3,
+	},
+	{
+		.name = "prepare flows",
+		.source = flow_migrate_source_pre,
+		.target = NULL,
+	},
+	{
+		.name = "transfer flows",
+		.source = flow_migrate_source,
+		.target = flow_migrate_target,
+	},
+	{ 0 },
+};
+
 /* Supported encoding versions, from latest (most preferred) to oldest */
 static const struct migrate_version versions[] = {
+	{ 3,	stages_v3, },
 	{ 2,	stages_v2, },
 	/* v1 was released, but not widely used.  It had bad endianness for the
 	 * MSS and omitted timestamps, which meant it usually wouldn't work.
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 11/13] dhcp: Select address for DHCP distribution
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (9 preceding siblings ...)
  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 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 12/13] dhcpv6: Select addresses for DHCPv6 distribution Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 13/13] ndp: Support advertising multiple prefixes in Router Advertisements Jon Maloy
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

We introduce a CONF_ADDR_DHCP flag to mark if an added address is
eligible for DHCP advertisement. By doing this once and for all
in the fwd_set_addr() function, the DHCP code only needs to check
for this flag to know that all criteria for advertisement are
fulfilled. Hence, we update the code in dhcp.c correspondingly.

We also let the conf_print() function use this flag to determine
and print the selected address.

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

---
v6: Split off from a commit handling both DHCP and DHCPv6
---
 conf.c    |  9 ++++-----
 dhcp.c    | 21 +++++++++++++--------
 dhcp.h    |  2 +-
 fwd.c     |  9 +++++++++
 migrate.c |  5 +++++
 passt.h   |  1 +
 6 files changed, 33 insertions(+), 14 deletions(-)

diff --git a/conf.c b/conf.c
index 320a9e4..512fa38 100644
--- a/conf.c
+++ b/conf.c
@@ -46,6 +46,7 @@
 #include "lineread.h"
 #include "isolation.h"
 #include "log.h"
+#include "fwd.h"
 #include "vhost_user.h"
 
 #define NETNS_RUN_DIR	"/run/netns"
@@ -1182,11 +1183,9 @@ static void conf_print(const struct ctx *c)
 			     inet_ntop(AF_INET, &c->ip4.map_host_loopback,
 				       buf, sizeof(buf)));
 
-		a = fwd_get_addr(c, AF_INET, 0, 0);
-		if (a && !c->no_dhcp) {
-			uint32_t mask;
-
-			mask = IN4_MASK(inany_prefix_len(a));
+		a = fwd_get_addr(c, AF_INET, CONF_ADDR_DHCP, 0);
+		if (a) {
+			uint32_t mask = IN4_MASK(inany_prefix_len(a));
 
 			info("DHCP:");
 			inany_ntop(&a->addr, buf, sizeof(buf));
diff --git a/dhcp.c b/dhcp.c
index 6d9def5..cdb3afb 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -31,6 +31,8 @@
 #include "passt.h"
 #include "tap.h"
 #include "log.h"
+#include "fwd.h"
+#include "conf.h"
 #include "dhcp.h"
 
 /**
@@ -300,23 +302,22 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
  *
  * Return: 0 if it's not a DHCP message, 1 if handled, -1 on failure
  */
-int dhcp(const struct ctx *c, struct iov_tail *data)
+int dhcp(struct ctx *c, struct iov_tail *data)
 {
-	const struct guest_addr *a = fwd_get_addr(c, AF_INET, 0, 0);
+	struct in_addr addr, mask, dst;
 	char macstr[ETH_ADDRSTRLEN];
+	const struct guest_addr *a;
 	size_t mlen, dlen, opt_len;
-	struct in_addr mask, dst;
+	struct udphdr uh_storage;
 	struct ethhdr eh_storage;
 	struct iphdr iph_storage;
-	struct udphdr uh_storage;
+	const struct udphdr *uh;
 	const struct ethhdr *eh;
 	const struct iphdr *iph;
-	const struct udphdr *uh;
 	struct msg m_storage;
 	struct msg const *m;
-	struct in_addr addr;
 	struct msg reply;
-	unsigned int i;
+	int i;
 
 	eh = IOV_REMOVE_HEADER(data, eh_storage);
 	iph = IOV_PEEK_HEADER(data, iph_storage);
@@ -346,7 +347,11 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	    m->op != BOOTREQUEST)
 		return -1;
 
-	ASSERT(a);
+	/* Select address to offer */
+	a = fwd_get_addr(c, AF_INET, CONF_ADDR_DHCP, 0);
+	if (!a)
+		return -1;
+
 	addr = *inany_v4(&a->addr);
 
 	reply.op		= BOOTREPLY;
diff --git a/dhcp.h b/dhcp.h
index cd50c99..7326c7d 100644
--- a/dhcp.h
+++ b/dhcp.h
@@ -6,7 +6,7 @@
 #ifndef DHCP_H
 #define DHCP_H
 
-int dhcp(const struct ctx *c, struct iov_tail *data);
+int dhcp(struct ctx *c, struct iov_tail *data);
 void dhcp_init(void);
 
 #endif /* DHCP_H */
diff --git a/fwd.c b/fwd.c
index b3f5dc0..e1c85dd 100644
--- a/fwd.c
+++ b/fwd.c
@@ -295,6 +295,15 @@ void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
 		return;
 	}
 
+	/* Determine advertisement eligibility */
+	if ((flags & (CONF_ADDR_HOST | CONF_ADDR_USER)) &&
+	    !(flags & CONF_ADDR_LINKLOCAL)) {
+		if (inany_v4(addr)) {
+			if (!c->no_dhcp)
+				flags |= CONF_ADDR_DHCP;
+		}
+	}
+
 	/* Add to head or tail, depending on flag */
 	if (flags & CONF_ADDR_OBSERVED) {
 		a = &arr[0];
diff --git a/migrate.c b/migrate.c
index 7b9a2f6..1d1e0e6 100644
--- a/migrate.c
+++ b/migrate.c
@@ -52,6 +52,7 @@ struct migrate_seen_addrs_v2 {
 #define MIGRATE_ADDR_HOST	BIT(1)
 #define MIGRATE_ADDR_LINKLOCAL	BIT(2)
 #define MIGRATE_ADDR_OBSERVED	BIT(3)
+#define MIGRATE_ADDR_DHCP	BIT(4)
 
 /**
  * struct migrate_addr_v3 - Wire format for a single address entry
@@ -83,6 +84,8 @@ static uint8_t flags_to_wire(uint8_t flags)
 		wire |= MIGRATE_ADDR_LINKLOCAL;
 	if (flags & CONF_ADDR_OBSERVED)
 		wire |= MIGRATE_ADDR_OBSERVED;
+	if (flags & CONF_ADDR_DHCP)
+		wire |= MIGRATE_ADDR_DHCP;
 
 	return wire;
 }
@@ -105,6 +108,8 @@ static uint8_t flags_from_wire(uint8_t wire)
 		flags |= CONF_ADDR_LINKLOCAL;
 	if (wire & MIGRATE_ADDR_OBSERVED)
 		flags |= CONF_ADDR_OBSERVED;
+	if (wire & MIGRATE_ADDR_DHCP)
+		flags |= CONF_ADDR_DHCP;
 
 	return flags;
 }
diff --git a/passt.h b/passt.h
index db2f10d..5ea1715 100644
--- a/passt.h
+++ b/passt.h
@@ -75,6 +75,7 @@ enum passt_modes {
 #define CONF_ADDR_HOST		BIT(1)		/* From host interface */
 #define CONF_ADDR_LINKLOCAL	BIT(2)		/* Link-local address */
 #define CONF_ADDR_OBSERVED	BIT(3)		/* Seen in guest traffic */
+#define CONF_ADDR_DHCP		BIT(4)		/* Advertise via DHCP (IPv4) */
 
 /**
  * struct guest_addr - Unified IPv4/IPv6 address entry
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 12/13] dhcpv6: Select addresses for DHCPv6 distribution
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (10 preceding siblings ...)
  2026-03-22  0:43 ` [PATCH v6 11/13] dhcp: Select address for DHCP distribution Jon Maloy
@ 2026-03-22  0:43 ` Jon Maloy
  2026-03-22  0:43 ` [PATCH v6 13/13] ndp: Support advertising multiple prefixes in Router Advertisements Jon Maloy
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

We introduce a CONF_ADDR_DHCP flag to mark if an added address is
eligible for DHCP advertisement. By doing this once and for all
in the fwd_set_addr() function, the DHCPv6 code only needs to check
for this flag to know that all criteria for advertisement are fulfilled.

We update the code in dhcpv6.c both to use the new flag and to make
it possible to send multiple addresses in a single reply message,
per RFC 8415.

We also let the conf_print() function use this flag to identify and
print the eligible addresses.

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

---
v6: -Refactored the DHCPv6 response structure to use a variable-length
     buffer for IA_ADDR options, hopefully making this part of the code
     slightly clearer.
---
 conf.c    | 36 +++++++++++++++------
 dhcpv6.c  | 97 ++++++++++++++++++++++++++++++++-----------------------
 fwd.c     |  4 +++
 migrate.c |  5 +++
 passt.h   |  1 +
 5 files changed, 94 insertions(+), 49 deletions(-)

diff --git a/conf.c b/conf.c
index 512fa38..de2fb7c 100644
--- a/conf.c
+++ b/conf.c
@@ -1213,24 +1213,42 @@ static void conf_print(const struct ctx *c)
 	}
 
 	if (c->ifi6) {
+		bool has_dhcpv6 = false;
+		const char *head;
+
 		if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback))
 			info("    NAT to host ::1: %s",
 			     inet_ntop(AF_INET6, &c->ip6.map_host_loopback,
 				       buf, sizeof(buf)));
 
-		if (!c->no_ndp && !c->no_dhcpv6)
-			info("NDP/DHCPv6:");
-		else if (!c->no_dhcpv6)
-			info("DHCPv6:");
-		else if (!c->no_ndp)
-			info("NDP:");
-		else
+		/* Check what we have to advertise */
+		for_each_addr(a, c, AF_INET6) {
+			if (a->flags & CONF_ADDR_DHCPV6)
+				has_dhcpv6 = true;
+		}
+
+		if (c->no_ndp && !has_dhcpv6)
 			goto dns6;
 
 		a = fwd_get_addr(c, AF_INET6, 0, CONF_ADDR_LINKLOCAL);
-		if (a)
+		if (!c->no_ndp && a) {
+			info("NDP:");
 			inany_ntop(&a->addr, buf, sizeof(buf));
-		info("    assign: %s", !a ? "" : buf);
+			info("    assign: %s", buf);
+		}
+
+		if (has_dhcpv6) {
+			info("DHCPv6:");
+			head = "assign";
+			for_each_addr(a, c, AF_INET6) {
+				if (!(a->flags & CONF_ADDR_DHCPV6))
+					continue;
+				inany_ntop(&a->addr, buf, sizeof(buf));
+				info("    %s: %s/%d", head, buf, a->prefix_len);
+				head = "      ";
+			}
+		}
+
 		inet_ntop(AF_INET6, &c->ip6.guest_gw, buf, sizeof(buf));
 		info("    router: %s", buf);
 		inet_ntop(AF_INET6, &c->ip6.our_tap_ll, buf, sizeof(buf));
diff --git a/dhcpv6.c b/dhcpv6.c
index 313c243..7c16da4 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -31,6 +31,8 @@
 #include "passt.h"
 #include "tap.h"
 #include "log.h"
+#include "fwd.h"
+#include "conf.h"
 
 /**
  * struct opt_hdr - DHCPv6 option header
@@ -202,56 +204,35 @@ struct msg_hdr {
 	uint32_t xid:24;
 } __attribute__((__packed__));
 
+/* Maximum variable part size: ia_addrs + client_id + dns + search + fqdn */
+#define RESP_VAR_MAX	(MAX_GUEST_ADDRS * sizeof(struct opt_ia_addr) + \
+			 sizeof(struct opt_client_id) + \
+			 sizeof(struct opt_dns_servers) + \
+			 sizeof(struct opt_dns_search) + \
+			 sizeof(struct opt_client_fqdn))
+
 /**
  * struct resp_t - Normal advertise and reply message
  * @hdr:		DHCP message header
  * @server_id:		Server Identifier option
  * @ia_na:		Non-temporary Address option
- * @ia_addr:		Address for IA_NA
- * @client_id:		Client Identifier, variable length
- * @dns_servers:	DNS Recursive Name Server, here just for storage size
- * @dns_search:		Domain Search List, here just for storage size
- * @client_fqdn:	Client FQDN, variable length
+ * @var:		Variable part: IA_ADDRs, client_id, dns, search, fqdn
  */
 static struct resp_t {
 	struct msg_hdr hdr;
 
 	struct opt_server_id server_id;
 	struct opt_ia_na ia_na;
-	struct opt_ia_addr ia_addr;
-	struct opt_client_id client_id;
-	struct opt_dns_servers dns_servers;
-	struct opt_dns_search dns_search;
-	struct opt_client_fqdn client_fqdn;
+	uint8_t var[RESP_VAR_MAX];
 } __attribute__((__packed__)) resp = {
 	{ 0 },
 	SERVER_ID,
 
-	{ { OPT_IA_NA,		OPT_SIZE_CONV(sizeof(struct opt_ia_na) +
-					      sizeof(struct opt_ia_addr) -
-					      sizeof(struct opt_hdr)) },
+	{ { OPT_IA_NA,		0 },  /* Length set dynamically */
 	  1, (uint32_t)~0U, (uint32_t)~0U
 	},
 
-	{ { OPT_IAAADR,		OPT_SIZE(ia_addr) },
-	  IN6ADDR_ANY_INIT, (uint32_t)~0U, (uint32_t)~0U
-	},
-
-	{ { OPT_CLIENTID,	0, },
-	  { 0 }
-	},
-
-	{ { OPT_DNS_SERVERS,	0, },
-	  { IN6ADDR_ANY_INIT }
-	},
-
-	{ { OPT_DNS_SEARCH,	0, },
-	  { 0 },
-	},
-
-	{ { OPT_CLIENT_FQDN, 0, },
-	  0, { 0 },
-	},
+	{ 0 },  /* Variable part filled dynamically */
 };
 
 static const struct opt_status_code sc_not_on_link = {
@@ -543,6 +524,42 @@ static size_t dhcpv6_client_fqdn_fill(const struct iov_tail *data,
 	return offset + sizeof(struct opt_hdr) + opt_len;
 }
 
+/**
+ * dhcpv6_ia_addr_fill() - Fill IA_ADDR options for all suitable addresses
+ * @c:		Execution context
+ *
+ * Fills IA_ADDRs in resp.var with all non-linklocal, non-observed addresses
+ * and updates resp.ia_na.hdr.l with the correct length.
+ *
+ * Return: number of addresses filled
+ */
+static int dhcpv6_ia_addr_fill(const struct ctx *c)
+{
+	struct opt_ia_addr *ia_addr = (struct opt_ia_addr *)resp.var;
+	const struct guest_addr *e;
+	int count = 0;
+
+	for_each_addr(e, c, AF_INET6) {
+		if (!(e->flags & CONF_ADDR_DHCPV6))
+			continue;
+
+		ia_addr[count].hdr.t = OPT_IAAADR;
+		ia_addr[count].hdr.l = htons(sizeof(struct opt_ia_addr) -
+					     sizeof(struct opt_hdr));
+		ia_addr[count].addr = e->addr.a6;
+		ia_addr[count].pref_lifetime = (uint32_t)~0U;
+		ia_addr[count].valid_lifetime = (uint32_t)~0U;
+		count++;
+	}
+
+	/* Update IA_NA length: header fields + all IA_ADDRs */
+	resp.ia_na.hdr.l = htons(sizeof(struct opt_ia_na) -
+				 sizeof(struct opt_hdr) +
+				 count * sizeof(struct opt_ia_addr));
+
+	return count;
+}
+
 /**
  * dhcpv6() - Check if this is a DHCPv6 message, reply as needed
  * @c:		Execution context
@@ -570,12 +587,14 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	struct opt_hdr client_id_storage;
 	/* cppcheck-suppress [variableScope,unmatchedSuppression] */
 	const struct in6_addr *src, *dst;
+	/* cppcheck-suppress [variableScope,unmatchedSuppression] */
 	struct opt_ia_na ia_storage;
 	const struct guest_addr *a;
 	struct msg_hdr mh_storage;
 	const struct msg_hdr *mh;
 	struct udphdr uh_storage;
 	const struct udphdr *uh;
+	int addr_count;
 	size_t mlen, n;
 
 	a = fwd_get_addr(c, AF_INET6, 0, CONF_ADDR_LINKLOCAL);
@@ -626,6 +645,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	if (ia && ntohs(ia->hdr.l) < MIN(OPT_VSIZE(ia_na), OPT_VSIZE(ia_ta)))
 		return -1;
 
+	addr_count = dhcpv6_ia_addr_fill(c);
 	resp.hdr.type = TYPE_REPLY;
 	switch (mh->type) {
 	case TYPE_REQUEST:
@@ -679,12 +699,14 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	if (ia)
 		resp.ia_na.iaid = ((struct opt_ia_na *)ia)->iaid;
 
+	/* Client_id goes right after the used IA_ADDRs */
+	n = offsetof(struct resp_t, var) +
+	    addr_count * sizeof(struct opt_ia_addr);
 	iov_to_buf(&client_id_base.iov[0], client_id_base.cnt,
-		   client_id_base.off, &resp.client_id,
+		   client_id_base.off, (char *)&resp + n,
 		   ntohs(client_id->l) + sizeof(struct opt_hdr));
 
-	n = offsetof(struct resp_t, client_id) +
-	    sizeof(struct opt_hdr) + ntohs(client_id->l);
+	n += sizeof(struct opt_hdr) + ntohs(client_id->l);
 	n = dhcpv6_dns_fill(c, (char *)&resp, n);
 	n = dhcpv6_client_fqdn_fill(data, c, (char *)&resp, n);
 
@@ -701,7 +723,6 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
  */
 void dhcpv6_init(const struct ctx *c)
 {
-	const struct guest_addr *a;
 	time_t y2k = 946684800; /* Epoch to 2000-01-01T00:00:00Z, no mktime() */
 	uint32_t duid_time;
 
@@ -714,8 +735,4 @@ void dhcpv6_init(const struct ctx *c)
 	       c->our_tap_mac, sizeof(c->our_tap_mac));
 	memcpy(resp_not_on_link.server_id.duid_lladdr,
 	       c->our_tap_mac, sizeof(c->our_tap_mac));
-
-	a = fwd_get_addr(c, AF_INET6, 0, CONF_ADDR_LINKLOCAL);
-	if (a)
-		resp.ia_addr.addr = a->addr.a6;
 }
diff --git a/fwd.c b/fwd.c
index e1c85dd..f867398 100644
--- a/fwd.c
+++ b/fwd.c
@@ -301,6 +301,10 @@ void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
 		if (inany_v4(addr)) {
 			if (!c->no_dhcp)
 				flags |= CONF_ADDR_DHCP;
+		} else {
+			/* DHCPv6 for IPv6 */
+			if (!c->no_dhcpv6)
+				flags |= CONF_ADDR_DHCPV6;
 		}
 	}
 
diff --git a/migrate.c b/migrate.c
index 1d1e0e6..105f624 100644
--- a/migrate.c
+++ b/migrate.c
@@ -53,6 +53,7 @@ struct migrate_seen_addrs_v2 {
 #define MIGRATE_ADDR_LINKLOCAL	BIT(2)
 #define MIGRATE_ADDR_OBSERVED	BIT(3)
 #define MIGRATE_ADDR_DHCP	BIT(4)
+#define MIGRATE_ADDR_DHCPV6	BIT(5)
 
 /**
  * struct migrate_addr_v3 - Wire format for a single address entry
@@ -86,6 +87,8 @@ static uint8_t flags_to_wire(uint8_t flags)
 		wire |= MIGRATE_ADDR_OBSERVED;
 	if (flags & CONF_ADDR_DHCP)
 		wire |= MIGRATE_ADDR_DHCP;
+	if (flags & CONF_ADDR_DHCPV6)
+		wire |= MIGRATE_ADDR_DHCPV6;
 
 	return wire;
 }
@@ -110,6 +113,8 @@ static uint8_t flags_from_wire(uint8_t wire)
 		flags |= CONF_ADDR_OBSERVED;
 	if (wire & MIGRATE_ADDR_DHCP)
 		flags |= CONF_ADDR_DHCP;
+	if (wire & MIGRATE_ADDR_DHCPV6)
+		flags |= CONF_ADDR_DHCPV6;
 
 	return flags;
 }
diff --git a/passt.h b/passt.h
index 5ea1715..c4c1f04 100644
--- a/passt.h
+++ b/passt.h
@@ -76,6 +76,7 @@ enum passt_modes {
 #define CONF_ADDR_LINKLOCAL	BIT(2)		/* Link-local address */
 #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) */
 
 /**
  * struct guest_addr - Unified IPv4/IPv6 address entry
-- 
2.52.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 13/13] ndp: Support advertising multiple prefixes in Router Advertisements
  2026-03-22  0:43 [PATCH v6 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (11 preceding siblings ...)
  2026-03-22  0:43 ` [PATCH v6 12/13] dhcpv6: Select addresses for DHCPv6 distribution Jon Maloy
@ 2026-03-22  0:43 ` Jon Maloy
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-03-22  0:43 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

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


^ permalink raw reply	[flat|nested] 14+ messages in thread

end of thread, other threads:[~2026-03-22  0:43 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [PATCH v6 13/13] ndp: Support advertising multiple prefixes in Router Advertisements Jon Maloy

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