public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
* [PATCH v8 00/14] Introduce multiple addresses
@ 2026-06-26  2:45 Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 01/14] dhcpv6: Fix reply destination to match client's source address Jon Maloy
                   ` (13 more replies)
  0 siblings, 14 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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.

v7: - Replaced commit #1 with one that fixes a return address
      issue with DHCPv6
    - Modified for_each_addr() macro to take 4 arguments
    - Many more fixes and changes based on feedback and own
      findings.

v8: - New commit replacing ctx->addr_fixed flag with CONF_ADDR_USER.
    - Moved migrate commit to last in series
    - Numerous smaller and larger fixes based on feedback from
      David Gibson and Stefano Brivio.


Jon Maloy (14):
  dhcpv6: Fix reply destination to match client's source address
  passt, pasta: Introduce unified multi-address data structures
  tap, conf: Replace addr_fixed with CONF_ADDR_USER flag check
  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
  netlink, pasta: refactor function pasta_ns_conf()
  conf, pasta: Track observed guest IPv4 addresses in unified address
    array
  conf, pasta: Track observed guest IPv6 addresses in unified address
    array
  dhcp: Select address for DHCP distribution
  dhcpv6: Select addresses for DHCPv6 distribution
  ndp: Support advertising multiple prefixes in Router Advertisements
  migrate: Update protocol to v3 for multi-address support

 arp.c     |  20 +++-
 conf.c    | 201 ++++++++++++++++++++---------------
 dhcp.c    |  23 ++--
 dhcpv6.c  | 113 ++++++++++++--------
 dhcpv6.h  |   2 +-
 fwd.c     | 312 ++++++++++++++++++++++++++++++++++++++++--------------
 fwd.h     |   7 +-
 inany.h   |  32 ++++++
 ip.h      |   2 +
 migrate.c | 245 ++++++++++++++++++++++++++++++++++++++++--
 ndp.c     | 127 +++++++++++++++-------
 netlink.c |  99 +++++++++--------
 netlink.h |   7 +-
 passt.1   |   5 +-
 passt.h   |  81 +++++++++++---
 pasta.c   | 228 ++++++++++++++++++++-------------------
 tap.c     |  40 ++-----
 tap.h     |   2 -
 18 files changed, 1063 insertions(+), 483 deletions(-)

-- 
2.52.0


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

* [PATCH v8 01/14] dhcpv6: Fix reply destination to match client's source address
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 02/14] passt, pasta: Introduce unified multi-address data structures Jon Maloy
                   ` (12 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, david, jmaloy, passt-dev

tap_ip6_daddr() selects the reply destination based on our source
address type (link-local), so it always returns addr_ll_seen. But if
the client sent from a global address, we would reply to an address
different from what the client is expecting. Since RFC 9915 (section
18.3.10) allows clients to use global addresses for DHCPv6, we now
correct this, and always respond to the address the client was using.

We also remove a redundant addr_ll_seen assignment, since this is
already done by tap.c when processing IPv6 packets.

Note: if the client uses a global source address, our reply will
still have a link-local source, creating a scope mismatch. Fixing
this properly would require a mechanism to allocate a global address
for the DHCPv6 server, which we currently don't have. Responding to
the client's actual source address is still a net improvement over the
previous behavior of replying to an unrelated cached address.

Note 2: This commit isn't actually a fix to an observed problem, but
rather an answer to a theoretical issue, adding completeness to the
mechanism and simplifying subsequent changes in this series.

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

---
v8: -Updated commit log, acknowledging concerns expressed by David and
     Stefano.
    -Some minor changes also addressing feedback from the same persons.
---
 dhcpv6.c | 16 ++++++++--------
 dhcpv6.h |  2 +-
 tap.c    | 15 ---------------
 tap.h    |  2 --
 4 files changed, 9 insertions(+), 26 deletions(-)

diff --git a/dhcpv6.c b/dhcpv6.c
index 97c04e2c..29c7e320 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -370,12 +370,14 @@ notonlink:
 /**
  * dhcpv6_send_ia_notonlink() - Send NotOnLink status
  * @c:			Execution context
+ * @caddr:		Source address of client message (reply destination)
  * @ia_base:		Non-appropriate IA_NA or IA_TA base
  * @client_id_base:	Client ID message option base
  * @len:		Client ID length
  * @xid:		Transaction ID for message exchange
  */
 static void dhcpv6_send_ia_notonlink(struct ctx *c,
+				     const struct in6_addr *caddr,
 				     const struct iov_tail *ia_base,
 				     const struct iov_tail *client_id_base,
 				     int len, uint32_t xid)
@@ -405,8 +407,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,
-		      xid, &resp_not_on_link, n);
+	tap_udp6_send(c, src, 547, caddr, 546, xid, &resp_not_on_link, n);
 }
 
 /**
@@ -590,8 +591,6 @@ 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;
-
 	src = &c->ip6.our_tap_ll;
 
 	mh = IOV_REMOVE_HEADER(data, mh_storage);
@@ -630,8 +629,10 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 
 		if (dhcpv6_ia_notonlink(data, &c->ip6.addr)) {
 
-			dhcpv6_send_ia_notonlink(c, data, &client_id_base,
-						 ntohs(client_id->l), mh->xid);
+			dhcpv6_send_ia_notonlink(c, saddr, data,
+						 &client_id_base,
+						 ntohs(client_id->l),
+						 mh->xid);
 
 			return 1;
 		}
@@ -680,8 +681,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);
+	tap_udp6_send(c, src, 547, saddr, 546, mh->xid, &resp, n);
 	c->ip6.addr_seen = c->ip6.addr;
 
 	return 1;
diff --git a/dhcpv6.h b/dhcpv6.h
index c706dfdb..1015a1a7 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 *saddr, const struct in6_addr *daddr);
 void dhcpv6_init(const struct ctx *c);
 
 #endif /* DHCPV6_H */
diff --git a/tap.c b/tap.c
index 6d93c7ce..d4189617 100644
--- a/tap.c
+++ b/tap.c
@@ -160,21 +160,6 @@ void tap_send_single(const struct ctx *c, const void *data, size_t l2len)
 	}
 }
 
-/**
- * tap_ip6_daddr() - Normal IPv6 destination address for inbound packets
- * @c:		Execution context
- * @src:	Source address
- *
- * Return: pointer to IPv6 address
- */
-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;
-}
-
 /**
  * tap_push_l2h() - Build an L2 header for an inbound packet
  * @c:		Execution context
diff --git a/tap.h b/tap.h
index 07ca0965..b335933f 100644
--- a/tap.h
+++ b/tap.h
@@ -96,8 +96,6 @@ void tap_udp4_send(const struct ctx *c, struct in_addr src, in_port_t sport,
 		   const void *in, size_t dlen);
 void tap_icmp4_send(const struct ctx *c, struct in_addr src, struct in_addr dst,
 		    const void *in, const void *src_mac, size_t l4len);
-const struct in6_addr *tap_ip6_daddr(const struct ctx *c,
-				     const struct in6_addr *src);
 void *tap_push_ip6h(struct ipv6hdr *ip6h,
 		    const struct in6_addr *src, const struct in6_addr *dst,
 		    size_t l4len, uint8_t proto, uint32_t flow);
-- 
2.52.0


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

* [PATCH v8 02/14] passt, pasta: Introduce unified multi-address data structures
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 01/14] dhcpv6: Fix reply destination to match client's source address Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 03/14] tap, conf: Replace addr_fixed with CONF_ADDR_USER flag check Jon Maloy
                   ` (11 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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
  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].

In conf_ip6(), the explicit IN6_IS_ADDR_UNSPECIFIED() check on the
address at the end of the function is no longer needed: for
host-discovered addresses, it is now checked inside the if (!a) block
before calling fwd_set_addr(); for user-provided addresses (via -a),
validation already rejects unspecified addresses at parse time.

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.

v7: -Introduced CONF_ADDR_GENERATED flag
    -Other fixes based on feedback from David and Stefano.
    -I changed signature of inany_prefix_len(), but I did not change
     its semantics, since the premise of David's comment is wrong: the
     caller does *not* explicitly know he is dealing with an IPv4 address.
     In fact, there are examples later in this series where it may be an
     IPv6 address, and the caller just trusts he gets the return value in
     the appropriate format.
    -Intoduced the inverse of inany_prefix_len(), called inany_prefix_len6()
     which always returns the prefix in IPv6 or mapped IPv4 format.
     The name of the function isn't great, but any alternative I came up
     with became too long to be practical.

v8: -Added inany_prefix_v4() (David G.) to extract IPv4 address and
     prefix length simultaneously, replacing inany_prefix_len().
    -Removed both inany_prefix_len() and inany_prefix_len6()
     auto-detecting functions. Prefix length attached to an inany is
     now always stored in IPv6 format (96-128 for IPv4-mapped).
    -fwd_set_addr() now requires prefix_len in IPv6 format; callers
     updated to pass prefix_len + 96 for IPv4 addresses.
    -Fixed -n option to store prefix_len in IPv6 format.
    -Fixed addr_seen regression: when address was set via -a,
     addr_seen was not initialised in conf_ip4().
    -Replaced assert() with graceful no-op in dhcp.c and ndp.c when
     no address is configured (David G.).
    -Note: addr_seen is still initialised both at conf_ip*() time
     and at DHCP/DHCPv6 send time (David's concern). This
     inconsistency will be addressed in a follow-up.
    -Introduced a new CONF_ADDR_ANY macro tobe used for address
     selection, as suggested by Stefano.
    -Deferred introduction of an explicit scope field (replacing
     CONF_ADDR_LINKLOCAL flag) to a later commit or series.
---
 arp.c    |  12 ++++-
 conf.c   | 142 +++++++++++++++++++++++++++++++------------------------
 dhcp.c   |  19 ++++++--
 dhcpv6.c |  15 ++++--
 fwd.c    | 111 +++++++++++++++++++++++++++++++++++--------
 fwd.h    |   4 ++
 inany.h  |  28 +++++++++++
 ip.h     |   2 +
 ndp.c    |  19 ++++++--
 passt.h  |  66 +++++++++++++++++++++++---
 pasta.c  |  26 ++++++----
 tap.c    |   8 +++-
 12 files changed, 342 insertions(+), 110 deletions(-)

diff --git a/arp.c b/arp.c
index bb042e95..0392861d 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;
+
 	if (ah->ar_hrd != htons(ARPHRD_ETHER)	||
 	    ah->ar_pro != htons(ETH_P_IP)	||
 	    ah->ar_hln != ETH_ALEN		||
@@ -54,7 +56,8 @@ 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)))
+	a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
+	if (a && !memcmp(am->tip, inany_v4(&a->addr), sizeof(am->tip)))
 		return true;
 
 	return false;
@@ -123,12 +126,17 @@ int arp(const struct ctx *c, struct iov_tail *data)
  */
 void arp_send_init_req(const struct ctx *c)
 {
+	const struct guest_addr *a;
 	struct {
 		struct ethhdr eh;
 		struct arphdr ah;
 		struct arpmsg am;
 	} __attribute__((__packed__)) req;
 
+	a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
+	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 +153,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 4755a9f4..ac8facc8 100644
--- a/conf.c
+++ b/conf.c
@@ -373,13 +373,16 @@ 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)
 {
+	const struct guest_addr *a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
+	struct ip4_ctx *ip4 = &c->ip4;
+
 	if (!ifi)
 		ifi = nl_get_ext_if(nl_sock, AF_INET);
 
@@ -398,60 +401,58 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
 		}
 	}
 
