public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
* [PATCH v5 00/13] Introduce multiple addresses and late binding
@ 2026-02-22 17:44 Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 01/13] ip: Introduce unified multi-address data structures Jon Maloy
                   ` (12 more replies)
  0 siblings, 13 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

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

o All addresses are stored as union inany_addr
o User configured addresses are marked with a USER flag.
o Host provided addresses are marked with a HOST flag.
o Link local addresses are also marked with a LINKLOCAL flag.
o Addresses the guest is actually using are marked with an OBSERVED flag.
o The address selected for IPv4 DHCP offers is marked with a DHCP flag.

We also include a commit to support late binding of interfaces,
addresses, and routes.

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.

Jon Maloy (13):
  ip: Introduce unified multi-address data structures
  ip: Introduce for_each_addr() macro for address iteration
  fwd: Unify guest accessibility checks with unified address array
  arp: Check all configured addresses in ARP filtering
  netlink: Return prefix length for IPv6 addresses in nl_addr_get()
  conf: Allow multiple -a/--address options per address family
  ip: Track observed guest IPv4 addresses in unified address array
  ip: Track observed guest IPv6 addresses in unified address array
  migrate: Rename v1 address functions to v2 for clarity
  migrate: Update protocol to v3 for multi-address support
  dhcp, dhcpv6: Select addresses for DHCP distribution
  ndp: Support advertising multiple prefixes in Router Advertisement
  netlink: Add host-side monitoring for late template interface binding

 arp.c        |  15 +-
 conf.c       | 200 +++++++++++++++----------
 conf.h       |   9 ++
 dhcp.c       |  30 ++--
 dhcp.h       |   2 +-
 dhcpv6.c     |  78 +++++++---
 dhcpv6.h     |   2 +-
 epoll_type.h |   2 +
 fwd.c        | 263 +++++++++++++++++++++++++--------
 fwd.h        |   9 ++
 inany.h      |   3 +
 ip.h         |   5 +
 isolation.c  |   5 +
 migrate.c    | 183 ++++++++++++++++++++---
 ndp.c        | 134 ++++++++++++-----
 netlink.c    | 401 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 netlink.h    |   3 +
 passt.c      |   5 +
 passt.h      | 104 +++++++++++--
 pasta.c      |  39 +++--
 tap.c        |  67 ++++++---
 21 files changed, 1282 insertions(+), 277 deletions(-)

-- 
2.52.0


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

* [PATCH v5 01/13] ip: Introduce unified multi-address data structures
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 02/13] ip: Introduce for_each_addr() macro for address iteration Jon Maloy
                   ` (11 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

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

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

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

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

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

v4: -Updated according to changes in previous commits
    -Updated according to feedback from David G.
    -Squashed IP4_MASK macro commit into this one
---
 arp.c    |  10 ++++-
 conf.c   | 124 +++++++++++++++++++++++++++++++++----------------------
 conf.h   |   7 ++++
 dhcp.c   |  13 ++++--
 dhcpv6.c |   9 ++--
 fwd.c    |  18 ++++----
 ip.h     |   5 +++
 ndp.c    |  17 ++++++--
 passt.h  |  64 ++++++++++++++++++++++++----
 pasta.c  |  21 ++++++----
 tap.c    |  11 ++++-
 11 files changed, 211 insertions(+), 88 deletions(-)

diff --git a/arp.c b/arp.c
index bb042e9..99a6a67 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)
 {
+	struct inany_addr_entry *e = first_v4(c);
+
 	if (ah->ar_hrd != htons(ARPHRD_ETHER)	||
 	    ah->ar_pro != htons(ETH_P_IP)	||
 	    ah->ar_hln != ETH_ALEN		||
@@ -54,7 +56,7 @@ static bool ignore_arp(const struct ctx *c,
 		return true;
 
 	/* Don't resolve the guest's assigned address, either. */
-	if (!memcmp(am->tip, &c->ip4.addr, sizeof(am->tip)))
+	if (e && !memcmp(am->tip, inany_v4(&e->addr), sizeof(am->tip)))
 		return true;
 
 	return false;
@@ -123,12 +125,16 @@ int arp(const struct ctx *c, struct iov_tail *data)
  */
 void arp_send_init_req(const struct ctx *c)
 {
+	struct inany_addr_entry *e = first_v4(c);
 	struct {
 		struct ethhdr eh;
 		struct arphdr ah;
 		struct arpmsg am;
 	} __attribute__((__packed__)) req;
 
+	if (!e)
+		return;
+
 	/* Ethernet header */
 	req.eh.h_proto = htons(ETH_P_ARP);
 	memcpy(req.eh.h_dest, MAC_BROADCAST, sizeof(req.eh.h_dest));
@@ -145,7 +151,7 @@ void arp_send_init_req(const struct ctx *c)
 	memcpy(req.am.sha,	c->our_tap_mac,		sizeof(req.am.sha));
 	memcpy(req.am.sip,	&c->ip4.our_tap_addr,	sizeof(req.am.sip));
 	memcpy(req.am.tha,	MAC_BROADCAST,		sizeof(req.am.tha));
-	memcpy(req.am.tip,	&c->ip4.addr,		sizeof(req.am.tip));
+	memcpy(req.am.tip,	inany_v4(&e->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 11d8453..f2ff8af 100644
--- a/conf.c
+++ b/conf.c
@@ -42,6 +42,7 @@
 #include "tap.h"
 #include "udp.h"
 #include "tcp.h"
+#include "conf.h"
 #include "pasta.h"
 #include "lineread.h"
 #include "isolation.h"
@@ -701,13 +702,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)
 {
+	struct inany_addr_entry *e = first_v4(c);
+	struct ip4_ctx *ip4 = &c->ip4;
+
 	if (!ifi)
 		ifi = nl_get_ext_if(nl_sock, AF_INET);
 
@@ -726,60 +730,60 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
 		}
 	}
 
-	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr)) {
+	if (!e) {
+		struct in_addr addr;
+		int prefix_len = 0;
 		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;
+		e = &c->addrs[c->addr_count++];
+		e->addr = inany_from_v4(addr);
+		e->prefix_len = prefix_len + 96;
+		e->flags = CONF_ADDR_HOST;
+		ip4->addr_seen = addr;
 	}
 
-	ip4->addr_seen = ip4->addr;
-
 	ip4->our_tap_addr = ip4->guest_gw;
 
-	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr))
-		return 0;
-
 	return ifi;
 }
 
 /**
  * conf_ip4_local() - Configure IPv4 addresses and attributes for local mode
- * @ip4:	IPv4 context (will be written)
+ * @c:		Execution context (will be written)
  */
-static void conf_ip4_local(struct ip4_ctx *ip4)
+static void conf_ip4_local(struct ctx *c)
 {
-	ip4->addr_seen = ip4->addr = IP4_LL_GUEST_ADDR;
-	ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
-	ip4->prefix_len = IP4_LL_PREFIX_LEN;
+	struct inany_addr_entry *e = &c->addrs[c->addr_count++];
+	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;
+	e->addr = inany_from_v4(IP4_LL_GUEST_ADDR);
+	e->prefix_len = IP4_LL_PREFIX_LEN + 96;
+	e->flags = CONF_ADDR_HOST | CONF_ADDR_LINKLOCAL;
 }
 
 /**
  * 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 inany_addr_entry *e = first_v6(c);
+	struct ip6_ctx *ip6 = &c->ip6;
+	union inany_addr addr;
 	int prefix_len = 0;
 	int rc;
 
@@ -800,21 +804,26 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
 		}
 	}
 
-	rc = nl_addr_get(nl_sock, ifi, AF_INET6,
-			 IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ? &ip6->addr : NULL,
+	rc = nl_addr_get(nl_sock, ifi, AF_INET6, &addr.a6,
 			 &prefix_len, &ip6->our_tap_ll);
 	if (rc < 0) {
 		debug("Couldn't discover IPv6 address: %s", strerror_(-rc));
 		return 0;
 	}
 
-	ip6->addr_seen = ip6->addr;
+	if (!e) {
+		e = &c->addrs[c->addr_count++];
+		e->addr = addr;
+		e->prefix_len = prefix_len ? prefix_len : 64;
+		e->flags = CONF_ADDR_HOST;
+	}
+
+	ip6->addr_seen = e->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;
@@ -822,13 +831,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;
 }
 
 /**
@@ -1111,6 +1120,7 @@ static void conf_print(const struct ctx *c)
 {
 	char buf4[INET_ADDRSTRLEN], buf6[INET6_ADDRSTRLEN];
 	char bufmac[ETH_ADDRSTRLEN], ifn[IFNAMSIZ];
+	struct inany_addr_entry *e;
 	int i;
 
 	if (c->ifi4 > 0 || c->ifi6 > 0) {
@@ -1154,14 +1164,16 @@ static void conf_print(const struct ctx *c)
 			     inet_ntop(AF_INET, &c->ip4.map_host_loopback,
 				       buf4, sizeof(buf4)));
 
-		if (!c->no_dhcp) {
+		e = first_v4(c);
+		if (e && !c->no_dhcp) {
 			uint32_t mask;
 
-			mask = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+			mask = IN4_MASK(inany_prefix4(e));
 
 			info("DHCP:");
 			info("    assign: %s",
-			     inet_ntop(AF_INET, &c->ip4.addr, buf4, sizeof(buf4)));
+			     inet_ntop(AF_INET, inany_v4(&e->addr),
+				       buf4, sizeof(buf4)));
 			info("    mask: %s",
 			     inet_ntop(AF_INET, &mask,        buf4, sizeof(buf4)));
 			info("    router: %s",
@@ -1200,8 +1212,9 @@ static void conf_print(const struct ctx *c)
 		else
 			goto dns6;
 
-		info("    assign: %s",
-		     inet_ntop(AF_INET6, &c->ip6.addr, buf6, sizeof(buf6)));
+		e = first_v6(c);
+		info("    assign: %s", !e ? "" :
+		     inet_ntop(AF_INET6, &e->addr.a6, buf6, sizeof(buf6)));
 		info("    router: %s",
 		     inet_ntop(AF_INET6, &c->ip6.guest_gw, buf6, sizeof(buf6)));
 		info("    our link-local: %s",
@@ -1837,6 +1850,7 @@ void conf(struct ctx *c, int argc, char **argv)
 			break;
 		}
 		case 'a': {
+			struct inany_addr_entry *e;
 			union inany_addr addr;
 			uint8_t prefix_len;
 
@@ -1860,19 +1874,27 @@ void conf(struct ctx *c, int argc, char **argv)
 			    IN6_IS_ADDR_V4COMPAT(&addr.a6))
 				die("Invalid address: %s", optarg);
 
+			/* Legacy behaviour: overwrite existing address if any */
 			if (inany_v4(&addr)) {
-				c->ip4.addr = *inany_v4(&addr);
-				c->ip4.prefix_len = prefix_len - 96;
+				e = first_v4(c);
+				if (!e)
+					e = &c->addrs[c->addr_count++];
 				if (c->mode == MODE_PASTA)
 					c->ip4.no_copy_addrs = true;
 			} else {
-				c->ip6.addr = addr.a6;
+				e = first_v6(c);
+				if (!e)
+					e = &c->addrs[c->addr_count++];
 				if (c->mode == MODE_PASTA)
 					c->ip6.no_copy_addrs = true;
 			}
+			e->prefix_len = prefix_len;
+			e->addr = addr;
+			e->flags = CONF_ADDR_USER;
 			break;
 		}
 		case 'n': {
+			struct inany_addr_entry *e;
 			int plen;
 
 			if (addr_has_prefix_len)
@@ -1883,7 +1905,9 @@ void conf(struct ctx *c, int argc, char **argv)
 				die("Invalid prefix length: %s", optarg);
 
 			prefix_len_from_opt = plen + 96;
-			c->ip4.prefix_len = plen;
+			e = first_v4(c);
+			if (e)
+				e->prefix_len = prefix_len_from_opt;
 			break;
 		}
 		case 'M':
@@ -2042,9 +2066,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)",
@@ -2067,7 +2091,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;
 	}
@@ -2075,7 +2099,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;
 	}
@@ -2184,7 +2208,7 @@ void conf(struct ctx *c, int argc, char **argv)
 	if (!c->ifi6) {
 		c->no_ndp = 1;
 		c->no_dhcpv6 = 1;
-	} else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
+	} else if (!first_v6(c)) {
 		c->no_dhcpv6 = 1;
 	}
 
diff --git a/conf.h b/conf.h
index b45ad74..bfad36f 100644
--- a/conf.h
+++ b/conf.h
@@ -6,6 +6,13 @@
 #ifndef CONF_H
 #define CONF_H
 
+/* Flags indicating origin and role of an address
+ * To be used in struct inany_addr_entry
+ */
+#define CONF_ADDR_USER		BIT(0)		/* User set via -a */
+#define CONF_ADDR_HOST		BIT(1)		/* From host interface */
+#define CONF_ADDR_LINKLOCAL	BIT(2)		/* Link-local address */
+
 enum passt_modes conf_mode(int argc, char *argv[]);
 void conf(struct ctx *c, int argc, char **argv);
 
diff --git a/dhcp.c b/dhcp.c
index 1ff8cba..af473ee 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -302,6 +302,7 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
  */
 int dhcp(const struct ctx *c, struct iov_tail *data)
 {
+	struct inany_addr_entry *e = first_v4(c);
 	char macstr[ETH_ADDRSTRLEN];
 	size_t mlen, dlen, opt_len;
 	struct in_addr mask, dst;
@@ -313,6 +314,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	const struct udphdr *uh;
 	struct msg m_storage;
 	struct msg const *m;
+	struct in_addr addr;
 	struct msg reply;
 	unsigned int i;
 
@@ -344,6 +346,9 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	    m->op != BOOTREQUEST)
 		return -1;
 
+	ASSERT(e);
+	addr = *inany_v4(&e->addr);
+
 	reply.op		= BOOTREPLY;
 	reply.htype		= m->htype;
 	reply.hlen		= m->hlen;
@@ -352,7 +357,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	reply.secs		= 0;
 	reply.flags		= m->flags;
 	reply.ciaddr		= m->ciaddr;
-	reply.yiaddr		= c->ip4.addr;
+	reply.yiaddr		= addr;
 	reply.siaddr		= 0;
 	reply.giaddr		= m->giaddr;
 	memcpy(&reply.chaddr,	m->chaddr,	sizeof(reply.chaddr));
@@ -404,7 +409,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 
 	info("    from %s", eth_ntop(m->chaddr, macstr, sizeof(macstr)));
 