-	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr)) {
+	if (!a) {
+		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 = ip4->addr;
-
+	a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
+	ip4->addr_seen = *inany_v4(&a->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_GENERATED | 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)
 {
+	struct ip6_ctx *ip6 = &c->ip6;
+	const struct guest_addr *a;
+	union inany_addr addr;
 	int prefix_len = 0;
 	int rc;
 
@@ -472,21 +473,28 @@ 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;
+	a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 0);
+	if (!a) {
+		if (IN6_IS_ADDR_UNSPECIFIED(&addr))
+			return 0;
+
+		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;
@@ -494,13 +502,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;
 }
 
 /**
@@ -784,6 +792,7 @@ enum passt_modes conf_mode(int argc, char *argv[])
 static void conf_print(const struct ctx *c)
 {
 	char buf[INANY_ADDRSTRLEN];
+	const struct guest_addr *a;
 	int i;
 
 	if (c->fd_control_listen >= 0)
@@ -831,16 +840,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, CONF_ADDR_ANY, 0);
+		if (a && !c->no_dhcp) {
 			uint32_t mask;
+			int plen = 0;
 
-			mask = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+			inany_prefix_v4(&a->addr, a->prefix_len, &plen);
+			mask = IN4_MASK(plen);
 
 			info("DHCP:");
 			info("    assign: %s",
-			     inet_ntop(AF_INET, &c->ip4.addr, buf, sizeof(buf)));
+			     inany_ntop(&a->addr, buf, sizeof(buf)));
 			info("    mask: %s",
-			     inet_ntop(AF_INET, &mask,        buf, sizeof(buf)));
+			     inet_ntop(AF_INET, &mask, buf, sizeof(buf)));
 			info("    router: %s",
 			     inet_ntop(AF_INET, &c->ip4.guest_gw,
 				       buf, sizeof(buf)));
@@ -851,8 +863,8 @@ static void conf_print(const struct ctx *c)
 				break;
 			if (!i)
 				info("DNS:");
-			inet_ntop(AF_INET, &c->ip4.dns[i], buf, sizeof(buf));
-			info("    %s", buf);
+			info("    %s", inet_ntop(AF_INET, &c->ip4.dns[i],
+						 buf, sizeof(buf)));
 		}
 
 		for (i = 0; *c->dns_search[i].n; i++) {
@@ -877,13 +889,15 @@ static void conf_print(const struct ctx *c)
 		else
 			goto dns6;
 
-		info("    assign: %s",
-		     inet_ntop(AF_INET6, &c->ip6.addr, buf, sizeof(buf)));
+		a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY,
+				 CONF_ADDR_LINKLOCAL);
+		if (a)
+			info("    assign: %s",
+			     inany_ntop(&a->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)));
+		     inet_ntop(AF_INET6, &c->ip6.our_tap_ll, buf, sizeof(buf)));
 
 dns6:
 		for (i = 0; i < ARRAY_SIZE(c->ip6.dns); i++) {
@@ -891,8 +905,10 @@ dns6:
 			    break;
 			if (!i)
 				info("DNS:");
-			inet_ntop(AF_INET6, &c->ip6.dns[i], buf, sizeof(buf));
-			info("    %s", buf);
+			info("    %s",
+			     inet_ntop(AF_INET6, &c->ip6.dns[i],
+				       buf, sizeof(buf)));
+
 		}
 
 		for (i = 0; *c->dns_search[i].n; i++) {
@@ -1580,21 +1596,19 @@ 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.addr = *inany_v4(&addr);
-				c->ip4.prefix_len = prefix_len - 96;
+				c->ip4.no_copy_addrs = true;
 				c->ip4.addr_fixed = true;
-				if (c->mode == MODE_PASTA)
-					c->ip4.no_copy_addrs = true;
 			} else {
-				c->ip6.addr = addr.a6;
+				c->ip6.no_copy_addrs = true;
 				c->ip6.addr_fixed = true;
-				if (c->mode == MODE_PASTA)
-					c->ip6.no_copy_addrs = true;
 			}
 			break;
 		}
 		case 'n': {
+			struct guest_addr *a;
 			int plen;
 
 			if (addr_has_prefix_len)
@@ -1604,8 +1618,12 @@ void conf(struct ctx *c, int argc, char **argv)
 			if (plen < 0)
 				die("Invalid prefix length: %s", optarg);
 
-			prefix_len_from_opt = plen + 96;
-			c->ip4.prefix_len = plen;
+			prefix_len_from_opt = plen;
+
+			for_each_addr(a, c->addrs, c->addr_count, AF_INET) {
+				a->prefix_len = plen + 96;
+				break;
+			}
 			break;
 		}
 		case 'M':
@@ -1801,9 +1819,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)",
@@ -1826,7 +1844,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;
 	}
@@ -1834,7 +1852,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;
 	}
@@ -1905,7 +1923,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, CONF_ADDR_ANY, 0)) {
 		c->no_dhcpv6 = 1;
 	}
 
diff --git a/dhcp.c b/dhcp.c
index 1ff8cba9..727485f7 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -303,6 +303,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)
 {
 	char macstr[ETH_ADDRSTRLEN];
+	const struct guest_addr *a;
 	size_t mlen, dlen, opt_len;
 	struct in_addr mask, dst;
 	struct ethhdr eh_storage;
@@ -313,8 +314,10 @@ 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;
+	int plen;
 
 	eh = IOV_REMOVE_HEADER(data, eh_storage);
 	iph = IOV_PEEK_HEADER(data, iph_storage);
@@ -344,6 +347,14 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	    m->op != BOOTREQUEST)
 		return -1;
 
+	a = fwd_get_addr(c, AF_INET, CONF_ADDR_USER, 0);
+	if (!a)
+		a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
+	if (!a)
+		return -1;
+
+	addr = *inany_prefix_v4(&a->addr, a->prefix_len, &plen);
+
 	reply.op		= BOOTREPLY;
 	reply.htype		= m->htype;
 	reply.hlen		= m->hlen;
@@ -352,7 +363,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 +415,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(plen);
 	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 +423,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 +482,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 29c7e320..f64cbc24 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;
@@ -567,6 +567,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;
@@ -574,6 +575,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;
@@ -627,7 +630,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, saddr, data,
 						 &client_id_base,
@@ -682,7 +685,8 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	resp.hdr.xid = mh->xid;
 
 	tap_udp6_send(c, src, 547, saddr, 546, mh->xid, &resp, n);
-	c->ip6.addr_seen = c->ip6.addr;
+	if (a)
+		c->ip6.addr_seen = a->addr.a6;
 
 	return 1;
 }
@@ -694,6 +698,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 void dhcpv6_init(const struct ctx *c)
 {
 	time_t y2k = 946684800; /* Epoch to 2000-01-01T00:00:00Z, no mktime() */
+	const struct guest_addr *a;
 	uint32_t duid_time;
 
 	duid_time = htonl(difftime(time(NULL), y2k));
@@ -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 042158cf..f2ca34c2 100644
--- a/fwd.c
+++ b/fwd.c
@@ -243,6 +243,61 @@ 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 format (96-128 for IPv4-mapped)
+ *
+ * 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_each_addr(a, c->addrs, c->addr_count, inany_af(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 AF_UNSPEC for any)
+ * @incl:	Required flags (any-match), CONF_ADDR_ANY for any
+ * @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->addrs, c->addr_count, af) {
+		if (!(a->flags & incl))
+			continue;
+		if (a->flags & excl)
+			continue;
+		return a;
+	}
+
+	return NULL;
+}
+
 /* Forwarding table storage, generally accessed via pointers in struct ctx */
 static struct fwd_table fwd_in;
 static struct fwd_table fwd_out;
@@ -794,8 +849,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;
+
 	if (IN4_IS_ADDR_LOOPBACK(addr))
 		return false;
 
@@ -810,7 +867,8 @@ 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) ||
+	a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
+	if ((a && IN4_ARE_ADDR_EQUAL(addr, inany_v4(&a->addr))) ||
 	    IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
 		return false;
 
@@ -828,10 +886,13 @@ 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;
+
 	if (IN6_IS_ADDR_LOOPBACK(addr))
 		return false;
 
-	if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addr))
+	a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 0);
+	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
@@ -876,16 +937,21 @@ 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)
 {
-	if (inany_equals4(addr, &c->ip4.map_host_loopback))
+	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))
+	} 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
+	} else if (inany_equals4(addr, &c->ip4.map_guest_addr)) {
+		ga = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
+		*translated = ga ? ga->addr : inany_any4;
+	} else if (inany_equals6(addr, &c->ip6.map_guest_addr)) {
+		ga = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 0);
+		translated->a6 = ga ? ga->addr.a6 : in6addr_any;
+	} else {
 		*translated = *addr;
+	}
 }
 
 /**
@@ -990,16 +1056,23 @@ 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, *ga6;
+
+		ga4 = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
+		ga6 = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 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 b60697d9..d677440f 100644
--- a/fwd.h
+++ b/fwd.h
@@ -79,5 +79,9 @@ 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);
+const struct guest_addr *fwd_get_addr(const struct ctx *c, sa_family_t af,
+				      uint8_t incl, uint8_t excl);
 
 #endif /* FWD_H */
diff --git a/inany.h b/inany.h
index 6bf3ecaa..95756eb8 100644
--- a/inany.h
+++ b/inany.h
@@ -102,6 +102,34 @@ static inline struct in_addr *inany_v4(const union inany_addr *addr)
 	return (struct in_addr *)&addr->v4mapped.a4;
 }
 