-	mask.s_addr = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+	mask.s_addr = IN4_MASK(inany_prefix4(e));
 	memcpy(opts[1].s,  &mask,                sizeof(mask));
 	memcpy(opts[3].s,  &c->ip4.guest_gw,     sizeof(c->ip4.guest_gw));
 	memcpy(opts[54].s, &c->ip4.our_tap_addr, sizeof(c->ip4.our_tap_addr));
@@ -412,7 +417,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	/* If the gateway is not on the assigned subnet, send an option 121
 	 * (Classless Static Routing) adding a dummy route to it.
 	 */
-	if ((c->ip4.addr.s_addr & mask.s_addr)
+	if ((addr.s_addr & mask.s_addr)
 	    != (c->ip4.guest_gw.s_addr & mask.s_addr)) {
 		/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
 		opts[121].slen = 14;
@@ -471,7 +476,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	if (m->flags & FLAG_BROADCAST)
 		dst = in4addr_broadcast;
 	else
-		dst = c->ip4.addr;
+		dst = addr;
 
 	tap_udp4_send(c, c->ip4.our_tap_addr, 67, dst, 68, &reply, dlen);
 
diff --git a/dhcpv6.c b/dhcpv6.c
index 97c04e2..55c17cf 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -552,6 +552,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	   const struct in6_addr *saddr, const struct in6_addr *daddr)
 {
 	const struct opt_server_id *server_id = NULL;
+	struct inany_addr_entry *e = first_v6(c);
 	const struct opt_hdr *client_id = NULL;
 	/* The _storage variables can't be local to the blocks they're used in,
 	 * because IOV_*_HEADER() may return pointers to them which are
@@ -628,7 +629,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 (e && dhcpv6_ia_notonlink(data, &e->addr.a6)) {
 
 			dhcpv6_send_ia_notonlink(c, data, &client_id_base,
 						 ntohs(client_id->l), mh->xid);
@@ -682,7 +683,8 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 
 	tap_udp6_send(c, src, 547, tap_ip6_daddr(c, src), 546,
 		      mh->xid, &resp, n);
-	c->ip6.addr_seen = c->ip6.addr;
+	if (e)
+		c->ip6.addr_seen = e->addr.a6;
 
 	return 1;
 }
@@ -706,5 +708,6 @@ 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;
+	if (first_v6(c))
+		resp.ia_addr.addr = first_v6(c)->addr.a6;
 }
diff --git a/fwd.c b/fwd.c
index 4052b79..cf13ddd 100644
--- a/fwd.c
+++ b/fwd.c
@@ -890,6 +890,8 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
 static bool fwd_guest_accessible4(const struct ctx *c,
 				    const struct in_addr *addr)
 {
+	struct inany_addr_entry *e = first_v4(c);
+
 	if (IN4_IS_ADDR_LOOPBACK(addr))
 		return false;
 
@@ -904,7 +906,7 @@ static bool fwd_guest_accessible4(const struct ctx *c,
 	/* For IPv4, addr_seen is initialised to addr, so is always a valid
 	 * address
 	 */
-	if (IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr) ||
+	if ((e && IN4_ARE_ADDR_EQUAL(addr, inany_v4(&e->addr))) ||
 	    IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
 		return false;
 
@@ -925,7 +927,7 @@ static bool fwd_guest_accessible6(const struct ctx *c,
 	if (IN6_IS_ADDR_LOOPBACK(addr))
 		return false;
 
-	if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addr))
+	if (first_v6(c) && IN6_ARE_ADDR_EQUAL(addr, &first_v6(c)->addr.a6))
 		return false;
 
 	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
@@ -974,10 +976,10 @@ static void nat_outbound(const struct ctx *c, const union inany_addr *addr,
 		*translated = inany_loopback4;
 	else if (inany_equals6(addr, &c->ip6.map_host_loopback))
 		*translated = inany_loopback6;
-	else if (inany_equals4(addr, &c->ip4.map_guest_addr))
-		*translated = inany_from_v4(c->ip4.addr);
-	else if (inany_equals6(addr, &c->ip6.map_guest_addr))
-		translated->a6 = c->ip6.addr;
+	else if (first_v4(c) && inany_equals4(addr, &c->ip4.map_guest_addr))
+		*translated = first_v4(c)->addr;
+	else if (first_v6(c) && inany_equals6(addr, &c->ip6.map_guest_addr))
+		translated->a6 = first_v6(c)->addr.a6;
 	else
 		*translated = *addr;
 }
@@ -1085,10 +1087,10 @@ bool nat_inbound(const struct ctx *c, const union inany_addr *addr,
 		   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)) {
+		   first_v4(c) && inany_equals(addr, &first_v4(c)->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)) {
+		   first_v6(c) && inany_equals(addr, &first_v6(c)->addr)) {
 		translated->a6 = c->ip6.map_guest_addr;
 	} else if (fwd_guest_accessible(c, addr)) {
 		*translated = *addr;
diff --git a/ip.h b/ip.h
index 57d8661..c68c366 100644
--- a/ip.h
+++ b/ip.h
@@ -17,6 +17,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 \
@@ -136,6 +138,9 @@ static const struct in_addr in4addr_broadcast = { 0xffffffff };
 #define IPV6_MIN_MTU		1280
 #endif
 
+/* Maximum number of addresses in context address array */
+#define INANY_MAX_ADDRS		32
+
 int ip4_class_prefix_len(const struct in_addr *addr);
 
 #endif /* IP_H */
diff --git a/ndp.c b/ndp.c
index 1f2bcb0..ed8c6ae 100644
--- a/ndp.c
+++ b/ndp.c
@@ -257,7 +257,7 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
 			.valid_lifetime		= ~0U,
 			.pref_lifetime		= ~0U,
 		},
-		.prefix = c->ip6.addr,
+		.prefix = IN6ADDR_ANY_INIT,
 		.source_ll = {
 			.header = {
 				.type		= OPT_SRC_L2_ADDR,
@@ -265,8 +265,13 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
 			},
 		},
 	};
+	struct inany_addr_entry *e = first_v6(c);
 	unsigned char *ptr = NULL;
 
+	ASSERT(e);
+
+	ra.prefix = e->addr.a6;
+
 	ptr = &ra.var[0];
 
 	if (c->mtu) {
@@ -460,6 +465,7 @@ first:
  */
 void ndp_send_init_req(const struct ctx *c)
 {
+	struct inany_addr_entry *e = first_v6(c);
 	struct ndp_ns ns = {
 		.ih = {
 			.icmp6_type		= NS,
@@ -468,8 +474,13 @@ void ndp_send_init_req(const struct ctx *c)
 			.icmp6_solicited	= 0, /* Reserved */
 			.icmp6_override		= 0, /* Reserved */
 		},
-		.target_addr = c->ip6.addr
+		.target_addr = IN6ADDR_ANY_INIT
 	};
+
+	if (!e)
+		return;
+
+	ns.target_addr = e->addr.a6;
 	debug("Sending initial NDP NS request for guest MAC address");
-	ndp_send(c, &c->ip6.addr, &ns, sizeof(ns));
+	ndp_send(c, &e->addr.a6, &ns, sizeof(ns));
 }
diff --git a/passt.h b/passt.h
index 299185b..1aa71f0 100644
--- a/passt.h
+++ b/passt.h
@@ -64,11 +64,21 @@ enum passt_modes {
 	MODE_VU,
 };
 
+/**
+ * struct inany_addr_entry - 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 inany_addr_entry {
+	union inany_addr addr;
+	uint8_t prefix_len;
+	uint8_t flags;
+};
+
 /**
  * struct ip4_ctx - IPv4 execution context
- * @addr:		IPv4 address assigned to guest
  * @addr_seen:		Latest IPv4 address seen as source from tap
- * @prefixlen:		IPv4 prefix length (netmask)
  * @guest_gw:		IPv4 gateway as seen by the guest
  * @map_host_loopback:	Outbound connections to this address are NATted to the
  *                      host's 127.0.0.1
@@ -84,10 +94,7 @@ enum passt_modes {
  * @no_copy_addrs:	Don't copy all addresses when configuring namespace
  */
 struct ip4_ctx {
-	/* PIF_TAP addresses */
-	struct in_addr addr;
 	struct in_addr addr_seen;
-	int prefix_len;
 	struct in_addr guest_gw;
 	struct in_addr map_host_loopback;
 	struct in_addr map_guest_addr;
@@ -107,7 +114,6 @@ struct ip4_ctx {
 
 /**
  * struct ip6_ctx - IPv6 execution context
- * @addr:		IPv6 address assigned to guest
  * @addr_seen:		Latest IPv6 global/site address seen as source from tap
  * @addr_ll_seen:	Latest IPv6 link-local address seen as source from tap
  * @guest_gw:		IPv6 gateway as seen by the guest
@@ -125,8 +131,6 @@ struct ip4_ctx {
  * @no_copy_addrs:	Don't copy all addresses when configuring namespace
  */
 struct ip6_ctx {
-	/* PIF_TAP addresses */
-	struct in6_addr addr;
 	struct in6_addr addr_seen;
 	struct in6_addr addr_ll_seen;
 	struct in6_addr guest_gw;
@@ -181,6 +185,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
@@ -258,6 +264,9 @@ struct ctx {
 	int ifi6;
 	struct ip6_ctx ip6;
 
+	struct inany_addr_entry addrs[INANY_MAX_ADDRS];
+	int addr_count;
+
 	char pasta_ifn[IF_NAMESIZE];
 	unsigned int pasta_ifi;
 	int pasta_conf_ns;
@@ -296,6 +305,45 @@ struct ctx {
 	bool migrate_exit;
 };
 
+/**
+ * first_v4() - Get first IPv4 address entry
+ * @c:		Pointer to struct ctx
+ *
+ * Return: pointer to first IPv4 entry, or NULL if none
+ */
+static inline struct inany_addr_entry *first_v4(const struct ctx *c)
+{
+	for (int i = 0; i < c->addr_count; i++)
+		if (inany_v4(&c->addrs[i].addr))
+			return (struct inany_addr_entry *)&c->addrs[i];
+	return NULL;
+}
+
+/**
+ * first_v6() - Get first IPv6 address entry
+ * @c:		Pointer to struct ctx
+ *
+ * Return: pointer to first IPv6 entry, or NULL if none
+ */
+static inline struct inany_addr_entry *first_v6(const struct ctx *c)
+{
+	for (int i = 0; i < c->addr_count; i++)
+		if (!inany_v4(&c->addrs[i].addr))
+			return (struct inany_addr_entry *)&c->addrs[i];
+	return NULL;
+}
+
+/**
+ * inany_prefix4() - Get IPv4 prefix length from address entry
+ * @e:		Address entry (must be IPv4)
+ *
+ * Return: prefix length in IPv4 format (0-32)
+ */
+static inline int inany_prefix4(const struct inany_addr_entry *e)
+{
+	return e->prefix_len - 96;
+}
+
 void proto_update_l2_buf(const unsigned char *eth_d);
 
 #endif /* PASST_H */
diff --git a/pasta.c b/pasta.c
index bab945f..54010de 100644
--- a/pasta.c
+++ b/pasta.c
@@ -330,6 +330,7 @@ void pasta_ns_conf(struct ctx *c)
 
 	if (c->pasta_conf_ns) {
 		unsigned int flags = IFF_UP;
+		struct inany_addr_entry *e;
 
 		if (c->mtu)
 			nl_link_set_mtu(nl_sock_ns, c->pasta_ifi, c->mtu);
@@ -341,10 +342,12 @@ void pasta_ns_conf(struct ctx *c)
 
 		if (c->ifi4) {
 			if (c->ip4.no_copy_addrs) {
-				rc = nl_addr_set(nl_sock_ns, c->pasta_ifi,
-						 AF_INET,
-						 &c->ip4.addr,
-						 c->ip4.prefix_len);
+				e = first_v4(c);
+				if (e)
+					rc = nl_addr_set(nl_sock_ns,
+							 c->pasta_ifi, AF_INET,
+							 inany_v4(&e->addr),
+							 e->prefix_len - 96);
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi4,
 						 nl_sock_ns, c->pasta_ifi,
@@ -397,11 +400,13 @@ ipv4_done:
 					  0, IFF_NOARP);
 
 			if (c->ip6.no_copy_addrs) {
-				if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
+				e = first_v6(c);
+				if (e)
 					rc = nl_addr_set(nl_sock_ns,
-							 c->pasta_ifi, AF_INET6,
-							 &c->ip6.addr, 64);
-				}
+							 c->pasta_ifi,
+							 AF_INET6,
+							 &e->addr.a6,
+							 e->prefix_len);
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi6,
 						 nl_sock_ns, c->pasta_ifi,
diff --git a/tap.c b/tap.c
index eaa6111..7f79add 100644
--- a/tap.c
+++ b/tap.c
@@ -47,6 +47,7 @@
 #include "ip.h"
 #include "iov.h"
 #include "passt.h"
+#include "conf.h"
 #include "arp.h"
 #include "dhcp.h"
 #include "ndp.h"
@@ -951,8 +952,14 @@ resume:
 				c->ip6.addr_seen = *saddr;
 			}
 
-			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
-				c->ip6.addr = *saddr;
+			if (!first_v6(c) && c->addr_count < INANY_MAX_ADDRS) {
+				struct inany_addr_entry *e;
+
+				e = &c->addrs[c->addr_count++];
+				e->addr.a6 = *saddr;
+				e->prefix_len = 64;
+				e->flags = CONF_ADDR_LINKLOCAL;
+			}
 		} else if (!IN6_IS_ADDR_UNSPECIFIED(saddr)){
 			c->ip6.addr_seen = *saddr;
 		}
-- 
2.52.0


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

* [PATCH v5 02/13] ip: Introduce for_each_addr() macro for address iteration
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 01/13] ip: Introduce unified multi-address data structures Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 03/13] fwd: Unify guest accessibility checks with unified address array Jon Maloy
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

Add the for_each_addr() macro to iterate over addresses in the unified
array. The macro supports an address family filter parameter (AF_INET,
AF_INET6, or 0 for all) using a _next_addr_idx() helper function to
skip non-matching entries.

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

---
v1: - Broke out as separate commit
    - I kept the third argument, despite David's comment, since I still
      find it practical. If we want to iterate the list without filter
      we can just use AF_UNSPEC (== 0)
---
 passt.h | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/passt.h b/passt.h
index 1aa71f0..bb56998 100644
--- a/passt.h
+++ b/passt.h
@@ -344,6 +344,40 @@ static inline int inany_prefix4(const struct inany_addr_entry *e)
 	return e->prefix_len - 96;
 }
 
+/**
+ * _next_addr_idx() - Find next address index matching family filter
+ * @c:		Pointer to struct ctx
+ * @i:		Starting index
+ * @af:		Address family filter: AF_INET, AF_INET6, or 0 for all
+ *
+ * Return: next matching index, or addr_count if none found
+ */
+static inline int _next_addr_idx(const struct ctx *c, int i, sa_family_t af)
+{
+	for (; i < c->addr_count; i++) {
+		sa_family_t entry_af;
+
+		entry_af = inany_v4(&c->addrs[i].addr) ? AF_INET : AF_INET6;
+
+		if (!af || af == entry_af)
+			return i;
+	}
+	return i;
+}
+
+/**
+ * for_each_addr() - Iterate over addresses in unified array
+ * @e:		Pointer variable for current entry (struct inany_addr_entry *)
+ * @c:		Pointer to struct ctx
+ * @af:		Address family filter: AF_INET, AF_INET6, or 0 for all
+ *
+ * Note: @_i is the internal loop counter, uses _next_addr_idx() helper
+ */
+#define for_each_addr(e, c, af)						\
+	for (int _i = _next_addr_idx((c), 0, (af));			\
+	     _i < (c)->addr_count && ((e) = &(c)->addrs[_i], true);	\
+	     _i = _next_addr_idx((c), _i + 1, (af)))
+
 void proto_update_l2_buf(const unsigned char *eth_d);
 
 #endif /* PASST_H */
-- 
2.52.0


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

* [PATCH v5 03/13] fwd: Unify guest accessibility checks with unified address array
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 01/13] ip: Introduce unified multi-address data structures Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 02/13] ip: Introduce for_each_addr() macro for address iteration Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 04/13] arp: Check all configured addresses in ARP filtering Jon Maloy
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

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

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

diff --git a/fwd.c b/fwd.c
index cf13ddd..edf6a6b 100644
--- a/fwd.c
+++ b/fwd.c
@@ -880,19 +880,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)
 {
-	struct inany_addr_entry *e = first_v4(c);
+	const struct inany_addr_entry *e;
 
-	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,
@@ -900,66 +900,32 @@ 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))
-		return false;
-
-	/* For IPv4, addr_seen is initialised to addr, so is always a valid
-	 * address
-	 */
-	if ((e && IN4_ARE_ADDR_EQUAL(addr, inany_v4(&e->addr))) ||
-	    IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
+	if (inany_is_unspecified4(addr))
 		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)
-{
-	if (IN6_IS_ADDR_LOOPBACK(addr))
-		return false;
+	/* Check against all configured guest addresses */
+	for_each_addr(e, c, 0)
+		if (inany_equals(addr, &e->addr))
+			return false;
 
-	if (first_v6(c) && IN6_ARE_ADDR_EQUAL(addr, &first_v6(c)->addr.a6))
+	/* Also check addr_seen: it tracks the address the guest is actually
+	 * using, which may differ from configured addresses.
+	 */
+	if (inany_equals4(addr, &c->ip4.addr_seen))
 		return false;
 
 	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
 	 * 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) &&
-	    IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addr_seen))
+	if (!inany_v4(addr) &&
+	    !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen) &&
+	    inany_equals6(addr, &c->ip6.addr_seen))
 		return false;
 
 	return true;
 }
 
-/**
- * fwd_guest_accessible() - Is IPv[46] address guest-accessible
- * @c:		Execution context
- * @addr:	Host visible IPv[46] address
- *
- * Return: true if @addr on the host is accessible to the guest without
- *         translation, false otherwise
- */
-static bool fwd_guest_accessible(const struct ctx *c,
-				 const union inany_addr *addr)
-{
-	const struct in_addr *a4 = inany_v4(addr);
-
-	if (a4)
-		return fwd_guest_accessible4(c, a4);
-
-	return fwd_guest_accessible6(c, &addr->a6);
-}
-
 /**
  * nat_outbound() - Apply address translation for outbound (TAP to HOST)
  * @c:		Execution context
-- 
2.52.0


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

* [PATCH v5 04/13] arp: Check all configured addresses in ARP filtering
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (2 preceding siblings ...)
  2026-02-22 17:44 ` [PATCH v5 03/13] fwd: Unify guest accessibility checks with unified address array Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 05/13] netlink: Return prefix length for IPv6 addresses in nl_addr_get() Jon Maloy
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

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

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

---
v3: Adapted to single-array changes earlier in this series
---
 arp.c | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/arp.c b/arp.c
index 99a6a67..8beb2dd 100644
--- a/arp.c
+++ b/arp.c
@@ -41,7 +41,7 @@
 static bool ignore_arp(const struct ctx *c,
 		       const struct arphdr *ah, const struct arpmsg *am)
 {
-	struct inany_addr_entry *e = first_v4(c);
+	const struct inany_addr_entry *e;
 
 	if (ah->ar_hrd != htons(ARPHRD_ETHER)	||
 	    ah->ar_pro != htons(ETH_P_IP)	||
@@ -55,9 +55,10 @@ static bool ignore_arp(const struct ctx *c,
 	    !memcmp(am->sip, am->tip, sizeof(am->sip)))
 		return true;
 
-	/* Don't resolve the guest's assigned address, either. */
-	if (e && !memcmp(am->tip, inany_v4(&e->addr), sizeof(am->tip)))
-		return true;
+	/* Don't resolve any of the guest's addresses */
+	for_each_addr(e, c, AF_INET)
+		if (!memcmp(am->tip, inany_v4(&e->addr), sizeof(am->tip)))
+			return true;
 
 	return false;
 }
-- 
2.52.0


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

* [PATCH v5 05/13] netlink: Return prefix length for IPv6 addresses in nl_addr_get()
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (3 preceding siblings ...)
  2026-02-22 17:44 ` [PATCH v5 04/13] arp: Check all configured addresses in ARP filtering Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 06/13] conf: Allow multiple -a/--address options per address family Jon Maloy
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

nl_addr_get() was not setting the prefix_len output parameter for
IPv6 addresses, only for IPv4. This meant callers always got 0 for
IPv6, forcing them to use a hardcoded default (64).

Fix by assigning *prefix_len in the IPv6 case, matching the IPv4
behavior.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
 conf.c    | 2 +-
 netlink.c | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/conf.c b/conf.c
index f2ff8af..eb967c7 100644
--- a/conf.c
+++ b/conf.c
@@ -814,7 +814,7 @@ static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
 	if (!e) {
 		e = &c->addrs[c->addr_count++];
 		e->addr = addr;
-		e->prefix_len = prefix_len ? prefix_len : 64;
+		e->prefix_len = prefix_len;
 		e->flags = CONF_ADDR_HOST;
 	}
 
diff --git a/netlink.c b/netlink.c
index 82a2f0c..769cb23 100644
--- a/netlink.c
+++ b/netlink.c
@@ -752,7 +752,7 @@ int nl_addr_set_ll_nodad(int s, unsigned int ifi)
  * @ifi:	Interface index in outer network namespace
  * @af:		Address family
  * @addr:	Global address to fill
- * @prefix_len:	Mask or prefix length, to fill (for IPv4)
+ * @prefix_len:	Mask or prefix length, to fill
  * @addr_l:	Link-scoped address to fill (for IPv6)
  *
  * Return: 0 on success, negative error code on failure
@@ -797,7 +797,7 @@ int nl_addr_get(int s, unsigned int ifi, sa_family_t af,
 				   ifa->ifa_prefixlen > prefix_max) {
 				memcpy(addr, RTA_DATA(rta), RTA_PAYLOAD(rta));
 
-				prefix_max = ifa->ifa_prefixlen;
+				prefix_max = *prefix_len = ifa->ifa_prefixlen;
 			}
 
 			if (addr_l &&
-- 
2.52.0


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

* [PATCH v5 06/13] conf: Allow multiple -a/--address options per address family
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (4 preceding siblings ...)
  2026-02-22 17:44 ` [PATCH v5 05/13] netlink: Return prefix length for IPv6 addresses in nl_addr_get() Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 07/13] ip: Track observed guest IPv4 addresses in unified address array Jon Maloy
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

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

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

---
v2: - Adapted to previous code changes
v3: - Adapted to single-array strategy
    - Changes according to feedback from S. Brivio and G Gibson.
v4: - Stripped down and adapted after feedback from David G.
---
 conf.c  | 41 ++++++++++++++++++++++++-----------------
 pasta.c | 17 ++++++++++-------
 2 files changed, 34 insertions(+), 24 deletions(-)

diff --git a/conf.c b/conf.c
index eb967c7..35ef994 100644
--- a/conf.c
+++ b/conf.c
@@ -915,9 +915,10 @@ 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 32 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");
+	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"
@@ -1874,32 +1875,38 @@ void conf(struct ctx *c, int argc, char **argv)
 			    IN6_IS_ADDR_V4COMPAT(&addr.a6))
 				die("Invalid address: %s", optarg);
 
-			/* Legacy behaviour: overwrite existing address if any */
-			if (inany_v4(&addr)) {
-				e = first_v4(c);
-				if (!e)
-					e = &c->addrs[c->addr_count++];
-				if (c->mode == MODE_PASTA)
-					c->ip4.no_copy_addrs = true;
-			} else {
-				e = first_v6(c);
-				if (!e)
-					e = &c->addrs[c->addr_count++];
-				if (c->mode == MODE_PASTA)
-					c->ip6.no_copy_addrs = true;
-			}
+			for_each_addr(e, c, AF_UNSPEC)
+				if (inany_equals(&addr, &e->addr))
+					die("Address exists: %s", optarg);
+
+			if (c->addr_count >= INANY_MAX_ADDRS)
+				die("Max addresses is %d", INANY_MAX_ADDRS);
+
+			e = &c->addrs[c->addr_count++];
 			e->prefix_len = prefix_len;
 			e->addr = addr;
 			e->flags = CONF_ADDR_USER;