+/** inany_prefix_v4() - Extract IPv4 address and prefix length from inany
+ * @addr:	IPv4 (as mapped) or IPv6 address
+ * @prefix_len:	Prefix length in IPv6 format (96-128 for IPv4)
+ * @v4_plen:	Set to IPv4 prefix length (0-32) on success
+ *
+ * Return: IPv4 address if @addr is IPv4, NULL otherwise
+ */
+static inline struct in_addr *inany_prefix_v4(const union inany_addr *addr,
+					      int prefix_len, int *v4_plen)
+{
+	struct in_addr *v4 = inany_v4(addr);
+
+	if (!v4)
+		return NULL;
+	*v4_plen = prefix_len - 96;
+	return v4;
+}
+
+/** 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/ip.h b/ip.h
index aab9b86a..ac93c543 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 1f2bcb0c..a6a79055 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,15 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
 			},
 		},
 	};
+	const struct guest_addr *a;
 	unsigned char *ptr = NULL;
 
+	a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 0);
+	if (!a)
+		return;
+
+	ra.prefix = a->addr.a6;
+
 	ptr = &ra.var[0];
 
 	if (c->mtu) {
@@ -460,6 +466,7 @@ first:
  */
 void ndp_send_init_req(const struct ctx *c)
 {
+	const struct guest_addr *a;
 	struct ndp_ns ns = {
 		.ih = {
 			.icmp6_type		= NS,
@@ -468,8 +475,14 @@ 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
 	};
+
+	a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 0);
+	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 16506dcd..cafa35ea 100644
--- a/passt.h
+++ b/passt.h
@@ -64,11 +64,29 @@ enum passt_modes {
 	MODE_VU,
 };
 
+/* Maximum number of addresses in context address array */
+#define MAX_GUEST_ADDRS		32
+
+/**
+ * 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;
+#define CONF_ADDR_USER		BIT(0)		/* User set via -a */
+#define CONF_ADDR_HOST		BIT(1)		/* From host interface */
+#define CONF_ADDR_GENERATED	BIT(2)		/* Generated by PASST/PASTA */
+#define CONF_ADDR_LINKLOCAL	BIT(3)		/* Link-local address */
+#define CONF_ADDR_ANY		0xff		/* Match any flag */
+};
+
 /**
  * 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
@@ -87,9 +105,7 @@ enum passt_modes {
  */
 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;
@@ -110,7 +126,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
@@ -131,7 +146,6 @@ struct ip4_ctx {
  */
 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;
@@ -190,6 +204,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
@@ -273,6 +289,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;
@@ -317,6 +336,41 @@ struct ctx {
 
 extern struct ctx passt_ctx;
 
+/**
+ * next_addr_idx_() - Find next address index matching family filter
+ * @addrs:	Array of guest addresses
+ * @count:	Number of addresses in array
+ * @i:		Starting index
+ * @af:		Address family filter: AF_INET, AF_INET6, or AF_UNSPEC for all
+ *
+ * Return: next matching index, or count if none found
+ */
+static inline int next_addr_idx_(const struct guest_addr *addrs, int count,
+				 int i, sa_family_t af)
+{
+	for (; i < count; i++) {
+		sa_family_t entry_af;
+
+		entry_af = inany_v4(&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 array
+ * @a:		Pointer variable for current entry (struct guest_addr *)
+ * @addrs:	Array of guest addresses (e.g., c->addrs)
+ * @count:	Number of addresses (e.g., c->addr_count)
+ * @af:		Address family filter: AF_INET, AF_INET6, or 0 for all
+ */
+#define for_each_addr(a, addrs, count, af)				\
+	for (int i_ = next_addr_idx_((addrs), (count), 0, (af));	\
+	     i_ < (count) && ((a) = &(addrs)[i_], true);		\
+	     i_ = next_addr_idx_((addrs), (count), 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 4e7ee542..9ef3ac00 100644
--- a/pasta.c
+++ b/pasta.c
@@ -330,6 +330,8 @@ void pasta_ns_conf(struct ctx *c)
 
 	if (c->pasta_conf_ns) {
 		unsigned int flags = IFF_UP;
+		const struct guest_addr *a;
+		int plen = 0;
 
 		if (c->mtu)
 			nl_link_set_mtu(nl_sock_ns, c->pasta_ifi, c->mtu);
@@ -341,10 +343,17 @@ 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, CONF_ADDR_ANY, 0);
+				if (a) {
+					const struct in_addr *v4;
+
+					v4 = inany_prefix_v4(&a->addr,
+							     a->prefix_len,
+							     &plen);
+					rc = nl_addr_set(nl_sock_ns,
+							 c->pasta_ifi, AF_INET,
+							 v4, plen);
+				}
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi4,
 						 nl_sock_ns, c->pasta_ifi,
@@ -397,11 +406,12 @@ 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, CONF_ADDR_ANY, 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 d4189617..12630f58 100644
--- a/tap.c
+++ b/tap.c
@@ -1005,8 +1005,12 @@ resume:
 				c->ip6.addr_seen = *saddr;
 			}
 
-			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
-				c->ip6.addr = *saddr;
+			if (!c->ip6.addr_fixed &&
+			    !fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 0)) {
+				union inany_addr addr = { .a6 = *saddr };
+
+				fwd_set_addr(c, &addr, CONF_ADDR_LINKLOCAL, 64);
+			}
 		} else if (!c->ip6.addr_fixed &&
 			   !IN6_IS_ADDR_UNSPECIFIED(saddr)) {
 			c->ip6.addr_seen = *saddr;
-- 
2.52.0


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

* [PATCH v8 03/14] tap, conf: Replace addr_fixed with CONF_ADDR_USER flag check
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 01/14] dhcpv6: Fix reply destination to match client's source address Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 02/14] passt, pasta: Introduce unified multi-address data structures Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 04/14] fwd: Unify guest accessibility checks with unified address array Jon Maloy
                   ` (10 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, david, jmaloy, passt-dev

The addr_fixed boolean (per address family) was introduced to prevent
observed traffic from overwriting addr_seen when the user explicitly
set an address with -a/--address.

The unified address array already tracks this: fwd_set_addr() marks
user-provided addresses with CONF_ADDR_USER. We now replace all
addr_fixed checks with fwd_get_addr() lookups for CONF_ADDR_USER
and remove the now-redundant field from ip4_ctx and ip6_ctx.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
 conf.c  | 7 ++-----
 passt.h | 6 ------
 tap.c   | 9 ++++-----
 3 files changed, 6 insertions(+), 16 deletions(-)

diff --git a/conf.c b/conf.c
index ac8facc8..1c0c1786 100644
--- a/conf.c
+++ b/conf.c
@@ -1598,13 +1598,10 @@ void conf(struct ctx *c, int argc, char **argv)
 
 			/* Legacy behaviour: replace existing address if any */
 			fwd_set_addr(c, &addr, CONF_ADDR_USER, prefix_len);
-			if (inany_v4(&addr)) {
+			if (inany_v4(&addr))
 				c->ip4.no_copy_addrs = true;
-				c->ip4.addr_fixed = true;
-			} else {
+			else
 				c->ip6.no_copy_addrs = true;
-				c->ip6.addr_fixed = true;
-			}
 			break;
 		}
 		case 'n': {
diff --git a/passt.h b/passt.h
index cafa35ea..11ccff05 100644
--- a/passt.h
+++ b/passt.h
@@ -100,8 +100,6 @@ struct guest_addr {
  * @ifname_out:		Optional interface name to bind outbound sockets to
  * @no_copy_routes:	Don't copy all routes when configuring target namespace
  * @no_copy_addrs:	Don't copy all addresses when configuring namespace
- * @addr_fixed:		Address was given explicitly (-a): don't update
- *			addr_seen from traffic observed on tap
  */
 struct ip4_ctx {
 	/* PIF_TAP addresses */
@@ -121,7 +119,6 @@ struct ip4_ctx {
 
 	bool no_copy_routes;
 	bool no_copy_addrs;
-	bool addr_fixed;
 };
 
 /**
@@ -141,8 +138,6 @@ struct ip4_ctx {
  * @ifname_out:		Optional interface name to bind outbound sockets to
  * @no_copy_routes:	Don't copy all routes when configuring target namespace
  * @no_copy_addrs:	Don't copy all addresses when configuring namespace
- * @addr_fixed:		Address was given explicitly (-a): don't update
- *			addr_seen from traffic observed on tap
  */
 struct ip6_ctx {
 	/* PIF_TAP addresses */
@@ -163,7 +158,6 @@ struct ip6_ctx {
 
 	bool no_copy_routes;
 	bool no_copy_addrs;
-	bool addr_fixed;
 };
 
 #include <netinet/if_ether.h>
diff --git a/tap.c b/tap.c
index 12630f58..573a3f10 100644
--- a/tap.c
+++ b/tap.c
@@ -756,7 +756,7 @@ resume:
 			continue;
 		}
 
-		if (!c->ip4.addr_fixed &&
+		if (!fwd_get_addr(c, AF_INET, CONF_ADDR_USER, 0) &&
 		    iph->saddr && c->ip4.addr_seen.s_addr != iph->saddr)
 			c->ip4.addr_seen.s_addr = iph->saddr;
 
@@ -1000,18 +1000,17 @@ resume:
 		if (IN6_IS_ADDR_LINKLOCAL(saddr)) {
 			c->ip6.addr_ll_seen = *saddr;
 
-			if (!c->ip6.addr_fixed &&
+			if (!fwd_get_addr(c, AF_INET6, CONF_ADDR_USER, 0) &&
 			    IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen)) {
 				c->ip6.addr_seen = *saddr;
 			}
 
-			if (!c->ip6.addr_fixed &&
-			    !fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 0)) {
+			if (!fwd_get_addr(c, AF_INET6, CONF_ADDR_USER, 0)) {
 				union inany_addr addr = { .a6 = *saddr };
 
 				fwd_set_addr(c, &addr, CONF_ADDR_LINKLOCAL, 64);
 			}
-		} else if (!c->ip6.addr_fixed &&
+		} else if (!fwd_get_addr(c, AF_INET6, CONF_ADDR_USER, 0) &&
 			   !IN6_IS_ADDR_UNSPECIFIED(saddr)) {
 			c->ip6.addr_seen = *saddr;
 		}
-- 
2.52.0


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

* [PATCH v8 04/14] fwd: Unify guest accessibility checks with unified address array
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (2 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 03/14] tap, conf: Replace addr_fixed with CONF_ADDR_USER flag check Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 05/14] arp: Check all configured addresses in ARP filtering Jon Maloy
                   ` (9 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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>
Reviewed-by: David Gibson <david@gibson.dropbear.id.au>

---
v6: -Some fixes based on feedback from David Gibson
v7: -Added curly brackets to for_each_loop() in fwd_guest_accessible(),
     as suggested by Stefano Brivio.
---
 fwd.c | 69 +++++++++++++----------------------------------------------
 1 file changed, 15 insertions(+), 54 deletions(-)

diff --git a/fwd.c b/fwd.c
index f2ca34c2..9a88fa19 100644
--- a/fwd.c
+++ b/fwd.c
@@ -841,19 +841,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;
 
-	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,
@@ -861,38 +861,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
+	/* Check against all configured guest addresses */
+	for_each_addr(a, c->addrs, c->addr_count, 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.
 	 */
-	a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
-	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;
-
-	if (IN6_IS_ADDR_LOOPBACK(addr))
-		return false;
-
-	a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 0);
-	if (a && IN6_ARE_ADDR_EQUAL(addr, &a->addr.a6))
+	if (inany_equals4(addr, &c->ip4.addr_seen))
 		return false;
 
 	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
@@ -900,31 +880,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] 15+ messages in thread

* [PATCH v8 05/14] arp: Check all configured addresses in ARP filtering
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (3 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 04/14] fwd: Unify guest accessibility checks with unified address array Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 06/14] conf: Allow multiple -a/--address options per address family Jon Maloy
                   ` (8 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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.
v7: -Curly brackets in ignore_arp(), as suggested by Stefano.
    -I did not modify the for_each_addr(), macro, as Stefano suggested,
     since it also would require us to add an extra parameter indicating
     the array length.
---
 arp.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/arp.c b/arp.c
index 0392861d..364c105a 100644
--- a/arp.c
+++ b/arp.c
@@ -42,6 +42,7 @@ static bool ignore_arp(const struct ctx *c,
 		       const struct arphdr *ah, const struct arpmsg *am)
 {
 	const struct guest_addr *a;
+	union inany_addr addr;
 
 	if (ah->ar_hrd != htons(ARPHRD_ETHER)	||
 	    ah->ar_pro != htons(ETH_P_IP)	||
@@ -55,11 +56,12 @@ 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. */
-	a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
-	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->addrs, c->addr_count, AF_INET) {
+		if (inany_equals(&addr, &a->addr))
+			return true;
+	}
 	return false;
 }
 
-- 
2.52.0


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

* [PATCH v8 06/14] conf: Allow multiple -a/--address options per address family
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (4 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 05/14] arp: Check all configured addresses in ARP filtering Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 07/14] netlink, conf: Read all addresses from template interface at startup Jon Maloy
                   ` (7 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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.

Link: https://bugs.passt.top/show_bug.cgi?id=47
Cc: lemmi@nerd2nerd.org
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
v7: - Updated man page.
v8: - Adapted to previous changes in this series
    - Updated according to comments from David G.
---
 conf.c  |  7 ++++---
 passt.1 | 18 ++++++++----------
 pasta.c | 13 +++++++++----
 3 files changed, 21 insertions(+), 17 deletions(-)

diff --git a/conf.c b/conf.c
index 1c0c1786..8855a42e 100644
--- a/conf.c
+++ b/conf.c
@@ -595,9 +595,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"
@@ -1596,7 +1598,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/passt.1 b/passt.1
index 908fd4a4..254bd80e 100644
--- a/passt.1
+++ b/passt.1
@@ -169,16 +169,14 @@ An optional /\fIprefix_len\fR (0-32 for IPv4, 0-128 for IPv6) can be
 appended in CIDR notation (e.g. 192.0.2.1/24). This is an alternative to
 using the \fB-n\fR, \fB--netmask\fR option. Mixing CIDR notation with
 \fB-n\fR results in an error.
-If a prefix length is assigned to an IPv6 address using this method, it will
-in the current code version be overridden by the default value of 64.
-This option can be specified zero (for defaults) to two times (once for IPv4,
-once for IPv6).
-By default, assigned IPv4 and IPv6 addresses are taken from the host interfaces
-with the first default route, if any, for the corresponding IP version. If no
-default routes are available and there is any interface with any route for a
-given IP version, the first of these interfaces will be chosen instead. If no
-such interface exists for a given IP version, the link-local address 169.254.2.1
-is assigned for IPv4, and no additional address will be assigned for IPv6.
+This option can be given multiple times, indicating multiple different
+addresses. By default, assigned IPv4 and IPv6 addresses are taken from
+the host interfaces with the first default route, if any, for the
+corresponding IP version. If no default routes are available and there
+is any interface with any route for a given IP version, the first of
+these interfaces will be chosen instead. If no such interface exists for
+a given IP version, the link-local address 169.254.2.1 is assigned for
+IPv4, and no additional address will be assigned for IPv6.
 
 .TP
 .BR \-n ", " \-\-netmask " " \fImask
diff --git a/pasta.c b/pasta.c
index 9ef3ac00..aea52001 100644
--- a/pasta.c
+++ b/pasta.c
@@ -343,8 +343,9 @@ void pasta_ns_conf(struct ctx *c)
 
 		if (c->ifi4) {
 			if (c->ip4.no_copy_addrs) {
-				a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
-				if (a) {
+				int ac = c->addr_count;
+
+				for_each_addr(a, c->addrs, ac, AF_INET) {
 					const struct in_addr *v4;
 
 					v4 = inany_prefix_v4(&a->addr,
@@ -353,6 +354,8 @@ void pasta_ns_conf(struct ctx *c)
 					rc = nl_addr_set(nl_sock_ns,
 							 c->pasta_ifi, AF_INET,
 							 v4, plen);
+					if (rc < 0)
+						break;
 				}
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi4,
@@ -406,12 +409,14 @@ ipv4_done:
 					  0, IFF_NOARP);
 
 			if (c->ip6.no_copy_addrs) {
-				a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 0);
-				if (a)
+				for_each_addr(a, c->addrs, c->addr_count, 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,
-- 
2.52.0


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

* [PATCH v8 07/14] netlink, conf: Read all addresses from template interface at startup
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (5 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 06/14] conf: Allow multiple -a/--address options per address family Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 08/14] netlink, pasta: refactor function pasta_ns_conf() Jon Maloy
                   ` (6 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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>
Reviewed-by: David Gibson <david@gibson.dropbear.id.au>

---
v7: -Excluded all link local addresses from host to be added to array
    -Minor comment change suggested by Stefano

v8: -Adapted to previous changes in series
---
 conf.c    | 48 ++++++++++++++++++++++--------------------------
 netlink.c | 53 +++++++++++++++++++++++++++++------------------------
 netlink.h |  3 +--
 3 files changed, 52 insertions(+), 52 deletions(-)

diff --git a/conf.c b/conf.c
index 8855a42e..353f871c 100644
--- a/conf.c
+++ b/conf.c
@@ -380,8 +380,8 @@ static int conf_ip4_prefix(const char *arg)
  */
 static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
 {
-	const struct guest_addr *a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
 	struct ip4_ctx *ip4 = &c->ip4;
+	const struct guest_addr *a;
 
 	if (!ifi)
 		ifi = nl_get_ext_if(nl_sock, AF_INET);
@@ -401,24 +401,20 @@ static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
 		}
 	}
 
+	a = fwd_get_addr(c, AF_INET, CONF_ADDR_USER, 0);
 	if (!a) {
-		struct in_addr addr;
-		int prefix_len;
-		int rc = nl_addr_get(nl_sock, ifi, AF_INET,
-				     &addr, &prefix_len, NULL);
+		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))
+		a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
+		if (!rc || !a)
 			return 0;
-
-		fwd_set_addr(c, &inany_from_v4(addr), CONF_ADDR_HOST,
-			     prefix_len + 96);
 	}
 
-	a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
 	ip4->addr_seen = *inany_v4(&a->addr);
 	ip4->our_tap_addr = ip4->guest_gw;
 
@@ -452,8 +448,6 @@ static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
 {
 	struct ip6_ctx *ip6 = &c->ip6;
 	const struct guest_addr *a;
-	union inany_addr addr;
-	int prefix_len = 0;
 	int rc;
 
 	if (!ifi)
@@ -473,24 +467,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_ANY, 0);
+	a = fwd_get_addr(c, AF_INET6, CONF_ADDR_USER, 0);
 	if (!a) {
-		if (IN6_IS_ADDR_UNSPECIFIED(&addr))
+		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;
-
-		fwd_set_addr(c, &addr, CONF_ADDR_HOST, prefix_len);
-		ip6->addr_seen = addr.a6;
+		}
+		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 8d20dbb5..9aa64e5a 100644
--- a/netlink.c
+++ b/netlink.c
@@ -750,20 +750,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 IPv6 link-local addresses for the main array.
+ *
+ * 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;
@@ -774,6 +774,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));
@@ -787,27 +788,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;
 
-			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;
+			/* Skip link-local addresses (kernel auto-configures) */
+			if (ifa->ifa_scope == RT_SCOPE_LINK) {
+				if (af == AF_INET)
+					continue;
+				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));
+			fwd_set_addr(c, &addr, CONF_ADDR_HOST,
+				     ifa->ifa_prefixlen);
 
-				prefix_max_ll = ifa->ifa_prefixlen;
-			}
+			count++;
 		}
 	}
-	return status;
+
+	return status < 0 ? status : count;
 }
 
 /**
diff --git a/netlink.h b/netlink.h
index b22f485a..3af6d58b 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] 15+ messages in thread

* [PATCH v8 08/14] netlink, pasta: refactor function pasta_ns_conf()
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (6 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 07/14] netlink, conf: Read all addresses from template interface at startup Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 09/14] conf, pasta: Track observed guest IPv4 addresses in unified address array Jon Maloy
                   ` (5 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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>
Reviewed-by: David Gibson <david@gibson.dropbear.id.au>

---
v7: -Removed redundant argument 'af' in nl_addr_set()
    -Removed redundant label and 'goto's in pasta_ns_conf()
    -Since I excluded addition of all LINKLOCAL addresses from host
     address array in a previous commit I can now omit this test
     in pasta_conf_addrs()  as suggested by David.
    -Here is the example of agnostic usage of inany_prefix_len()
     I referred to in a previous commit.

v8: -Do prefix_len conversion inside nl_addr_set() instead of in
     the calling function.
    -In nl_addr_set(), branch on inany_prefix_v4() return value instead
     of inany_af() to avoid redundant AF check and potential static
     checker false positives. (David)
    -Swap branch order in pasta_conf_routes() for consistency with
     pasta_conf_addrs(). (David)
    -Defer investigation of pre-existing IFF_NOARP / DAD interaction
     oddity noted by David.
---
 netlink.c |  46 ++++++-----
 netlink.h |   4 +-
 pasta.c   | 240 ++++++++++++++++++++++++++----------------------------
 3 files changed, 141 insertions(+), 149 deletions(-)

diff --git a/netlink.c b/netlink.c
index 9aa64e5a..201dd255 100644
--- a/netlink.c
+++ b/netlink.c
@@ -866,15 +866,16 @@ int nl_addr_get_ll(int s, unsigned int ifi, struct in6_addr *addr)
  * nl_addr_set() - Set IP addresses for given interface and address family
  * @s:		Netlink socket
  * @ifi:	Interface index
- * @af:		Address family
  * @addr:	Global address to set
- * @prefix_len:	Mask or prefix length to set
+ * @prefix_len:	Prefix length (IPv6 format, 96-128 for IPv4-mapped)
  *
  * 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)
+int nl_addr_set(int s, unsigned int ifi, const union inany_addr *addr,
+		int prefix_len)
 {
+	int plen;
+	const struct in_addr *v4 = inany_prefix_v4(addr, prefix_len, &plen);
 	struct req_t {
 		struct nlmsghdr nlh;
 		struct ifaddrmsg ifa;
@@ -893,38 +894,41 @@ int nl_addr_set(int s, unsigned int ifi, sa_family_t af,
 			} a6;
 		} set;
 	} req = {
-		.ifa.ifa_family    = af,
 		.ifa.ifa_index     = ifi,
-		.ifa.ifa_prefixlen = prefix_len,
+		.ifa.ifa_prefixlen = v4 ? plen : prefix_len,
 		.ifa.ifa_scope	   = RT_SCOPE_UNIVERSE,
 	};
 	ssize_t len;
 
-	if (af == AF_INET6) {
+	if (v4) {
+		size_t rta_len = RTA_LENGTH(sizeof(req.set.a4.l));
+
+		req.ifa.ifa_family = AF_INET;
+
+		len = offsetof(struct req_t, set.a4) + sizeof(req.set.a4);
+
+		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, v4, sizeof(req.set.a4.a));
+		req.set.a4.rta_a.rta_len = rta_len;
+		req.set.a4.rta_a.rta_type = IFA_ADDRESS;
+	} else {
 		size_t rta_len = RTA_LENGTH(sizeof(req.set.a6.l));
 
+		req.ifa.ifa_family = AF_INET6;
+
 		/* By default, strictly speaking, it's duplicated */
 		req.ifa.ifa_flags = IFA_F_NODAD;
 
 		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 {
-		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));
-		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));
-		req.set.a4.rta_a.rta_len = rta_len;
-		req.set.a4.rta_a.rta_type = IFA_ADDRESS;
 	}
 
 	return nl_do(s, &req, RTM_NEWADDR, NLM_F_CREATE | NLM_F_EXCL, len);
diff --git a/netlink.h b/netlink.h
index 3af6d58b..ec859b74 100644
--- a/netlink.h
+++ b/netlink.h
@@ -22,8 +22,8 @@ int nl_route_dup(int s_src, unsigned int ifi_src,
 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);
+int nl_addr_set(int s, unsigned int ifi, 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 aea52001..56df2fdc 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,69 @@ 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->addrs, c->addr_count, af) {
+		int rc;
+
+		rc = nl_addr_set(nl_sock_ns, c->pasta_ifi, &a->addr,
+				 a->prefix_len);
+		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_dup(nl_sock, ifi, nl_sock_ns, c->pasta_ifi, af);
+
+	return nl_route_set_def(nl_sock_ns, c->pasta_ifi, af, gw);
+}
+
 /**
  * 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,131 +385,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;
-		int plen = 0;
-
-		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) {
-				int ac = c->addr_count;
-
-				for_each_addr(a, c->addrs, ac, AF_INET) {
-					const struct in_addr *v4;
-
-					v4 = inany_prefix_v4(&a->addr,
-							     a->prefix_len,
-							     &plen);
-					rc = nl_addr_set(nl_sock_ns,
-							 c->pasta_ifi, AF_INET,
-							 v4, plen);
-					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->addrs, c->addr_count, 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));
-			}
-		}
+	proto_update_l2_buf(c->guest_mac);
+
+	if (!c->pasta_conf_ns)
+		return;
+
+	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));
 	}
-ipv6_done:
 
-	proto_update_l2_buf(c->guest_mac);
+	if (!c->ifi6)
+		return;
+
+	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));
+	}
 }
 
 /**
@@ -536,7 +524,7 @@ void pasta_netns_quit_init(const struct ctx *c)
  * @c:		Execution context
  * @inotify_fd:	inotify file descriptor with watch on namespace directory
  */
-void pasta_netns_quit_inotify_handler(const struct ctx *c, int inotify_fd)
+void pasta_netns_quit_inotify_handler(struct ctx *c, int inotify_fd)
 {
 	char buf[sizeof(struct inotify_event) + NAME_MAX + 1]
 		__attribute__ ((aligned(__alignof__(struct inotify_event))));
@@ -562,7 +550,7 @@ void pasta_netns_quit_inotify_handler(const struct ctx *c, int inotify_fd)
  * @c:		Execution context
  * @ref:	epoll reference for timer descriptor
  */
-void pasta_netns_quit_timer_handler(const struct ctx *c, union epoll_ref ref)
+void pasta_netns_quit_timer_handler(struct ctx *c, union epoll_ref ref)
 {
 	uint64_t expirations;
 	ssize_t n;
-- 
2.52.0


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

* [PATCH v8 09/14] conf, pasta: Track observed guest IPv4 addresses in unified address array
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (7 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 08/14] netlink, pasta: refactor function pasta_ns_conf() Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 10/14] conf, pasta: Track observed guest IPv6 " Jon Maloy
                   ` (4 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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.
v7: - Changed fwd_set_addr() to only accept keeping one observed-only
      address per protocol, as suggested by David.
    - Eliminated redundant tap_check_src_addr4() call level.
    - I keep fwd_select_addr() for the same pragmatic reason it was
      introduced: to avoid ugly, deeply indented code that tends
      to wrap across several lines.
v8: - Refactoring of and fix to fwd_set_addr, after feedback from David.
    - Removed test for 'primary' in fwd_select_addr. (David)
    - Kept the use of fwd_select_addr() in fwd_nat_from_host(), because
      we aren't guaranteed there is (yet) an OBSERVED address at all
      in the array.
    - Removed exclusion of CONF_ADDR_LINKLOCAL in seen_addrs_source_v2().
    - In the current code tap.c::tap4/6_handler() does:
      if (iph->saddr && c->ip4.addr_seen.s_addr != iph->saddr)
          c->ip4.addr_seen.s_addr = iph->saddr;
      In the new code fwd_set_addr() will almost always return after
      a hit in slot 0 or 1, anything else will be extremely rare.
      Although there will be a few more instructions to execute,
      I don't share David's concern about performance here.
---
 conf.c    |   2 -
 fwd.c     | 123 +++++++++++++++++++++++++++++++++++++++++++++++-------
 fwd.h     |   3 ++
 migrate.c |  16 ++++++-
 passt.h   |   2 +-
 tap.c     |   9 ++--
 6 files changed, 131 insertions(+), 24 deletions(-)

diff --git a/conf.c b/conf.c
index 353f871c..32734188 100644
--- a/conf.c
+++ b/conf.c
@@ -415,7 +415,6 @@ static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
 			return 0;
 	}
 
-	ip4->addr_seen = *inany_v4(&a->addr);
 	ip4->our_tap_addr = ip4->guest_gw;
 
 	return ifi;
@@ -429,7 +428,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 9a88fa19..10ed6034 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"
@@ -256,21 +257,72 @@ 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 *tail = &c->addrs[c->addr_count - 1];
+	struct guest_addr *head = &c->addrs[0];
+	struct guest_addr *a, *rm = NULL;
+	int af_cnt = 0;
 
-	for_each_addr(a, c->addrs, c->addr_count, inany_af(addr)) {
-		goto found;
+	for_each_addr(a, c->addrs, c->addr_count, AF_UNSPEC) {
+		if (!inany_equals(&a->addr, addr))
+			continue;
+
+		/* Adjust and 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;
+
+		a->flags |= flags;
+		if (!(flags & CONF_ADDR_OBSERVED))
+			return;
+
+		/* Remove found observed address, re-add it later */
+		flags = a->flags;
+		prefix_len = a->prefix_len;
+		memmove(a, a + 1, (tail - a) * sizeof(*a));
+		c->addr_count--;
+		tail--;
+		break;
 	}
 
-	if (c->addr_count >= MAX_GUEST_ADDRS)
+	if (c->addr_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) {
+		memmove(&head[1], &head[0], c->addr_count * sizeof(*a));
+		a = &head[0];
+	} else {
+		a = &tail[1];
+	}
+	c->addr_count++;
+	tail++;
 	a->addr = *addr;
 	a->prefix_len = prefix_len;
 	a->flags = flags;
+
+	if (!(flags & CONF_ADDR_OBSERVED))
+		return;
+
+	/* Remove excess observed-only address if more than one */
+	for (int i = c->addr_count - 1; i >= 0; i--) {
+		a = &head[i];
+		if (inany_af(&a->addr) != inany_af(addr))
+			continue;
+		if (a->flags != CONF_ADDR_OBSERVED)
+			continue;
+		if (!rm)
+			rm = a;
+		af_cnt++;
+	}
+	if (af_cnt > 1) {
+		memmove(rm, rm + 1, (tail - rm) * sizeof(*rm));
+		c->addr_count--;
+	}
 }
 
 /**
@@ -840,6 +892,33 @@ 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
+ * @secondary:	Secondary flags to match
+ * @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,
+					 uint8_t primary, uint8_t secondary,
+					 uint8_t skip)
+{
+	const struct guest_addr *a;
+
+	a = fwd_get_addr(c, af, primary, skip);
+	if (a)
+		return a;
+
+	a = fwd_get_addr(c, af, secondary, skip);
+
+	return a;
+}
+
 /**
  * fwd_guest_accessible() - Is address guest-accessible
  * @c:		Execution context
@@ -869,11 +948,6 @@ static bool fwd_guest_accessible(const struct ctx *c,
 		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
@@ -1071,10 +1145,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)
@@ -1109,7 +1193,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 d677440f..6adf6b7a 100644
--- a/fwd.h
+++ b/fwd.h
@@ -83,5 +83,8 @@ void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
 		  uint8_t flags, int prefix_len);
 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,
+					 uint8_t primary, uint8_t secondary,
+					 uint8_t skip);
 
 #endif /* FWD_H */
diff --git a/migrate.c b/migrate.c
index 8937b85f..795f2818 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"
@@ -58,11 +60,17 @@ 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 any other non-LL address */
+	a = fwd_select_addr(c, AF_INET, CONF_ADDR_OBSERVED,
+			    CONF_ADDR_USER | CONF_ADDR_HOST, 0);
+	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)))
@@ -91,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 (!IN4_IS_ADDR_UNSPECIFIED(&addrs.addr4)) {
+		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 11ccff05..20ad7022 100644
--- a/passt.h
+++ b/passt.h
@@ -81,12 +81,12 @@ struct guest_addr {
 #define CONF_ADDR_HOST		BIT(1)		/* From host interface */
 #define CONF_ADDR_GENERATED	BIT(2)		/* Generated by PASST/PASTA */
 #define CONF_ADDR_LINKLOCAL	BIT(3)		/* Link-local address */
+#define CONF_ADDR_OBSERVED	BIT(4)		/* Seen in guest traffic */
 #define CONF_ADDR_ANY		0xff		/* Match any flag */
 };
 
 /**
  * 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
diff --git a/tap.c b/tap.c
index 573a3f10..93db2f86 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"
@@ -756,10 +757,12 @@ resume:
 			continue;
 		}
 
-		if (!fwd_get_addr(c, AF_INET, CONF_ADDR_USER, 0) &&
-		    iph->saddr && c->ip4.addr_seen.s_addr != iph->saddr)
-			c->ip4.addr_seen.s_addr = iph->saddr;
+		if (iph->saddr) {
+			const union inany_addr *addr;
 
+			addr = &inany_from_v4(*(struct in_addr *) &iph->saddr);
+			fwd_set_addr(c, addr, CONF_ADDR_OBSERVED, 0);
+		}
 		if (!iov_drop_header(&data, hlen))
 			continue;
 
-- 
2.52.0


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

* [PATCH v8 10/14] conf, pasta: Track observed guest IPv6 addresses in unified address array
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (8 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 09/14] conf, pasta: Track observed guest IPv4 addresses in unified address array Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 11/14] dhcp: Select address for DHCP distribution Jon Maloy
                   ` (3 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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.

The separate check against addr_seen in fwd_guest_accessible() can now
be removed because the observed address is now in the unified array,
and the existing for_each_addr() loop already checks against all
addresses, including this one.

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.

v7: - Added a commit at the beginning of the series addressing
      Stefanos's concern about DHCPv6 reply addresses.
    - Some other updates based on feedback from David and Stefano.

v8: - Adapted to previous changes in this series.
    - A couple of minor fixes in migrate.c (David)
    - Refactored parts of fwd_nat_from_host() to get v6 LINK_LOCAL
      selection criteria right. (David)
---
 conf.c    |  4 ----
 dhcpv6.c  |  4 +---
 fwd.c     | 58 +++++++++++++++++++++++++++++++++----------------------
 inany.h   |  3 +++
 migrate.c | 37 +++++++++++++++++++++++++++--------
 passt.h   |  4 ----
 pasta.c   | 11 +++++++----
 tap.c     | 19 +++++-------------
 8 files changed, 80 insertions(+), 60 deletions(-)

diff --git a/conf.c b/conf.c
index 32734188..69c1c439 100644
--- a/conf.c
+++ b/conf.c
@@ -473,7 +473,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) {
@@ -482,9 +481,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 f64cbc24..f5de90cd 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -567,8 +567,8 @@ 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;
+	const struct guest_addr *a;
 	struct msg_hdr mh_storage;
 	const struct msg_hdr *mh;
 	struct udphdr uh_storage;
@@ -685,8 +685,6 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	resp.hdr.xid = mh->xid;
 
 	tap_udp6_send(c, src, 547, saddr, 546, mh->xid, &resp, n);
-	if (a)
-		c->ip6.addr_seen = a->addr.a6;
 
 	return 1;
 }
diff --git a/fwd.c b/fwd.c
index 10ed6034..e4090f4d 100644
--- a/fwd.c
+++ b/fwd.c
@@ -949,14 +949,6 @@ static bool fwd_guest_accessible(const struct ctx *c,
 			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;
 }
 
@@ -1128,6 +1120,8 @@ uint8_t fwd_nat_from_host(const struct ctx *c,
 			  const struct fwd_rule *rule, uint8_t proto,
 			  const struct flowside *ini, struct flowside *tgt)
 {
+	const struct guest_addr *a = NULL;
+
 	/* Common for spliced and non-spliced cases */
 	tgt->eport = rule->to + (ini->oport - rule->first);
 
@@ -1148,8 +1142,6 @@ uint8_t fwd_nat_from_host(const struct ctx *c,
 			if (c->host_lo_to_ns_lo) {
 				tgt->eaddr = inany_loopback4;
 			} else {
-				const struct guest_addr *a;
-
 				a = fwd_select_addr(c, AF_INET,
 						    CONF_ADDR_OBSERVED,
 						    CONF_ADDR_USER |
@@ -1161,10 +1153,18 @@ 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 {
+				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;
 		}
 
@@ -1193,20 +1193,32 @@ uint8_t fwd_nat_from_host(const struct ctx *c,
 	tgt->oport = ini->eport;
 
 	if (inany_v4(&tgt->oaddr)) {
-		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)) {
+		a = fwd_select_addr(c, AF_INET6, CONF_ADDR_OBSERVED,
+				    CONF_ADDR_USER | CONF_ADDR_HOST,
+				    CONF_ADDR_LINKLOCAL);
 	} else {
-		if (inany_is_linklocal6(&tgt->oaddr))
-			tgt->eaddr.a6 = c->ip6.addr_ll_seen;
-		else
-			tgt->eaddr.a6 = c->ip6.addr_seen;
+		const struct guest_addr *tmp;
+
+		/* Preferably, we want an entry with both flags set */
+		for_each_addr(tmp, c->addrs, c->addr_count, AF_INET6) {
+			if (!(tmp->flags & CONF_ADDR_OBSERVED))
+				continue;
+			if (!(tmp->flags & CONF_ADDR_LINKLOCAL))
+				continue;
+			a = tmp;
+			break;
+		}
+		/* If not, LINK_LOCAL only will do */
+		if (!a)
+			a = fwd_get_addr(c, AF_INET6, CONF_ADDR_LINKLOCAL, 0);
 	}
 
+	if (!a)
+		return PIF_NONE;
+
+	tgt->eaddr = a->addr;
 	return PIF_TAP;
 }
diff --git a/inany.h b/inany.h
index 95756eb8..14390c29 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 795f2818..3ca97bf6 100644
--- a/migrate.c
+++ b/migrate.c
@@ -57,10 +57,7 @@ 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;
@@ -71,6 +68,18 @@ static int seen_addrs_source_v2(struct ctx *c,
 	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,17 +100,29 @@ 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;
+	struct in_addr addr4;
 
 	(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;
+	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);
+	}
 
-	if (!IN4_IS_ADDR_UNSPECIFIED(&addrs.addr4)) {
-		fwd_set_addr(c, &inany_from_v4(addrs.addr4),
+	addr4 = addrs.addr4;
+	if (!IN4_IS_ADDR_UNSPECIFIED(&addr4)) {
+		fwd_set_addr(c, &inany_from_v4(addr4),
 			     CONF_ADDR_OBSERVED, 0);
 	}
 	memcpy(c->guest_mac, addrs.mac, sizeof(c->guest_mac));
diff --git a/passt.h b/passt.h
index 20ad7022..005f7631 100644
--- a/passt.h
+++ b/passt.h
@@ -123,8 +123,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 {
  */
 struct ip6_ctx {
 	/* PIF_TAP addresses */
-	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 56df2fdc..575b9315 100644
--- a/pasta.c
+++ b/pasta.c
@@ -366,6 +366,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);
@@ -415,12 +416,14 @@ void pasta_ns_conf(struct ctx *c)
 	if (!c->ifi6)
 		return;
 
-	rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi,
-			    &c->ip6.addr_ll_seen);
-	if (rc < 0)
+	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_OBSERVED, 0);
+	}
 	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",
diff --git a/tap.c b/tap.c
index 93db2f86..e7d37082 100644
--- a/tap.c
+++ b/tap.c
@@ -1000,22 +1000,13 @@ resume:
 			continue;
 		}
 
-		if (IN6_IS_ADDR_LINKLOCAL(saddr)) {
-			c->ip6.addr_ll_seen = *saddr;
+		if (!IN6_IS_ADDR_UNSPECIFIED(saddr)) {
+			uint8_t flags = CONF_ADDR_OBSERVED;
 
-			if (!fwd_get_addr(c, AF_INET6, CONF_ADDR_USER, 0) &&
-			    IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen)) {
-				c->ip6.addr_seen = *saddr;
-			}
-
-			if (!fwd_get_addr(c, AF_INET6, CONF_ADDR_USER, 0)) {
-				union inany_addr addr = { .a6 = *saddr };
+			if (IN6_IS_ADDR_LINKLOCAL(saddr))
+				flags |= CONF_ADDR_LINKLOCAL;
 
-				fwd_set_addr(c, &addr, CONF_ADDR_LINKLOCAL, 64);
-			}
-		} else if (!fwd_get_addr(c, AF_INET6, CONF_ADDR_USER, 0) &&
-			   !IN6_IS_ADDR_UNSPECIFIED(saddr)) {
-			c->ip6.addr_seen = *saddr;
+			fwd_set_addr(c, &inany_from_v6(*saddr), flags, 0);
 		}
 
 		if (proto == IPPROTO_ICMPV6) {
-- 
2.52.0


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

* [PATCH v8 11/14] dhcp: Select address for DHCP distribution
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (9 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 10/14] conf, pasta: Track observed guest IPv6 " Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 12/14] dhcpv6: Select addresses for DHCPv6 distribution Jon Maloy
                   ` (2 subsequent siblings)
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, david, jmaloy, passt-dev

We introduce a CONF_ADDR_DHCPOFFER 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

v7: -Modified DHCP advertisement eligibility criteria IPv4 addresses:
     We now permit link-local addresses to be eligible if they were
     configured by the user.
    -Adapted to previous changes in this series

v8: - Adapted to previous changes in this series
    - Simplified DHCP eligibility branch in fwd_set_addr() (David)
    - Moved this commit to earlier in the series to avoid
      a technical migration protocol change (David).
    - Renamed CONF_ADDR_DHCP to CONF_ADDR_DHCPOFFER (Stefano)
---
 conf.c  |  5 +++--
 dhcp.c  | 12 ++++++------
 fwd.c   |  9 +++++++++
 passt.h |  1 +
 4 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/conf.c b/conf.c
index 69c1c439..f30c238e 100644
--- a/conf.c
+++ b/conf.c
@@ -47,6 +47,7 @@
 #include "lineread.h"
 #include "isolation.h"
 #include "log.h"
+#include "fwd.h"
 #include "vhost_user.h"
 #include "epoll_ctl.h"
 #include "conf.h"
@@ -832,8 +833,8 @@ 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, CONF_ADDR_ANY, 0);
-		if (a && !c->no_dhcp) {
+		a = fwd_get_addr(c, AF_INET, CONF_ADDR_DHCPOFFER, 0);
+		if (a) {
 			uint32_t mask;
 			int plen = 0;
 
diff --git a/dhcp.c b/dhcp.c
index 727485f7..a7a9765a 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"
 
 /**
@@ -302,19 +304,18 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
  */
 int dhcp(const struct ctx *c, struct iov_tail *data)
 {
+	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 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 plen;
@@ -347,9 +348,8 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	    m->op != BOOTREQUEST)
 		return -1;
 
-	a = fwd_get_addr(c, AF_INET, CONF_ADDR_USER, 0);
-	if (!a)
-		a = fwd_get_addr(c, AF_INET, CONF_ADDR_ANY, 0);
+	/* Select address to offer */
+	a = fwd_get_addr(c, AF_INET, CONF_ADDR_DHCPOFFER, 0);
 	if (!a)
 		return -1;
 
diff --git a/fwd.c b/fwd.c
index e4090f4d..669fc237 100644
--- a/fwd.c
+++ b/fwd.c
@@ -292,6 +292,15 @@ void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
 		return;
 	}
 
+	/* Determine advertisement eligibility */
+	if ((flags & CONF_ADDR_USER) ||
+	    (flags & CONF_ADDR_HOST && !(flags & CONF_ADDR_LINKLOCAL))) {
+		if (inany_v4(addr)) {
+			if (!c->no_dhcp)
+				flags |= CONF_ADDR_DHCPOFFER;
+		}
+	}
+
 	/* Add to head or tail, depending on flag */
 	if (flags & CONF_ADDR_OBSERVED) {
 		memmove(&head[1], &head[0], c->addr_count * sizeof(*a));
diff --git a/passt.h b/passt.h
index 005f7631..b323bd48 100644
--- a/passt.h
+++ b/passt.h
@@ -82,6 +82,7 @@ struct guest_addr {
 #define CONF_ADDR_GENERATED	BIT(2)		/* Generated by PASST/PASTA */
 #define CONF_ADDR_LINKLOCAL	BIT(3)		/* Link-local address */
 #define CONF_ADDR_OBSERVED	BIT(4)		/* Seen in guest traffic */
+#define CONF_ADDR_DHCPOFFER	BIT(5)		/* Advertise via DHCP (IPv4) */
 #define CONF_ADDR_ANY		0xff		/* Match any flag */
 };
 
-- 
2.52.0


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

* [PATCH v8 12/14] dhcpv6: Select addresses for DHCPv6 distribution
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (10 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 11/14] dhcp: Select address for DHCP distribution Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 13/14] ndp: Support advertising multiple prefixes in Router Advertisements Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 14/14] migrate: Update protocol to v3 for multi-address support Jon Maloy
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, david, jmaloy, passt-dev

We introduce a CONF_ADDR_DHCPV6OFFER flag to mark if an added address is
eligible for DHCPv6 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 9915.

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.

v7: -Adapted to previous changes in this series
    -Some minor changes based on feedback

v8: -Moved to earlier in series.
    -Refactored branch for advertisement eligibility
    -Renamed CONF_ADDR_DHCP to CONF_ADDR_DHCPOFFER (Stefano)
---
 conf.c   | 34 +++++++++++++++-----
 dhcpv6.c | 98 ++++++++++++++++++++++++++++++++------------------------
 fwd.c    |  3 ++
 passt.h  |  1 +
 4 files changed, 87 insertions(+), 49 deletions(-)

diff --git a/conf.c b/conf.c
index f30c238e..74368c2b 100644
--- a/conf.c
+++ b/conf.c
@@ -868,25 +868,43 @@ 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
+		for_each_addr(a, c->addrs, c->addr_count, AF_INET6) {
+			if (a->flags & CONF_ADDR_DHCPV6OFFER)
+				has_dhcpv6 = true;
+		}
+
+		if (c->no_ndp && !has_dhcpv6)
 			goto dns6;
 
 		a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY,
 				 CONF_ADDR_LINKLOCAL);
-		if (a)
+		if (!c->no_ndp && a) {
+			info("NDP:");
 			info("    assign: %s",
 			     inany_ntop(&a->addr, buf, sizeof(buf)));
+		}
+
+		if (has_dhcpv6) {
+			info("DHCPv6:");
+			head = "assign: ";
+			for_each_addr(a, c->addrs, c->addr_count, AF_INET6) {
+				if (!(a->flags & CONF_ADDR_DHCPV6OFFER))
+					continue;
+				info("    %s %s/%d", head,
+				     inany_ntop(&a->addr, buf, sizeof(buf)),
+				     a->prefix_len);
+				head = "        ";
+			}
+		}
+
 		info("    router: %s",
 		     inet_ntop(AF_INET6, &c->ip6.guest_gw, buf, sizeof(buf)));
 		info("    our link-local: %s",
diff --git a/dhcpv6.c b/dhcpv6.c
index f5de90cd..b704e59f 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 = {
@@ -540,6 +521,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 host or user provided
+ * 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->addrs, c->addr_count, AF_INET6) {
+		if (!(e->flags & CONF_ADDR_DHCPV6OFFER))
+			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
@@ -573,9 +590,10 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	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);
+	a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, CONF_ADDR_LINKLOCAL);
 
 	uh = IOV_REMOVE_HEADER(data, uh_storage);
 	if (!uh)
@@ -618,6 +636,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:
@@ -673,12 +692,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);
 
@@ -696,7 +717,6 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 void dhcpv6_init(const struct ctx *c)
 {
 	time_t y2k = 946684800; /* Epoch to 2000-01-01T00:00:00Z, no mktime() */
-	const struct guest_addr *a;
 	uint32_t duid_time;
 
 	duid_time = htonl(difftime(time(NULL), y2k));
@@ -708,8 +728,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 669fc237..2ebd5f24 100644
--- a/fwd.c
+++ b/fwd.c
@@ -298,6 +298,9 @@ void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
 		if (inany_v4(addr)) {
 			if (!c->no_dhcp)
 				flags |= CONF_ADDR_DHCPOFFER;
+		} else {
+			if (!c->no_dhcpv6)
+				flags |= CONF_ADDR_DHCPV6OFFER;
 		}
 	}
 
diff --git a/passt.h b/passt.h
index b323bd48..5fa21385 100644
--- a/passt.h
+++ b/passt.h
@@ -83,6 +83,7 @@ struct guest_addr {
 #define CONF_ADDR_LINKLOCAL	BIT(3)		/* Link-local address */
 #define CONF_ADDR_OBSERVED	BIT(4)		/* Seen in guest traffic */
 #define CONF_ADDR_DHCPOFFER	BIT(5)		/* Advertise via DHCP (IPv4) */
+#define CONF_ADDR_DHCPV6OFFER	BIT(6)		/* Advertise via DHCPv6 */
 #define CONF_ADDR_ANY		0xff		/* Match any flag */
 };
 
-- 
2.52.0


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

* [PATCH v8 13/14] ndp: Support advertising multiple prefixes in Router Advertisements
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (11 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 12/14] dhcpv6: Select addresses for DHCPv6 distribution Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  2026-06-26  2:45 ` [PATCH v8 14/14] migrate: Update protocol to v3 for multi-address support Jon Maloy
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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

v7: -Adapted to previous changes in series
    -Use struct initializer for source link-layer address option
    -Other minor fixes based on feedback from Stefano

v8: -Adapted to previous changes in series
    -Moved to before migration v3 commit
    -Some other minor fixes due to feedback from David.
---
 conf.c  |  20 ++++++----
 fwd.c   |   2 +
 ndp.c   | 120 +++++++++++++++++++++++++++++++++++++-------------------
 passt.h |   1 +
 4 files changed, 96 insertions(+), 47 deletions(-)

diff --git a/conf.c b/conf.c
index 74368c2b..05e1ef14 100644
--- a/conf.c
+++ b/conf.c
@@ -868,7 +868,7 @@ static void conf_print(const struct ctx *c)
 	}
 
 	if (c->ifi6) {
-		bool has_dhcpv6 = false;
+		bool has_slaac = false, has_dhcpv6 = false;
 		const char *head;
 
 		if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback))
@@ -877,19 +877,25 @@ static void conf_print(const struct ctx *c)
 				       buf, sizeof(buf)));
 
 		for_each_addr(a, c->addrs, c->addr_count, AF_INET6) {
+			if (a->flags & CONF_ADDR_SLAAC)
+				has_slaac = true;
 			if (a->flags & CONF_ADDR_DHCPV6OFFER)
 				has_dhcpv6 = true;
 		}
 
-		if (c->no_ndp && !has_dhcpv6)
+		if (!has_slaac && !has_dhcpv6)
 			goto dns6;
 
-		a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY,
-				 CONF_ADDR_LINKLOCAL);
-		if (!c->no_ndp && a) {
+		if (has_slaac) {
 			info("NDP:");
-			info("    assign: %s",
-			     inany_ntop(&a->addr, buf, sizeof(buf)));
+			head = "assign: ";
+			for_each_addr(a, c->addrs, c->addr_count, AF_INET6) {
+				if (!(a->flags & CONF_ADDR_SLAAC))
+					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 2ebd5f24..d4100042 100644
--- a/fwd.c
+++ b/fwd.c
@@ -301,6 +301,8 @@ void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
 		} else {
 			if (!c->no_dhcpv6)
 				flags |= CONF_ADDR_DHCPV6OFFER;
+			if (!c->no_ndp && prefix_len == 64)
+				flags |= CONF_ADDR_SLAAC;
 		}
 	}
 
diff --git a/ndp.c b/ndp.c
index a6a79055..d548a019 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
 
@@ -99,6 +101,16 @@ struct opt_prefix_info {
 	uint32_t reserved;
 } __attribute__((packed));
 
+/**
+ * struct ndp_prefix - 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
+ *
+ * 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->addrs, c->addr_count, AF_INET6) {
+		if (!(a->flags & CONF_ADDR_SLAAC))
+			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,33 +299,23 @@ 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;
-	unsigned char *ptr = NULL;
 
-	a = fwd_get_addr(c, AF_INET6, CONF_ADDR_ANY, 0);
-	if (!a)
-		return;
+	/* Fill prefix options */
+	prefix_len = ndp_prefix_fill(c, (unsigned char *)(hdr + 1));
 
-	ra.prefix = a->addr.a6;
+	/* Add source link-layer address option */
+	ptr = (unsigned char *)(hdr + 1) + prefix_len;
+	source_ll = (struct opt_l2_addr *)ptr;
+	*source_ll = (struct opt_l2_addr) {
+		.header = {
+			.type	= OPT_SRC_L2_ADDR,
+			.len	= 1,
+		},
+	};
 
-	ptr = &ra.var[0];
+	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;
@@ -347,10 +389,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 5fa21385..3dc5234e 100644
--- a/passt.h
+++ b/passt.h
@@ -84,6 +84,7 @@ struct guest_addr {
 #define CONF_ADDR_OBSERVED	BIT(4)		/* Seen in guest traffic */
 #define CONF_ADDR_DHCPOFFER	BIT(5)		/* Advertise via DHCP (IPv4) */
 #define CONF_ADDR_DHCPV6OFFER	BIT(6)		/* Advertise via DHCPv6 */
+#define CONF_ADDR_SLAAC		BIT(7)		/* Advertise via NDP/RA (/64) */
 #define CONF_ADDR_ANY		0xff		/* Match any flag */
 };
 
-- 
2.52.0


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

* [PATCH v8 14/14] migrate: Update protocol to v3 for multi-address support
  2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
                   ` (12 preceding siblings ...)
  2026-06-26  2:45 ` [PATCH v8 13/14] ndp: Support advertising multiple prefixes in Router Advertisements Jon Maloy
@ 2026-06-26  2:45 ` Jon Maloy
  13 siblings, 0 replies; 15+ messages in thread
From: Jon Maloy @ 2026-06-26  2:45 UTC (permalink / raw)
  To: sbrivio, 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 it 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

v7: - Using uint32_t instead of uint8_t for fields in migration format
    - Replaced term "wire format" with "migration format"
    - Some other minor changes after feedback from Stefano.

v8: - Moved commit to end of series, to keep the new protocol
      version complete from the beginning.
    - Let flags_to_migration() and flags_from_migration() take uint32_t
      and return value in correct network format.
    - Expanded addr_count to uint32_t
    - Changed read/write method for scalars.
    - Internalized endianness conversions to addr_target/source_v3()
      functions.
    - All above suggested by David.
---
 migrate.c | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 198 insertions(+)

diff --git a/migrate.c b/migrate.c
index 3ca97bf6..82176743 100644
--- a/migrate.c
+++ b/migrate.c
@@ -45,6 +45,89 @@ struct migrate_seen_addrs_v2 {
 	unsigned char mac[ETH_ALEN];
 } __attribute__((packed));
 
+/**
+ * Migration format flags for address migration (v3)
+ * These are stable values - do not change existing assignments
+ * Note that CONF_ADDR_GENERATED is excluded, since it is redundant
+ * and easily can be re-generated at the target
+ */
+#define MIGR_ADDR_USER		BIT(0)
+#define MIGR_ADDR_HOST		BIT(1)
+#define MIGR_ADDR_LINKLOCAL	BIT(2)
+#define MIGR_ADDR_OBSERVED	BIT(3)
+#define MIGR_ADDR_DHCPOFFER	BIT(4)
+#define MIGR_ADDR_DHCPV6OFFER	BIT(5)
+#define MIGR_ADDR_SLAAC		BIT(6)
+
+/**
+ * struct migrate_addr_v3 - Migration format for a single address entry
+ * @addr:	IPv6 or IPv4-mapped address (16 bytes)
+ * @prefix_len:	Prefix length
+ * @flags:	MIGR_ADDR_* flags (migration format)
+ */
+struct migrate_addr_v3 {
+	struct in6_addr addr;
+	uint32_t prefix_len;
+	uint32_t flags;
+} __attribute__((__packed__));
+
+/**
+ * flags_to_migration() - Convert internal flags to stable migration format
+ * @flags:	Internal CONF_ADDR_* flags
+ *
+ * Return: Migration format MIGR_ADDR_* flags
+ */
+static uint32_t flags_to_migration(uint8_t flags)
+{
+	uint32_t migration = 0;
+
+	if (flags & CONF_ADDR_USER)
+		migration |= MIGR_ADDR_USER;
+	if (flags & CONF_ADDR_HOST)
+		migration |= MIGR_ADDR_HOST;
+	if (flags & CONF_ADDR_LINKLOCAL)
+		migration |= MIGR_ADDR_LINKLOCAL;
+	if (flags & CONF_ADDR_OBSERVED)
+		migration |= MIGR_ADDR_OBSERVED;
+	if (flags & CONF_ADDR_DHCPOFFER)
+		migration |= MIGR_ADDR_DHCPOFFER;
+	if (flags & CONF_ADDR_DHCPV6OFFER)
+		migration |= MIGR_ADDR_DHCPV6OFFER;
+	if (flags & CONF_ADDR_SLAAC)
+		migration |= MIGR_ADDR_SLAAC;
+
+	return htonl(migration);
+}
+
+/**
+ * flags_from_migration() - Convert migration format flags to internal format
+ * @migration:	Migration format MIGR_ADDR_* flags
+ *
+ * Return: Internal CONF_ADDR_* flags
+ */
+static uint8_t flags_from_migration(uint32_t migr)
+{
+	uint32_t migration = ntohl(migr);
+	uint8_t flags = 0;
+
+	if (migration & MIGR_ADDR_USER)
+		flags |= CONF_ADDR_USER;
+	if (migration & MIGR_ADDR_HOST)
+		flags |= CONF_ADDR_HOST;
+	if (migration & MIGR_ADDR_LINKLOCAL)
+		flags |= CONF_ADDR_LINKLOCAL;
+	if (migration & MIGR_ADDR_OBSERVED)
+		flags |= CONF_ADDR_OBSERVED;
+	if (migration & MIGR_ADDR_DHCPOFFER)
+		flags |= CONF_ADDR_DHCPOFFER;
+	if (migration & MIGR_ADDR_DHCPV6OFFER)
+		flags |= CONF_ADDR_DHCPV6OFFER;
+	if (migration & MIGR_ADDR_SLAAC)
+		flags |= CONF_ADDR_SLAAC;
+
+	return flags;
+}
+
 /**
  * seen_addrs_source_v2() - Copy and send guest observed addresses from source
  * @c:		Execution context
@@ -130,6 +213,100 @@ 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 migration format. Each field is
+ * serialised explicitly to avoid coupling the migration 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)
+{
+	uint32_t addr_count = c->addr_count;
+	const struct guest_addr *a;
+
+	(void)stage;
+
+	/* Send count first */
+	if (write_u32(fd, addr_count))
+		return errno;
+
+	/* Send each address in stable migration format */
+	for_each_addr(a, c->addrs, c->addr_count, AF_UNSPEC) {
+		struct migrate_addr_v3 migration = {
+			.addr = a->addr.a6,
+			.prefix_len = htonl(a->prefix_len),
+			.flags = flags_to_migration(a->flags),
+		};
+
+		if (write_all_buf(fd, &migration, sizeof(migration)))
+			return errno;
+	}
+
+	/* Send MAC address */
+	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 migration 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)
+{
+	uint32_t addr_count, i;
+
+	(void)stage;
+
+	if (read_u32(fd, &addr_count))
+		return errno;
+
+	if (addr_count > MAX_GUEST_ADDRS) {
+		warn("Truncated list of received migrated addresses");
+		addr_count = MAX_GUEST_ADDRS;
+	}
+	/* Read each address from stable migration format */
+	for (i = 0; i < addr_count; i++) {
+		struct migrate_addr_v3 migration;
+		struct guest_addr addr;
+
+		if (read_all_buf(fd, &migration, sizeof(migration)))
+			return errno;
+
+		addr.addr.a6 = migration.addr;
+		addr.prefix_len = ntohl(migration.prefix_len);
+		addr.flags = flags_from_migration(migration.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[] = {
 	{
@@ -150,8 +327,29 @@ static const struct migrate_stage stages_v2[] = {
 	{ 0 },
 };
 
+/* Stages for version 3 (all addresses, with flags) */
+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] 15+ messages in thread

end of thread, other threads:[~2026-06-26  2:45 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-26  2:45 [PATCH v8 00/14] Introduce multiple addresses Jon Maloy
2026-06-26  2:45 ` [PATCH v8 01/14] dhcpv6: Fix reply destination to match client's source address Jon Maloy
2026-06-26  2:45 ` [PATCH v8 02/14] passt, pasta: Introduce unified multi-address data structures Jon Maloy
2026-06-26  2:45 ` [PATCH v8 03/14] tap, conf: Replace addr_fixed with CONF_ADDR_USER flag check Jon Maloy
2026-06-26  2:45 ` [PATCH v8 04/14] fwd: Unify guest accessibility checks with unified address array Jon Maloy
2026-06-26  2:45 ` [PATCH v8 05/14] arp: Check all configured addresses in ARP filtering Jon Maloy
2026-06-26  2:45 ` [PATCH v8 06/14] conf: Allow multiple -a/--address options per address family Jon Maloy
2026-06-26  2:45 ` [PATCH v8 07/14] netlink, conf: Read all addresses from template interface at startup Jon Maloy
2026-06-26  2:45 ` [PATCH v8 08/14] netlink, pasta: refactor function pasta_ns_conf() Jon Maloy
2026-06-26  2:45 ` [PATCH v8 09/14] conf, pasta: Track observed guest IPv4 addresses in unified address array Jon Maloy
2026-06-26  2:45 ` [PATCH v8 10/14] conf, pasta: Track observed guest IPv6 " Jon Maloy
2026-06-26  2:45 ` [PATCH v8 11/14] dhcp: Select address for DHCP distribution Jon Maloy
2026-06-26  2:45 ` [PATCH v8 12/14] dhcpv6: Select addresses for DHCPv6 distribution Jon Maloy
2026-06-26  2:45 ` [PATCH v8 13/14] ndp: Support advertising multiple prefixes in Router Advertisements Jon Maloy
2026-06-26  2:45 ` [PATCH v8 14/14] migrate: Update protocol to v3 for multi-address support 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).