+
+			if (c->mode != MODE_PASTA)
+				break;
+
+			if (inany_v4(&addr))
+				c->ip4.no_copy_addrs = true;
+			else
+				c->ip6.no_copy_addrs = true;
 			break;
 		}
 		case 'n': {
 			struct inany_addr_entry *e;
-			int plen;
+			int plen, i = 0;
 
 			if (addr_has_prefix_len)
 				die("Redundant prefix length specification");
 
+			for_each_addr(e, c, AF_INET)
+				if (++i > 1)
+					die("Use -n only when one address");
+
 			plen = conf_ip4_prefix(optarg);
 			if (plen < 0)
 				die("Invalid prefix length: %s", optarg);
diff --git a/pasta.c b/pasta.c
index 54010de..7a081e7 100644
--- a/pasta.c
+++ b/pasta.c
@@ -342,12 +342,14 @@ void pasta_ns_conf(struct ctx *c)
 
 		if (c->ifi4) {
 			if (c->ip4.no_copy_addrs) {
-				e = first_v4(c);
-				if (e)
+				for_each_addr(e, c, AF_INET) {
 					rc = nl_addr_set(nl_sock_ns,
 							 c->pasta_ifi, AF_INET,
 							 inany_v4(&e->addr),
-							 e->prefix_len - 96);
+							 inany_prefix4(e));
+					if (rc < 0)
+						break;
+				}
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi4,
 						 nl_sock_ns, c->pasta_ifi,
@@ -400,13 +402,14 @@ ipv4_done:
 					  0, IFF_NOARP);
 
 			if (c->ip6.no_copy_addrs) {
-				e = first_v6(c);
-				if (e)
+				for_each_addr(e, c, AF_INET6) {
 					rc = nl_addr_set(nl_sock_ns,
 							 c->pasta_ifi,
-							 AF_INET6,
-							 &e->addr.a6,
+							 AF_INET6, &e->addr.a6,
 							 e->prefix_len);
+					if (rc < 0)
+						break;
+				}
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi6,
 						 nl_sock_ns, c->pasta_ifi,
-- 
2.52.0


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

* [PATCH v5 07/13] ip: Track observed guest IPv4 addresses in unified address array
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (5 preceding siblings ...)
  2026-02-22 17:44 ` [PATCH v5 06/13] conf: Allow multiple -a/--address options per address family Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 08/13] ip: Track observed guest IPv6 " Jon Maloy
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

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

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

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

---
v4: - Removed migration protocol update, to be added in later commit
    - Allow only one OBSERVED address at a time
    - Some other changes based on feedback from David G
v5: - Allowing multiple observed IPv4 addresses
---
 conf.c    |   2 -
 conf.h    |   1 +
 fwd.c     | 223 +++++++++++++++++++++++++++++++++++++++++++++++-------
 fwd.h     |   9 +++
 inany.h   |   3 +
 migrate.c |  17 ++++-
 passt.h   |   2 -
 tap.c     |  17 ++++-
 8 files changed, 239 insertions(+), 35 deletions(-)

diff --git a/conf.c b/conf.c
index 35ef994..faa60ec 100644
--- a/conf.c
+++ b/conf.c
@@ -747,7 +747,6 @@ static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
 		e->addr = inany_from_v4(addr);
 		e->prefix_len = prefix_len + 96;
 		e->flags = CONF_ADDR_HOST;
-		ip4->addr_seen = addr;
 	}
 
 	ip4->our_tap_addr = ip4->guest_gw;
@@ -764,7 +763,6 @@ static void conf_ip4_local(struct ctx *c)
 	struct inany_addr_entry *e = &c->addrs[c->addr_count++];
 	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;
 	e->addr = inany_from_v4(IP4_LL_GUEST_ADDR);
diff --git a/conf.h b/conf.h
index bfad36f..8b10ac6 100644
--- a/conf.h
+++ b/conf.h
@@ -12,6 +12,7 @@
 #define CONF_ADDR_USER		BIT(0)		/* User set via -a */
 #define CONF_ADDR_HOST		BIT(1)		/* From host interface */
 #define CONF_ADDR_LINKLOCAL	BIT(2)		/* Link-local address */
+#define CONF_ADDR_OBSERVED	BIT(3)		/* Seen in guest traffic */
 
 enum passt_modes conf_mode(int argc, char *argv[]);
 void conf(struct ctx *c, int argc, char **argv);
diff --git a/fwd.c b/fwd.c
index edf6a6b..9141e37 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"
@@ -879,6 +880,159 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
 		((ini->oport == 53) || (ini->oport == 853));
 }
 
+/**
+ * fwd_get_addr() - Get guest address entry matching criteria
+ * @c:		Execution context
+ * @af:		Address family (AF_INET, AF_INET6, or 0 for any)
+ * @incl:	Flags that must be present (any-match)
+ * @excl:	Flags that must not be present
+ *
+ * Return: first address entry matching criteria, or NULL
+ */
+const struct inany_addr_entry *fwd_get_addr(const struct ctx *c,
+					      sa_family_t af,
+					      uint8_t incl, uint8_t excl)
+{
+	const struct inany_addr_entry *e;
+
+	for_each_addr(e, c, af) {
+		if (incl && !(e->flags & incl))
+			continue;
+		if (e->flags & excl)
+			continue;
+		return e;
+	}
+
+	return NULL;
+}
+
+/**
+ * fwd_select_addr() - Select address with priority-based search
+ * @c:		Execution context
+ * @af:		Address family (AF_INET or AF_INET6)
+ * @first:	First priority flags to match (or 0 to skip)
+ * @second:	Second priority flags to match (or 0 to skip)
+ * @third:	Third priority flags to match (or 0 to skip)
+ * @skip:	Flags to exclude from search
+ *
+ * Search for address entries in priority order.
+ *
+ * Return: pointer to selected address entry, or NULL if none found
+ */
+struct inany_addr_entry *fwd_select_addr(const struct ctx *c, int af,
+					 int first, int second, int third,
+					 int skip)
+{
+	const struct inany_addr_entry *e;
+
+	if (first) {
+		e = fwd_get_addr(c, af, first, skip);
+		if (e)
+			return (struct inany_addr_entry *)e;
+	}
+
+	if (second) {
+		e = fwd_get_addr(c, af, second, skip);
+		if (e)
+			return (struct inany_addr_entry *)e;
+	}
+
+	if (third) {
+		e = fwd_get_addr(c, af, third, skip);
+		if (e)
+			return (struct inany_addr_entry *)e;
+	}
+
+	return NULL;
+}
+
+/**
+ * fwd_set_addr() - Update address entry, adding one if needed
+ * @c:		Execution context
+ * @addr:	Address to add and/or set flags/prefix on
+ * @flags:	Flags to set
+ * @prefix_len:	Prefix length (0 to leave unchanged, if any)
+ *
+ * If CONF_ADDR_OBSERVED is in @flags, insert at position 0.
+ */
+void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
+		  uint8_t flags, int prefix_len)
+{
+	struct inany_addr_entry *e;
+	int i;
+
+	for (i = 0; i < c->addr_count; i++) {
+		e = &c->addrs[i];
+		if (!inany_equals(&e->addr, addr))
+			continue;
+
+		/* Update prefix_len if provided and applicable */
+		if (prefix_len && !(e->flags & CONF_ADDR_USER))
+			e->prefix_len = prefix_len;
+
+		/* Nothing more to change */
+		if ((e->flags & flags) == flags)
+			return;
+
+		/* OBSERVED address moves to position 0: remove, re-add later */
+		if (flags & CONF_ADDR_OBSERVED) {
+			flags |= e->flags;
+			prefix_len = e->prefix_len;
+			memmove(e, e + 1, (c->addr_count - i - 1) * sizeof(*e));
+			c->addr_count--;
+			break;
+		}
+
+		e->flags |= flags;
+		return;
+	}
+
+	if (c->addr_count >= INANY_MAX_ADDRS) {
+		debug("Address table full, can't add address");
+		return;
+	}
+
+	if (flags & CONF_ADDR_OBSERVED) {
+		memmove(&c->addrs[1], &c->addrs[0], c->addr_count * sizeof(*e));
+		e = &c->addrs[0];
+	} else {
+		e = &c->addrs[c->addr_count];
+	}
+	c->addr_count++;
+	e->addr = *addr;
+	e->prefix_len = prefix_len;
+	e->flags = flags;
+}
+
+/**
+ * fwd_remove_addr() - Remove an address from the unified array
+ * @c:		Execution context
+ * @addr:	Address to remove
+ *
+ * User-configured addresses (CONF_ADDR_USER) are not removed.
+ *
+ * Return: true if removed, false if not found or user-configured
+ */
+bool fwd_remove_addr(struct ctx *c, const union inany_addr *addr)
+{
+	int i;
+
+	for (i = 0; i < c->addr_count; i++) {
+		if (!inany_equals(&c->addrs[i].addr, addr))
+			continue;
+
+		if (c->addrs[i].flags & CONF_ADDR_USER)
+			return false;
+
+		c->addr_count--;
+		memmove(&c->addrs[i], &c->addrs[i + 1],
+			(c->addr_count - i) * sizeof(c->addrs[0]));
+		return true;
+	}
+
+	return false;
+}
+
 /**
  * fwd_guest_accessible() - Is address guest-accessible
  * @c:		Execution context
@@ -903,26 +1057,11 @@ static bool fwd_guest_accessible(const struct ctx *c,
 	if (inany_is_unspecified4(addr))
 		return false;
 
-	/* Check against all configured guest addresses */
+	/* Check against all configured and observed guest addresses */
 	for_each_addr(e, c, 0)
 		if (inany_equals(addr, &e->addr))
 			return false;
 
-	/* Also check addr_seen: it tracks the address the guest is actually
-	 * using, which may differ from configured addresses.
-	 */
-	if (inany_equals4(addr, &c->ip4.addr_seen))
-		return false;
-
-	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
-	 * address the guest will take until we see it.  Only check against it
-	 * if it has been set to a real address.
-	 */
-	if (!inany_v4(addr) &&
-	    !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen) &&
-	    inany_equals6(addr, &c->ip6.addr_seen))
-		return false;
-
 	return true;
 }
 
@@ -1099,16 +1238,36 @@ 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 inany_addr_entry *e;
+
+				e = fwd_select_addr(c, AF_INET,
+						    CONF_ADDR_OBSERVED,
+						    CONF_ADDR_USER |
+						    CONF_ADDR_HOST, 0, 0);
+				if (!e)
+					return PIF_NONE;
+
+				tgt->eaddr = e->addr;
+			}
 			tgt->oaddr = inany_any4;
 		} else {
-			if (c->host_lo_to_ns_lo)
+			if (c->host_lo_to_ns_lo) {
 				tgt->eaddr = inany_loopback6;
-			else
-				tgt->eaddr.a6 = c->ip6.addr_seen;
+			} else {
+				const struct inany_addr_entry *e;
+
+				e = fwd_select_addr(c, AF_INET6,
+						    CONF_ADDR_OBSERVED,
+						    CONF_ADDR_USER |
+						    CONF_ADDR_HOST,
+						    0, CONF_ADDR_LINKLOCAL);
+				if (!e)
+					return PIF_NONE;
+				tgt->eaddr = e->addr;
+			}
 			tgt->oaddr = inany_any6;
 		}
 
@@ -1137,12 +1296,22 @@ 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 inany_addr_entry *e;
+
+		e = fwd_get_addr(c, AF_INET, CONF_ADDR_OBSERVED, 0);
+		if (!e)
+			return PIF_NONE;
+		tgt->eaddr = e->addr;
 	} else {
-		if (inany_is_linklocal6(&tgt->oaddr))
-			tgt->eaddr.a6 = c->ip6.addr_ll_seen;
-		else
-			tgt->eaddr.a6 = c->ip6.addr_seen;
+		bool linklocal = inany_is_linklocal6(&tgt->oaddr);
+		int excl = linklocal ? 0 : CONF_ADDR_LINKLOCAL;
+		const struct inany_addr_entry *e;
+
+		e = fwd_select_addr(c, AF_INET6, CONF_ADDR_OBSERVED,
+				    CONF_ADDR_USER | CONF_ADDR_HOST, 0, excl);
+		if (!e)
+			return PIF_NONE;
+		tgt->eaddr = e->addr;
 	}
 
 	return PIF_TAP;
diff --git a/fwd.h b/fwd.h
index 1607011..698b407 100644
--- a/fwd.h
+++ b/fwd.h
@@ -15,6 +15,15 @@ struct flowside;
 
 void fwd_probe_ephemeral(void);
 bool fwd_port_is_ephemeral(in_port_t port);
+const struct inany_addr_entry *fwd_get_addr(const struct ctx *c,
+					      sa_family_t af,
+					      uint8_t incl, uint8_t excl);
+struct inany_addr_entry *fwd_select_addr(const struct ctx *c, int af,
+					 int first, int second, int third,
+					 int skip);
+void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
+		  uint8_t flags, int prefix_len);
+bool fwd_remove_addr(struct ctx *c, const union inany_addr *addr);
 
 /**
  * struct fwd_rule - Forwarding rule governing a range of ports
diff --git a/inany.h b/inany.h
index a6069de..fd913ae 100644
--- a/inany.h
+++ b/inany.h
@@ -54,6 +54,9 @@ extern const union inany_addr inany_any4;
 #define inany_from_v4(a4)	\
 	((union inany_addr)INANY_INIT4((a4)))
 
+#define inany_from_v6(addr)	\
+	((union inany_addr){ .a6 = (addr) })
+
 /** 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 13bacab..f026e95 100644
--- a/migrate.c
+++ b/migrate.c
@@ -18,6 +18,8 @@
 #include "util.h"
 #include "ip.h"
 #include "passt.h"
+#include "conf.h"
+#include "fwd.h"
 #include "inany.h"
 #include "flow.h"
 #include "flow_table.h"
@@ -57,11 +59,16 @@ static int seen_addrs_source_v1(struct ctx *c,
 	struct migrate_seen_addrs_v1 addrs = {
 		.addr6 = c->ip6.addr_seen,
 		.addr6_ll = c->ip6.addr_ll_seen,
-		.addr4 = c->ip4.addr_seen,
 	};
+	const struct inany_addr_entry *e;
 
 	(void)stage;
 
+	/* IPv4 observed address */
+	e = fwd_get_addr(c, AF_INET, CONF_ADDR_OBSERVED, 0);
+	if (e)
+		addrs.addr4 = *inany_v4(&e->addr);
+
 	memcpy(addrs.mac, c->guest_mac, sizeof(addrs.mac));
 
 	if (write_all_buf(fd, &addrs, sizeof(addrs)))
@@ -82,6 +89,7 @@ static int seen_addrs_target_v1(struct ctx *c,
 				const struct migrate_stage *stage, int fd)
 {
 	struct migrate_seen_addrs_v1 addrs;
+	struct in_addr addr4;
 
 	(void)stage;
 
@@ -90,7 +98,12 @@ static int seen_addrs_target_v1(struct ctx *c,
 
 	c->ip6.addr_seen = addrs.addr6;
 	c->ip6.addr_ll_seen = addrs.addr6_ll;
-	c->ip4.addr_seen = addrs.addr4;
+
+	/* Copy from packed struct to avoid alignment issues */
+	addr4 = addrs.addr4;
+	if (addr4.s_addr)
+		fwd_set_addr(c, &inany_from_v4(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 bb56998..b808a19 100644
--- a/passt.h
+++ b/passt.h
@@ -78,7 +78,6 @@ struct inany_addr_entry {
 
 /**
  * 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
@@ -94,7 +93,6 @@ struct inany_addr_entry {
  * @no_copy_addrs:	Don't copy all addresses when configuring namespace
  */
 struct ip4_ctx {
-	struct in_addr addr_seen;
 	struct in_addr guest_gw;
 	struct in_addr map_host_loopback;
 	struct in_addr map_guest_addr;
diff --git a/tap.c b/tap.c
index 7f79add..30e52f7 100644
--- a/tap.c
+++ b/tap.c
@@ -48,6 +48,7 @@
 #include "iov.h"
 #include "passt.h"
 #include "conf.h"
+#include "fwd.h"
 #include "arp.h"
 #include "dhcp.h"
 #include "ndp.h"
@@ -162,6 +163,17 @@ void tap_send_single(const struct ctx *c, const void *data, size_t l2len)
 	}
 }
 
+/**
+ * tap_check_src_addr4() - Note an IPv4 address seen in guest traffic
+ * @c:		Execution context
+ * @addr:	IPv4 address seen as source from guest
+ */
+static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
+{
+	if (addr->s_addr)
+		fwd_set_addr(c, &inany_from_v4(*addr), CONF_ADDR_OBSERVED, 0);
+}
+
 /**
  * tap_ip6_daddr() - Normal IPv6 destination address for inbound packets
  * @c:		Execution context
@@ -772,8 +784,9 @@ resume:
 			continue;
 		}
 
-		if (iph->saddr && c->ip4.addr_seen.s_addr != iph->saddr)
-			c->ip4.addr_seen.s_addr = iph->saddr;
+		if (iph->saddr)
+			tap_check_src_addr4(c,
+					    (const struct in_addr *)&iph->saddr);
 
 		if (!iov_drop_header(&data, hlen))
 			continue;
-- 
2.52.0


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

* [PATCH v5 08/13] ip: Track observed guest IPv6 addresses in unified address array
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (6 preceding siblings ...)
  2026-02-22 17:44 ` [PATCH v5 07/13] ip: Track observed guest IPv4 addresses in unified address array Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 09/13] migrate: Rename v1 address functions to v2 for clarity Jon Maloy
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

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

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

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

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

---
v5: - Made to use same algorithm and function as IPv4 for inserting
      observed into the array.
---
 conf.c    |  2 --
 dhcpv6.c  |  6 +-----
 dhcpv6.h  |  2 +-
 migrate.c | 27 +++++++++++++++++++-------
 passt.h   |  4 ----
 pasta.c   | 17 +++++++++++++----
 tap.c     | 57 +++++++++++++++++++++++++++++++++----------------------
 7 files changed, 69 insertions(+), 46 deletions(-)

diff --git a/conf.c b/conf.c
index faa60ec..f622424 100644
--- a/conf.c
+++ b/conf.c
@@ -816,8 +816,6 @@ static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
 		e->flags = CONF_ADDR_HOST;
 	}
 
-	ip6->addr_seen = e->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 55c17cf..ba200e5 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -549,7 +549,7 @@ static size_t dhcpv6_client_fqdn_fill(const struct iov_tail *data,
  * Return: 0 if it's not a DHCPv6 message, 1 if handled, -1 on failure
  */
 int dhcpv6(struct ctx *c, struct iov_tail *data,
-	   const struct in6_addr *saddr, const struct in6_addr *daddr)
+	   const struct in6_addr *daddr)
 {
 	const struct opt_server_id *server_id = NULL;
 	struct inany_addr_entry *e = first_v6(c);
@@ -591,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);
@@ -683,8 +681,6 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 
 	tap_udp6_send(c, src, 547, tap_ip6_daddr(c, src), 546,
 		      mh->xid, &resp, n);
-	if (e)
-		c->ip6.addr_seen = e->addr.a6;
 
 	return 1;
 }
diff --git a/dhcpv6.h b/dhcpv6.h
index c706dfd..8cbc769 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);
+	   struct in6_addr *daddr);
 void dhcpv6_init(const struct ctx *c);
 
 #endif /* DHCPV6_H */
diff --git a/migrate.c b/migrate.c
index f026e95..1e2830f 100644
--- a/migrate.c
+++ b/migrate.c
@@ -56,10 +56,7 @@ struct migrate_seen_addrs_v1 {
 static int seen_addrs_source_v1(struct ctx *c,
 				const struct migrate_stage *stage, int fd)
 {
-	struct migrate_seen_addrs_v1 addrs = {
-		.addr6 = c->ip6.addr_seen,
-		.addr6_ll = c->ip6.addr_ll_seen,
-	};
+	struct migrate_seen_addrs_v1 addrs = { 0 };
 	const struct inany_addr_entry *e;
 
 	(void)stage;
@@ -69,6 +66,15 @@ static int seen_addrs_source_v1(struct ctx *c,
 	if (e)
 		addrs.addr4 = *inany_v4(&e->addr);
 
+	/* IPv6 observed address */
+	e = fwd_get_addr(c, AF_INET6, CONF_ADDR_OBSERVED, 0);
+	if (e) {
+		if (e->flags & CONF_ADDR_LINKLOCAL)
+			addrs.addr6_ll = e->addr.a6;
+		else
+			addrs.addr6 = e->addr.a6;
+	}
+
 	memcpy(addrs.mac, c->guest_mac, sizeof(addrs.mac));
 
 	if (write_all_buf(fd, &addrs, sizeof(addrs)))
@@ -89,6 +95,7 @@ static int seen_addrs_target_v1(struct ctx *c,
 				const struct migrate_stage *stage, int fd)
 {
 	struct migrate_seen_addrs_v1 addrs;
+	struct in6_addr addr6, addr6_ll;
 	struct in_addr addr4;
 
 	(void)stage;
@@ -96,14 +103,20 @@ static int seen_addrs_target_v1(struct ctx *c,
 	if (read_all_buf(fd, &addrs, sizeof(addrs)))
 		return errno;
 
-	c->ip6.addr_seen = addrs.addr6;
-	c->ip6.addr_ll_seen = addrs.addr6_ll;
-
 	/* Copy from packed struct to avoid alignment issues */
 	addr4 = addrs.addr4;
+	addr6 = addrs.addr6;
+	addr6_ll = addrs.addr6_ll;
+
 	if (addr4.s_addr)
 		fwd_set_addr(c, &inany_from_v4(addr4), CONF_ADDR_OBSERVED, 0);
 
+	/* Prefer global over link-local if both present */
+	if (!IN6_IS_ADDR_UNSPECIFIED(&addr6))
+		fwd_set_addr(c, &inany_from_v6(addr6), CONF_ADDR_OBSERVED, 0);
+	else if (!IN6_IS_ADDR_UNSPECIFIED(&addr6_ll))
+		fwd_set_addr(c, &inany_from_v6(addr6_ll), 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 b808a19..e7489ca 100644
--- a/passt.h
+++ b/passt.h
@@ -112,8 +112,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]
@@ -129,8 +127,6 @@ struct ip4_ctx {
  * @no_copy_addrs:	Don't copy all addresses when configuring namespace
  */
 struct ip6_ctx {
-	struct in6_addr addr_seen;
-	struct in6_addr addr_ll_seen;
 	struct in6_addr guest_gw;
 	struct in6_addr map_host_loopback;
 	struct in6_addr map_guest_addr;
diff --git a/pasta.c b/pasta.c
index 7a081e7..56b2e0a 100644
--- a/pasta.c
+++ b/pasta.c
@@ -46,6 +46,8 @@
 
 #include "util.h"
 #include "passt.h"
+#include "conf.h"
+#include "fwd.h"
 #include "isolation.h"
 #include "netlink.h"
 #include "log.h"
@@ -384,12 +386,15 @@ void pasta_ns_conf(struct ctx *c)
 ipv4_done:
 
 		if (c->ifi6) {
-			rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi,
-					    &c->ip6.addr_ll_seen);
-			if (rc < 0) {
+			struct in6_addr addr_ll;
+
+			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_OBSERVED, 0);
 
 			rc = nl_addr_set_ll_nodad(nl_sock_ns, c->pasta_ifi);
 			if (rc < 0) {
@@ -403,6 +408,10 @@ ipv4_done:
 
 			if (c->ip6.no_copy_addrs) {
 				for_each_addr(e, c, AF_INET6) {
+					/* Skip, kernel auto-configures */
+					if (e->flags & CONF_ADDR_LINKLOCAL)
+						continue;
+
 					rc = nl_addr_set(nl_sock_ns,
 							 c->pasta_ifi,
 							 AF_INET6, &e->addr.a6,
diff --git a/tap.c b/tap.c
index 30e52f7..875c2bf 100644
--- a/tap.c
+++ b/tap.c
@@ -174,6 +174,17 @@ static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
 		fwd_set_addr(c, &inany_from_v4(*addr), CONF_ADDR_OBSERVED, 0);
 }
 
+/**
+ * tap_check_src_addr6() - Note an IPv6 address seen in guest traffic
+ * @c:		Execution context
+ * @addr:	IPv6 address seen as source from guest
+ */
+static void tap_check_src_addr6(struct ctx *c, const struct in6_addr *addr)
+{
+	if (!IN6_IS_ADDR_UNSPECIFIED(addr))
+		fwd_set_addr(c, &inany_from_v6(*addr), CONF_ADDR_OBSERVED, 0);
+}
+
 /**
  * tap_ip6_daddr() - Normal IPv6 destination address for inbound packets
  * @c:		Execution context
@@ -184,9 +195,25 @@ static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
 const struct in6_addr *tap_ip6_daddr(const struct ctx *c,
 				     const struct in6_addr *src)
 {
-	if (IN6_IS_ADDR_LINKLOCAL(src))
-		return &c->ip6.addr_ll_seen;
-	return &c->ip6.addr_seen;
+	const struct inany_addr_entry *e;
+
+	if (IN6_IS_ADDR_LINKLOCAL(src)) {
+		/* Link-local: first LL address (observed is at front) */
+		e = fwd_get_addr(c, AF_INET6, CONF_ADDR_LINKLOCAL, 0);
+	} else {
+		/* Global: observed non-LL first, then any non-LL */
+		e = fwd_get_addr(c, AF_INET6, CONF_ADDR_OBSERVED,
+				 CONF_ADDR_LINKLOCAL);
+		if (!e)
+			e = fwd_get_addr(c, AF_INET6, 0, CONF_ADDR_LINKLOCAL);
+	}
+
+	if (e)
+		return &e->addr.a6;
+
+	/* Last resort: first IPv6 address */
+	e = first_v6(c);
+	return e ? &e->addr.a6 : &in6addr_any;
 }
 
 /**
@@ -786,7 +813,7 @@ resume:
 
 		if (iph->saddr)
 			tap_check_src_addr4(c,
-					    (const struct in_addr *)&iph->saddr);
+					   (const struct in_addr *)&iph->saddr);
 
 		if (!iov_drop_header(&data, hlen))
 			continue;
@@ -958,24 +985,8 @@ resume:
 			continue;
 		}
 
-		if (IN6_IS_ADDR_LINKLOCAL(saddr)) {
-			c->ip6.addr_ll_seen = *saddr;
-
-			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen)) {
-				c->ip6.addr_seen = *saddr;
-			}
-
-			if (!first_v6(c) && c->addr_count < INANY_MAX_ADDRS) {
-				struct inany_addr_entry *e;
-
-				e = &c->addrs[c->addr_count++];
-				e->addr.a6 = *saddr;
-				e->prefix_len = 64;
-				e->flags = CONF_ADDR_LINKLOCAL;
-			}
-		} else if (!IN6_IS_ADDR_UNSPECIFIED(saddr)){
-			c->ip6.addr_seen = *saddr;
-		}
+		if (!IN6_IS_ADDR_UNSPECIFIED(saddr))
+			tap_check_src_addr6(c, saddr);
 
 		if (proto == IPPROTO_ICMPV6) {
 			struct iov_tail ndp_data;
@@ -1006,7 +1017,7 @@ resume:
 		if (proto == IPPROTO_UDP) {
 			struct iov_tail uh_data = data;
 
-			if (dhcpv6(c, &uh_data, saddr, daddr))
+			if (dhcpv6(c, &uh_data, daddr))
 				continue;
 		}
 
-- 
2.52.0


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

* [PATCH v5 09/13] migrate: Rename v1 address functions to v2 for clarity
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (7 preceding siblings ...)
  2026-02-22 17:44 ` [PATCH v5 08/13] ip: Track observed guest IPv6 " Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 10/13] migrate: Update protocol to v3 for multi-address support Jon Maloy
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

Some migration address structures and functions have a _v1 suffix.
This is confusing, since they are currently handling version 2 of
the migration protocol. We are now going to introduce a new version
3 of the protocol, so we choose to give these functions the correct
suffix _v2 instead. This is in correspondence with current reality,
and will help make a clearer distinction between the old and the new
versions of those functions.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
Reviewed-by: David Gibson <david@gibson.dropbear.id.au>
---
 migrate.c | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/migrate.c b/migrate.c
index 1e2830f..1990067 100644
--- a/migrate.c
+++ b/migrate.c
@@ -31,13 +31,13 @@
 #define MIGRATE_MAGIC		0xB1BB1D1B0BB1D1B0
 
 /**
- * struct migrate_seen_addrs_v1 - Migratable guest addresses for v1 state stream
+ * struct migrate_seen_addrs_v2 - Migratable guest addresses for v2 protocol
  * @addr6:	Observed guest IPv6 address
  * @addr6_ll:	Observed guest IPv6 link-local address
  * @addr4:	Observed guest IPv4 address
  * @mac:	Observed guest MAC address
  */
-struct migrate_seen_addrs_v1 {
+struct migrate_seen_addrs_v2 {
 	struct in6_addr addr6;
 	struct in6_addr addr6_ll;
 	struct in_addr addr4;
@@ -45,7 +45,7 @@ struct migrate_seen_addrs_v1 {
 } __attribute__((packed));
 
 /**
- * seen_addrs_source_v1() - Copy and send guest observed addresses from source
+ * seen_addrs_source_v2() - Copy and send guest observed addresses from source
  * @c:		Execution context
  * @stage:	Migration stage, unused
  * @fd:		File descriptor for state transfer
@@ -53,10 +53,10 @@ struct migrate_seen_addrs_v1 {
  * Return: 0 on success, positive error code on failure
  */
 /* cppcheck-suppress [constParameterCallback, unmatchedSuppression] */
-static int seen_addrs_source_v1(struct ctx *c,
+static int seen_addrs_source_v2(struct ctx *c,
 				const struct migrate_stage *stage, int fd)
 {
-	struct migrate_seen_addrs_v1 addrs = { 0 };
+	struct migrate_seen_addrs_v2 addrs = { 0 };
 	const struct inany_addr_entry *e;
 
 	(void)stage;
@@ -84,17 +84,17 @@ static int seen_addrs_source_v1(struct ctx *c,
 }
 
 /**
- * seen_addrs_target_v1() - Receive and use guest observed addresses on target
+ * seen_addrs_target_v2() - Receive and use guest observed addresses on target
  * @c:		Execution context
  * @stage:	Migration stage, unused
  * @fd:		File descriptor for state transfer
  *
  * Return: 0 on success, positive error code on failure
  */
-static int seen_addrs_target_v1(struct ctx *c,
+static int seen_addrs_target_v2(struct ctx *c,
 				const struct migrate_stage *stage, int fd)
 {
-	struct migrate_seen_addrs_v1 addrs;
+	struct migrate_seen_addrs_v2 addrs;
 	struct in6_addr addr6, addr6_ll;
 	struct in_addr addr4;
 
@@ -126,8 +126,8 @@ static int seen_addrs_target_v1(struct ctx *c,
 static const struct migrate_stage stages_v2[] = {
 	{
 		.name = "observed addresses",
-		.source = seen_addrs_source_v1,
-		.target = seen_addrs_target_v1,
+		.source = seen_addrs_source_v2,
+		.target = seen_addrs_target_v2,
 	},
 	{
 		.name = "prepare flows",
-- 
2.52.0


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

* [PATCH v5 10/13] migrate: Update protocol to v3 for multi-address support
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (8 preceding siblings ...)
  2026-02-22 17:44 ` [PATCH v5 09/13] migrate: Rename v1 address functions to v2 for clarity Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 11/13] dhcp, dhcpv6: Select addresses for DHCP distribution Jon Maloy
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

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

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

---
v4: - Broke out as separate commit
    - Made number of transferable addresses variable
---
 migrate.c | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 123 insertions(+)

diff --git a/migrate.c b/migrate.c
index 1990067..c2c2403 100644
--- a/migrate.c
+++ b/migrate.c
@@ -122,6 +122,108 @@ 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 with their flags. The receiver can filter
+ * based on flags as needed. This provides forward compatibility if
+ * future versions need different address types.
+ *
+ * Return: 0 on success, positive error code on failure
+ */
+/* cppcheck-suppress [constParameterCallback, unmatchedSuppression] */
+static int addrs_source_v3(struct ctx *c,
+			   const struct migrate_stage *stage, int fd)
+{
+	uint8_t addr_count = c->addr_count;
+
+	(void)stage;
+
+	/* Send count, then all addresses with flags, then MAC */
+	if (write_all_buf(fd, &addr_count, sizeof(addr_count)))
+		return errno;
+
+	if (addr_count && write_all_buf(fd, c->addrs,
+					addr_count * sizeof(c->addrs[0])))
+		return errno;
+
+	if (write_all_buf(fd, c->guest_mac, ETH_ALEN))
+		return errno;
+
+	return 0;
+}
+
+/**
+ * migrate_merge_addr() - Merge migrated address into local array
+ * @c:		Execution context
+ * @addr:	Address entry from migration source
+ *
+ * If the address already exists locally, merge the flags (preserving
+ * local flags and adding migrated ones). Otherwise add as new entry.
+ */
+static void migrate_merge_addr(struct ctx *c,
+			       const struct inany_addr_entry *addr)
+{
+	struct inany_addr_entry *e;
+
+	if (inany_is_unspecified(&addr->addr))
+		return;
+
+	/* Check if address already exists, merge flags */
+	for_each_addr(e, c, 0) {
+		if (inany_equals(&e->addr, &addr->addr)) {
+			e->flags |= addr->flags;
+			return;
+		}
+	}
+
+	/* Add new entry if there's room */
+	if (c->addr_count < INANY_MAX_ADDRS)
+		c->addrs[c->addr_count++] = *addr;
+}
+
+/**
+ * addrs_target_v3() - Receive addresses on target
+ * @c:		Execution context
+ * @stage:	Migration stage, unused
+ * @fd:		File descriptor for state transfer
+ *
+ * Receive all address entries and merge only observed addresses into local
+ * array. Source sends all addresses for forward compatibility, but target
+ * only applies those marked as observed by guest traffic.
+ *
+ * Return: 0 on success, positive error code on failure
+ */
+static int addrs_target_v3(struct ctx *c,
+			   const struct migrate_stage *stage, int fd)
+{
+	struct inany_addr_entry addrs[INANY_MAX_ADDRS];
+	uint8_t addr_count, i;
+
+	(void)stage;
+
+	if (read_all_buf(fd, &addr_count, sizeof(addr_count)))
+		return errno;
+
+	if (addr_count > INANY_MAX_ADDRS)
+		addr_count = INANY_MAX_ADDRS;
+
+	if (addr_count && read_all_buf(fd, addrs, addr_count * sizeof(addrs[0])))
+		return errno;
+
+	if (read_all_buf(fd, c->guest_mac, ETH_ALEN))
+		return errno;
+
+	for (i = 0; i < addr_count; i++)
+		if (addrs[i].flags & CONF_ADDR_OBSERVED)
+			migrate_merge_addr(c, &addrs[i]);
+
+	return 0;
+}
+
 /* Stages for version 2 */
 static const struct migrate_stage stages_v2[] = {
 	{
@@ -142,8 +244,29 @@ static const struct migrate_stage stages_v2[] = {
 	{ 0 },
 };
 
+/* Stages for version 3 (multiple observed IPv4 addresses) */
+static const struct migrate_stage stages_v3[] = {
+	{
+		.name = "addresses",
+		.source = addrs_source_v3,
+		.target = addrs_target_v3,
+	},
+	{
+		.name = "prepare flows",
+		.source = flow_migrate_source_pre,
+		.target = NULL,
+	},
+	{
+		.name = "transfer flows",
+		.source = flow_migrate_source,
+		.target = flow_migrate_target,
+	},
+	{ 0 },
+};
+
 /* Supported encoding versions, from latest (most preferred) to oldest */
 static const struct migrate_version versions[] = {
+	{ 3,	stages_v3, },
 	{ 2,	stages_v2, },
 	/* v1 was released, but not widely used.  It had bad endianness for the
 	 * MSS and omitted timestamps, which meant it usually wouldn't work.
-- 
2.52.0


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

* [PATCH v5 11/13] dhcp, dhcpv6: Select addresses for DHCP distribution
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (9 preceding siblings ...)
  2026-02-22 17:44 ` [PATCH v5 10/13] migrate: Update protocol to v3 for multi-address support Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 12/13] ndp: Support advertising multiple prefixes in Router Advertisement Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 13/13] netlink: Add host-side monitoring for late template interface binding Jon Maloy
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

We update DHCP to select and mark the best address to advertise among
the potentially multiple addresses in the address array.

We also extend DHCPv6 to advertise all suitable IPv6 addresses
(excluding observed and link-local addresses) in a single reply
message, per RFC 8415.

Finally, we update conf_print() to reflect above changes.

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

---
v5: - Replaced fwd_first_usable_addr() with new helper functions
      to support these changes.
    - Broke out NDP part to a separate commit
---
 conf.c   | 73 +++++++++++++++++++++++++++++++++++-------------------
 conf.h   |  1 +
 dhcp.c   | 25 ++++++++++++-------
 dhcp.h   |  2 +-
 dhcpv6.c | 75 ++++++++++++++++++++++++++++++++++++++++++++------------
 5 files changed, 126 insertions(+), 50 deletions(-)

diff --git a/conf.c b/conf.c
index f622424..ea6dc07 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"
 
 #define NETNS_RUN_DIR	"/run/netns"
@@ -1116,11 +1117,12 @@ enum passt_modes conf_mode(int argc, char *argv[])
 static void conf_print(const struct ctx *c)
 {
 	char buf4[INET_ADDRSTRLEN], buf6[INET6_ADDRSTRLEN];
-	char bufmac[ETH_ADDRSTRLEN], ifn[IFNAMSIZ];
-	struct inany_addr_entry *e;
+	char bufmac[ETH_ADDRSTRLEN];
 	int i;
 
 	if (c->ifi4 > 0 || c->ifi6 > 0) {
+		char ifn[IFNAMSIZ];
+
 		info("Template interface: %s%s%s%s%s",
 		     c->ifi4 > 0 ? if_indextoname(c->ifi4, ifn) : "",
 		     c->ifi4 > 0 ? " (IPv4)" : "",
@@ -1161,21 +1163,23 @@ static void conf_print(const struct ctx *c)
 			     inet_ntop(AF_INET, &c->ip4.map_host_loopback,
 				       buf4, sizeof(buf4)));
 
-		e = first_v4(c);
-		if (e && !c->no_dhcp) {
-			uint32_t mask;
-
-			mask = IN4_MASK(inany_prefix4(e));
-
-			info("DHCP:");
-			info("    assign: %s",
-			     inet_ntop(AF_INET, inany_v4(&e->addr),
-				       buf4, sizeof(buf4)));
-			info("    mask: %s",
-			     inet_ntop(AF_INET, &mask,        buf4, sizeof(buf4)));
-			info("    router: %s",
-			     inet_ntop(AF_INET, &c->ip4.guest_gw,
-				       buf4, sizeof(buf4)));
+		if (!c->no_dhcp) {
+			const struct inany_addr_entry *e;
+
+			e = fwd_select_addr(c, AF_INET, CONF_ADDR_USER,
+					    CONF_ADDR_HOST, 0, CONF_ADDR_OBSERVED);
+			if (e) {
+				uint32_t mask = IN4_MASK(inany_prefix4(e));
+
+				info("DHCP:");
+				info("    assign: %s",
+				     inany_ntop(&e->addr, buf4, sizeof(buf4)));
+				info("    mask: %s",
+				     inet_ntop(AF_INET, &mask, buf4, sizeof(buf4)));
+				info("    router: %s",
+				     inet_ntop(AF_INET, &c->ip4.guest_gw,
+					       buf4, sizeof(buf4)));
+			}
 		}
 
 		for (i = 0; i < ARRAY_SIZE(c->ip4.dns); i++) {
@@ -1209,14 +1213,33 @@ static void conf_print(const struct ctx *c)
 		else
 			goto dns6;
 
-		e = first_v6(c);
-		info("    assign: %s", !e ? "" :
-		     inet_ntop(AF_INET6, &e->addr.a6, buf6, sizeof(buf6)));
-		info("    router: %s",
-		     inet_ntop(AF_INET6, &c->ip6.guest_gw, buf6, sizeof(buf6)));
-		info("    our link-local: %s",
-		     inet_ntop(AF_INET6, &c->ip6.our_tap_ll,
-			       buf6, sizeof(buf6)));
+		{
+			int skip = CONF_ADDR_OBSERVED | CONF_ADDR_LINKLOCAL;
+			const struct inany_addr_entry *e;
+			int first = 1;
+
+			for_each_addr(e, c, AF_INET6) {
+				if (e->flags & skip)
+					continue;
+				/* NDP requires /64 for SLAAC */
+				if (!c->no_ndp && e->prefix_len != 64)
+					continue;
+
+				info("    %s: %s/%d",
+				     first ? "assign" : "      ",
+				     inany_ntop(&e->addr, buf6, sizeof(buf6)),
+				     e->prefix_len);
+				first = 0;
+			}
+			if (!first) {
+				info("    router: %s",
+				     inet_ntop(AF_INET6, &c->ip6.guest_gw,
+					       buf6, sizeof(buf6)));
+				info("    our link-local: %s",
+				     inet_ntop(AF_INET6, &c->ip6.our_tap_ll,
+					       buf6, sizeof(buf6)));
+			}
+		}
 
 dns6:
 		for (i = 0; i < ARRAY_SIZE(c->ip6.dns); i++) {
diff --git a/conf.h b/conf.h
index 8b10ac6..b7d5fd2 100644
--- a/conf.h
+++ b/conf.h
@@ -13,6 +13,7 @@
 #define CONF_ADDR_HOST		BIT(1)		/* From host interface */
 #define CONF_ADDR_LINKLOCAL	BIT(2)		/* Link-local address */
 #define CONF_ADDR_OBSERVED	BIT(3)		/* Seen in guest traffic */
+#define CONF_ADDR_DHCP		BIT(4)		/* IPv4: assigned via DHCP */
 
 enum passt_modes conf_mode(int argc, char *argv[]);
 void conf(struct ctx *c, int argc, char **argv);
diff --git a/dhcp.c b/dhcp.c
index af473ee..b7eeac1 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -31,6 +31,8 @@
 #include "passt.h"
 #include "tap.h"
 #include "log.h"
+#include "fwd.h"
+#include "conf.h"
 #include "dhcp.h"
 
 /**
@@ -300,23 +302,22 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
  *
  * Return: 0 if it's not a DHCP message, 1 if handled, -1 on failure
  */
-int dhcp(const struct ctx *c, struct iov_tail *data)
+int dhcp(struct ctx *c, struct iov_tail *data)
 {
-	struct inany_addr_entry *e = first_v4(c);
+	struct in_addr addr, mask, dst;
 	char macstr[ETH_ADDRSTRLEN];
+	struct inany_addr_entry *e;
 	size_t mlen, dlen, opt_len;
-	struct in_addr mask, dst;
+	struct udphdr uh_storage;
 	struct ethhdr eh_storage;
 	struct iphdr iph_storage;
-	struct udphdr uh_storage;
+	const struct udphdr *uh;
 	const struct ethhdr *eh;
 	const struct iphdr *iph;
-	const struct udphdr *uh;
 	struct msg m_storage;
 	struct msg const *m;
-	struct in_addr addr;
 	struct msg reply;
-	unsigned int i;
+	int i;
 
 	eh = IOV_REMOVE_HEADER(data, eh_storage);
 	iph = IOV_PEEK_HEADER(data, iph_storage);
@@ -346,7 +347,13 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	    m->op != BOOTREQUEST)
 		return -1;
 
-	ASSERT(e);
+	/* Select address to offer */
+	e = fwd_select_addr(c, AF_INET, CONF_ADDR_DHCP,
+			    CONF_ADDR_USER, CONF_ADDR_HOST, CONF_ADDR_OBSERVED);
+	if (!e)
+		return -1;
+
+	e->flags |= CONF_ADDR_DHCP;
 	addr = *inany_v4(&e->addr);
 
 	reply.op		= BOOTREPLY;
@@ -409,7 +416,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 
 	info("    from %s", eth_ntop(m->chaddr, macstr, sizeof(macstr)));
 
-	mask.s_addr = IN4_MASK(inany_prefix4(e));
+	mask.s_addr = e ? IN4_MASK(inany_prefix4(e)) : 0;
 	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));
diff --git a/dhcp.h b/dhcp.h
index cd50c99..7326c7d 100644
--- a/dhcp.h
+++ b/dhcp.h
@@ -6,7 +6,7 @@
 #ifndef DHCP_H
 #define DHCP_H
 
-int dhcp(const struct ctx *c, struct iov_tail *data);
+int dhcp(struct ctx *c, struct iov_tail *data);
 void dhcp_init(void);
 
 #endif /* DHCP_H */
diff --git a/dhcpv6.c b/dhcpv6.c
index ba200e5..3bc2e6d 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
@@ -207,7 +209,7 @@ struct msg_hdr {
  * @hdr:		DHCP message header
  * @server_id:		Server Identifier option
  * @ia_na:		Non-temporary Address option
- * @ia_addr:		Address for IA_NA
+ * @ia_addr:		Addresses for IA_NA (variable, up to INANY_MAX_ADDRS)
  * @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
@@ -218,7 +220,7 @@ static struct resp_t {
 
 	struct opt_server_id server_id;
 	struct opt_ia_na ia_na;
-	struct opt_ia_addr ia_addr;
+	struct opt_ia_addr ia_addr[INANY_MAX_ADDRS];
 	struct opt_client_id client_id;
 	struct opt_dns_servers dns_servers;
 	struct opt_dns_search dns_search;
@@ -227,15 +229,11 @@ static struct resp_t {
 	{ 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
-	},
+	{ { { 0 }, IN6ADDR_ANY_INIT, 0, 0 } },  /* IA_ADDRs filled dynamically */
 
 	{ { OPT_CLIENTID,	0, },
 	  { 0 }
@@ -539,6 +537,44 @@ 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 resp.ia_addr[] with all non-linklocal, non-observed addresses and
+ * updates resp.ia_na.hdr.l with the correct length.
+ *
+ * Return: number of addresses filled
+ */
+static int dhcpv6_ia_addr_fill(const struct ctx *c)
+{
+	int skip = CONF_ADDR_OBSERVED | CONF_ADDR_LINKLOCAL;
+	const struct inany_addr_entry *e;
+	int count = 0;
+
+	for_each_addr(e, c, AF_INET6) {
+		if (e->flags & skip)
+			continue;
+		if (count >= INANY_MAX_ADDRS)
+			break;
+
+		resp.ia_addr[count].hdr.t = OPT_IAAADR;
+		resp.ia_addr[count].hdr.l = htons(sizeof(struct opt_ia_addr) -
+						  sizeof(struct opt_hdr));
+		resp.ia_addr[count].addr = e->addr.a6;
+		resp.ia_addr[count].pref_lifetime = (uint32_t)~0U;
+		resp.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
@@ -552,7 +588,6 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	   const struct in6_addr *daddr)
 {
 	const struct opt_server_id *server_id = NULL;
-	struct inany_addr_entry *e = first_v6(c);
 	const struct opt_hdr *client_id = NULL;
 	/* The _storage variables can't be local to the blocks they're used in,
 	 * because IOV_*_HEADER() may return pointers to them which are
@@ -567,11 +602,14 @@ 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;
+	struct in6_addr *ia_addr = NULL;
+	struct in6_addr first_addr;
 	const struct in6_addr *src;
 	struct msg_hdr mh_storage;
 	const struct msg_hdr *mh;
 	struct udphdr uh_storage;
 	const struct udphdr *uh;
+	int addr_count;
 	size_t mlen, n;
 
 	uh = IOV_REMOVE_HEADER(data, uh_storage);
@@ -615,6 +653,12 @@ 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;
 
+	/* Fill IA_ADDRs for all suitable addresses */
+	addr_count = dhcpv6_ia_addr_fill(c);
+	if (addr_count > 0) {
+		first_addr = resp.ia_addr[0].addr;
+		ia_addr = &first_addr;
+	}
 	resp.hdr.type = TYPE_REPLY;
 	switch (mh->type) {
 	case TYPE_REQUEST:
@@ -627,7 +671,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 		if (mh->type == TYPE_CONFIRM && server_id)
 			return -1;
 
-		if (e && dhcpv6_ia_notonlink(data, &e->addr.a6)) {
+		if (ia_addr && dhcpv6_ia_notonlink(data, ia_addr)) {
 
 			dhcpv6_send_ia_notonlink(c, data, &client_id_base,
 						 ntohs(client_id->l), mh->xid);
@@ -668,12 +712,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, ia_addr) +
+	    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);
 
@@ -704,6 +750,5 @@ 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));
 
-	if (first_v6(c))
-		resp.ia_addr.addr = first_v6(c)->addr.a6;
+	/* Address is set dynamically in dhcpv6() */
 }
-- 
2.52.0


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

* [PATCH v5 12/13] ndp: Support advertising multiple prefixes in Router Advertisement
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (10 preceding siblings ...)
  2026-02-22 17:44 ` [PATCH v5 11/13] dhcp, dhcpv6: Select addresses for DHCP distribution Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  2026-02-22 17:44 ` [PATCH v5 13/13] netlink: Add host-side monitoring for late template interface binding Jon Maloy
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

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

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
 ndp.c | 127 +++++++++++++++++++++++++++++++++++++++-------------------
 1 file changed, 85 insertions(+), 42 deletions(-)

diff --git a/ndp.c b/ndp.c
index ed8c6ae..cf64f3f 100644
--- a/ndp.c
+++ b/ndp.c
@@ -32,6 +32,8 @@
 #include "passt.h"
 #include "tap.h"
 #include "log.h"
+#include "fwd.h"
+#include "conf.h"
 
 #define	RT_LIFETIME	65535
 
@@ -82,7 +84,7 @@ struct ndp_na {
 } __attribute__((packed));
 
 /**
- * struct opt_prefix_info - Prefix Information option
+ * struct opt_prefix_info - Prefix Information option header
  * @header:		Option header
  * @prefix_len:		The number of leading bits in the Prefix that are valid
  * @prefix_flags:	Flags associated with the prefix
@@ -99,6 +101,16 @@ struct opt_prefix_info {
 	uint32_t reserved;
 } __attribute__((packed));
 
+/**
+ * struct ndp_prefix - Complete Prefix Information option with prefix
+ * @info:		Prefix Information option header
+ * @prefix:		IPv6 prefix
+ */
+struct ndp_prefix {
+	struct opt_prefix_info info;
+	struct in6_addr prefix;
+} __attribute__((__packed__));
+
 /**
  * struct opt_mtu - Maximum transmission unit (MTU) option
  * @header:		Option header
@@ -140,27 +152,23 @@ struct opt_dnssl {
 } __attribute__((packed));
 
 /**
- * struct ndp_ra - NDP Router Advertisement (RA) message
+ * struct ndp_ra_hdr - NDP Router Advertisement fixed header
  * @ih:			ICMPv6 header
  * @reachable:		Reachability time, after confirmation (ms)
  * @retrans:		Time between retransmitted NS messages (ms)
- * @prefix_info:	Prefix Information option
- * @prefix:		IPv6 prefix
- * @mtu:		MTU option
- * @source_ll:		Target link-layer address
- * @var:		Variable fields
  */
-struct ndp_ra {
+struct ndp_ra_hdr {
 	struct icmp6hdr ih;
 	uint32_t reachable;
 	uint32_t retrans;
-	struct opt_prefix_info prefix_info;
-	struct in6_addr prefix;
-	struct opt_l2_addr source_ll;
+} __attribute__((__packed__));
 
-	unsigned char var[sizeof(struct opt_mtu) + sizeof(struct opt_rdnss) +
-			  sizeof(struct opt_dnssl)];
-} __attribute__((packed, aligned(__alignof__(struct in6_addr))));
+/* Maximum RA message size: header + prefixes + source_ll + mtu + rdnss + dnssl */
+#define NDP_RA_MAX_SIZE	(sizeof(struct ndp_ra_hdr) + \
+			 INANY_MAX_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,46 @@ void ndp_unsolicited_na(const struct ctx *c, const struct in6_addr *addr)
 		ndp_na(c, &in6addr_ll_all_nodes, addr);
 }
 
+/**
+ * ndp_prefix_fill() - Fill prefix options for all suitable addresses
+ * @c:		Execution context
+ * @buf:	Buffer to write prefix options into
+ *
+ * Fills buffer with Prefix Information options for all non-linklocal,
+ * non-observed addresses with prefix_len == 64 (required for SLAAC).
+ *
+ * Return: number of bytes written
+ */
+static size_t ndp_prefix_fill(const struct ctx *c, unsigned char *buf)
+{
+	int skip = CONF_ADDR_OBSERVED | CONF_ADDR_LINKLOCAL;
+	const struct inany_addr_entry *e;
+	struct ndp_prefix *p;
+	size_t offset = 0;
+
+	for_each_addr(e, c, AF_INET6) {
+		if (e->flags & skip)
+			continue;
+		/* SLAAC requires /64 prefix */
+		if (e->prefix_len != 64)
+			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 = e->addr.a6;
+
+		offset += sizeof(struct ndp_prefix);
+	}
+
+	return offset;
+}
+
 /**
  * ndp_ra() - Send an NDP Router Advertisement (RA) message
  * @c:		Execution context
@@ -238,7 +286,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,32 +303,22 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
 			.icmp6_rt_lifetime	= htons_constant(RT_LIFETIME),
 			.icmp6_addrconf_managed	= 1,
 		},
-		.prefix_info = {
-			.header = {
-				.type		= OPT_PREFIX_INFO,
-				.len		= 4,
-			},
-			.prefix_len		= 64,
-			.prefix_flags		= 0xc0,	/* prefix flags: L, A */
-			.valid_lifetime		= ~0U,
-			.pref_lifetime		= ~0U,
-		},
-		.prefix = IN6ADDR_ANY_INIT,
-		.source_ll = {
-			.header = {
-				.type		= OPT_SRC_L2_ADDR,
-				.len		= 1,
-			},
-		},
 	};
-	struct inany_addr_entry *e = first_v6(c);
-	unsigned char *ptr = NULL;
-
-	ASSERT(e);
 
-	ra.prefix = e->addr.a6;
+	/* Fill prefix options */
+	prefix_len = ndp_prefix_fill(c, buf + sizeof(struct ndp_ra_hdr));
+	if (prefix_len == 0) {
+		/* No suitable prefixes to advertise */
+		return;
+	}
 
-	ptr = &ra.var[0];
+	/* Add source link-layer address option */
+	ptr = buf + sizeof(struct ndp_ra_hdr) + prefix_len;
+	source_ll = (struct opt_l2_addr *)ptr;
+	source_ll->header.type = OPT_SRC_L2_ADDR;
+	source_ll->header.len = 1;
+	memcpy(source_ll->mac, c->our_tap_mac, ETH_ALEN);
+	ptr += sizeof(struct opt_l2_addr);
 
 	if (c->mtu) {
 		struct opt_mtu *mtu = (struct opt_mtu *)ptr;
@@ -346,10 +392,7 @@ 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);
 }
 
 /**
-- 
2.52.0


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

* [PATCH v5 13/13] netlink: Add host-side monitoring for late template interface binding
  2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
                   ` (11 preceding siblings ...)
  2026-02-22 17:44 ` [PATCH v5 12/13] ndp: Support advertising multiple prefixes in Router Advertisement Jon Maloy
@ 2026-02-22 17:44 ` Jon Maloy
  12 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-22 17:44 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

When pasta starts without an active template interface (e.g., WiFi
not yet connected), it falls back to local mode. This change adds
support for late binding: when the template interface gets an address
or a default route later, pasta detects this via a host-side netlink
socket and propagates the configuration to the namespace.

Late binding occurs when:
- A specific interface is given via -I and later gets an address/route.
- No interface is specified, and any interface gets an address/route.
  In the latter case the first discovered interface is adopted as
  template.

In this commit we add a host-side netlink socket to monitor link,
address, and route changes on the template interface. We add a
corresponding nl_linkaddr_host_handler() function to process such
events and propagate the changes to the namespace.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
 epoll_type.h |   2 +
 isolation.c  |   5 +
 netlink.c    | 397 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 netlink.h    |   3 +
 passt.c      |   5 +
 5 files changed, 407 insertions(+), 5 deletions(-)

diff --git a/epoll_type.h b/epoll_type.h
index a90ffb6..cd17a64 100644
--- a/epoll_type.h
+++ b/epoll_type.h
@@ -46,6 +46,8 @@ enum epoll_type {
 	EPOLL_TYPE_REPAIR,
 	/* Netlink neighbour subscription socket */
 	EPOLL_TYPE_NL_NEIGH,
+	/* Netlink link/address subscription socket for late binding */
+	EPOLL_TYPE_NL_LINKADDR_HOST,
 
 	EPOLL_NUM_TYPES,
 };
diff --git a/isolation.c b/isolation.c
index b25f349..8087563 100644
--- a/isolation.c
+++ b/isolation.c
@@ -356,6 +356,11 @@ int isolate_prefork(const struct ctx *c)
 	if (c->mode == MODE_PASTA) {
 		/* Keep CAP_SYS_ADMIN, so we can enter the netns */
 		ns_caps |= BIT(CAP_SYS_ADMIN);
+		/* Keep CAP_NET_ADMIN for dynamic interface configuration,
+		 * so we can propagate addresses and routes when template
+		 * interface comes up after start
+		 */
+		ns_caps |= BIT(CAP_NET_ADMIN);
 		/* Keep CAP_NET_BIND_SERVICE, so we can splice
 		 * outbound connections to low port numbers
 		 */
diff --git a/netlink.c b/netlink.c
index 769cb23..a1790a4 100644
--- a/netlink.c
+++ b/netlink.c
@@ -37,6 +37,14 @@
 #include "ip.h"
 #include "netlink.h"
 #include "epoll_ctl.h"
+#include "conf.h"
+#include "arp.h"
+#include "ndp.h"
+#include "tap.h"
+#include "fwd.h"
+
+/* Default namespace interface name */
+extern const char *pasta_default_ifn;
 
 /* Same as RTA_NEXT() but for nexthops: RTNH_NEXT() doesn't take 'attrlen' */
 #define RTNH_NEXT_AND_DEC(rtnh, attrlen)				\
@@ -56,10 +64,14 @@
 #define NLBUFSIZ 65536
 
 /* Socket in init, in target namespace, sequence (just needs to be monotonic) */
-int nl_sock		 = -1;
-int nl_sock_ns		 = -1;
-static int nl_sock_neigh = -1;
-static int nl_seq	 = 1;
+int nl_sock			 = -1;
+int nl_sock_ns			 = -1;
+static int nl_sock_neigh	 = -1;
+static int nl_sock_linkaddr_host = -1;
+static int nl_seq		 = 1;
+
+static int nl_addr_del(int s, unsigned int ifi, sa_family_t af,
+		       const void *addr, int prefix_len);
 
 /**
  * nl_sock_init_do() - Set up netlink sockets in init or target namespace
@@ -91,6 +103,329 @@ static int nl_sock_init_do(void *arg)
 	return 0;
 }
 
+/**
+ * nl_linkaddr_host_msg_read() - Handle host-side link/addr/route changes
+ * @c:		Execution context
+ * @nh:	Netlink message header
+ *
+ * Monitor template interface changes and propagate to namespace.
+ * Supports late binding: if no template was detected at startup,
+ * adopt the interface specified by -I when it gets an address.
+ */
+static void nl_linkaddr_host_msg_read(struct ctx *c, const struct nlmsghdr *nh)
+{
+	if (nh->nlmsg_type == NLMSG_DONE || nh->nlmsg_type == NLMSG_ERROR)
+		return;
+
+	if (nh->nlmsg_type == RTM_NEWADDR || nh->nlmsg_type == RTM_DELADDR) {
+		bool is_new = (nh->nlmsg_type == RTM_NEWADDR);
+		const struct ifaddrmsg *ifa = NLMSG_DATA(nh);
+		char buf[INET6_ADDRSTRLEN];
+		unsigned int template_ifi;
+		union inany_addr inany;
+		char ifname[IFNAMSIZ];
+		struct rtattr *rta;
+		void *addr = NULL;
+		bool is_default;
+		sa_family_t af;
+		int prefix_len;
+		bool is_match;
+		bool unbound;
+		size_t na;
+		int rc;
+
+		if (!if_indextoname(ifa->ifa_index, ifname))
+			snprintf(ifname, sizeof(ifname), "?");
+
+		/* Get template interface index */
+		if (ifa->ifa_family == AF_INET)
+			template_ifi = c->ifi4;
+		else if (ifa->ifa_family == AF_INET6)
+			template_ifi = c->ifi6;
+		else
+			return;
+
+		/* Check for late binding conditions */
+		is_default = !strcmp(c->pasta_ifn, pasta_default_ifn);
+		is_match = !strcmp(ifname, c->pasta_ifn);
+		unbound = (ifa->ifa_family == AF_INET) ?
+			  c->ifi4 <= 0 : c->ifi6 <= 0;
+
+		if (unbound && (is_default || is_match)) {
+			debug("Late binding: using %s as %s template", ifname,
+			      ifa->ifa_family == AF_INET ? "IPv4" : "IPv6");
+
+			if (ifa->ifa_family == AF_INET) {
+				c->ifi4 = ifa->ifa_index;
+				template_ifi = c->ifi4;
+			} else {
+				c->ifi6 = ifa->ifa_index;
+				template_ifi = c->ifi6;
+			}
+
+			if (is_default)
+				snprintf(c->pasta_ifn, sizeof(c->pasta_ifn),
+					 "%s", ifname);
+		}
+
+		if (ifa->ifa_index != template_ifi)
+			return;
+
+		rta = IFA_RTA(ifa);
+		na = IFA_PAYLOAD(nh);
+
+		for (; RTA_OK(rta, na); rta = RTA_NEXT(rta, na)) {
+			if (ifa->ifa_family == AF_INET &&
+			    rta->rta_type == IFA_LOCAL) {
+				addr = RTA_DATA(rta);
+				break;
+			} else if (ifa->ifa_family == AF_INET6 &&
+				   rta->rta_type == IFA_ADDRESS) {
+				addr = RTA_DATA(rta);
+				break;
+			}
+		}
+
+		if (!addr) {
+			info("No addr found in netlink linkaddr message");
+			return;
+		}
+
+		af = ifa->ifa_family;
+		inany_from_af(&inany, af, addr);
+		inet_ntop(af, addr, buf, sizeof(buf));
+
+		/* IPv4 prefix stored as IPv4-mapped, so add 96 bits */
+		prefix_len = ifa->ifa_prefixlen + (af == AF_INET ? 96 : 0);
+
+		if (!is_new) {
+			fwd_remove_addr(c, &inany);
+			nl_addr_del(nl_sock_ns, c->pasta_ifi,
+				    af, addr, ifa->ifa_prefixlen);
+			return;
+		}
+
+		rc = nl_addr_set(nl_sock_ns, c->pasta_ifi,
+				 af, addr, ifa->ifa_prefixlen);
+		if (rc < 0) {
+			debug("Failed to add %s/%u to ns: %s",
+			      buf, ifa->ifa_prefixlen, strerror_(-rc));
+		} else {
+			fwd_set_addr(c, &inany,
+				     CONF_ADDR_HOST | CONF_ADDR_OBSERVED,
+				     prefix_len);
+			debug("Added %s/%u to namespace",
+			      buf, ifa->ifa_prefixlen);
+		}
+		return;
+	}
+
+	if (nh->nlmsg_type == RTM_NEWROUTE || nh->nlmsg_type == RTM_DELROUTE) {
+		bool is_new = (nh->nlmsg_type == RTM_NEWROUTE);
+		const struct rtmsg *rtm = NLMSG_DATA(nh);
+		struct rtattr *rta = RTM_RTA(rtm);
+		size_t na = RTM_PAYLOAD(nh);
+		unsigned int template_ifi;
+		char ifname[IFNAMSIZ];
+		unsigned int oif = 0;
+		void *gw = NULL;
+		bool is_default;
+		bool is_match;
+		bool unbound;
+
+		/* We are only interested in default routes */
+		if (rtm->rtm_dst_len != 0)
+			return;
+
+		for (; RTA_OK(rta, na); rta = RTA_NEXT(rta, na)) {
+			if (rta->rta_type == RTA_GATEWAY)
+				gw = RTA_DATA(rta);
+			else if (rta->rta_type == RTA_OIF)
+				oif = *(unsigned int *)RTA_DATA(rta);
+		}
+
+		if (!gw || !oif)
+			return;
+
+		/* Get interface name for late binding check */
+		if (!if_indextoname(oif, ifname))
+			return;
+
+		/* Check for late binding conditions */
+		is_default = !strcmp(c->pasta_ifn, pasta_default_ifn);
+		is_match = !strcmp(ifname, c->pasta_ifn);
+
+		if (rtm->rtm_family == AF_INET)
+			template_ifi = c->ifi4;
+		else if (rtm->rtm_family == AF_INET6)
+			template_ifi = c->ifi6;
+		else
+			return;
+
+		unbound = (rtm->rtm_family == AF_INET) ?
+			  c->ifi4 <= 0 : c->ifi6 <= 0;
+
+		if (unbound && (is_default || is_match)) {
+			debug("Late binding (route): using %s as %s template",
+			      ifname,
+			      rtm->rtm_family == AF_INET ? "IPv4" : "IPv6");
+
+			if (rtm->rtm_family == AF_INET) {
+				c->ifi4 = oif;
+				template_ifi = c->ifi4;
+			} else {
+				c->ifi6 = oif;
+				template_ifi = c->ifi6;
+			}
+
+			if (is_default)
+				snprintf(c->pasta_ifn, sizeof(c->pasta_ifn),
+					 "%s", ifname);
+		}
+
+		if (oif != template_ifi)
+			return;
+
+		if (rtm->rtm_family == AF_INET) {
+			char buf[INET_ADDRSTRLEN];
+
+			if (!is_new) {
+				c->ip4.guest_gw = (struct in_addr){ 0 };
+				c->ip4.our_tap_addr = (struct in_addr){ 0 };
+				return;
+			}
+			c->ip4.guest_gw = *(struct in_addr *)gw;
+			c->ip4.our_tap_addr = c->ip4.guest_gw;
+			nl_route_set_def(nl_sock_ns, c->pasta_ifi, AF_INET, gw);
+			inet_ntop(AF_INET, &c->ip4.guest_gw, buf, sizeof(buf));
+			debug("Set IPv4 default route via %s", buf);
+		} else if (rtm->rtm_family == AF_INET6) {
+			char buf[INET6_ADDRSTRLEN];
+
+			if (!is_new) {
+				c->ip6.guest_gw = (struct in6_addr){ 0 };
+				return;
+			}
+			c->ip6.guest_gw = *(struct in6_addr *)gw;
+			nl_route_set_def(nl_sock_ns, c->pasta_ifi,
+					 AF_INET6, gw);
+			inet_ntop(AF_INET6, &c->ip6.guest_gw, buf, sizeof(buf));
+			debug("Set IPv6 default route via %s", buf);
+		}
+	}
+}
+
+/**
+ * nl_linkaddr_host_handler() - Handle events from host link/addr notifier
+ * @c:		Execution context
+ *
+ * Monitor template interface changes and propagate to namespace
+ */
+void nl_linkaddr_host_handler(struct ctx *c)
+{
+	char buf[NLBUFSIZ];
+
+	for (;;) {
+		ssize_t n = recv(nl_sock_linkaddr_host, buf, sizeof(buf),
+				 MSG_DONTWAIT);
+		struct nlmsghdr *nh = (struct nlmsghdr *)buf;
+
+		if (n < 0) {
+			if (errno == EINTR)
+				continue;
+			if (errno != EAGAIN)
+				info("Host recv() error: %s", strerror_(errno));
+			break;
+		}
+
+		info("Host netlink: received %zd bytes", n);
+
+		for (; NLMSG_OK(nh, n); nh = NLMSG_NEXT(nh, n))
+			nl_linkaddr_host_msg_read(c, nh);
+	}
+}
+
+/**
+ * nl_linkaddr_host_init_do() - Create host-side link/addr notifier socket
+ * @arg:	Unused
+ *
+ * Return: 0 on success, -1 on failure
+ */
+static int nl_linkaddr_host_init_do(void *arg)
+{
+	struct sockaddr_nl addr = {
+		.nl_family = AF_NETLINK,
+		.nl_groups = RTMGRP_LINK |
+			     RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR |
+			     RTMGRP_IPV4_ROUTE | RTMGRP_IPV6_ROUTE,
+	};
+
+	(void)arg;
+
+	nl_sock_linkaddr_host = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC,
+				       NETLINK_ROUTE);
+	if (nl_sock_linkaddr_host < 0) {
+		debug("socket() failed for host: %s", strerror_(errno));
+		return -1;
+	}
+
+	if (bind(nl_sock_linkaddr_host, (struct sockaddr *)&addr,
+		 sizeof(addr)) < 0) {
+		debug("bind() failed for host: %s", strerror_(errno));
+		close(nl_sock_linkaddr_host);
+		nl_sock_linkaddr_host = -1;
+		return -1;
+	}
+
+	debug("host socket fd=%d", nl_sock_linkaddr_host);
+	return 0;
+}
+
+/**
+ * nl_linkaddr_notify_init() - Initialize host link/address change notifier
+ * @c:		Execution context
+ *
+ * In PASTA mode, create a host-side netlink socket to monitor template
+ * interface changes and propagate them to the namespace (late binding).
+ *
+ * Return: 0 on success, -1 on failure
+ */
+int nl_linkaddr_notify_init(const struct ctx *c)
+{
+	union epoll_ref ref = { .type = EPOLL_TYPE_NL_LINKADDR_HOST };
+	struct epoll_event ev = { .events = EPOLLIN };
+
+	if (c->mode != MODE_PASTA)
+		return 0;
+
+	if (nl_sock_linkaddr_host >= 0) {
+		debug("host notifier already initialized (fd=%d)",
+		      nl_sock_linkaddr_host);
+		return 0;
+	}
+
+	nl_linkaddr_host_init_do(NULL);
+
+	if (nl_sock_linkaddr_host < 0) {
+		warn("Failed to create host link/addr notifier socket");
+		return -1;
+	}
+
+	ev.data.u64 = ref.u64;
+	if (epoll_ctl(c->epollfd, EPOLL_CTL_ADD,
+		      nl_sock_linkaddr_host, &ev) == -1) {
+		warn("epoll_ctl() failed on host notifier: %s",
+		     strerror_(errno));
+		close(nl_sock_linkaddr_host);
+		nl_sock_linkaddr_host = -1;
+		return -1;
+	}
+
+	info("Host netlink socket fd=%d, pasta_ifn=%s",
+	     nl_sock_linkaddr_host, c->pasta_ifn);
+
+	return 0;
+}
 /**
  * nl_sock_init() - Call nl_sock_init_do(), won't return on failure
  * @c:		Execution context
@@ -516,7 +851,7 @@ int nl_route_set_def(int s, unsigned int ifi, sa_family_t af, const void *gw)
 		req.set.r4.rta_gw.rta_len = rta_len;
 	}
 
-	return nl_do(s, &req, RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, len);
+	return nl_do(s, &req, RTM_NEWROUTE, NLM_F_CREATE | NLM_F_REPLACE, len);
 }
 
 /**
@@ -927,6 +1262,58 @@ int nl_addr_set(int s, unsigned int ifi, sa_family_t af,
 	return nl_do(s, &req, RTM_NEWADDR, NLM_F_CREATE | NLM_F_EXCL, len);
 }
 
+/**
+ * nl_addr_del() - Delete IP address from given interface
+ * @s:		Netlink socket
+ * @ifi:	Interface index
+ * @af:		Address family
+ * @addr:	Address to delete
+ * @prefix_len:	Prefix length
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+static int nl_addr_del(int s, unsigned int ifi, sa_family_t af,
+		       const void *addr, int prefix_len)
+{
+	struct req_t {
+		struct nlmsghdr nlh;
+		struct ifaddrmsg ifa;
+		union {
+			struct {
+				struct rtattr rta_l;
+				struct in_addr l;
+			} a4;
+			struct {
+				struct rtattr rta_l;
+				struct in6_addr l;
+			} a6;
+		} del;
+	} req = {
+		.ifa.ifa_family    = af,
+		.ifa.ifa_index     = ifi,
+		.ifa.ifa_prefixlen = prefix_len,
+	};
+	ssize_t len;
+
+	if (af == AF_INET6) {
+		size_t rta_len = RTA_LENGTH(sizeof(req.del.a6.l));
+
+		len = offsetof(struct req_t, del.a6) + sizeof(req.del.a6);
+		memcpy(&req.del.a6.l, addr, sizeof(req.del.a6.l));
+		req.del.a6.rta_l.rta_len = rta_len;
+		req.del.a6.rta_l.rta_type = IFA_LOCAL;
+	} else {
+		size_t rta_len = RTA_LENGTH(sizeof(req.del.a4.l));
+
+		len = offsetof(struct req_t, del.a4) + sizeof(req.del.a4);
+		memcpy(&req.del.a4.l, addr, sizeof(req.del.a4.l));
+		req.del.a4.rta_l.rta_len = rta_len;
+		req.del.a4.rta_l.rta_type = IFA_LOCAL;
+	}
+
+	return nl_do(s, &req, RTM_DELADDR, 0, len);
+}
+
 /**
  * nl_addr_dup() - Copy IP addresses for given interface and address family
  * @s_src:	Netlink socket in source network namespace
diff --git a/netlink.h b/netlink.h
index 8f1e9b9..c19d3a3 100644
--- a/netlink.h
+++ b/netlink.h
@@ -33,4 +33,7 @@ int nl_link_set_flags(int s, unsigned int ifi,
 int nl_neigh_notify_init(const struct ctx *c);
 void nl_neigh_notify_handler(const struct ctx *c);
 
+int nl_linkaddr_notify_init(const struct ctx *c);
+void nl_linkaddr_host_handler(struct ctx *c);
+
 #endif /* NETLINK_H */
diff --git a/passt.c b/passt.c
index 7488a84..64163df 100644
--- a/passt.c
+++ b/passt.c
@@ -80,6 +80,7 @@ char *epoll_type_str[] = {
 	[EPOLL_TYPE_REPAIR_LISTEN]	= "TCP_REPAIR helper listening socket",
 	[EPOLL_TYPE_REPAIR]		= "TCP_REPAIR helper socket",
 	[EPOLL_TYPE_NL_NEIGH]		= "netlink neighbour notifier socket",
+	[EPOLL_TYPE_NL_LINKADDR_HOST]	= "host link/address notifier socket",
 };
 static_assert(ARRAY_SIZE(epoll_type_str) == EPOLL_NUM_TYPES,
 	      "epoll_type_str[] doesn't match enum epoll_type");
@@ -303,6 +304,9 @@ static void passt_worker(void *opaque, int nfds, struct epoll_event *events)
 		case EPOLL_TYPE_NL_NEIGH:
 			nl_neigh_notify_handler(c);
 			break;
+		case EPOLL_TYPE_NL_LINKADDR_HOST:
+			nl_linkaddr_host_handler(c);
+			break;
 		default:
 			/* Can't happen */
 			ASSERT(0);
@@ -413,6 +417,7 @@ int main(int argc, char **argv)
 
 	fwd_neigh_table_init(&c);
 	nl_neigh_notify_init(&c);
+	nl_linkaddr_notify_init(&c);
 
 	if (!c.foreground) {
 		if ((devnull_fd = open("/dev/null", O_RDWR | O_CLOEXEC)) < 0)
-- 
2.52.0


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

end of thread, other threads:[~2026-02-22 17:45 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-02-22 17:44 [PATCH v5 00/13] Introduce multiple addresses and late binding Jon Maloy
2026-02-22 17:44 ` [PATCH v5 01/13] ip: Introduce unified multi-address data structures Jon Maloy
2026-02-22 17:44 ` [PATCH v5 02/13] ip: Introduce for_each_addr() macro for address iteration Jon Maloy
2026-02-22 17:44 ` [PATCH v5 03/13] fwd: Unify guest accessibility checks with unified address array Jon Maloy
2026-02-22 17:44 ` [PATCH v5 04/13] arp: Check all configured addresses in ARP filtering Jon Maloy
2026-02-22 17:44 ` [PATCH v5 05/13] netlink: Return prefix length for IPv6 addresses in nl_addr_get() Jon Maloy
2026-02-22 17:44 ` [PATCH v5 06/13] conf: Allow multiple -a/--address options per address family Jon Maloy
2026-02-22 17:44 ` [PATCH v5 07/13] ip: Track observed guest IPv4 addresses in unified address array Jon Maloy
2026-02-22 17:44 ` [PATCH v5 08/13] ip: Track observed guest IPv6 " Jon Maloy
2026-02-22 17:44 ` [PATCH v5 09/13] migrate: Rename v1 address functions to v2 for clarity Jon Maloy
2026-02-22 17:44 ` [PATCH v5 10/13] migrate: Update protocol to v3 for multi-address support Jon Maloy
2026-02-22 17:44 ` [PATCH v5 11/13] dhcp, dhcpv6: Select addresses for DHCP distribution Jon Maloy
2026-02-22 17:44 ` [PATCH v5 12/13] ndp: Support advertising multiple prefixes in Router Advertisement Jon Maloy
2026-02-22 17:44 ` [PATCH v5 13/13] netlink: Add host-side monitoring for late template interface binding 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).