public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
* [PATCH v2 0/9] Introduce multiple addresses
@ 2026-01-18 22:16 Jon Maloy
  2026-01-18 22:16 ` [PATCH v2 1/9] conf: Support CIDR notation for -a/--address option Jon Maloy
                   ` (8 more replies)
  0 siblings, 9 replies; 19+ messages in thread
From: Jon Maloy @ 2026-01-18 22:16 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

This version contains what I perceive as the least controversial
parts of my previous RFC series. It basically makes address
handling behave like before, but now allowing multiple addresses
both at the host side and the guest side.

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?
   

Jon Maloy (9):
  conf: Support CIDR notation for -a/--address option
  ip: Introduce unified multi-address data structures
  conf: Refactor conf_print() for multi-address support
  fwd: Check all configured addresses in guest accessibility functions
  arp: Check all configured addresses in ARP filtering
  conf: Allow multiple -a/--address options per address family
  pasta: Unify address configuration paths using address array
  ip: Track observed guest IPv4 addresses in unified address array
  ip: Track observed guest IPv6 addresses in unified address array

 arp.c     |  12 ++-
 conf.c    | 290 +++++++++++++++++++++++++++++++++++++-----------------
 dhcp.c    |   8 +-
 dhcpv6.c  |  10 +-
 dhcpv6.h  |   2 +-
 fwd.c     | 137 ++++++++++++++++++++------
 inany.c   |  29 ++++++
 inany.h   |  19 ++++
 ip.c      |  21 ++++
 ip.h      |   6 ++
 migrate.c | 105 ++++++++++++++++++--
 ndp.c     |   6 +-
 netlink.c |  84 ++++++++++++++++
 netlink.h |   5 +
 passt.1   |  17 +++-
 passt.h   |  26 ++---
 pasta.c   |  54 ++++++----
 tap.c     | 116 ++++++++++++++++++----
 18 files changed, 744 insertions(+), 203 deletions(-)

-- 
2.52.0


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

* [PATCH v2 1/9] conf: Support CIDR notation for -a/--address option
  2026-01-18 22:16 [PATCH v2 0/9] Introduce multiple addresses Jon Maloy
@ 2026-01-18 22:16 ` Jon Maloy
  2026-01-19  5:02   ` David Gibson
  2026-01-21  8:15   ` Stefano Brivio
  2026-01-18 22:16 ` [PATCH v2 2/9] ip: Introduce unified multi-address data structures Jon Maloy
                   ` (7 subsequent siblings)
  8 siblings, 2 replies; 19+ messages in thread
From: Jon Maloy @ 2026-01-18 22:16 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

Extend the -a/--address option to accept addresses in CIDR notation
(e.g., 192.168.1.1/24 or 2001:db8::1/64) as an alternative to using
separate -a and -n options.

We add a new conf_addr_prefix_len() helper function that:
- Parses address strings with optional /prefix_len suffix
- Validates prefix length based on address family (0-32 for IPv4,
  0-128 for IPv6), including handling of IPv4-to-IPv6 mapping case.
- Returns address family via union inany_addr output parameter

For IPv4, the prefix length is stored in ip4.prefix_len when provided.
Mixing -n and CIDR notation results in an error to catch likely user
mistakes.

Also fix a bug in conf_ip4_prefix() that was incorrectly using the
global 'optarg' instead of its 'arg' parameter.

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

---
v3: Fixes after feedback from Laurent, David and Stefano
    Notably, updated man page for the -a option

v4: Fixes based on feedback from David G:
  - Handling prefix length adjustment when IPv4-to-IPv6 mapping
  - Removed redundant !IN6_IS_ADDR_V4MAPPED(&addr.a6) test
  - Simplified tests of acceptable address types
  - Merged documentation and code commits
  - Some documentation text clarifications
---
 conf.c  | 97 +++++++++++++++++++++++++++++++++++++++++++++++----------
 inany.c | 29 +++++++++++++++++
 inany.h |  1 +
 ip.c    | 21 +++++++++++++
 ip.h    |  2 ++
 passt.1 | 17 +++++++---
 6 files changed, 145 insertions(+), 22 deletions(-)

diff --git a/conf.c b/conf.c
index 2942c8c..7178a0e 100644
--- a/conf.c
+++ b/conf.c
@@ -682,7 +682,7 @@ static int conf_ip4_prefix(const char *arg)
 			return -1;
 	} else {
 		errno = 0;
-		len = strtoul(optarg, NULL, 0);
+		len = strtoul(arg, NULL, 0);
 		if (len > 32 || errno)
 			return -1;
 	}
@@ -690,6 +690,52 @@ static int conf_ip4_prefix(const char *arg)
 	return len;
 }
 
+/**
+ * conf_addr_prefix_len() - Parse address with optional prefix length
+ * @arg:	Address string, optionally with /prefix_len suffix (modified)
+ * @addr:	Output for parsed address
+ * @prefix_len: Output for prefix length (0 if not specified)
+ *
+ * Return: AF_INET for IPv4, AF_INET6 for IPv6, -1 on error
+ */
+static int conf_addr_prefix_len(char *arg, union inany_addr *addr,
+				int *prefix_len)
+{
+	char *slash;
+
+	*prefix_len = 0;
+
+	/* Check for /prefix_len suffix */
+	slash = strchr(arg, '/');
+	if (slash) {
+		unsigned long len;
+		char *end;
+
+		*slash = '\0';
+		errno = 0;
+		len = strtoul(slash + 1, &end, 10);
+		if (errno || *end)
+			return -1;
+
+		*prefix_len = len;
+	}
+
+	if (!inany_prefix_pton(arg, addr, prefix_len))
+		return -1;
+
+	if (inany_v4(addr)) {
+		if (*prefix_len > 32)
+			return -1;
+
+		return AF_INET;
+	}
+
+	if (*prefix_len > 128)
+		return -1;
+
+	return AF_INET6;
+}
+
 /**
  * conf_ip4() - Verify or detect IPv4 support, get relevant addresses
  * @ifi:	Host interface to attempt (0 to determine one)
@@ -896,7 +942,7 @@ static void usage(const char *name, FILE *f, int status)
 		"    a zero value disables assignment\n"
 		"    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\n"
+		"  -a, --address ADDR	Assign IPv4 or IPv6 address ADDR[/PREFIXLEN]\n"
 		"    can be specified zero to two times (for IPv4 and IPv6)\n"
 		"    default: use addresses from interface with default route\n"
 		"  -n, --netmask MASK	Assign IPv4 MASK, dot-decimal or bits\n"
@@ -1499,6 +1545,7 @@ void conf(struct ctx *c, int argc, char **argv)
 	const char *logname = (c->mode == MODE_PASTA) ? "pasta" : "passt";
 	char userns[PATH_MAX] = { 0 }, netns[PATH_MAX] = { 0 };
 	bool copy_addrs_opt = false, copy_routes_opt = false;
+	bool prefix_from_cidr = false, prefix_from_opt = false;
 	enum fwd_ports_mode fwd_default = FWD_NONE;
 	bool v4_only = false, v6_only = false;
 	unsigned dns4_idx = 0, dns6_idx = 0;
@@ -1808,35 +1855,51 @@ void conf(struct ctx *c, int argc, char **argv)
 			c->mtu = mtu;
 			break;
 		}
-		case 'a':
-			if (inet_pton(AF_INET6, optarg, &c->ip6.addr)	&&
-			    !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)	&&
-			    !IN6_IS_ADDR_LOOPBACK(&c->ip6.addr)		&&
-			    !IN6_IS_ADDR_V4MAPPED(&c->ip6.addr)		&&
-			    !IN6_IS_ADDR_V4COMPAT(&c->ip6.addr)		&&
-			    !IN6_IS_ADDR_MULTICAST(&c->ip6.addr)) {
+		case 'a': {
+			union inany_addr addr;
+			const struct in_addr *a4;
+			int prefix_len = 0;
+			int af;
+
+			af = conf_addr_prefix_len(optarg, &addr, &prefix_len);
+
+			if (inany_is_unspecified(&addr) ||
+			    inany_is_multicast(&addr) ||
+			    inany_is_loopback(&addr) ||
+			    IN6_IS_ADDR_V4COMPAT(&addr.a6))
+				die("Invalid address: %s", optarg);
+
+			if (af == AF_INET6) {
+				c->ip6.addr = addr.a6;
 				if (c->mode == MODE_PASTA)
 					c->ip6.no_copy_addrs = true;
 				break;
 			}
 
-			if (inet_pton(AF_INET, optarg, &c->ip4.addr)	&&
-			    !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.addr)	&&
-			    !IN4_IS_ADDR_BROADCAST(&c->ip4.addr)	&&
-			    !IN4_IS_ADDR_LOOPBACK(&c->ip4.addr)		&&
-			    !IN4_IS_ADDR_MULTICAST(&c->ip4.addr)) {
-				if (c->mode == MODE_PASTA)
-					c->ip4.no_copy_addrs = true;
+			a4 = inany_v4(&addr);
+			if (af == AF_INET && a4) {
+				c->ip4.addr = *a4;
+				if (prefix_len) {
+					if (prefix_from_opt)
+						die("Can't mix CIDR with -n");
+					prefix_from_cidr = true;
+				} else {
+					prefix_len = ip4_default_prefix_len(a4);
+				}
+				c->ip4.prefix_len = prefix_len;
 				break;
 			}
 
 			die("Invalid address: %s", optarg);
 			break;
+		}
 		case 'n':
+			if (prefix_from_cidr)
+				die("Can't use both -n and CIDR prefix length");
 			c->ip4.prefix_len = conf_ip4_prefix(optarg);
 			if (c->ip4.prefix_len < 0)
 				die("Invalid netmask: %s", optarg);
-
+			prefix_from_opt = true;
 			break;
 		case 'M':
 			parse_mac(c->our_tap_mac, optarg);
diff --git a/inany.c b/inany.c
index 7680439..f142a76 100644
--- a/inany.c
+++ b/inany.c
@@ -57,3 +57,32 @@ int inany_pton(const char *src, union inany_addr *dst)
 
 	return 0;
 }
+
+/** inany_prefix_pton - Parse an IPv[46] address with prefix length adjustment
+ * @src:	IPv[46] address string
+ * @dst:	output buffer, filled with parsed address
+ * @prefix_len: pointer to prefix length
+ *
+ * Return: on success, 1, if no parseable address is found, 0
+ */
+int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len)
+{
+	/* First try parsing as plain IPv4 */
+	if (inet_pton(AF_INET, src, &dst->v4mapped.a4)) {
+		memset(&dst->v4mapped.zero, 0, sizeof(dst->v4mapped.zero));
+		memset(&dst->v4mapped.one, 0xff, sizeof(dst->v4mapped.one));
+		return 1;
+	}
+
+	/* Try parsing as IPv6, adjust prefix length if mapped IPv4 address */
+	if (inet_pton(AF_INET6, src, &dst->a6)) {
+		if (inany_v4(dst) && prefix_len && *prefix_len > 0) {
+			if (*prefix_len < 96)
+				return 0;
+			*prefix_len -= 96;
+		}
+		return 1;
+	}
+
+	return 0;
+}
diff --git a/inany.h b/inany.h
index 61b36fb..36865f9 100644
--- a/inany.h
+++ b/inany.h
@@ -295,5 +295,6 @@ static inline void inany_siphash_feed(struct siphash_state *state,
 
 const char *inany_ntop(const union inany_addr *src, char *dst, socklen_t size);
 int inany_pton(const char *src, union inany_addr *dst);
+int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len);
 
 #endif /* INANY_H */
diff --git a/ip.c b/ip.c
index 9a7f4c5..2519c71 100644
--- a/ip.c
+++ b/ip.c
@@ -13,6 +13,8 @@
  */
 
 #include <stddef.h>
+#include <netinet/in.h>
+
 #include "util.h"
 #include "ip.h"
 
@@ -67,3 +69,22 @@ found:
 	*proto = nh;
 	return true;
 }
+
+/**
+ * ip4_default_prefix_len() - Get default prefix length for IPv4 address
+ * @addr:	IPv4 address
+ *
+ * Return: prefix length based on address class (8/16/24), or 32 for other
+ */
+int ip4_default_prefix_len(const struct in_addr *addr)
+{
+	in_addr_t a = ntohl(addr->s_addr);
+
+	if (IN_CLASSA(a))
+		return 8;
+	if (IN_CLASSB(a))
+		return 16;
+	if (IN_CLASSC(a))
+		return 24;
+	return 32;
+}
diff --git a/ip.h b/ip.h
index 5830b92..e5f5198 100644
--- a/ip.h
+++ b/ip.h
@@ -135,4 +135,6 @@ static const struct in_addr in4addr_broadcast = { 0xffffffff };
 #define IPV6_MIN_MTU		1280
 #endif
 
+int ip4_default_prefix_len(const struct in_addr *addr);
+
 #endif /* IP_H */
diff --git a/passt.1 b/passt.1
index db0d662..7ca03be 100644
--- a/passt.1
+++ b/passt.1
@@ -156,10 +156,14 @@ By default, the advertised MTU is 65520 bytes, that is, the maximum 802.3 MTU
 minus the length of a 802.3 header, rounded to 32 bits (IPv4 words).
 
 .TP
-.BR \-a ", " \-\-address " " \fIaddr
+.BR \-a ", " \-\-address " " \fIaddr\fR[\fB/\fR\fIprefix_len\fR]
 Assign IPv4 \fIaddr\fR via DHCP (\fByiaddr\fR), or \fIaddr\fR via DHCPv6 (option
 5) and an \fIaddr\fR-based prefix via NDP Router Advertisement (option type 3)
 for an IPv6 \fIaddr\fR.
+An optional \fB/\fR\fIprefix_len\fR (0-32 for IPv4, 0-128 for IPv6) can be
+appended in CIDR notation (e.g., 192.168.1.1/24). This is an alternative to
+using the \fB-n\fR, \fB--netmask\fR option. Mixing CIDR notation with
+\fB-n\fR results in an error.
 This option can be specified zero (for defaults) to two times (once for IPv4,
 once for IPv6).
 By default, assigned IPv4 and IPv6 addresses are taken from the host interfaces
@@ -172,10 +176,13 @@ is assigned for IPv4, and no additional address will be assigned for IPv6.
 .TP
 .BR \-n ", " \-\-netmask " " \fImask
 Assign IPv4 netmask \fImask\fR, expressed as dot-decimal or number of bits, via
-DHCP (option 1).
-By default, the netmask associated to the host address matching the assigned one
-is used. If there's no matching address on the host, the netmask is determined
-according to the CIDR block of the assigned address (RFC 4632).
+DHCP (option 1). Alternatively, the prefix length can be specified using CIDR
+notation with the \fB-a\fR, \fB--address\fR option (e.g., \fB-a\fR 192.168.1.1/24).
+Mixing \fB-n\fR with CIDR notation results in an error.
+If no address is indicated, the netmask associated with the adopted host address,
+if any, is used. If an address is indicated, but without a prefix length, the
+netmask is determined based on the corresponding network class. In all other
+cases, the netmask is determined by using the indicated prefix length.
 
 .TP
 .BR \-M ", " \-\-mac-addr " " \fIaddr
-- 
2.52.0


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

* [PATCH v2 2/9] ip: Introduce unified multi-address data structures
  2026-01-18 22:16 [PATCH v2 0/9] Introduce multiple addresses Jon Maloy
  2026-01-18 22:16 ` [PATCH v2 1/9] conf: Support CIDR notation for -a/--address option Jon Maloy
@ 2026-01-18 22:16 ` Jon Maloy
  2026-01-19  7:22   ` David Gibson
  2026-01-21 13:02   ` Stefano Brivio
  2026-01-18 22:16 ` [PATCH v2 3/9] conf: Refactor conf_print() for multi-address support Jon Maloy
                   ` (6 subsequent siblings)
  8 siblings, 2 replies; 19+ messages in thread
From: Jon Maloy @ 2026-01-18 22:16 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 arrays.

- We add an ip4_addr_entry and an ip6_addr_entry struct containing
  address and prefix length.

- We set the array sizes to IP4_MAX_ADDRS=8 and IP6_MAX_ADDRS=16,
  respectively.

The only functional change is that the IPv6 prefix length now is
properly stored instead of being hardcoded to 64 even when set
via the -a option.

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

---
v2: Using inany_addr instead of protocol specific addresses as
    entry address field.
---
 arp.c    |  4 +--
 conf.c   | 78 +++++++++++++++++++++++++++++++++++---------------------
 dhcp.c   |  8 +++---
 dhcpv6.c |  6 ++---
 fwd.c    | 12 ++++-----
 inany.h  | 16 ++++++++++++
 ip.h     |  4 +++
 ndp.c    |  6 ++---
 passt.h  | 16 +++++++-----
 pasta.c  | 12 +++++----
 tap.c    |  4 +--
 11 files changed, 106 insertions(+), 60 deletions(-)

diff --git a/arp.c b/arp.c
index bb042e9..bc77a9f 100644
--- a/arp.c
+++ b/arp.c
@@ -54,7 +54,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 (!memcmp(am->tip, inany_v4(&c->ip4.addrs[0].addr), sizeof(am->tip)))
 		return true;
 
 	return false;
@@ -145,7 +145,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(&c->ip4.addrs[0].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 7178a0e..9fc5dca 100644
--- a/conf.c
+++ b/conf.c
@@ -763,33 +763,33 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
 		}
 	}
 
-	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr)) {
+	if (!ip4->addr_count) {
+		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;
 		}
+		ip4->addrs[0].addr = inany_from_v4(addr);
+		ip4->addrs[0].prefix_len = prefix_len;
+		ip4->addrs[0].flags = INANY_ADDR_HOST;
+		ip4->addr_count = 1;
 	}
 
-	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;
+	if (!ip4->addrs[0].prefix_len) {
+		const struct in_addr *a4 = inany_v4(&ip4->addrs[0].addr);
+
+		ip4->addrs[0].prefix_len = ip4_default_prefix_len(a4);
 	}
 
-	ip4->addr_seen = ip4->addr;
+	ip4->addr_seen = *inany_v4(&ip4->addrs[0].addr);
 
 	ip4->our_tap_addr = ip4->guest_gw;
 
-	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr))
+	if (inany_is_unspecified(&ip4->addrs[0].addr))
 		return 0;
 
 	return ifi;
@@ -801,9 +801,11 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
  */
 static void conf_ip4_local(struct ip4_ctx *ip4)
 {
-	ip4->addr_seen = ip4->addr = IP4_LL_GUEST_ADDR;
+	ip4->addrs[0].addr = inany_from_v4(IP4_LL_GUEST_ADDR);
+	ip4->addr_seen = *inany_v4(&ip4->addrs[0].addr);
 	ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
-	ip4->prefix_len = IP4_LL_PREFIX_LEN;
+	ip4->addrs[0].prefix_len = IP4_LL_PREFIX_LEN;
+	ip4->addr_count = 1;
 
 	ip4->no_copy_addrs = ip4->no_copy_routes = true;
 }
@@ -838,19 +840,25 @@ 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,
+			 ip6->addr_count ? NULL : &ip6->addrs[0].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 (!ip6->addr_count) {
+		ip6->addrs[0].prefix_len = prefix_len ? prefix_len : 64;
+		ip6->addrs[0].flags = INANY_ADDR_HOST;
+		ip6->addr_count = 1;
+	}
+
+	ip6->addr_seen = ip6->addrs[0].addr.a6;
 
 	if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
 		ip6->our_tap_ll = ip6->guest_gw;
 
-	if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ||
+	if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addrs[0].addr.a6) ||
 	    IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
 		return 0;
 
@@ -1193,11 +1201,13 @@ static void conf_print(const struct ctx *c)
 		if (!c->no_dhcp) {
 			uint32_t mask;
 
-			mask = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+			mask = htonl(0xffffffff <<
+				     (32 - c->ip4.addrs[0].prefix_len));
 
 			info("DHCP:");
 			info("    assign: %s",
-			     inet_ntop(AF_INET, &c->ip4.addr, buf4, sizeof(buf4)));
+			     inet_ntop(AF_INET, inany_v4(&c->ip4.addrs[0].addr),
+				       buf4, sizeof(buf4)));
 			info("    mask: %s",
 			     inet_ntop(AF_INET, &mask,        buf4, sizeof(buf4)));
 			info("    router: %s",
@@ -1235,7 +1245,8 @@ static void conf_print(const struct ctx *c)
 			goto dns6;
 
 		info("    assign: %s",
-		     inet_ntop(AF_INET6, &c->ip6.addr, buf6, sizeof(buf6)));
+		     inet_ntop(AF_INET6, &c->ip6.addrs[0].addr.a6,
+			       buf6, sizeof(buf6)));
 		info("    router: %s",
 		     inet_ntop(AF_INET6, &c->ip6.guest_gw, buf6, sizeof(buf6)));
 		info("    our link-local: %s",
@@ -1870,15 +1881,20 @@ void conf(struct ctx *c, int argc, char **argv)
 				die("Invalid address: %s", optarg);
 
 			if (af == AF_INET6) {
-				c->ip6.addr = addr.a6;
+				c->ip6.addrs[0].addr.a6 = addr.a6;
+				c->ip6.addrs[0].flags |= INANY_ADDR_CONFIGURED;
+				c->ip6.addr_count = 1;
 				if (c->mode == MODE_PASTA)
 					c->ip6.no_copy_addrs = true;
 				break;
 			}
 
 			a4 = inany_v4(&addr);
+
 			if (af == AF_INET && a4) {
-				c->ip4.addr = *a4;
+				c->ip4.addrs[0].addr = inany_from_v4(*a4);
+				c->ip4.addrs[0].flags |= INANY_ADDR_CONFIGURED;
+				c->ip4.addr_count = 1;
 				if (prefix_len) {
 					if (prefix_from_opt)
 						die("Can't mix CIDR with -n");
@@ -1886,21 +1902,25 @@ void conf(struct ctx *c, int argc, char **argv)
 				} else {
 					prefix_len = ip4_default_prefix_len(a4);
 				}
-				c->ip4.prefix_len = prefix_len;
+				c->ip4.addrs[0].prefix_len = prefix_len;
 				break;
 			}
 
 			die("Invalid address: %s", optarg);
 			break;
 		}
-		case 'n':
+		case 'n': {
+			int plen;
+
 			if (prefix_from_cidr)
 				die("Can't use both -n and CIDR prefix length");
-			c->ip4.prefix_len = conf_ip4_prefix(optarg);
-			if (c->ip4.prefix_len < 0)
+			plen = conf_ip4_prefix(optarg);
+			if (plen < 0)
 				die("Invalid netmask: %s", optarg);
+			c->ip4.addrs[0].prefix_len = plen;
 			prefix_from_opt = true;
 			break;
+		}
 		case 'M':
 			parse_mac(c->our_tap_mac, optarg);
 			break;
@@ -2185,7 +2205,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 (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addrs[0].addr.a6)) {
 		c->no_dhcpv6 = 1;
 	}
 
diff --git a/dhcp.c b/dhcp.c
index 6b9c2e3..d2afc3b 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -352,7 +352,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		= *inany_v4(&c->ip4.addrs[0].addr);
 	reply.siaddr		= 0;
 	reply.giaddr		= m->giaddr;
 	memcpy(&reply.chaddr,	m->chaddr,	sizeof(reply.chaddr));
@@ -404,7 +404,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 = htonl(0xffffffff << (32 - c->ip4.addrs[0].prefix_len));
 	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 +412,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 ((inany_v4(&c->ip4.addrs[0].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;
@@ -469,7 +469,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 = *inany_v4(&c->ip4.addrs[0].addr);
 
 	tap_udp4_send(c, c->ip4.our_tap_addr, 67, dst, 68, &reply, dlen);
 
diff --git a/dhcpv6.c b/dhcpv6.c
index e4df0db..f45dece 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -625,7 +625,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 (dhcpv6_ia_notonlink(data, &c->ip6.addrs[0].addr.a6)) {
 
 			dhcpv6_send_ia_notonlink(c, data, &client_id_base,
 						 ntohs(client_id->l), mh->xid);
@@ -679,7 +679,7 @@ 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;
+	c->ip6.addr_seen = c->ip6.addrs[0].addr.a6;
 
 	return 1;
 }
@@ -703,5 +703,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));
 
-	resp.ia_addr.addr	= c->ip6.addr;
+	resp.ia_addr.addr	= c->ip6.addrs[0].addr.a6;
 }
diff --git a/fwd.c b/fwd.c
index 44a0e10..8d8151b 100644
--- a/fwd.c
+++ b/fwd.c
@@ -516,7 +516,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 (IN4_ARE_ADDR_EQUAL(addr, inany_v4(&c->ip4.addrs[0].addr)) ||
 	    IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
 		return false;
 
@@ -537,7 +537,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 (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addrs[0].addr.a6))
 		return false;
 
 	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
@@ -587,9 +587,9 @@ static void nat_outbound(const struct ctx *c, const union inany_addr *addr,
 	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);
+		*translated = c->ip4.addrs[0].addr;
 	else if (inany_equals6(addr, &c->ip6.map_guest_addr))
-		translated->a6 = c->ip6.addr;
+		translated->a6 = c->ip6.addrs[0].addr.a6;
 	else
 		*translated = *addr;
 }
@@ -710,10 +710,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)) {
+		   inany_equals(addr, &c->ip4.addrs[0].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)) {
+		   inany_equals6(addr, &c->ip6.addrs[0].addr.a6)) {
 		translated->a6 = c->ip6.map_guest_addr;
 	} else if (fwd_guest_accessible(c, addr)) {
 		*translated = *addr;
diff --git a/inany.h b/inany.h
index 36865f9..07bfc3d 100644
--- a/inany.h
+++ b/inany.h
@@ -297,4 +297,20 @@ const char *inany_ntop(const union inany_addr *src, char *dst, socklen_t size);
 int inany_pton(const char *src, union inany_addr *dst);
 int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len);
 
+/* Flags for struct inany_addr_entry */
+#define INANY_ADDR_CONFIGURED	(1 << 0)	/* User set via -a */
+#define INANY_ADDR_HOST	(1 << 1)	/* From host interface */
+
+/**
+ * struct inany_addr_entry - Unified IPv4/IPv6 address entry
+ * @addr:		IPv4 (as mapped) or IPv6 address
+ * @prefix_len:	Prefix length (0-32 for IPv4, 0-128 for IPv6)
+ * @flags:		INANY_ADDR_* flags
+ */
+struct inany_addr_entry {
+	union inany_addr	addr;
+	uint16_t		prefix_len;
+	uint16_t		flags;
+};
+
 #endif /* INANY_H */
diff --git a/ip.h b/ip.h
index e5f5198..78bd43a 100644
--- a/ip.h
+++ b/ip.h
@@ -135,6 +135,10 @@ static const struct in_addr in4addr_broadcast = { 0xffffffff };
 #define IPV6_MIN_MTU		1280
 #endif
 
+/* Maximum number of addresses per address family */
+#define IP4_MAX_ADDRS		8
+#define IP6_MAX_ADDRS		16
+
 int ip4_default_prefix_len(const struct in_addr *addr);
 
 #endif /* IP_H */
diff --git a/ndp.c b/ndp.c
index eb9e313..5248fd6 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 = c->ip6.addrs[0].addr.a6,
 		.source_ll = {
 			.header = {
 				.type		= OPT_SRC_L2_ADDR,
@@ -466,8 +466,8 @@ void ndp_send_init_req(const struct ctx *c)
 			.icmp6_solicited	= 0, /* Reserved */
 			.icmp6_override		= 0, /* Reserved */
 		},
-		.target_addr = c->ip6.addr
+		.target_addr = c->ip6.addrs[0].addr.a6
 	};
 	debug("Sending initial NDP NS request for guest MAC address");
-	ndp_send(c, &c->ip6.addr, &ns, sizeof(ns));
+	ndp_send(c, &c->ip6.addrs[0].addr.a6, &ns, sizeof(ns));
 }
diff --git a/passt.h b/passt.h
index 79d01dd..9c0c3fe 100644
--- a/passt.h
+++ b/passt.h
@@ -66,9 +66,9 @@ enum passt_modes {
 
 /**
  * struct ip4_ctx - IPv4 execution context
- * @addr:		IPv4 address assigned to guest
+ * @addrs:		IPv4 addresses assigned to guest
+ * @addr_count:	Number of addresses in addrs[] array
  * @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
@@ -85,9 +85,10 @@ enum passt_modes {
  */
 struct ip4_ctx {
 	/* PIF_TAP addresses */
-	struct in_addr addr;
+	struct inany_addr_entry addrs[IP4_MAX_ADDRS];
+	int addr_count;
+
 	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 +108,8 @@ struct ip4_ctx {
 
 /**
  * struct ip6_ctx - IPv6 execution context
- * @addr:		IPv6 address assigned to guest
+ * @addrs:		IPv6 addresses assigned to guest
+ * @addr_count:		Number of addresses in addrs[] array
  * @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
@@ -126,7 +128,9 @@ struct ip4_ctx {
  */
 struct ip6_ctx {
 	/* PIF_TAP addresses */
-	struct in6_addr addr;
+	struct inany_addr_entry addrs[IP6_MAX_ADDRS];
+	int addr_count;
+
 	struct in6_addr addr_seen;
 	struct in6_addr addr_ll_seen;
 	struct in6_addr guest_gw;
diff --git a/pasta.c b/pasta.c
index c307b8a..1bb3dd0 100644
--- a/pasta.c
+++ b/pasta.c
@@ -340,8 +340,8 @@ void pasta_ns_conf(struct ctx *c)
 			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);
+						 inany_v4(&c->ip4.addrs[0].addr),
+						 c->ip4.addrs[0].prefix_len);
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi4,
 						 nl_sock_ns, c->pasta_ifi,
@@ -387,10 +387,12 @@ void pasta_ns_conf(struct ctx *c)
 					  0, IFF_NOARP);
 
 			if (c->ip6.no_copy_addrs) {
-				if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
+				struct in6_addr *a = &c->ip6.addrs[0].addr.a6;
+
+				if (!IN6_IS_ADDR_UNSPECIFIED(a)) {
 					rc = nl_addr_set(nl_sock_ns,
-							 c->pasta_ifi, AF_INET6,
-							 &c->ip6.addr, 64);
+							 c->pasta_ifi,
+							 AF_INET6, a, 64);
 				}
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi6,
diff --git a/tap.c b/tap.c
index 9d1344b..7c50013 100644
--- a/tap.c
+++ b/tap.c
@@ -951,8 +951,8 @@ resume:
 				c->ip6.addr_seen = *saddr;
 			}
 
-			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
-				c->ip6.addr = *saddr;
+			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addrs[0].addr.a6))
+				c->ip6.addrs[0].addr.a6 = *saddr;
 		} else if (!IN6_IS_ADDR_UNSPECIFIED(saddr)){
 			c->ip6.addr_seen = *saddr;
 		}
-- 
2.52.0


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

* [PATCH v2 3/9] conf: Refactor conf_print() for multi-address support
  2026-01-18 22:16 [PATCH v2 0/9] Introduce multiple addresses Jon Maloy
  2026-01-18 22:16 ` [PATCH v2 1/9] conf: Support CIDR notation for -a/--address option Jon Maloy
  2026-01-18 22:16 ` [PATCH v2 2/9] ip: Introduce unified multi-address data structures Jon Maloy
@ 2026-01-18 22:16 ` Jon Maloy
  2026-01-19  7:25   ` David Gibson
  2026-01-21 13:02   ` Stefano Brivio
  2026-01-18 22:16 ` [PATCH v2 4/9] fwd: Check all configured addresses in guest accessibility functions Jon Maloy
                   ` (5 subsequent siblings)
  8 siblings, 2 replies; 19+ messages in thread
From: Jon Maloy @ 2026-01-18 22:16 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

As a preparation for multiple address support, we refactor the
conf_print() function to handle this properly.

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

diff --git a/conf.c b/conf.c
index 9fc5dca..3ecd1a0 100644
--- a/conf.c
+++ b/conf.c
@@ -1199,20 +1199,28 @@ static void conf_print(const struct ctx *c)
 				       buf4, sizeof(buf4)));
 
 		if (!c->no_dhcp) {
-			uint32_t mask;
-
-			mask = htonl(0xffffffff <<
-				     (32 - c->ip4.addrs[0].prefix_len));
-
-			info("DHCP:");
-			info("    assign: %s",
-			     inet_ntop(AF_INET, inany_v4(&c->ip4.addrs[0].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 < c->ip4.addr_count; i++) {
+				const struct inany_addr_entry *e;
+				uint32_t mask;
+
+				e = &c->ip4.addrs[i];
+				if (!(e->flags & INANY_ADDR_CONFIGURED) &&
+				    c->ip4.addr_count > 1)
+					continue;
+
+				mask = htonl(0xffffffff << (32 - e->prefix_len));
+
+				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)));
+				break;
+			}
 		}
 
 		for (i = 0; !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns[i]); i++) {
@@ -1230,30 +1238,34 @@ static void conf_print(const struct ctx *c)
 	}
 
 	if (c->ifi6) {
+		bool do_slaac = !c->no_ndp || !c->no_dhcpv6;
+
 		if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback))
 			info("    NAT to host ::1: %s",
 			     inet_ntop(AF_INET6, &c->ip6.map_host_loopback,
 				       buf6, sizeof(buf6)));
 
-		if (!c->no_ndp && !c->no_dhcpv6)
-			info("NDP/DHCPv6:");
-		else if (!c->no_dhcpv6)
-			info("DHCPv6:");
-		else if (!c->no_ndp)
-			info("NDP:");
-		else
-			goto dns6;
-
-		info("    assign: %s",
-		     inet_ntop(AF_INET6, &c->ip6.addrs[0].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)));
-
-dns6:
+		if (do_slaac) {
+			if (!c->no_ndp && !c->no_dhcpv6)
+				info("NDP/DHCPv6:");
+			else if (!c->no_dhcpv6)
+				info("DHCPv6:");
+			else
+				info("NDP:");
+
+			for (i = 0; i < c->ip6.addr_count; i++) {
+				info("    assign: %s",
+				     inet_ntop(AF_INET6, &c->ip6.addrs[i].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)));
+		}
+
 		for (i = 0; !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.dns[i]); i++) {
 			if (!i)
 				info("DNS:");
-- 
2.52.0


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

* [PATCH v2 4/9] fwd: Check all configured addresses in guest accessibility functions
  2026-01-18 22:16 [PATCH v2 0/9] Introduce multiple addresses Jon Maloy
                   ` (2 preceding siblings ...)
  2026-01-18 22:16 ` [PATCH v2 3/9] conf: Refactor conf_print() for multi-address support Jon Maloy
@ 2026-01-18 22:16 ` Jon Maloy
  2026-01-19  7:29   ` David Gibson
  2026-01-18 22:16 ` [PATCH v2 5/9] arp: Check all configured addresses in ARP filtering Jon Maloy
                   ` (4 subsequent siblings)
  8 siblings, 1 reply; 19+ messages in thread
From: Jon Maloy @ 2026-01-18 22:16 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

As a preparation for handling multiple addresses, we update
fwd_guest_accessible4() and fwd_guest_accessible6() to check
against all addresses in the addrs[] array.

This ensures that when multiple addresses are configured via -a options,
inbound traffic for any of them is correctly detected as having no valid
forwarding path, and subsequently dropped. This occurs when a peer
address collides with an address the guest is using, and we have no
translation for it.

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

---
v2: Updated commit log to make it clearer
---
 fwd.c | 22 ++++++++++++++++------
 1 file changed, 16 insertions(+), 6 deletions(-)

diff --git a/fwd.c b/fwd.c
index 8d8151b..f1db34c 100644
--- a/fwd.c
+++ b/fwd.c
@@ -502,6 +502,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)
 {
+	int i;
+
 	if (IN4_IS_ADDR_LOOPBACK(addr))
 		return false;
 
@@ -513,11 +515,15 @@ static bool fwd_guest_accessible4(const struct ctx *c,
 	if (IN4_IS_ADDR_UNSPECIFIED(addr))
 		return false;
 
-	/* For IPv4, addr_seen is initialised to addr, so is always a valid
-	 * address
+	/* Check against all configured guest addresses */
+	for (i = 0; i < c->ip4.addr_count; i++)
+		if (IN4_ARE_ADDR_EQUAL(addr, inany_v4(&c->ip4.addrs[i].addr)))
+			return false;
+
+	/* Also check addr_seen: it tracks the address the guest is actually
+	 * using, which may differ from configured addresses.
 	 */
-	if (IN4_ARE_ADDR_EQUAL(addr, inany_v4(&c->ip4.addrs[0].addr)) ||
-	    IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
+	if (IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
 		return false;
 
 	return true;
@@ -534,11 +540,15 @@ static bool fwd_guest_accessible4(const struct ctx *c,
 static bool fwd_guest_accessible6(const struct ctx *c,
 				  const struct in6_addr *addr)
 {
+	int i;
+
 	if (IN6_IS_ADDR_LOOPBACK(addr))
 		return false;
 
-	if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addrs[0].addr.a6))
-		return false;
+	/* Check against all configured guest addresses */
+	for (i = 0; i < c->ip6.addr_count; i++)
+		if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addrs[i].addr.a6))
+			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
-- 
2.52.0


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

* [PATCH v2 5/9] arp: Check all configured addresses in ARP filtering
  2026-01-18 22:16 [PATCH v2 0/9] Introduce multiple addresses Jon Maloy
                   ` (3 preceding siblings ...)
  2026-01-18 22:16 ` [PATCH v2 4/9] fwd: Check all configured addresses in guest accessibility functions Jon Maloy
@ 2026-01-18 22:16 ` Jon Maloy
  2026-01-19  8:28   ` David Gibson
  2026-01-18 22:16 ` [PATCH v2 6/9] conf: Allow multiple -a/--address options per address family Jon Maloy
                   ` (3 subsequent siblings)
  8 siblings, 1 reply; 19+ messages in thread
From: Jon Maloy @ 2026-01-18 22:16 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 addrs[] array.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
 arp.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/arp.c b/arp.c
index bc77a9f..d8063e2 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)
 {
+	int i;
+
 	if (ah->ar_hrd != htons(ARPHRD_ETHER)	||
 	    ah->ar_pro != htons(ETH_P_IP)	||
 	    ah->ar_hln != ETH_ALEN		||
@@ -53,9 +55,11 @@ static bool ignore_arp(const struct ctx *c,
 	    !memcmp(am->sip, am->tip, sizeof(am->sip)))
 		return true;
 
-	/* Don't resolve the guest's assigned address, either. */
-	if (!memcmp(am->tip, inany_v4(&c->ip4.addrs[0].addr), sizeof(am->tip)))
-		return true;
+	/* Don't resolve any of the guest's addresses */
+	for (i = 0; i < c->ip4.addr_count; i++)
+		if (!memcmp(am->tip, inany_v4(&c->ip4.addrs[i].addr),
+			    sizeof(am->tip)))
+			return true;
 
 	return false;
 }
-- 
2.52.0


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

* [PATCH v2 6/9] conf: Allow multiple -a/--address options per address family
  2026-01-18 22:16 [PATCH v2 0/9] Introduce multiple addresses Jon Maloy
                   ` (4 preceding siblings ...)
  2026-01-18 22:16 ` [PATCH v2 5/9] arp: Check all configured addresses in ARP filtering Jon Maloy
@ 2026-01-18 22:16 ` Jon Maloy
  2026-01-19  8:41   ` David Gibson
  2026-01-18 22:16 ` [PATCH v2 7/9] pasta: Unify address configuration paths using address array Jon Maloy
                   ` (2 subsequent siblings)
  8 siblings, 1 reply; 19+ messages in thread
From: Jon Maloy @ 2026-01-18 22:16 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

We enable configuration of multiple IPv4 and IPv6 addresses by allowing
repeated use of the -a/--address option.

- We update option parsing to append addresses to the addrs[] array.
- Each address specified via -a does initially get a class-based default
  prefix.
- If no -a option is given, address and prefix are inherited from
  the template interface.
- If a prefix length is to be added, it has to be done in CIDR format,
  except for the very first address.
- We configure all indicated addresses in the namespace interface.

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

---
v2: Adapted to previous code changes
---
 conf.c  | 42 +++++++++++++++++++++++++++++-------------
 pasta.c | 24 ++++++++++++++++++------
 2 files changed, 47 insertions(+), 19 deletions(-)

diff --git a/conf.c b/conf.c
index 3ecd1a0..32a754d 100644
--- a/conf.c
+++ b/conf.c
@@ -789,7 +789,7 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
 
 	ip4->our_tap_addr = ip4->guest_gw;
 
-	if (inany_is_unspecified(&ip4->addrs[0].addr))
+	if (!ip4->addr_count)
 		return 0;
 
 	return ifi;
@@ -858,8 +858,7 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
 	if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
 		ip6->our_tap_ll = ip6->guest_gw;
 
-	if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addrs[0].addr.a6) ||
-	    IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
+	if (!ip6->addr_count || IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
 		return 0;
 
 	return ifi;
@@ -951,9 +950,11 @@ static void usage(const char *name, FILE *f, int status)
 		"    default: 65520: maximum 802.3 MTU minus 802.3 header\n"
 		"                    length, rounded to 32 bits (IPv4 words)\n"
 		"  -a, --address ADDR	Assign IPv4 or IPv6 address ADDR[/PREFIXLEN]\n"
-		"    can be specified zero to two times (for IPv4 and IPv6)\n"
+		"    can be specified multiple times (limit: %d IPv4, %d IPv6)\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",
+		IP4_MAX_ADDRS, IP6_MAX_ADDRS);
+	FPRINTF(f,
 		"    default: netmask from matching address on the host\n"
 		"  -M, --mac-addr ADDR	Use source MAC address ADDR\n"
 		"    default: 9a:55:9a:55:9a:55 (locally administered)\n"
@@ -1882,6 +1883,7 @@ void conf(struct ctx *c, int argc, char **argv)
 			union inany_addr addr;
 			const struct in_addr *a4;
 			int prefix_len = 0;
+			unsigned int i;
 			int af;
 
 			af = conf_addr_prefix_len(optarg, &addr, &prefix_len);
@@ -1893,9 +1895,15 @@ void conf(struct ctx *c, int argc, char **argv)
 				die("Invalid address: %s", optarg);
 
 			if (af == AF_INET6) {
-				c->ip6.addrs[0].addr.a6 = addr.a6;
-				c->ip6.addrs[0].flags |= INANY_ADDR_CONFIGURED;
-				c->ip6.addr_count = 1;
+				i = c->ip6.addr_count;
+
+				if (i >= IP6_MAX_ADDRS)
+					die("Too many IPv6 addresses");
+
+				c->ip6.addrs[i].addr.a6 = addr.a6;
+				c->ip6.addrs[i].prefix_len = prefix_len;
+				c->ip6.addrs[i].flags = INANY_ADDR_CONFIGURED;
+				c->ip6.addr_count++;
 				if (c->mode == MODE_PASTA)
 					c->ip6.no_copy_addrs = true;
 				break;
@@ -1904,10 +1912,15 @@ void conf(struct ctx *c, int argc, char **argv)
 			a4 = inany_v4(&addr);
 
 			if (af == AF_INET && a4) {
-				c->ip4.addrs[0].addr = inany_from_v4(*a4);
-				c->ip4.addrs[0].flags |= INANY_ADDR_CONFIGURED;
-				c->ip4.addr_count = 1;
-				if (prefix_len) {
+				i = c->ip4.addr_count;
+
+				if (i >= IP4_MAX_ADDRS)
+					die("Too many IPv4 addresses");
+
+				c->ip4.addrs[i].addr = inany_from_v4(*a4);
+				c->ip4.addrs[i].prefix_len = prefix_len;
+				c->ip4.addrs[i].flags = INANY_ADDR_CONFIGURED;
+				if (i == 0 && prefix_len) {
 					if (prefix_from_opt)
 						die("Can't mix CIDR with -n");
 					prefix_from_cidr = true;
@@ -1915,6 +1928,9 @@ void conf(struct ctx *c, int argc, char **argv)
 					prefix_len = ip4_default_prefix_len(a4);
 				}
 				c->ip4.addrs[0].prefix_len = prefix_len;
+				c->ip4.addr_count++;
+				if (c->mode == MODE_PASTA)
+					c->ip4.no_copy_addrs = true;
 				break;
 			}
 
@@ -2217,7 +2233,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.addrs[0].addr.a6)) {
+	} else if (!c->ip6.addr_count) {
 		c->no_dhcpv6 = 1;
 	}
 
diff --git a/pasta.c b/pasta.c
index 1bb3dd0..27ce6a7 100644
--- a/pasta.c
+++ b/pasta.c
@@ -338,10 +338,16 @@ 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,
-						 inany_v4(&c->ip4.addrs[0].addr),
-						 c->ip4.addrs[0].prefix_len);
+				int i;
+
+				for (i = 0; i < c->ip4.addr_count; i++) {
+					rc = nl_addr_set(nl_sock_ns,
+							 c->pasta_ifi, AF_INET,
+							 inany_v4(&c->ip4.addrs[i].addr),
+							 c->ip4.addrs[i].prefix_len);
+					if (rc < 0)
+						break;
+				}
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi4,
 						 nl_sock_ns, c->pasta_ifi,
@@ -387,12 +393,18 @@ void pasta_ns_conf(struct ctx *c)
 					  0, IFF_NOARP);
 
 			if (c->ip6.no_copy_addrs) {
-				struct in6_addr *a = &c->ip6.addrs[0].addr.a6;
+				struct in6_addr *a;
+				int i;
 
-				if (!IN6_IS_ADDR_UNSPECIFIED(a)) {
+				for (i = 0; i < c->ip6.addr_count; i++) {
+					a = &c->ip6.addrs[i].addr.a6;
+					if (IN6_IS_ADDR_UNSPECIFIED(a))
+						continue;
 					rc = nl_addr_set(nl_sock_ns,
 							 c->pasta_ifi,
 							 AF_INET6, a, 64);
+					if (rc < 0)
+						break;
 				}
 			} else {
 				rc = nl_addr_dup(nl_sock, c->ifi6,
-- 
2.52.0


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

* [PATCH v2 7/9] pasta: Unify address configuration paths using address array
  2026-01-18 22:16 [PATCH v2 0/9] Introduce multiple addresses Jon Maloy
                   ` (5 preceding siblings ...)
  2026-01-18 22:16 ` [PATCH v2 6/9] conf: Allow multiple -a/--address options per address family Jon Maloy
@ 2026-01-18 22:16 ` Jon Maloy
  2026-01-18 22:16 ` [PATCH v2 8/9] ip: Track observed guest IPv4 addresses in unified " Jon Maloy
  2026-01-18 22:16 ` [PATCH v2 9/9] ip: Track observed guest IPv6 " Jon Maloy
  8 siblings, 0 replies; 19+ messages in thread
From: Jon Maloy @ 2026-01-18 22:16 UTC (permalink / raw)
  To: sbrivio, dgibson, david, jmaloy, passt-dev

Until now, pasta has maintained two separate code paths for
setting up addresses in the guest namespace:

- "copy addrs" path: When --config-net is used without -a, pasta
   would call nl_addr_dup() to copy addresses directly from the host
   template interface to the guest.

- "no_copy_addrs" path: When -a is specified, pasta would iterate
   through the addrs[] array and call nl_addr_set() for each entry.

This commit unifies these paths by adding a new nl_addr_get_all()
function that retrieves all addresses from an interface into the
addrs[] array, marking them with INANY_ADDR_HOST flag.

We modify conf_ip4() and conf_ip6() to always populate the address
array, either from command-line -a options or from host interface
discovery.

This gives us a single code path for all address configuration scenarios
and a more consistent address handling regardless of source.

Suggested-by: David Gibson <david@gibson.dropbear.id.au>
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
 conf.c    | 82 ++++++++++++++++++++++++++++-------------------------
 netlink.c | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 netlink.h |  5 ++++
 passt.h   |  4 ---
 pasta.c   | 36 ++++++++++--------------
 5 files changed, 147 insertions(+), 64 deletions(-)

diff --git a/conf.c b/conf.c
index 32a754d..22c2222 100644
--- a/conf.c
+++ b/conf.c
@@ -745,6 +745,8 @@ static int conf_addr_prefix_len(char *arg, union inany_addr *addr,
  */
 static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
 {
+	int i;
+
 	if (!ifi)
 		ifi = nl_get_ext_if(nl_sock, AF_INET);
 
@@ -764,25 +766,25 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
 	}
 
 	if (!ip4->addr_count) {
-		struct in_addr addr;
-		int prefix_len = 0;
-		int rc = nl_addr_get(nl_sock, ifi, AF_INET,
-				     &addr, &prefix_len, NULL);
-		if (rc < 0) {
+		int count = nl_addr_get_all(nl_sock, ifi, AF_INET,
+					    ip4->addrs, IP4_MAX_ADDRS, NULL);
+		if (count < 0) {
 			debug("Couldn't discover IPv4 address: %s",
-			      strerror_(-rc));
+			      strerror_(-count));
+			return 0;
+		}
+		if (count == 0) {
+			debug("No IPv4 address on interface");
 			return 0;
 		}
-		ip4->addrs[0].addr = inany_from_v4(addr);
-		ip4->addrs[0].prefix_len = prefix_len;
-		ip4->addrs[0].flags = INANY_ADDR_HOST;
-		ip4->addr_count = 1;
+		ip4->addr_count = count;
 	}
 
-	if (!ip4->addrs[0].prefix_len) {
-		const struct in_addr *a4 = inany_v4(&ip4->addrs[0].addr);
+	for (i = 0; i < (int)ip4->addr_count; i++) {
+		const struct in_addr *a4 = inany_v4(&ip4->addrs[i].addr);
 
-		ip4->addrs[0].prefix_len = ip4_default_prefix_len(a4);
+		if (!ip4->addrs[i].prefix_len)
+			ip4->addrs[i].prefix_len = ip4_default_prefix_len(a4);
 	}
 
 	ip4->addr_seen = *inany_v4(&ip4->addrs[0].addr);
@@ -807,7 +809,7 @@ static void conf_ip4_local(struct ip4_ctx *ip4)
 	ip4->addrs[0].prefix_len = IP4_LL_PREFIX_LEN;
 	ip4->addr_count = 1;
 
-	ip4->no_copy_addrs = ip4->no_copy_routes = true;
+	ip4->no_copy_routes = true;
 }
 
 /**
@@ -819,7 +821,6 @@ static void conf_ip4_local(struct ip4_ctx *ip4)
  */
 static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
 {
-	int prefix_len = 0;
 	int rc;
 
 	if (!ifi)
@@ -839,18 +840,29 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
 		}
 	}
 
-	rc = nl_addr_get(nl_sock, ifi, AF_INET6,
-			 ip6->addr_count ? NULL : &ip6->addrs[0].addr.a6,
-			 &prefix_len, &ip6->our_tap_ll);
-	if (rc < 0) {
-		debug("Couldn't discover IPv6 address: %s", strerror_(-rc));
-		return 0;
-	}
-
 	if (!ip6->addr_count) {
-		ip6->addrs[0].prefix_len = prefix_len ? prefix_len : 64;
-		ip6->addrs[0].flags = INANY_ADDR_HOST;
-		ip6->addr_count = 1;
+		int count = nl_addr_get_all(nl_sock, ifi, AF_INET6,
+					    ip6->addrs, IP6_MAX_ADDRS,
+					    &ip6->our_tap_ll);
+		if (count < 0) {
+			debug("Couldn't discover IPv6 address: %s",
+			      strerror_(-count));
+			return 0;
+		}
+		if (count == 0) {
+			debug("No IPv6 address on interface");
+			return 0;
+		}
+		ip6->addr_count = count;
+	} else {
+		/* Even with -a, we still need the link-local for our_tap_ll */
+		rc = nl_addr_get(nl_sock, ifi, AF_INET6,
+				 NULL, NULL, &ip6->our_tap_ll);
+		if (rc < 0) {
+			debug("Couldn't discover IPv6 link-local: %s",
+			      strerror_(-rc));
+			return 0;
+		}
 	}
 
 	ip6->addr_seen = ip6->addrs[0].addr.a6;
@@ -872,7 +884,7 @@ static void conf_ip6_local(struct ip6_ctx *ip6)
 {
 	ip6->our_tap_ll = ip6->guest_gw = IP6_LL_GUEST_GW;
 
-	ip6->no_copy_addrs = ip6->no_copy_routes = true;
+	ip6->no_copy_routes = true;
 }
 
 /**
@@ -1723,8 +1735,7 @@ void conf(struct ctx *c, int argc, char **argv)
 			if (c->mode != MODE_PASTA)
 				die("--no-copy-addrs is for pasta mode only");
 
-			warn("--no-copy-addrs will be dropped soon");
-			c->ip4.no_copy_addrs = c->ip6.no_copy_addrs = true;
+			warn("--no-copy-addrs is deprecated and has no effect");
 			copy_addrs_opt = true;
 			break;
 		case 20:
@@ -1883,8 +1894,7 @@ void conf(struct ctx *c, int argc, char **argv)
 			union inany_addr addr;
 			const struct in_addr *a4;
 			int prefix_len = 0;
-			unsigned int i;
-			int af;
+			int af, i;
 
 			af = conf_addr_prefix_len(optarg, &addr, &prefix_len);
 
@@ -1901,11 +1911,9 @@ void conf(struct ctx *c, int argc, char **argv)
 					die("Too many IPv6 addresses");
 
 				c->ip6.addrs[i].addr.a6 = addr.a6;
-				c->ip6.addrs[i].prefix_len = prefix_len;
+				c->ip6.addrs[i].prefix_len = prefix_len ? prefix_len : 64;
 				c->ip6.addrs[i].flags = INANY_ADDR_CONFIGURED;
 				c->ip6.addr_count++;
-				if (c->mode == MODE_PASTA)
-					c->ip6.no_copy_addrs = true;
 				break;
 			}
 
@@ -1918,19 +1926,17 @@ void conf(struct ctx *c, int argc, char **argv)
 					die("Too many IPv4 addresses");
 
 				c->ip4.addrs[i].addr = inany_from_v4(*a4);
-				c->ip4.addrs[i].prefix_len = prefix_len;
 				c->ip4.addrs[i].flags = INANY_ADDR_CONFIGURED;
-				if (i == 0 && prefix_len) {
+				if (prefix_len) {
 					if (prefix_from_opt)
 						die("Can't mix CIDR with -n");
+					c->ip4.addrs[i].prefix_len = prefix_len;
 					prefix_from_cidr = true;
 				} else {
 					prefix_len = ip4_default_prefix_len(a4);
 				}
 				c->ip4.addrs[0].prefix_len = prefix_len;
 				c->ip4.addr_count++;
-				if (c->mode == MODE_PASTA)
-					c->ip4.no_copy_addrs = true;
 				break;
 			}
 
diff --git a/netlink.c b/netlink.c
index 82a2f0c..8c4412c 100644
--- a/netlink.c
+++ b/netlink.c
@@ -35,6 +35,7 @@
 #include "passt.h"
 #include "log.h"
 #include "ip.h"
+#include "inany.h"
 #include "netlink.h"
 #include "epoll_ctl.h"
 
@@ -812,6 +813,89 @@ int nl_addr_get(int s, unsigned int ifi, sa_family_t af,
 	return status;
 }
 
+/**
+ * nl_addr_get_all() - Read all addresses for a given interface into an array
+ * @s:		Netlink socket
+ * @ifi:	Interface index
+ * @af:	Address family
+ * @addrs:	Array of address entries to fill
+ * @max_addrs:	Maximum number of addresses to store
+ * @addr_ll:	Fill with link-local address if non-NULL
+ *
+ * Return: number of addresses found on success, negative error code on failure
+ */
+int nl_addr_get_all(int s, unsigned int ifi, sa_family_t af,
+		    struct inany_addr_entry *addrs, int max_addrs,
+		    void *addr_ll)
+{
+	struct req_t {
+		struct nlmsghdr nlh;
+		struct ifaddrmsg ifa;
+	} req = {
+		.ifa.ifa_family = af,
+		.ifa.ifa_index  = ifi,
+	};
+	struct nlmsghdr *nh;
+	char buf[NLBUFSIZ];
+	ssize_t status;
+	int count = 0;
+	uint32_t seq;
+
+	seq = nl_send(s, &req, RTM_GETADDR, NLM_F_DUMP, sizeof(req));
+	nl_foreach_oftype(nh, status, s, buf, seq, RTM_NEWADDR) {
+		struct ifaddrmsg *ifa = (struct ifaddrmsg *)NLMSG_DATA(nh);
+		struct rtattr *rta;
+		size_t na;
+
+		if (ifa->ifa_index != ifi || ifa->ifa_flags & IFA_F_DEPRECATED)
+			continue;
+
+		/* Add link-local address if requested */
+		if (ifa->ifa_scope == RT_SCOPE_LINK &&
+		    addr_ll && af == AF_INET6) {
+			for (rta = IFA_RTA(ifa), na = IFA_PAYLOAD(nh);
+			     RTA_OK(rta, na); rta = RTA_NEXT(rta, na)) {
+				if (rta->rta_type == IFA_ADDRESS) {
+					memcpy(addr_ll, RTA_DATA(rta),
+					       RTA_PAYLOAD(rta));
+					break;
+				}
+			}
+			continue;
+		}
+
+		for (rta = IFA_RTA(ifa), na = IFA_PAYLOAD(nh); RTA_OK(rta, na);
+		     rta = RTA_NEXT(rta, na)) {
+			if (af == AF_INET  && rta->rta_type != IFA_LOCAL)
+				continue;
+
+			if (af == AF_INET6 && rta->rta_type != IFA_ADDRESS)
+				continue;
+
+			if (count >= max_addrs)
+				break;
+
+			if (af == AF_INET) {
+				struct in_addr a4;
+
+				memcpy(&a4, RTA_DATA(rta), sizeof(a4));
+				addrs[count].addr = inany_from_v4(a4);
+			} else {
+				memcpy(&addrs[count].addr.a6, RTA_DATA(rta),
+				       sizeof(addrs[count].addr.a6));
+			}
+			addrs[count].prefix_len = ifa->ifa_prefixlen;
+			addrs[count].flags = INANY_ADDR_HOST;
+			count++;
+		}
+	}
+
+	if (status < 0)
+		return status;
+
+	return count;
+}
+
 /**
  * nl_addr_get_ll() - Get first IPv6 link-local address for a given interface
  * @s:		Netlink socket
diff --git a/netlink.h b/netlink.h
index 8f1e9b9..c983922 100644
--- a/netlink.h
+++ b/netlink.h
@@ -6,6 +6,8 @@
 #ifndef NETLINK_H
 #define NETLINK_H
 
+struct inany_addr_entry;
+
 extern int nl_sock;
 extern int nl_sock_ns;
 
@@ -17,6 +19,9 @@ int nl_route_dup(int s_src, unsigned int ifi_src,
 		 int s_dst, unsigned int ifi_dst, sa_family_t af);
 int nl_addr_get(int s, unsigned int ifi, sa_family_t af,
 		void *addr, int *prefix_len, void *addr_l);
+int nl_addr_get_all(int s, unsigned int ifi, sa_family_t af,
+		    struct inany_addr_entry *addrs, int max_addrs,
+		    void *addr_ll);
 bool nl_neigh_mac_get(int s, const union inany_addr *addr, int ifi,
 		      unsigned char *mac);
 int nl_addr_set(int s, unsigned int ifi, sa_family_t af,
diff --git a/passt.h b/passt.h
index 9c0c3fe..929b474 100644
--- a/passt.h
+++ b/passt.h
@@ -81,7 +81,6 @@ enum passt_modes {
  * @addr_out:		Optional source address for outbound traffic
  * @ifname_out:		Optional interface name to bind outbound sockets to
  * @no_copy_routes:	Don't copy all routes when configuring target namespace
- * @no_copy_addrs:	Don't copy all addresses when configuring namespace
  */
 struct ip4_ctx {
 	/* PIF_TAP addresses */
@@ -103,7 +102,6 @@ struct ip4_ctx {
 	char ifname_out[IFNAMSIZ];
 
 	bool no_copy_routes;
-	bool no_copy_addrs;
 };
 
 /**
@@ -124,7 +122,6 @@ struct ip4_ctx {
  * @addr_out:		Optional source address for outbound traffic
  * @ifname_out:		Optional interface name to bind outbound sockets to
  * @no_copy_routes:	Don't copy all routes when configuring target namespace
- * @no_copy_addrs:	Don't copy all addresses when configuring namespace
  */
 struct ip6_ctx {
 	/* PIF_TAP addresses */
@@ -147,7 +144,6 @@ struct ip6_ctx {
 	char ifname_out[IFNAMSIZ];
 
 	bool no_copy_routes;
-	bool no_copy_addrs;
 };
 
 #include <netinet/if_ether.h>
diff --git a/pasta.c b/pasta.c
index 27ce6a7..faf1a73 100644
--- a/pasta.c
+++ b/pasta.c
@@ -337,21 +337,15 @@ void pasta_ns_conf(struct ctx *c)
 		nl_link_set_flags(nl_sock_ns, c->pasta_ifi, flags, flags);
 
 		if (c->ifi4) {
-			if (c->ip4.no_copy_addrs) {
-				int i;
-
-				for (i = 0; i < c->ip4.addr_count; i++) {
-					rc = nl_addr_set(nl_sock_ns,
-							 c->pasta_ifi, AF_INET,
-							 inany_v4(&c->ip4.addrs[i].addr),
-							 c->ip4.addrs[i].prefix_len);
-					if (rc < 0)
-						break;
-				}
-			} else {
-				rc = nl_addr_dup(nl_sock, c->ifi4,
-						 nl_sock_ns, c->pasta_ifi,
-						 AF_INET);
+			int i;
+
+			for (i = 0; i < c->ip4.addr_count; i++) {
+				rc = nl_addr_set(nl_sock_ns,
+						 c->pasta_ifi, AF_INET,
+						 inany_v4(&c->ip4.addrs[i].addr),
+						 c->ip4.addrs[i].prefix_len);
+				if (rc < 0)
+					break;
 			}
 
 			if (rc < 0) {
@@ -392,7 +386,7 @@ void pasta_ns_conf(struct ctx *c)
 			nl_link_set_flags(nl_sock_ns, c->pasta_ifi,
 					  0, IFF_NOARP);
 
-			if (c->ip6.no_copy_addrs) {
+		{
 				struct in6_addr *a;
 				int i;
 
@@ -400,16 +394,14 @@ void pasta_ns_conf(struct ctx *c)
 					a = &c->ip6.addrs[i].addr.a6;
 					if (IN6_IS_ADDR_UNSPECIFIED(a))
 						continue;
+
 					rc = nl_addr_set(nl_sock_ns,
-							 c->pasta_ifi,
-							 AF_INET6, a, 64);
+							 c->pasta_ifi, AF_INET6,
+							 a,
+							 c->ip6.addrs[i].prefix_len);
 					if (rc < 0)
 						break;
 				}
-			} else {
-				rc = nl_addr_dup(nl_sock, c->ifi6,
-						 nl_sock_ns, c->pasta_ifi,
-						 AF_INET6);
 			}
 
 			if (rc < 0) {
-- 
2.52.0


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

* [PATCH v2 8/9] ip: Track observed guest IPv4 addresses in unified address array
  2026-01-18 22:16 [PATCH v2 0/9] Introduce multiple addresses Jon Maloy
                   ` (6 preceding siblings ...)
  2026-01-18 22:16 ` [PATCH v2 7/9] pasta: Unify address configuration paths using address array Jon Maloy
@ 2026-01-18 22:16 ` Jon Maloy
  2026-01-18 22:16 ` [PATCH v2 9/9] ip: Track observed guest IPv6 " Jon Maloy
  8 siblings, 0 replies; 19+ messages in thread
From: Jon Maloy @ 2026-01-18 22:16 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 INANY_ADDR_OBSERVED flag in the corresponding entry in
the new address array. If the seen address is not present in the
array we add it first.

Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
 conf.c    |  3 ---
 fwd.c     | 49 ++++++++++++++++++++++++++++++++++++++-----------
 inany.h   |  1 +
 migrate.c | 44 ++++++++++++++++++++++++++++++++++++++++++--
 passt.h   |  3 ---
 tap.c     | 38 +++++++++++++++++++++++++++++++++-----
 6 files changed, 114 insertions(+), 24 deletions(-)

diff --git a/conf.c b/conf.c
index 22c2222..8f0091d 100644
--- a/conf.c
+++ b/conf.c
@@ -787,8 +787,6 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
 			ip4->addrs[i].prefix_len = ip4_default_prefix_len(a4);
 	}
 
-	ip4->addr_seen = *inany_v4(&ip4->addrs[0].addr);
-
 	ip4->our_tap_addr = ip4->guest_gw;
 
 	if (!ip4->addr_count)
@@ -804,7 +802,6 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
 static void conf_ip4_local(struct ip4_ctx *ip4)
 {
 	ip4->addrs[0].addr = inany_from_v4(IP4_LL_GUEST_ADDR);
-	ip4->addr_seen = *inany_v4(&ip4->addrs[0].addr);
 	ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
 	ip4->addrs[0].prefix_len = IP4_LL_PREFIX_LEN;
 	ip4->addr_count = 1;
diff --git a/fwd.c b/fwd.c
index f1db34c..08b0b83 100644
--- a/fwd.c
+++ b/fwd.c
@@ -491,6 +491,29 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
 		((ini->oport == 53) || (ini->oport == 853));
 }
 
+/**
+ * fwd_guest_addr4() - Get first observed IPv4 guest address
+ * @c:		Execution context
+ *
+ * Return: pointer to first observed IPv4 address, or first address if none
+ *         observed, or NULL if no addresses
+ */
+static const struct in_addr *fwd_guest_addr4(const struct ctx *c)
+{
+	int i;
+
+	/* Find first observed address */
+	for (i = 0; i < c->ip4.addr_count; i++)
+		if (c->ip4.addrs[i].flags & INANY_ADDR_OBSERVED)
+			return inany_v4(&c->ip4.addrs[i].addr);
+
+	/* Fallback to first address */
+	if (c->ip4.addr_count > 0)
+		return inany_v4(&c->ip4.addrs[0].addr);
+
+	return NULL;
+}
+
 /**
  * fwd_guest_accessible4() - Is IPv4 address guest-accessible
  * @c:		Execution context
@@ -515,17 +538,11 @@ static bool fwd_guest_accessible4(const struct ctx *c,
 	if (IN4_IS_ADDR_UNSPECIFIED(addr))
 		return false;
 
-	/* Check against all configured guest addresses */
+	/* Check against all guest addresses */
 	for (i = 0; i < c->ip4.addr_count; i++)
 		if (IN4_ARE_ADDR_EQUAL(addr, inany_v4(&c->ip4.addrs[i].addr)))
 			return false;
 
-	/* Also check addr_seen: it tracks the address the guest is actually
-	 * using, which may differ from configured addresses.
-	 */
-	if (IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
-		return false;
-
 	return true;
 }
 
@@ -768,10 +785,16 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto,
 		 * match.
 		 */
 		if (inany_v4(&ini->eaddr)) {
-			if (c->host_lo_to_ns_lo)
+			const struct in_addr *guest_addr;
+
+			if (c->host_lo_to_ns_lo) {
 				tgt->eaddr = inany_loopback4;
-			else
-				tgt->eaddr = inany_from_v4(c->ip4.addr_seen);
+			} else {
+				guest_addr = fwd_guest_addr4(c);
+				if (!guest_addr)
+					return PIF_NONE;
+				tgt->eaddr = inany_from_v4(*guest_addr);
+			}
 			tgt->oaddr = inany_any4;
 		} else {
 			if (c->host_lo_to_ns_lo)
@@ -803,7 +826,11 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto,
 	tgt->oport = ini->eport;
 
 	if (inany_v4(&tgt->oaddr)) {
-		tgt->eaddr = inany_from_v4(c->ip4.addr_seen);
+		const struct in_addr *guest_addr = fwd_guest_addr4(c);
+
+		if (!guest_addr)
+			return PIF_NONE;
+		tgt->eaddr = inany_from_v4(*guest_addr);
 	} else {
 		if (inany_is_linklocal6(&tgt->oaddr))
 			tgt->eaddr.a6 = c->ip6.addr_ll_seen;
diff --git a/inany.h b/inany.h
index 07bfc3d..a334da9 100644
--- a/inany.h
+++ b/inany.h
@@ -300,6 +300,7 @@ int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len);
 /* Flags for struct inany_addr_entry */
 #define INANY_ADDR_CONFIGURED	(1 << 0)	/* User set via -a */
 #define INANY_ADDR_HOST	(1 << 1)	/* From host interface */
+#define INANY_ADDR_OBSERVED	(1 << 2)	/* Seen in guest traffic */
 
 /**
  * struct inany_addr_entry - Unified IPv4/IPv6 address entry
diff --git a/migrate.c b/migrate.c
index 48d63a0..01be6f1 100644
--- a/migrate.c
+++ b/migrate.c
@@ -57,8 +57,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,
 	};
+	int i;
+
+	/* Find first observed IPv4 address */
+	for (i = 0; i < c->ip4.addr_count; i++) {
+		if (c->ip4.addrs[i].flags & INANY_ADDR_OBSERVED) {
+			addrs.addr4 = *inany_v4(&c->ip4.addrs[i].addr);
+			break;
+		}
+	}
 
 	(void)stage;
 
@@ -70,6 +78,33 @@ static int seen_addrs_source_v1(struct ctx *c,
 	return 0;
 }
 
+/**
+ * migrate_observed_addr4() - Add observed IPv4 address to ip4_ctx address array
+ * @c:		Execution context
+ * @addr:	IPv4 address to add
+ */
+static void migrate_observed_addr4(struct ctx *c, const struct in_addr *addr)
+{
+	int i;
+
+	if (IN4_IS_ADDR_UNSPECIFIED(addr))
+		return;
+
+	for (i = 0; i < c->ip4.addr_count; i++) {
+		if (IN4_ARE_ADDR_EQUAL(addr, inany_v4(&c->ip4.addrs[i].addr))) {
+			c->ip4.addrs[i].flags |= INANY_ADDR_OBSERVED;
+			return;
+		}
+	}
+
+	if (c->ip4.addr_count < IP4_MAX_ADDRS) {
+		c->ip4.addrs[c->ip4.addr_count].addr = inany_from_v4(*addr);
+		c->ip4.addrs[c->ip4.addr_count].prefix_len = 0;
+		c->ip4.addrs[c->ip4.addr_count].flags = INANY_ADDR_OBSERVED;
+		c->ip4.addr_count++;
+	}
+}
+
 /**
  * seen_addrs_target_v1() - Receive and use guest observed addresses on target
  * @c:		Execution context
@@ -82,6 +117,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 +126,11 @@ 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;
+
+	/* Avoid alignment warning */
+	addr4 = addrs.addr4;
+	migrate_observed_addr4(c, &addr4);
+
 	memcpy(c->guest_mac, addrs.mac, sizeof(c->guest_mac));
 
 	return 0;
diff --git a/passt.h b/passt.h
index 929b474..db09da5 100644
--- a/passt.h
+++ b/passt.h
@@ -68,7 +68,6 @@ enum passt_modes {
  * struct ip4_ctx - IPv4 execution context
  * @addrs:		IPv4 addresses assigned to guest
  * @addr_count:	Number of addresses in addrs[] array
- * @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
@@ -87,7 +86,6 @@ struct ip4_ctx {
 	struct inany_addr_entry addrs[IP4_MAX_ADDRS];
 	int addr_count;
 
-	struct in_addr addr_seen;
 	struct in_addr guest_gw;
 	struct in_addr map_host_loopback;
 	struct in_addr map_guest_addr;
@@ -127,7 +125,6 @@ struct ip6_ctx {
 	/* PIF_TAP addresses */
 	struct inany_addr_entry addrs[IP6_MAX_ADDRS];
 	int addr_count;
-
 	struct in6_addr addr_seen;
 	struct in6_addr addr_ll_seen;
 	struct in6_addr guest_gw;
diff --git a/tap.c b/tap.c
index 7c50013..1a52adb 100644
--- a/tap.c
+++ b/tap.c
@@ -161,6 +161,37 @@ 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
+ *
+ * Add the address to ip4.addrs[] with OBSERVED flag if not already present.
+ */
+static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
+{
+	int i;
+
+	/* Check if already in array */
+	for (i = 0; i < c->ip4.addr_count; i++) {
+		if (IN4_ARE_ADDR_EQUAL(addr, inany_v4(&c->ip4.addrs[i].addr))) {
+			c->ip4.addrs[i].flags |= INANY_ADDR_OBSERVED;
+			return;
+		}
+	}
+
+	/* Add new entry if space available */
+	if (c->ip4.addr_count < IP4_MAX_ADDRS) {
+		c->ip4.addrs[c->ip4.addr_count].addr = inany_from_v4(*addr);
+		c->ip4.addrs[c->ip4.addr_count].prefix_len = 0;
+		c->ip4.addrs[c->ip4.addr_count].flags = INANY_ADDR_OBSERVED;
+		debug("added new IPv4 address at index %d", c->ip6.addr_count);
+		c->ip4.addr_count++;
+	} else {
+		warn("IPv4 address table full, can't add new address");
+	}
+}
+
 /**
  * tap_ip6_daddr() - Normal IPv6 destination address for inbound packets
  * @c:		Execution context
@@ -771,8 +802,8 @@ 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;
@@ -950,9 +981,6 @@ resume:
 			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen)) {
 				c->ip6.addr_seen = *saddr;
 			}
-
-			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addrs[0].addr.a6))
-				c->ip6.addrs[0].addr.a6 = *saddr;
 		} else if (!IN6_IS_ADDR_UNSPECIFIED(saddr)){
 			c->ip6.addr_seen = *saddr;
 		}
-- 
2.52.0


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

* [PATCH v2 9/9] ip: Track observed guest IPv6 addresses in unified address array
  2026-01-18 22:16 [PATCH v2 0/9] Introduce multiple addresses Jon Maloy
                   ` (7 preceding siblings ...)
  2026-01-18 22:16 ` [PATCH v2 8/9] ip: Track observed guest IPv4 addresses in unified " Jon Maloy
@ 2026-01-18 22:16 ` Jon Maloy
  8 siblings, 0 replies; 19+ messages in thread
From: Jon Maloy @ 2026-01-18 22:16 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 INANY_ADDR_OBSERVED and a new
INANY_ADDR_LINKLOCAL flag in the corresponding entry in the
address array. If the seen address is not present in the array
we add it first.

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>
---
 conf.c    |  2 --
 dhcpv6.c  |  6 ++---
 dhcpv6.h  |  2 +-
 fwd.c     | 70 +++++++++++++++++++++++++++++++++++++------------
 inany.h   |  3 ++-
 migrate.c | 67 ++++++++++++++++++++++++++++++++++++++++-------
 passt.h   |  7 ++---
 pasta.c   | 16 ++++++++++--
 tap.c     | 78 +++++++++++++++++++++++++++++++++++++++++++++----------
 9 files changed, 198 insertions(+), 53 deletions(-)

diff --git a/conf.c b/conf.c
index 8f0091d..d670bb7 100644
--- a/conf.c
+++ b/conf.c
@@ -862,8 +862,6 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
 		}
 	}
 
-	ip6->addr_seen = ip6->addrs[0].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 f45dece..027b4be 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -546,10 +546,11 @@ static size_t dhcpv6_client_fqdn_fill(const struct iov_tail *data,
  * Return: 0 if it's not a DHCPv6 message, 1 if handled, -1 on failure
  */
 int dhcpv6(struct ctx *c, struct iov_tail *data,
-	   const struct in6_addr *saddr, const struct in6_addr *daddr)
+	   const struct in6_addr *daddr)
 {
 	const struct opt_server_id *server_id = NULL;
 	const struct opt_hdr *client_id = NULL;
+
 	/* The _storage variables can't be local to the blocks they're used in,
 	 * because IOV_*_HEADER() may return pointers to them which are
 	 * dereferenced afterwards. Since we don't have Rust-like lifetime
@@ -587,8 +588,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);
@@ -679,7 +678,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);
-	c->ip6.addr_seen = c->ip6.addrs[0].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/fwd.c b/fwd.c
index 08b0b83..dfdb078 100644
--- a/fwd.c
+++ b/fwd.c
@@ -514,6 +514,44 @@ static const struct in_addr *fwd_guest_addr4(const struct ctx *c)
 	return NULL;
 }
 
+/**
+ * fwd_guest_addr6() - Get first observed IPv6 guest address
+ * @c:		Execution context
+ * @linklocal:	If true, find link-local address; if false, find global
+ *
+ * Return: pointer to first observed IPv6 address of requested scope,
+ *         or first address of that scope if none observed, or NULL
+ */
+static const struct in6_addr *fwd_guest_addr6(const struct ctx *c,
+					      bool linklocal)
+{
+	uint16_t want = INANY_ADDR_OBSERVED;
+	int i;
+
+	if (linklocal)
+		want |= INANY_ADDR_LINKLOCAL;
+
+	/* Find first observed address of matching scope */
+	for (i = 0; i < c->ip6.addr_count; i++) {
+		bool is_ll = !!(c->ip6.addrs[i].flags & INANY_ADDR_LINKLOCAL);
+
+		if (is_ll != linklocal)
+			continue;
+		if (c->ip6.addrs[i].flags & INANY_ADDR_OBSERVED)
+			return &c->ip6.addrs[i].addr.a6;
+	}
+
+	/* Fallback to first address of matching scope */
+	for (i = 0; i < c->ip6.addr_count; i++) {
+		bool is_ll = IN6_IS_ADDR_LINKLOCAL(&c->ip6.addrs[i].addr.a6);
+
+		if (is_ll == linklocal)
+			return &c->ip6.addrs[i].addr.a6;
+	}
+
+	return NULL;
+}
+
 /**
  * fwd_guest_accessible4() - Is IPv4 address guest-accessible
  * @c:		Execution context
@@ -562,19 +600,11 @@ static bool fwd_guest_accessible6(const struct ctx *c,
 	if (IN6_IS_ADDR_LOOPBACK(addr))
 		return false;
 
-	/* Check against all configured guest addresses */
+	/* Check against all guest addresses (configured, host, or observed) */
 	for (i = 0; i < c->ip6.addr_count; i++)
 		if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addrs[i].addr.a6))
 			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))
-		return false;
-
 	return true;
 }
 
@@ -797,10 +827,16 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto,
 			}
 			tgt->oaddr = inany_any4;
 		} else {
-			if (c->host_lo_to_ns_lo)
+			const struct in6_addr *guest_addr6;
+
+			if (c->host_lo_to_ns_lo) {
 				tgt->eaddr = inany_loopback6;
-			else
-				tgt->eaddr.a6 = c->ip6.addr_seen;
+			} else {
+				guest_addr6 = fwd_guest_addr6(c, false);
+				if (!guest_addr6)
+					return PIF_NONE;
+				tgt->eaddr.a6 = *guest_addr6;
+			}
 			tgt->oaddr = inany_any6;
 		}
 
@@ -832,10 +868,12 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto,
 			return PIF_NONE;
 		tgt->eaddr = inany_from_v4(*guest_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);
+		const struct in6_addr *guest_addr6 = fwd_guest_addr6(c, linklocal);
+
+		if (!guest_addr6)
+			return PIF_NONE;
+		tgt->eaddr.a6 = *guest_addr6;
 	}
 
 	return PIF_TAP;
diff --git a/inany.h b/inany.h
index a334da9..ae310e3 100644
--- a/inany.h
+++ b/inany.h
@@ -299,8 +299,9 @@ int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len);
 
 /* Flags for struct inany_addr_entry */
 #define INANY_ADDR_CONFIGURED	(1 << 0)	/* User set via -a */
-#define INANY_ADDR_HOST	(1 << 1)	/* From host interface */
+#define INANY_ADDR_HOST		(1 << 1)	/* From host interface */
 #define INANY_ADDR_OBSERVED	(1 << 2)	/* Seen in guest traffic */
+#define INANY_ADDR_LINKLOCAL	(1 << 3)	/* Link-local address */
 
 /**
  * struct inany_addr_entry - Unified IPv4/IPv6 address entry
diff --git a/migrate.c b/migrate.c
index 01be6f1..52f0cdf 100644
--- a/migrate.c
+++ b/migrate.c
@@ -54,12 +54,11 @@ 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 };
 	int i;
 
+	(void)stage;
+
 	/* Find first observed IPv4 address */
 	for (i = 0; i < c->ip4.addr_count; i++) {
 		if (c->ip4.addrs[i].flags & INANY_ADDR_OBSERVED) {
@@ -68,7 +67,22 @@ static int seen_addrs_source_v1(struct ctx *c,
 		}
 	}
 
-	(void)stage;
+	/* Find first observed IPv6 addresses (global and link-local) */
+	for (i = 0; i < c->ip6.addr_count; i++) {
+		struct in6_addr tmp;
+
+		if (!(c->ip6.addrs[i].flags & INANY_ADDR_OBSERVED))
+			continue;
+		if (c->ip6.addrs[i].flags & INANY_ADDR_LINKLOCAL) {
+			tmp = addrs.addr6_ll;
+			if (IN6_IS_ADDR_UNSPECIFIED(&tmp))
+				addrs.addr6_ll = c->ip6.addrs[i].addr.a6;
+		} else {
+			tmp = addrs.addr6;
+			if (IN6_IS_ADDR_UNSPECIFIED(&tmp))
+				addrs.addr6 = c->ip6.addrs[i].addr.a6;
+		}
+	}
 
 	memcpy(addrs.mac, c->guest_mac, sizeof(addrs.mac));
 
@@ -105,6 +119,38 @@ static void migrate_observed_addr4(struct ctx *c, const struct in_addr *addr)
 	}
 }
 
+/**
+ * migrate_observed_addr6() - Add observed IPv6 address to ip6_ctx address array
+ * @c:		Execution context
+ * @addr:	IPv6 address to add
+ */
+static void migrate_observed_addr6(struct ctx *c, const struct in6_addr *addr)
+{
+	uint16_t flags = INANY_ADDR_OBSERVED;
+	int i;
+
+	if (IN6_IS_ADDR_UNSPECIFIED(addr))
+		return;
+
+	if (IN6_IS_ADDR_LINKLOCAL(addr))
+		flags |= INANY_ADDR_LINKLOCAL;
+
+	for (i = 0; i < c->ip6.addr_count; i++) {
+		if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addrs[i].addr.a6)) {
+			c->ip6.addrs[i].flags |= flags;
+			return;
+		}
+	}
+
+	if (c->ip6.addr_count < IP6_MAX_ADDRS) {
+		c->ip6.addrs[c->ip6.addr_count].addr.a6 = *addr;
+		c->ip6.addrs[c->ip6.addr_count].prefix_len = 0;
+		c->ip6.addrs[c->ip6.addr_count].flags = flags;
+		debug("added new entry at index %d", c->ip6.addr_count);
+		c->ip6.addr_count++;
+	}
+}
+
 /**
  * seen_addrs_target_v1() - Receive and use guest observed addresses on target
  * @c:		Execution context
@@ -117,6 +163,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;
@@ -124,12 +171,14 @@ 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;
-
-	/* Avoid alignment warning */
+	/* Copy from packed struct to avoid alignment issues */
 	addr4 = addrs.addr4;
+	addr6 = addrs.addr6;
+	addr6_ll = addrs.addr6_ll;
+
 	migrate_observed_addr4(c, &addr4);
+	migrate_observed_addr6(c, &addr6);
+	migrate_observed_addr6(c, &addr6_ll);
 
 	memcpy(c->guest_mac, addrs.mac, sizeof(c->guest_mac));
 
diff --git a/passt.h b/passt.h
index db09da5..665ce2b 100644
--- a/passt.h
+++ b/passt.h
@@ -104,10 +104,8 @@ struct ip4_ctx {
 
 /**
  * struct ip6_ctx - IPv6 execution context
- * @addrs:		IPv6 addresses assigned to guest
+ * @addrs:		IPv6 addresses assigned to guest (with flags)
  * @addr_count:		Number of addresses in addrs[] array
- * @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]
@@ -125,8 +123,7 @@ struct ip6_ctx {
 	/* PIF_TAP addresses */
 	struct inany_addr_entry addrs[IP6_MAX_ADDRS];
 	int addr_count;
-	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 faf1a73..aee1fb4 100644
--- a/pasta.c
+++ b/pasta.c
@@ -369,11 +369,19 @@ void pasta_ns_conf(struct ctx *c)
 		}
 
 		if (c->ifi6) {
-			rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi,
-					    &c->ip6.addr_ll_seen);
+			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 if (c->ip6.addr_count < IP6_MAX_ADDRS) {
+				int idx = c->ip6.addr_count++;
+
+				c->ip6.addrs[idx].addr.a6 = addr_ll;
+				c->ip6.addrs[idx].prefix_len = 64;
+				c->ip6.addrs[idx].flags =
+					INANY_ADDR_OBSERVED | INANY_ADDR_LINKLOCAL;
 			}
 
 			rc = nl_addr_set_ll_nodad(nl_sock_ns, c->pasta_ifi);
@@ -391,6 +399,10 @@ void pasta_ns_conf(struct ctx *c)
 				int i;
 
 				for (i = 0; i < c->ip6.addr_count; i++) {
+					/* Skip link-local - kernel auto-configures */
+					if (c->ip6.addrs[i].flags & INANY_ADDR_LINKLOCAL)
+						continue;
+
 					a = &c->ip6.addrs[i].addr.a6;
 					if (IN6_IS_ADDR_UNSPECIFIED(a))
 						continue;
diff --git a/tap.c b/tap.c
index 1a52adb..f18bfd1 100644
--- a/tap.c
+++ b/tap.c
@@ -192,6 +192,42 @@ static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
 	}
 }
 
+/**
+ * tap_check_src_addr6() - Note an IPv6 address seen in guest traffic
+ * @c:		Execution context
+ * @addr:	IPv6 address seen as source from guest
+ *
+ * Add the address to ip6.addrs[] with OBSERVED flag if not already present.
+ * Link-local addresses are also marked with LINKLOCAL flag.
+ */
+static void tap_check_src_addr6(struct ctx *c, const struct in6_addr *addr)
+{
+	uint16_t flags = INANY_ADDR_OBSERVED;
+	int i;
+
+	if (IN6_IS_ADDR_LINKLOCAL(addr))
+		flags |= INANY_ADDR_LINKLOCAL;
+
+	/* Check if already in array */
+	for (i = 0; i < c->ip6.addr_count; i++) {
+		if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addrs[i].addr.a6)) {
+			c->ip6.addrs[i].flags |= flags;
+			return;
+		}
+	}
+
+	/* Add new entry if space available */
+	if (c->ip6.addr_count < IP6_MAX_ADDRS) {
+		c->ip6.addrs[c->ip6.addr_count].addr.a6 = *addr;
+		c->ip6.addrs[c->ip6.addr_count].prefix_len = 0;
+		c->ip6.addrs[c->ip6.addr_count].flags = flags;
+		debug("added new IPv6 address at index %d", c->ip6.addr_count);
+		c->ip6.addr_count++;
+	} else {
+		warn("IPv6 address table full, can't add address");
+	}
+}
+
 /**
  * tap_ip6_daddr() - Normal IPv6 destination address for inbound packets
  * @c:		Execution context
@@ -202,9 +238,32 @@ 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;
+	bool want_ll = IN6_IS_ADDR_LINKLOCAL(src);
+	int i;
+
+	/* Find first observed address of matching scope */
+	for (i = 0; i < c->ip6.addr_count; i++) {
+		bool is_ll = !!(c->ip6.addrs[i].flags & INANY_ADDR_LINKLOCAL);
+
+		if (is_ll != want_ll)
+			continue;
+		if (c->ip6.addrs[i].flags & INANY_ADDR_OBSERVED)
+			return &c->ip6.addrs[i].addr.a6;
+	}
+
+	/* Fallback to first address of matching scope */
+	for (i = 0; i < c->ip6.addr_count; i++) {
+		bool is_ll = IN6_IS_ADDR_LINKLOCAL(&c->ip6.addrs[i].addr.a6);
+
+		if (is_ll == want_ll)
+			return &c->ip6.addrs[i].addr.a6;
+	}
+
+	/* Last resort: return first address */
+	if (c->ip6.addr_count > 0)
+		return &c->ip6.addrs[0].addr.a6;
+
+	return &in6addr_any;
 }
 
 /**
@@ -975,15 +1034,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;
-			}
-		} 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;
@@ -1014,7 +1066,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] 19+ messages in thread

* Re: [PATCH v2 1/9] conf: Support CIDR notation for -a/--address option
  2026-01-18 22:16 ` [PATCH v2 1/9] conf: Support CIDR notation for -a/--address option Jon Maloy
@ 2026-01-19  5:02   ` David Gibson
  2026-01-21  8:15   ` Stefano Brivio
  1 sibling, 0 replies; 19+ messages in thread
From: David Gibson @ 2026-01-19  5:02 UTC (permalink / raw)
  To: Jon Maloy; +Cc: sbrivio, dgibson, passt-dev

[-- Attachment #1: Type: text/plain, Size: 12024 bytes --]

On Sun, Jan 18, 2026 at 05:16:04PM -0500, Jon Maloy wrote:
> Extend the -a/--address option to accept addresses in CIDR notation
> (e.g., 192.168.1.1/24 or 2001:db8::1/64) as an alternative to using
> separate -a and -n options.
> 
> We add a new conf_addr_prefix_len() helper function that:
> - Parses address strings with optional /prefix_len suffix
> - Validates prefix length based on address family (0-32 for IPv4,
>   0-128 for IPv6), including handling of IPv4-to-IPv6 mapping case.
> - Returns address family via union inany_addr output parameter
> 
> For IPv4, the prefix length is stored in ip4.prefix_len when provided.
> Mixing -n and CIDR notation results in an error to catch likely user
> mistakes.
> 
> Also fix a bug in conf_ip4_prefix() that was incorrectly using the
> global 'optarg' instead of its 'arg' parameter.
> 
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>
> 
> ---
> v3: Fixes after feedback from Laurent, David and Stefano
>     Notably, updated man page for the -a option
> 
> v4: Fixes based on feedback from David G:
>   - Handling prefix length adjustment when IPv4-to-IPv6 mapping
>   - Removed redundant !IN6_IS_ADDR_V4MAPPED(&addr.a6) test
>   - Simplified tests of acceptable address types
>   - Merged documentation and code commits
>   - Some documentation text clarifications
> ---
>  conf.c  | 97 +++++++++++++++++++++++++++++++++++++++++++++++----------
>  inany.c | 29 +++++++++++++++++
>  inany.h |  1 +
>  ip.c    | 21 +++++++++++++
>  ip.h    |  2 ++
>  passt.1 | 17 +++++++---
>  6 files changed, 145 insertions(+), 22 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index 2942c8c..7178a0e 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -682,7 +682,7 @@ static int conf_ip4_prefix(const char *arg)
>  			return -1;
>  	} else {
>  		errno = 0;
> -		len = strtoul(optarg, NULL, 0);
> +		len = strtoul(arg, NULL, 0);
>  		if (len > 32 || errno)
>  			return -1;
>  	}
> @@ -690,6 +690,52 @@ static int conf_ip4_prefix(const char *arg)
>  	return len;
>  }
>  
> +/**
> + * conf_addr_prefix_len() - Parse address with optional prefix length
> + * @arg:	Address string, optionally with /prefix_len suffix (modified)
> + * @addr:	Output for parsed address
> + * @prefix_len: Output for prefix length (0 if not specified)
> + *
> + * Return: AF_INET for IPv4, AF_INET6 for IPv6, -1 on error
> + */
> +static int conf_addr_prefix_len(char *arg, union inany_addr *addr,
> +				int *prefix_len)
> +{
> +	char *slash;
> +
> +	*prefix_len = 0;
> +
> +	/* Check for /prefix_len suffix */
> +	slash = strchr(arg, '/');
> +	if (slash) {
> +		unsigned long len;
> +		char *end;
> +
> +		*slash = '\0';
> +		errno = 0;
> +		len = strtoul(slash + 1, &end, 10);
> +		if (errno || *end)
> +			return -1;
> +
> +		*prefix_len = len;
> +	}
> +
> +	if (!inany_prefix_pton(arg, addr, prefix_len))
> +		return -1;

Oh, sorry, I wasn't clear.  My idea was that inany_prefix_pton() would
handle the parsing (strchr(), strtoul() etc.) of the prefix length
internally, rather than doing that here then adjusting it in there.

> +
> +	if (inany_v4(addr)) {
> +		if (*prefix_len > 32)
> +			return -1;
> +
> +		return AF_INET;
> +	}
> +
> +	if (*prefix_len > 128)
> +		return -1;
> +
> +	return AF_INET6;
> +}
> +
>  /**
>   * conf_ip4() - Verify or detect IPv4 support, get relevant addresses
>   * @ifi:	Host interface to attempt (0 to determine one)
> @@ -896,7 +942,7 @@ static void usage(const char *name, FILE *f, int status)
>  		"    a zero value disables assignment\n"
>  		"    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\n"
> +		"  -a, --address ADDR	Assign IPv4 or IPv6 address ADDR[/PREFIXLEN]\n"
>  		"    can be specified zero to two times (for IPv4 and IPv6)\n"
>  		"    default: use addresses from interface with default route\n"
>  		"  -n, --netmask MASK	Assign IPv4 MASK, dot-decimal or bits\n"
> @@ -1499,6 +1545,7 @@ void conf(struct ctx *c, int argc, char **argv)
>  	const char *logname = (c->mode == MODE_PASTA) ? "pasta" : "passt";
>  	char userns[PATH_MAX] = { 0 }, netns[PATH_MAX] = { 0 };
>  	bool copy_addrs_opt = false, copy_routes_opt = false;
> +	bool prefix_from_cidr = false, prefix_from_opt = false;
>  	enum fwd_ports_mode fwd_default = FWD_NONE;
>  	bool v4_only = false, v6_only = false;
>  	unsigned dns4_idx = 0, dns6_idx = 0;
> @@ -1808,35 +1855,51 @@ void conf(struct ctx *c, int argc, char **argv)
>  			c->mtu = mtu;
>  			break;
>  		}
> -		case 'a':
> -			if (inet_pton(AF_INET6, optarg, &c->ip6.addr)	&&
> -			    !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)	&&
> -			    !IN6_IS_ADDR_LOOPBACK(&c->ip6.addr)		&&
> -			    !IN6_IS_ADDR_V4MAPPED(&c->ip6.addr)		&&
> -			    !IN6_IS_ADDR_V4COMPAT(&c->ip6.addr)		&&
> -			    !IN6_IS_ADDR_MULTICAST(&c->ip6.addr)) {
> +		case 'a': {
> +			union inany_addr addr;
> +			const struct in_addr *a4;
> +			int prefix_len = 0;
> +			int af;
> +
> +			af = conf_addr_prefix_len(optarg, &addr, &prefix_len);
> +
> +			if (inany_is_unspecified(&addr) ||
> +			    inany_is_multicast(&addr) ||
> +			    inany_is_loopback(&addr) ||
> +			    IN6_IS_ADDR_V4COMPAT(&addr.a6))
> +				die("Invalid address: %s", optarg);
> +
> +			if (af == AF_INET6) {
> +				c->ip6.addr = addr.a6;
>  				if (c->mode == MODE_PASTA)
>  					c->ip6.no_copy_addrs = true;
>  				break;
>  			}
>  
> -			if (inet_pton(AF_INET, optarg, &c->ip4.addr)	&&
> -			    !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.addr)	&&
> -			    !IN4_IS_ADDR_BROADCAST(&c->ip4.addr)	&&
> -			    !IN4_IS_ADDR_LOOPBACK(&c->ip4.addr)		&&
> -			    !IN4_IS_ADDR_MULTICAST(&c->ip4.addr)) {
> -				if (c->mode == MODE_PASTA)
> -					c->ip4.no_copy_addrs = true;
> +			a4 = inany_v4(&addr);
> +			if (af == AF_INET && a4) {
> +				c->ip4.addr = *a4;
> +				if (prefix_len) {
> +					if (prefix_from_opt)
> +						die("Can't mix CIDR with -n");
> +					prefix_from_cidr = true;
> +				} else {
> +					prefix_len = ip4_default_prefix_len(a4);
> +				}
> +				c->ip4.prefix_len = prefix_len;
>  				break;
>  			}
>  
>  			die("Invalid address: %s", optarg);
>  			break;
> +		}
>  		case 'n':
> +			if (prefix_from_cidr)
> +				die("Can't use both -n and CIDR prefix length");
>  			c->ip4.prefix_len = conf_ip4_prefix(optarg);
>  			if (c->ip4.prefix_len < 0)
>  				die("Invalid netmask: %s", optarg);
> -
> +			prefix_from_opt = true;
>  			break;
>  		case 'M':
>  			parse_mac(c->our_tap_mac, optarg);
> diff --git a/inany.c b/inany.c
> index 7680439..f142a76 100644
> --- a/inany.c
> +++ b/inany.c
> @@ -57,3 +57,32 @@ int inany_pton(const char *src, union inany_addr *dst)
>  
>  	return 0;
>  }
> +
> +/** inany_prefix_pton - Parse an IPv[46] address with prefix length adjustment
> + * @src:	IPv[46] address string
> + * @dst:	output buffer, filled with parsed address
> + * @prefix_len: pointer to prefix length
> + *
> + * Return: on success, 1, if no parseable address is found, 0
> + */
> +int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len)
> +{
> +	/* First try parsing as plain IPv4 */
> +	if (inet_pton(AF_INET, src, &dst->v4mapped.a4)) {
> +		memset(&dst->v4mapped.zero, 0, sizeof(dst->v4mapped.zero));
> +		memset(&dst->v4mapped.one, 0xff, sizeof(dst->v4mapped.one));
> +		return 1;
> +	}
> +
> +	/* Try parsing as IPv6, adjust prefix length if mapped IPv4 address */
> +	if (inet_pton(AF_INET6, src, &dst->a6)) {
> +		if (inany_v4(dst) && prefix_len && *prefix_len > 0) {
> +			if (*prefix_len < 96)
> +				return 0;
> +			*prefix_len -= 96;

Because inany_addr is essentially an IPv6 representation, I think it
makes more sense to always return the prefix_length as it would be for
IPv6, not dependent on the address type.  (So, add 96 in the
inet_pton() case, rather than subtracting 96 in the explicit v4-mapped
case).

> +		}
> +		return 1;
> +	}
> +
> +	return 0;
> +}
> diff --git a/inany.h b/inany.h
> index 61b36fb..36865f9 100644
> --- a/inany.h
> +++ b/inany.h
> @@ -295,5 +295,6 @@ static inline void inany_siphash_feed(struct siphash_state *state,
>  
>  const char *inany_ntop(const union inany_addr *src, char *dst, socklen_t size);
>  int inany_pton(const char *src, union inany_addr *dst);
> +int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len);
>  
>  #endif /* INANY_H */
> diff --git a/ip.c b/ip.c
> index 9a7f4c5..2519c71 100644
> --- a/ip.c
> +++ b/ip.c
> @@ -13,6 +13,8 @@
>   */
>  
>  #include <stddef.h>
> +#include <netinet/in.h>
> +
>  #include "util.h"
>  #include "ip.h"
>  
> @@ -67,3 +69,22 @@ found:
>  	*proto = nh;
>  	return true;
>  }
> +
> +/**
> + * ip4_default_prefix_len() - Get default prefix length for IPv4 address
> + * @addr:	IPv4 address
> + *
> + * Return: prefix length based on address class (8/16/24), or 32 for other
> + */
> +int ip4_default_prefix_len(const struct in_addr *addr)
> +{
> +	in_addr_t a = ntohl(addr->s_addr);
> +
> +	if (IN_CLASSA(a))
> +		return 8;
> +	if (IN_CLASSB(a))
> +		return 16;
> +	if (IN_CLASSC(a))
> +		return 24;
> +	return 32;
> +}
> diff --git a/ip.h b/ip.h
> index 5830b92..e5f5198 100644
> --- a/ip.h
> +++ b/ip.h
> @@ -135,4 +135,6 @@ static const struct in_addr in4addr_broadcast = { 0xffffffff };
>  #define IPV6_MIN_MTU		1280
>  #endif
>  
> +int ip4_default_prefix_len(const struct in_addr *addr);
> +
>  #endif /* IP_H */
> diff --git a/passt.1 b/passt.1
> index db0d662..7ca03be 100644
> --- a/passt.1
> +++ b/passt.1
> @@ -156,10 +156,14 @@ By default, the advertised MTU is 65520 bytes, that is, the maximum 802.3 MTU
>  minus the length of a 802.3 header, rounded to 32 bits (IPv4 words).
>  
>  .TP
> -.BR \-a ", " \-\-address " " \fIaddr
> +.BR \-a ", " \-\-address " " \fIaddr\fR[\fB/\fR\fIprefix_len\fR]
>  Assign IPv4 \fIaddr\fR via DHCP (\fByiaddr\fR), or \fIaddr\fR via DHCPv6 (option
>  5) and an \fIaddr\fR-based prefix via NDP Router Advertisement (option type 3)
>  for an IPv6 \fIaddr\fR.
> +An optional \fB/\fR\fIprefix_len\fR (0-32 for IPv4, 0-128 for IPv6) can be
> +appended in CIDR notation (e.g., 192.168.1.1/24). This is an alternative to
> +using the \fB-n\fR, \fB--netmask\fR option. Mixing CIDR notation with
> +\fB-n\fR results in an error.
>  This option can be specified zero (for defaults) to two times (once for IPv4,
>  once for IPv6).
>  By default, assigned IPv4 and IPv6 addresses are taken from the host interfaces
> @@ -172,10 +176,13 @@ is assigned for IPv4, and no additional address will be assigned for IPv6.
>  .TP
>  .BR \-n ", " \-\-netmask " " \fImask
>  Assign IPv4 netmask \fImask\fR, expressed as dot-decimal or number of bits, via
> -DHCP (option 1).
> -By default, the netmask associated to the host address matching the assigned one
> -is used. If there's no matching address on the host, the netmask is determined
> -according to the CIDR block of the assigned address (RFC 4632).
> +DHCP (option 1). Alternatively, the prefix length can be specified using CIDR
> +notation with the \fB-a\fR, \fB--address\fR option (e.g., \fB-a\fR 192.168.1.1/24).
> +Mixing \fB-n\fR with CIDR notation results in an error.
> +If no address is indicated, the netmask associated with the adopted host address,
> +if any, is used. If an address is indicated, but without a prefix length, the
> +netmask is determined based on the corresponding network class. In all other
> +cases, the netmask is determined by using the indicated prefix length.
>  
>  .TP
>  .BR \-M ", " \-\-mac-addr " " \fIaddr
> -- 
> 2.52.0
> 

-- 
David Gibson (he or they)	| I'll have my music baroque, and my code
david AT gibson.dropbear.id.au	| minimalist, thank you, not the other way
				| around.
http://www.ozlabs.org/~dgibson

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v2 2/9] ip: Introduce unified multi-address data structures
  2026-01-18 22:16 ` [PATCH v2 2/9] ip: Introduce unified multi-address data structures Jon Maloy
@ 2026-01-19  7:22   ` David Gibson
  2026-01-21 13:02   ` Stefano Brivio
  1 sibling, 0 replies; 19+ messages in thread
From: David Gibson @ 2026-01-19  7:22 UTC (permalink / raw)
  To: Jon Maloy; +Cc: sbrivio, dgibson, passt-dev

[-- Attachment #1: Type: text/plain, Size: 19257 bytes --]

On Sun, Jan 18, 2026 at 05:16:05PM -0500, Jon Maloy wrote:
> As preparation for supporting multiple addresses per interface, we
> replace the single addr/prefix_len fields with arrays.
> 
> - We add an ip4_addr_entry and an ip6_addr_entry struct containing
>   address and prefix length.
> 
> - We set the array sizes to IP4_MAX_ADDRS=8 and IP6_MAX_ADDRS=16,
>   respectively.
> 
> The only functional change is that the IPv6 prefix length now is
> properly stored instead of being hardcoded to 64 even when set
> via the -a option.
> 
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>
> 
> ---
> v2: Using inany_addr instead of protocol specific addresses as
>     entry address field.
> ---
>  arp.c    |  4 +--
>  conf.c   | 78 +++++++++++++++++++++++++++++++++++---------------------
>  dhcp.c   |  8 +++---
>  dhcpv6.c |  6 ++---
>  fwd.c    | 12 ++++-----
>  inany.h  | 16 ++++++++++++
>  ip.h     |  4 +++
>  ndp.c    |  6 ++---
>  passt.h  | 16 +++++++-----
>  pasta.c  | 12 +++++----
>  tap.c    |  4 +--
>  11 files changed, 106 insertions(+), 60 deletions(-)
> 
> diff --git a/arp.c b/arp.c
> index bb042e9..bc77a9f 100644
> --- a/arp.c
> +++ b/arp.c
> @@ -54,7 +54,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 (!memcmp(am->tip, inany_v4(&c->ip4.addrs[0].addr), sizeof(am->tip)))
>  		return true;
>  
>  	return false;
> @@ -145,7 +145,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(&c->ip4.addrs[0].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 7178a0e..9fc5dca 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -763,33 +763,33 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
>  		}
>  	}
>  
> -	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr)) {
> +	if (!ip4->addr_count) {
> +		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;
>  		}
> +		ip4->addrs[0].addr = inany_from_v4(addr);

Using an inany_addr type for an array that's strictly for IPv4
addresses seems weird to me.  And kind of likely to provoke static
checker warnings for the many, many unchecked inany_from_v4() calls
that are introduced.  Using inany_addr doesn't seem worth it if we're
not also unifying at least sone v4/v6 paths and that doesn't seem the
case.

> +		ip4->addrs[0].prefix_len = prefix_len;
> +		ip4->addrs[0].flags = INANY_ADDR_HOST;
> +		ip4->addr_count = 1;
>  	}
>  
> -	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;
> +	if (!ip4->addrs[0].prefix_len) {
> +		const struct in_addr *a4 = inany_v4(&ip4->addrs[0].addr);
> +
> +		ip4->addrs[0].prefix_len = ip4_default_prefix_len(a4);
>  	}
>  
> -	ip4->addr_seen = ip4->addr;
> +	ip4->addr_seen = *inany_v4(&ip4->addrs[0].addr);
>  
>  	ip4->our_tap_addr = ip4->guest_gw;
>  
> -	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr))
> +	if (inany_is_unspecified(&ip4->addrs[0].addr))
>  		return 0;

If this is the case, we must set addr_count back to 0.  Or perhaps
safer, don't advance it in the first place until we're sure we have a
valid address.

>  
>  	return ifi;
> @@ -801,9 +801,11 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
>   */
>  static void conf_ip4_local(struct ip4_ctx *ip4)
>  {
> -	ip4->addr_seen = ip4->addr = IP4_LL_GUEST_ADDR;
> +	ip4->addrs[0].addr = inany_from_v4(IP4_LL_GUEST_ADDR);
> +	ip4->addr_seen = *inany_v4(&ip4->addrs[0].addr);
>  	ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
> -	ip4->prefix_len = IP4_LL_PREFIX_LEN;
> +	ip4->addrs[0].prefix_len = IP4_LL_PREFIX_LEN;
> +	ip4->addr_count = 1;
>  
>  	ip4->no_copy_addrs = ip4->no_copy_routes = true;
>  }
> @@ -838,19 +840,25 @@ 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,
> +			 ip6->addr_count ? NULL : &ip6->addrs[0].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 (!ip6->addr_count) {
> +		ip6->addrs[0].prefix_len = prefix_len ? prefix_len : 64;
> +		ip6->addrs[0].flags = INANY_ADDR_HOST;
> +		ip6->addr_count = 1;
> +	}
> +
> +	ip6->addr_seen = ip6->addrs[0].addr.a6;
>  
>  	if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
>  		ip6->our_tap_ll = ip6->guest_gw;
>  
> -	if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ||
> +	if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addrs[0].addr.a6) ||
>  	    IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
>  		return 0;
>  
> @@ -1193,11 +1201,13 @@ static void conf_print(const struct ctx *c)
>  		if (!c->no_dhcp) {
>  			uint32_t mask;
>  
> -			mask = htonl(0xffffffff << (32 - c->ip4.prefix_len));
> +			mask = htonl(0xffffffff <<
> +				     (32 - c->ip4.addrs[0].prefix_len));
>  
>  			info("DHCP:");
>  			info("    assign: %s",
> -			     inet_ntop(AF_INET, &c->ip4.addr, buf4, sizeof(buf4)));
> +			     inet_ntop(AF_INET, inany_v4(&c->ip4.addrs[0].addr),
> +				       buf4, sizeof(buf4)));
>  			info("    mask: %s",
>  			     inet_ntop(AF_INET, &mask,        buf4, sizeof(buf4)));
>  			info("    router: %s",
> @@ -1235,7 +1245,8 @@ static void conf_print(const struct ctx *c)
>  			goto dns6;
>  
>  		info("    assign: %s",
> -		     inet_ntop(AF_INET6, &c->ip6.addr, buf6, sizeof(buf6)));
> +		     inet_ntop(AF_INET6, &c->ip6.addrs[0].addr.a6,
> +			       buf6, sizeof(buf6)));
>  		info("    router: %s",
>  		     inet_ntop(AF_INET6, &c->ip6.guest_gw, buf6, sizeof(buf6)));
>  		info("    our link-local: %s",
> @@ -1870,15 +1881,20 @@ void conf(struct ctx *c, int argc, char **argv)
>  				die("Invalid address: %s", optarg);
>  
>  			if (af == AF_INET6) {
> -				c->ip6.addr = addr.a6;
> +				c->ip6.addrs[0].addr.a6 = addr.a6;
> +				c->ip6.addrs[0].flags |= INANY_ADDR_CONFIGURED;
> +				c->ip6.addr_count = 1;
>  				if (c->mode == MODE_PASTA)
>  					c->ip6.no_copy_addrs = true;
>  				break;
>  			}
>  
>  			a4 = inany_v4(&addr);
> +
>  			if (af == AF_INET && a4) {
> -				c->ip4.addr = *a4;
> +				c->ip4.addrs[0].addr = inany_from_v4(*a4);
> +				c->ip4.addrs[0].flags |= INANY_ADDR_CONFIGURED;
> +				c->ip4.addr_count = 1;
>  				if (prefix_len) {
>  					if (prefix_from_opt)
>  						die("Can't mix CIDR with -n");
> @@ -1886,21 +1902,25 @@ void conf(struct ctx *c, int argc, char **argv)
>  				} else {
>  					prefix_len = ip4_default_prefix_len(a4);
>  				}
> -				c->ip4.prefix_len = prefix_len;
> +				c->ip4.addrs[0].prefix_len = prefix_len;
>  				break;
>  			}
>  
>  			die("Invalid address: %s", optarg);
>  			break;
>  		}
> -		case 'n':
> +		case 'n': {
> +			int plen;
> +
>  			if (prefix_from_cidr)
>  				die("Can't use both -n and CIDR prefix length");
> -			c->ip4.prefix_len = conf_ip4_prefix(optarg);
> -			if (c->ip4.prefix_len < 0)
> +			plen = conf_ip4_prefix(optarg);
> +			if (plen < 0)
>  				die("Invalid netmask: %s", optarg);
> +			c->ip4.addrs[0].prefix_len = plen;

How will this behave if addr_count > 1?

>  			prefix_from_opt = true;
>  			break;
> +		}
>  		case 'M':
>  			parse_mac(c->our_tap_mac, optarg);
>  			break;
> @@ -2185,7 +2205,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 (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addrs[0].addr.a6)) {
>  		c->no_dhcpv6 = 1;
>  	}
>  
> diff --git a/dhcp.c b/dhcp.c
> index 6b9c2e3..d2afc3b 100644
> --- a/dhcp.c
> +++ b/dhcp.c
> @@ -352,7 +352,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		= *inany_v4(&c->ip4.addrs[0].addr);
>  	reply.siaddr		= 0;
>  	reply.giaddr		= m->giaddr;
>  	memcpy(&reply.chaddr,	m->chaddr,	sizeof(reply.chaddr));
> @@ -404,7 +404,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 = htonl(0xffffffff << (32 - c->ip4.addrs[0].prefix_len));
>  	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 +412,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 ((inany_v4(&c->ip4.addrs[0].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;
> @@ -469,7 +469,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 = *inany_v4(&c->ip4.addrs[0].addr);
>  
>  	tap_udp4_send(c, c->ip4.our_tap_addr, 67, dst, 68, &reply, dlen);
>  
> diff --git a/dhcpv6.c b/dhcpv6.c
> index e4df0db..f45dece 100644
> --- a/dhcpv6.c
> +++ b/dhcpv6.c
> @@ -625,7 +625,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 (dhcpv6_ia_notonlink(data, &c->ip6.addrs[0].addr.a6)) {
>  
>  			dhcpv6_send_ia_notonlink(c, data, &client_id_base,
>  						 ntohs(client_id->l), mh->xid);
> @@ -679,7 +679,7 @@ 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;
> +	c->ip6.addr_seen = c->ip6.addrs[0].addr.a6;
>  
>  	return 1;
>  }
> @@ -703,5 +703,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));
>  
> -	resp.ia_addr.addr	= c->ip6.addr;
> +	resp.ia_addr.addr	= c->ip6.addrs[0].addr.a6;
>  }
> diff --git a/fwd.c b/fwd.c
> index 44a0e10..8d8151b 100644
> --- a/fwd.c
> +++ b/fwd.c
> @@ -516,7 +516,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 (IN4_ARE_ADDR_EQUAL(addr, inany_v4(&c->ip4.addrs[0].addr)) ||
>  	    IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
>  		return false;
>  
> @@ -537,7 +537,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 (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addrs[0].addr.a6))
>  		return false;
>  
>  	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
> @@ -587,9 +587,9 @@ static void nat_outbound(const struct ctx *c, const union inany_addr *addr,
>  	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);
> +		*translated = c->ip4.addrs[0].addr;
>  	else if (inany_equals6(addr, &c->ip6.map_guest_addr))
> -		translated->a6 = c->ip6.addr;
> +		translated->a6 = c->ip6.addrs[0].addr.a6;
>  	else
>  		*translated = *addr;
>  }
> @@ -710,10 +710,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)) {
> +		   inany_equals(addr, &c->ip4.addrs[0].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)) {
> +		   inany_equals6(addr, &c->ip6.addrs[0].addr.a6)) {
>  		translated->a6 = c->ip6.map_guest_addr;
>  	} else if (fwd_guest_accessible(c, addr)) {
>  		*translated = *addr;
> diff --git a/inany.h b/inany.h
> index 36865f9..07bfc3d 100644
> --- a/inany.h
> +++ b/inany.h
> @@ -297,4 +297,20 @@ const char *inany_ntop(const union inany_addr *src, char *dst, socklen_t size);
>  int inany_pton(const char *src, union inany_addr *dst);
>  int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len);
>  
> +/* Flags for struct inany_addr_entry */
> +#define INANY_ADDR_CONFIGURED	(1 << 0)	/* User set via -a */
> +#define INANY_ADDR_HOST	(1 << 1)	/* From host interface */

These flags aren't really anything to do with the addresses
themselves, but with how we're configuring them.  They belong
somewhere else (conf.h or passt.h, I think).

> +/**
> + * struct inany_addr_entry - Unified IPv4/IPv6 address entry
> + * @addr:		IPv4 (as mapped) or IPv6 address
> + * @prefix_len:	Prefix length (0-32 for IPv4, 0-128 for IPv6)
> + * @flags:		INANY_ADDR_* flags
> + */
> +struct inany_addr_entry {
> +	union inany_addr	addr;
> +	uint16_t		prefix_len;
> +	uint16_t		flags;
> +};

Same with this structure.

> +
>  #endif /* INANY_H */
> diff --git a/ip.h b/ip.h
> index e5f5198..78bd43a 100644
> --- a/ip.h
> +++ b/ip.h
> @@ -135,6 +135,10 @@ static const struct in_addr in4addr_broadcast = { 0xffffffff };
>  #define IPV6_MIN_MTU		1280
>  #endif
>  
> +/* Maximum number of addresses per address family */
> +#define IP4_MAX_ADDRS		8
> +#define IP6_MAX_ADDRS		16
> +
>  int ip4_default_prefix_len(const struct in_addr *addr);
>  
>  #endif /* IP_H */
> diff --git a/ndp.c b/ndp.c
> index eb9e313..5248fd6 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 = c->ip6.addrs[0].addr.a6,
>  		.source_ll = {
>  			.header = {
>  				.type		= OPT_SRC_L2_ADDR,
> @@ -466,8 +466,8 @@ void ndp_send_init_req(const struct ctx *c)
>  			.icmp6_solicited	= 0, /* Reserved */
>  			.icmp6_override		= 0, /* Reserved */
>  		},
> -		.target_addr = c->ip6.addr
> +		.target_addr = c->ip6.addrs[0].addr.a6
>  	};
>  	debug("Sending initial NDP NS request for guest MAC address");
> -	ndp_send(c, &c->ip6.addr, &ns, sizeof(ns));
> +	ndp_send(c, &c->ip6.addrs[0].addr.a6, &ns, sizeof(ns));
>  }
> diff --git a/passt.h b/passt.h
> index 79d01dd..9c0c3fe 100644
> --- a/passt.h
> +++ b/passt.h
> @@ -66,9 +66,9 @@ enum passt_modes {
>  
>  /**
>   * struct ip4_ctx - IPv4 execution context
> - * @addr:		IPv4 address assigned to guest
> + * @addrs:		IPv4 addresses assigned to guest
> + * @addr_count:	Number of addresses in addrs[] array
>   * @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
> @@ -85,9 +85,10 @@ enum passt_modes {
>   */
>  struct ip4_ctx {
>  	/* PIF_TAP addresses */
> -	struct in_addr addr;
> +	struct inany_addr_entry addrs[IP4_MAX_ADDRS];
> +	int addr_count;
> +
>  	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 +108,8 @@ struct ip4_ctx {
>  
>  /**
>   * struct ip6_ctx - IPv6 execution context
> - * @addr:		IPv6 address assigned to guest
> + * @addrs:		IPv6 addresses assigned to guest
> + * @addr_count:		Number of addresses in addrs[] array
>   * @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
> @@ -126,7 +128,9 @@ struct ip4_ctx {
>   */
>  struct ip6_ctx {
>  	/* PIF_TAP addresses */
> -	struct in6_addr addr;
> +	struct inany_addr_entry addrs[IP6_MAX_ADDRS];
> +	int addr_count;
> +
>  	struct in6_addr addr_seen;
>  	struct in6_addr addr_ll_seen;
>  	struct in6_addr guest_gw;
> diff --git a/pasta.c b/pasta.c
> index c307b8a..1bb3dd0 100644
> --- a/pasta.c
> +++ b/pasta.c
> @@ -340,8 +340,8 @@ void pasta_ns_conf(struct ctx *c)
>  			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);
> +						 inany_v4(&c->ip4.addrs[0].addr),
> +						 c->ip4.addrs[0].prefix_len);
>  			} else {
>  				rc = nl_addr_dup(nl_sock, c->ifi4,
>  						 nl_sock_ns, c->pasta_ifi,
> @@ -387,10 +387,12 @@ void pasta_ns_conf(struct ctx *c)
>  					  0, IFF_NOARP);
>  
>  			if (c->ip6.no_copy_addrs) {
> -				if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
> +				struct in6_addr *a = &c->ip6.addrs[0].addr.a6;
> +
> +				if (!IN6_IS_ADDR_UNSPECIFIED(a)) {
>  					rc = nl_addr_set(nl_sock_ns,
> -							 c->pasta_ifi, AF_INET6,
> -							 &c->ip6.addr, 64);
> +							 c->pasta_ifi,
> +							 AF_INET6, a, 64);
>  				}
>  			} else {
>  				rc = nl_addr_dup(nl_sock, c->ifi6,
> diff --git a/tap.c b/tap.c
> index 9d1344b..7c50013 100644
> --- a/tap.c
> +++ b/tap.c
> @@ -951,8 +951,8 @@ resume:
>  				c->ip6.addr_seen = *saddr;
>  			}
>  
> -			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
> -				c->ip6.addr = *saddr;
> +			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addrs[0].addr.a6))
> +				c->ip6.addrs[0].addr.a6 = *saddr;
>  		} else if (!IN6_IS_ADDR_UNSPECIFIED(saddr)){
>  			c->ip6.addr_seen = *saddr;
>  		}
> -- 
> 2.52.0
> 

-- 
David Gibson (he or they)	| I'll have my music baroque, and my code
david AT gibson.dropbear.id.au	| minimalist, thank you, not the other way
				| around.
http://www.ozlabs.org/~dgibson

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v2 3/9] conf: Refactor conf_print() for multi-address support
  2026-01-18 22:16 ` [PATCH v2 3/9] conf: Refactor conf_print() for multi-address support Jon Maloy
@ 2026-01-19  7:25   ` David Gibson
  2026-01-21 13:02   ` Stefano Brivio
  1 sibling, 0 replies; 19+ messages in thread
From: David Gibson @ 2026-01-19  7:25 UTC (permalink / raw)
  To: Jon Maloy; +Cc: sbrivio, dgibson, passt-dev

[-- Attachment #1: Type: text/plain, Size: 3995 bytes --]

On Sun, Jan 18, 2026 at 05:16:06PM -0500, Jon Maloy wrote:
> As a preparation for multiple address support, we refactor the
> conf_print() function to handle this properly.
> 
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>
> ---
>  conf.c | 78 +++++++++++++++++++++++++++++++++-------------------------
>  1 file changed, 45 insertions(+), 33 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index 9fc5dca..3ecd1a0 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -1199,20 +1199,28 @@ static void conf_print(const struct ctx *c)
>  				       buf4, sizeof(buf4)));
>  
>  		if (!c->no_dhcp) {
> -			uint32_t mask;
> -
> -			mask = htonl(0xffffffff <<
> -				     (32 - c->ip4.addrs[0].prefix_len));
> -
> -			info("DHCP:");
> -			info("    assign: %s",
> -			     inet_ntop(AF_INET, inany_v4(&c->ip4.addrs[0].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 < c->ip4.addr_count; i++) {
> +				const struct inany_addr_entry *e;
> +				uint32_t mask;
> +
> +				e = &c->ip4.addrs[i];
> +				if (!(e->flags & INANY_ADDR_CONFIGURED) &&
> +				    c->ip4.addr_count > 1)
> +					continue;

This doesn't seem right - addresses taken from the host will also be
used for DHCP.

> +
> +				mask = htonl(0xffffffff << (32 - e->prefix_len));
> +
> +				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)));
> +				break;
> +			}
>  		}
>  
>  		for (i = 0; !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns[i]); i++) {
> @@ -1230,30 +1238,34 @@ static void conf_print(const struct ctx *c)
>  	}
>  
>  	if (c->ifi6) {
> +		bool do_slaac = !c->no_ndp || !c->no_dhcpv6;
> +
>  		if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback))
>  			info("    NAT to host ::1: %s",
>  			     inet_ntop(AF_INET6, &c->ip6.map_host_loopback,
>  				       buf6, sizeof(buf6)));
>  
> -		if (!c->no_ndp && !c->no_dhcpv6)
> -			info("NDP/DHCPv6:");
> -		else if (!c->no_dhcpv6)
> -			info("DHCPv6:");
> -		else if (!c->no_ndp)
> -			info("NDP:");
> -		else
> -			goto dns6;
> -
> -		info("    assign: %s",
> -		     inet_ntop(AF_INET6, &c->ip6.addrs[0].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)));
> -
> -dns6:
> +		if (do_slaac) {
> +			if (!c->no_ndp && !c->no_dhcpv6)
> +				info("NDP/DHCPv6:");
> +			else if (!c->no_dhcpv6)
> +				info("DHCPv6:");
> +			else
> +				info("NDP:");
> +
> +			for (i = 0; i < c->ip6.addr_count; i++) {
> +				info("    assign: %s",
> +				     inet_ntop(AF_INET6, &c->ip6.addrs[i].addr.a6,
> +					       buf6, sizeof(buf6)));

There's an inany_ntop()... if you're going to use inany_ntop() for
IPv6 only addresses you might at least use the helper functions
already implemented for it.

> +			}
> +			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)));
> +		}
> +
>  		for (i = 0; !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.dns[i]); i++) {
>  			if (!i)
>  				info("DNS:");
> -- 
> 2.52.0
> 

-- 
David Gibson (he or they)	| I'll have my music baroque, and my code
david AT gibson.dropbear.id.au	| minimalist, thank you, not the other way
				| around.
http://www.ozlabs.org/~dgibson

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v2 4/9] fwd: Check all configured addresses in guest accessibility functions
  2026-01-18 22:16 ` [PATCH v2 4/9] fwd: Check all configured addresses in guest accessibility functions Jon Maloy
@ 2026-01-19  7:29   ` David Gibson
  0 siblings, 0 replies; 19+ messages in thread
From: David Gibson @ 2026-01-19  7:29 UTC (permalink / raw)
  To: Jon Maloy; +Cc: sbrivio, dgibson, passt-dev

[-- Attachment #1: Type: text/plain, Size: 3137 bytes --]

On Sun, Jan 18, 2026 at 05:16:07PM -0500, Jon Maloy wrote:
> As a preparation for handling multiple addresses, we update
> fwd_guest_accessible4() and fwd_guest_accessible6() to check
> against all addresses in the addrs[] array.
> 
> This ensures that when multiple addresses are configured via -a options,
> inbound traffic for any of them is correctly detected as having no valid
> forwarding path, and subsequently dropped. This occurs when a peer
> address collides with an address the guest is using, and we have no
> translation for it.
> 
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>
> 
> ---
> v2: Updated commit log to make it clearer
> ---
>  fwd.c | 22 ++++++++++++++++------
>  1 file changed, 16 insertions(+), 6 deletions(-)
> 
> diff --git a/fwd.c b/fwd.c
> index 8d8151b..f1db34c 100644
> --- a/fwd.c
> +++ b/fwd.c
> @@ -502,6 +502,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)

fwd_guest_accesible[46]() are only ever called via
fwd_guest_accessible() which takes an inny_addr.  Again, you can
simplify this a bunch by actually exploiting the fact that the arrays
are now also inany_addr, rather than having an inany_addr but still
having separate v4 and v6 paths everywhere.

>  {
> +	int i;
> +
>  	if (IN4_IS_ADDR_LOOPBACK(addr))
>  		return false;
>  
> @@ -513,11 +515,15 @@ static bool fwd_guest_accessible4(const struct ctx *c,
>  	if (IN4_IS_ADDR_UNSPECIFIED(addr))
>  		return false;
>  
> -	/* For IPv4, addr_seen is initialised to addr, so is always a valid
> -	 * address
> +	/* Check against all configured guest addresses */
> +	for (i = 0; i < c->ip4.addr_count; i++)
> +		if (IN4_ARE_ADDR_EQUAL(addr, inany_v4(&c->ip4.addrs[i].addr)))
> +			return false;
> +
> +	/* Also check addr_seen: it tracks the address the guest is actually
> +	 * using, which may differ from configured addresses.
>  	 */
> -	if (IN4_ARE_ADDR_EQUAL(addr, inany_v4(&c->ip4.addrs[0].addr)) ||
> -	    IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
> +	if (IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
>  		return false;
>  
>  	return true;
> @@ -534,11 +540,15 @@ static bool fwd_guest_accessible4(const struct ctx *c,
>  static bool fwd_guest_accessible6(const struct ctx *c,
>  				  const struct in6_addr *addr)
>  {
> +	int i;
> +
>  	if (IN6_IS_ADDR_LOOPBACK(addr))
>  		return false;
>  
> -	if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addrs[0].addr.a6))
> -		return false;
> +	/* Check against all configured guest addresses */
> +	for (i = 0; i < c->ip6.addr_count; i++)
> +		if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addrs[i].addr.a6))
> +			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
> -- 
> 2.52.0
> 

-- 
David Gibson (he or they)	| I'll have my music baroque, and my code
david AT gibson.dropbear.id.au	| minimalist, thank you, not the other way
				| around.
http://www.ozlabs.org/~dgibson

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v2 5/9] arp: Check all configured addresses in ARP filtering
  2026-01-18 22:16 ` [PATCH v2 5/9] arp: Check all configured addresses in ARP filtering Jon Maloy
@ 2026-01-19  8:28   ` David Gibson
  0 siblings, 0 replies; 19+ messages in thread
From: David Gibson @ 2026-01-19  8:28 UTC (permalink / raw)
  To: Jon Maloy; +Cc: sbrivio, dgibson, passt-dev

[-- Attachment #1: Type: text/plain, Size: 1564 bytes --]

On Sun, Jan 18, 2026 at 05:16:08PM -0500, Jon Maloy wrote:
> As a preparation for handling multiple addresses, we update ignore_arp()
> to check against all addresses in the addrs[] array.
> 
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>

LGTM, except insofar as I'm still not convinced the v4 only array of
inany_addrs makes sense.

> ---
>  arp.c | 10 +++++++---
>  1 file changed, 7 insertions(+), 3 deletions(-)
> 
> diff --git a/arp.c b/arp.c
> index bc77a9f..d8063e2 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)
>  {
> +	int i;
> +
>  	if (ah->ar_hrd != htons(ARPHRD_ETHER)	||
>  	    ah->ar_pro != htons(ETH_P_IP)	||
>  	    ah->ar_hln != ETH_ALEN		||
> @@ -53,9 +55,11 @@ static bool ignore_arp(const struct ctx *c,
>  	    !memcmp(am->sip, am->tip, sizeof(am->sip)))
>  		return true;
>  
> -	/* Don't resolve the guest's assigned address, either. */
> -	if (!memcmp(am->tip, inany_v4(&c->ip4.addrs[0].addr), sizeof(am->tip)))
> -		return true;
> +	/* Don't resolve any of the guest's addresses */
> +	for (i = 0; i < c->ip4.addr_count; i++)
> +		if (!memcmp(am->tip, inany_v4(&c->ip4.addrs[i].addr),
> +			    sizeof(am->tip)))
> +			return true;
>  
>  	return false;
>  }
> -- 
> 2.52.0
> 

-- 
David Gibson (he or they)	| I'll have my music baroque, and my code
david AT gibson.dropbear.id.au	| minimalist, thank you, not the other way
				| around.
http://www.ozlabs.org/~dgibson

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v2 6/9] conf: Allow multiple -a/--address options per address family
  2026-01-18 22:16 ` [PATCH v2 6/9] conf: Allow multiple -a/--address options per address family Jon Maloy
@ 2026-01-19  8:41   ` David Gibson
  0 siblings, 0 replies; 19+ messages in thread
From: David Gibson @ 2026-01-19  8:41 UTC (permalink / raw)
  To: Jon Maloy; +Cc: sbrivio, dgibson, passt-dev

[-- Attachment #1: Type: text/plain, Size: 6717 bytes --]

On Sun, Jan 18, 2026 at 05:16:09PM -0500, Jon Maloy wrote:
> We enable configuration of multiple IPv4 and IPv6 addresses by allowing
> repeated use of the -a/--address option.
> 
> - We update option parsing to append addresses to the addrs[] array.
> - Each address specified via -a does initially get a class-based default
>   prefix.
> - If no -a option is given, address and prefix are inherited from
>   the template interface.
> - If a prefix length is to be added, it has to be done in CIDR format,
>   except for the very first address.
> - We configure all indicated addresses in the namespace interface.
> 
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>
> 
> ---
> v2: Adapted to previous code changes
> ---
>  conf.c  | 42 +++++++++++++++++++++++++++++-------------
>  pasta.c | 24 ++++++++++++++++++------
>  2 files changed, 47 insertions(+), 19 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index 3ecd1a0..32a754d 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -789,7 +789,7 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
>  
>  	ip4->our_tap_addr = ip4->guest_gw;
>  
> -	if (inany_is_unspecified(&ip4->addrs[0].addr))
> +	if (!ip4->addr_count)
>  		return 0;
>  
>  	return ifi;
> @@ -858,8 +858,7 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
>  	if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
>  		ip6->our_tap_ll = ip6->guest_gw;
>  
> -	if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addrs[0].addr.a6) ||
> -	    IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
> +	if (!ip6->addr_count || IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
>  		return 0;
>  
>  	return ifi;
> @@ -951,9 +950,11 @@ static void usage(const char *name, FILE *f, int status)
>  		"    default: 65520: maximum 802.3 MTU minus 802.3 header\n"
>  		"                    length, rounded to 32 bits (IPv4 words)\n"
>  		"  -a, --address ADDR	Assign IPv4 or IPv6 address ADDR[/PREFIXLEN]\n"
> -		"    can be specified zero to two times (for IPv4 and IPv6)\n"
> +		"    can be specified multiple times (limit: %d IPv4, %d IPv6)\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",
> +		IP4_MAX_ADDRS, IP6_MAX_ADDRS);
> +	FPRINTF(f,
>  		"    default: netmask from matching address on the host\n"
>  		"  -M, --mac-addr ADDR	Use source MAC address ADDR\n"
>  		"    default: 9a:55:9a:55:9a:55 (locally administered)\n"
> @@ -1882,6 +1883,7 @@ void conf(struct ctx *c, int argc, char **argv)
>  			union inany_addr addr;
>  			const struct in_addr *a4;
>  			int prefix_len = 0;
> +			unsigned int i;
>  			int af;
>  
>  			af = conf_addr_prefix_len(optarg, &addr, &prefix_len);
> @@ -1893,9 +1895,15 @@ void conf(struct ctx *c, int argc, char **argv)
>  				die("Invalid address: %s", optarg);
>  
>  			if (af == AF_INET6) {
> -				c->ip6.addrs[0].addr.a6 = addr.a6;
> -				c->ip6.addrs[0].flags |= INANY_ADDR_CONFIGURED;
> -				c->ip6.addr_count = 1;
> +				i = c->ip6.addr_count;
> +
> +				if (i >= IP6_MAX_ADDRS)
> +					die("Too many IPv6 addresses");
> +
> +				c->ip6.addrs[i].addr.a6 = addr.a6;
> +				c->ip6.addrs[i].prefix_len = prefix_len;
> +				c->ip6.addrs[i].flags = INANY_ADDR_CONFIGURED;
> +				c->ip6.addr_count++;

This is getting moderately deeply nested.  Maybe worth creating an
"add_address" helper?  That could also be helpful if we later
integrate the addr_seen stuff into the same table.

>  				if (c->mode == MODE_PASTA)
>  					c->ip6.no_copy_addrs = true;
>  				break;
> @@ -1904,10 +1912,15 @@ void conf(struct ctx *c, int argc, char **argv)
>  			a4 = inany_v4(&addr);
>  
>  			if (af == AF_INET && a4) {
> -				c->ip4.addrs[0].addr = inany_from_v4(*a4);
> -				c->ip4.addrs[0].flags |= INANY_ADDR_CONFIGURED;
> -				c->ip4.addr_count = 1;
> -				if (prefix_len) {
> +				i = c->ip4.addr_count;
> +
> +				if (i >= IP4_MAX_ADDRS)
> +					die("Too many IPv4 addresses");
> +
> +				c->ip4.addrs[i].addr = inany_from_v4(*a4);
> +				c->ip4.addrs[i].prefix_len = prefix_len;
> +				c->ip4.addrs[i].flags = INANY_ADDR_CONFIGURED;
> +				if (i == 0 && prefix_len) {
>  					if (prefix_from_opt)
>  						die("Can't mix CIDR with -n");
>  					prefix_from_cidr = true;
> @@ -1915,6 +1928,9 @@ void conf(struct ctx *c, int argc, char **argv)
>  					prefix_len = ip4_default_prefix_len(a4);
>  				}
>  				c->ip4.addrs[0].prefix_len = prefix_len;
> +				c->ip4.addr_count++;
> +				if (c->mode == MODE_PASTA)
> +					c->ip4.no_copy_addrs = true;
>  				break;
>  			}
>  
> @@ -2217,7 +2233,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.addrs[0].addr.a6)) {
> +	} else if (!c->ip6.addr_count) {
>  		c->no_dhcpv6 = 1;

Kind of pre-existing, but AFAICT we entirely disable IPv6 (ifi6 == 0)
if we have no addresses - that would seem to superseded disabling just
DHCPv6.

>  	}
>  
> diff --git a/pasta.c b/pasta.c
> index 1bb3dd0..27ce6a7 100644
> --- a/pasta.c
> +++ b/pasta.c
> @@ -338,10 +338,16 @@ 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,
> -						 inany_v4(&c->ip4.addrs[0].addr),
> -						 c->ip4.addrs[0].prefix_len);
> +				int i;
> +
> +				for (i = 0; i < c->ip4.addr_count; i++) {
> +					rc = nl_addr_set(nl_sock_ns,
> +							 c->pasta_ifi, AF_INET,
> +							 inany_v4(&c->ip4.addrs[i].addr),
> +							 c->ip4.addrs[i].prefix_len);
> +					if (rc < 0)
> +						break;
> +				}
>  			} else {
>  				rc = nl_addr_dup(nl_sock, c->ifi4,
>  						 nl_sock_ns, c->pasta_ifi,
> @@ -387,12 +393,18 @@ void pasta_ns_conf(struct ctx *c)
>  					  0, IFF_NOARP);
>  
>  			if (c->ip6.no_copy_addrs) {
> -				struct in6_addr *a = &c->ip6.addrs[0].addr.a6;
> +				struct in6_addr *a;
> +				int i;
>  
> -				if (!IN6_IS_ADDR_UNSPECIFIED(a)) {
> +				for (i = 0; i < c->ip6.addr_count; i++) {
> +					a = &c->ip6.addrs[i].addr.a6;
> +					if (IN6_IS_ADDR_UNSPECIFIED(a))
> +						continue;
>  					rc = nl_addr_set(nl_sock_ns,
>  							 c->pasta_ifi,
>  							 AF_INET6, a, 64);
> +					if (rc < 0)
> +						break;
>  				}
>  			} else {
>  				rc = nl_addr_dup(nl_sock, c->ifi6,
> -- 
> 2.52.0
> 

-- 
David Gibson (he or they)	| I'll have my music baroque, and my code
david AT gibson.dropbear.id.au	| minimalist, thank you, not the other way
				| around.
http://www.ozlabs.org/~dgibson

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

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

* Re: [PATCH v2 1/9] conf: Support CIDR notation for -a/--address option
  2026-01-18 22:16 ` [PATCH v2 1/9] conf: Support CIDR notation for -a/--address option Jon Maloy
  2026-01-19  5:02   ` David Gibson
@ 2026-01-21  8:15   ` Stefano Brivio
  1 sibling, 0 replies; 19+ messages in thread
From: Stefano Brivio @ 2026-01-21  8:15 UTC (permalink / raw)
  To: Jon Maloy; +Cc: dgibson, david, passt-dev

On Sun, 18 Jan 2026 17:16:04 -0500
Jon Maloy <jmaloy@redhat.com> wrote:

> Extend the -a/--address option to accept addresses in CIDR notation
> (e.g., 192.168.1.1/24 or 2001:db8::1/64) as an alternative to using
> separate -a and -n options.
> 
> We add a new conf_addr_prefix_len() helper function that:
> - Parses address strings with optional /prefix_len suffix
> - Validates prefix length based on address family (0-32 for IPv4,
>   0-128 for IPv6), including handling of IPv4-to-IPv6 mapping case.
> - Returns address family via union inany_addr output parameter
> 
> For IPv4, the prefix length is stored in ip4.prefix_len when provided.
> Mixing -n and CIDR notation results in an error to catch likely user
> mistakes.
> 
> Also fix a bug in conf_ip4_prefix() that was incorrectly using the
> global 'optarg' instead of its 'arg' parameter.
> 
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>
> 
> ---
> v3: Fixes after feedback from Laurent, David and Stefano
>     Notably, updated man page for the -a option
> 
> v4: Fixes based on feedback from David G:
>   - Handling prefix length adjustment when IPv4-to-IPv6 mapping
>   - Removed redundant !IN6_IS_ADDR_V4MAPPED(&addr.a6) test
>   - Simplified tests of acceptable address types
>   - Merged documentation and code commits
>   - Some documentation text clarifications
> ---
>  conf.c  | 97 +++++++++++++++++++++++++++++++++++++++++++++++----------
>  inany.c | 29 +++++++++++++++++
>  inany.h |  1 +
>  ip.c    | 21 +++++++++++++
>  ip.h    |  2 ++
>  passt.1 | 17 +++++++---
>  6 files changed, 145 insertions(+), 22 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index 2942c8c..7178a0e 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -682,7 +682,7 @@ static int conf_ip4_prefix(const char *arg)
>  			return -1;
>  	} else {
>  		errno = 0;
> -		len = strtoul(optarg, NULL, 0);
> +		len = strtoul(arg, NULL, 0);
>  		if (len > 32 || errno)
>  			return -1;
>  	}
> @@ -690,6 +690,52 @@ static int conf_ip4_prefix(const char *arg)
>  	return len;
>  }
>  
> +/**
> + * conf_addr_prefix_len() - Parse address with optional prefix length
> + * @arg:	Address string, optionally with /prefix_len suffix (modified)
> + * @addr:	Output for parsed address
> + * @prefix_len: Output for prefix length (0 if not specified)

Nit:              ^ this should be a tab, not a space

> + *
> + * Return: AF_INET for IPv4, AF_INET6 for IPv6, -1 on error
> + */
> +static int conf_addr_prefix_len(char *arg, union inany_addr *addr,
> +				int *prefix_len)

cppcheck reports:

conf.c:710:39: style: Parameter 'arg' can be declared as pointer to const [constParameterPointer]
static int conf_addr_prefix_len(char *arg, union inany_addr *addr,
                                      ^

> +{
> +	char *slash;
> +
> +	*prefix_len = 0;
> +
> +	/* Check for /prefix_len suffix */
> +	slash = strchr(arg, '/');
> +	if (slash) {
> +		unsigned long len;
> +		char *end;
> +
> +		*slash = '\0';
> +		errno = 0;
> +		len = strtoul(slash + 1, &end, 10);
> +		if (errno || *end)
> +			return -1;
> +
> +		*prefix_len = len;
> +	}
> +
> +	if (!inany_prefix_pton(arg, addr, prefix_len))
> +		return -1;
> +
> +	if (inany_v4(addr)) {
> +		if (*prefix_len > 32)
> +			return -1;
> +
> +		return AF_INET;
> +	}
> +
> +	if (*prefix_len > 128)
> +		return -1;
> +
> +	return AF_INET6;
> +}
> +
>  /**
>   * conf_ip4() - Verify or detect IPv4 support, get relevant addresses
>   * @ifi:	Host interface to attempt (0 to determine one)
> @@ -896,7 +942,7 @@ static void usage(const char *name, FILE *f, int status)
>  		"    a zero value disables assignment\n"
>  		"    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\n"
> +		"  -a, --address ADDR	Assign IPv4 or IPv6 address ADDR[/PREFIXLEN]\n"
>  		"    can be specified zero to two times (for IPv4 and IPv6)\n"
>  		"    default: use addresses from interface with default route\n"
>  		"  -n, --netmask MASK	Assign IPv4 MASK, dot-decimal or bits\n"
> @@ -1499,6 +1545,7 @@ void conf(struct ctx *c, int argc, char **argv)
>  	const char *logname = (c->mode == MODE_PASTA) ? "pasta" : "passt";
>  	char userns[PATH_MAX] = { 0 }, netns[PATH_MAX] = { 0 };
>  	bool copy_addrs_opt = false, copy_routes_opt = false;
> +	bool prefix_from_cidr = false, prefix_from_opt = false;

Nit: this should go one line above.

>  	enum fwd_ports_mode fwd_default = FWD_NONE;
>  	bool v4_only = false, v6_only = false;
>  	unsigned dns4_idx = 0, dns6_idx = 0;
> @@ -1808,35 +1855,51 @@ void conf(struct ctx *c, int argc, char **argv)
>  			c->mtu = mtu;
>  			break;
>  		}
> -		case 'a':
> -			if (inet_pton(AF_INET6, optarg, &c->ip6.addr)	&&
> -			    !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)	&&
> -			    !IN6_IS_ADDR_LOOPBACK(&c->ip6.addr)		&&
> -			    !IN6_IS_ADDR_V4MAPPED(&c->ip6.addr)		&&
> -			    !IN6_IS_ADDR_V4COMPAT(&c->ip6.addr)		&&
> -			    !IN6_IS_ADDR_MULTICAST(&c->ip6.addr)) {
> +		case 'a': {
> +			union inany_addr addr;
> +			const struct in_addr *a4;
> +			int prefix_len = 0;
> +			int af;
> +
> +			af = conf_addr_prefix_len(optarg, &addr, &prefix_len);

conf_addr_prefix_len() returns -1 on error, but it's not checked here,
and there are no other callers.

> +
> +			if (inany_is_unspecified(&addr) ||

...as a result of not checking for that, clang-tidy here complains that:

/home/sbrivio/passt/inany.h:94:7: error: The left operand of '==' is a garbage value [clang-analyzer-core.UndefinedBinaryOperatorResult,-warnings-as-errors]
   94 |         if (!IN6_IS_ADDR_V4MAPPED(&addr->a6))
      |              ^
/usr/include/netinet/in.h:464:35: note: expanded from macro 'IN6_IS_ADDR_V4MAPPED'
  464 |       __a->__in6_u.__u6_addr32[0] == 0                                        \
      |                                   ^
/home/sbrivio/passt/conf.c:1571:25: note: Assuming field 'mode' is not equal to MODE_PASTA
 1571 |         const char *logname = (c->mode == MODE_PASTA) ? "pasta" : "passt";
      |                                ^~~~~~~~~~~~~~~~~~~~~
/home/sbrivio/passt/conf.c:1571:24: note: '?' condition is false
 1571 |         const char *logname = (c->mode == MODE_PASTA) ? "pasta" : "passt";
      |                               ^
/home/sbrivio/passt/conf.c:1589:9: note: Field 'mode' is not equal to MODE_PASTA
 1589 |         if (c->mode == MODE_PASTA) {
      |                ^
/home/sbrivio/passt/conf.c:1589:2: note: Taking false branch
 1589 |         if (c->mode == MODE_PASTA) {
      |         ^
/home/sbrivio/passt/conf.c:1594:6: note: Assuming the condition is false
 1594 |         if (tap_l2_max_len(c) - ETH_HLEN < max_mtu)
      |             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/sbrivio/passt/conf.c:1594:2: note: Taking false branch
 1594 |         if (tap_l2_max_len(c) - ETH_HLEN < max_mtu)
      |         ^
/home/sbrivio/passt/conf.c:1605:3: note: Control jumps to 'case 97:'  at line 1884
 1605 |                 switch (name) {
      |                 ^
/home/sbrivio/passt/conf.c:1890:9: note: Calling 'conf_addr_prefix_len'
 1890 |                         af = conf_addr_prefix_len(optarg, &addr, &prefix_len);
      |                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/sbrivio/passt/conf.c:719:6: note: Assuming 'slash' is non-null
  719 |         if (slash) {
      |             ^~~~~
/home/sbrivio/passt/conf.c:719:2: note: Taking true branch
  719 |         if (slash) {
      |         ^
/home/sbrivio/passt/conf.c:726:7: note: Assuming the condition is true
  726 |                 if (errno || *end)
      |                     ^
/usr/include/errno.h:38:16: note: expanded from macro 'errno'
   38 | # define errno (*__errno_location ())
      |                ^~~~~~~~~~~~~~~~~~~~~~
/home/sbrivio/passt/conf.c:726:13: note: Left side of '||' is true
  726 |                 if (errno || *end)
      |                           ^
/home/sbrivio/passt/conf.c:1890:9: note: Returning from 'conf_addr_prefix_len'
 1890 |                         af = conf_addr_prefix_len(optarg, &addr, &prefix_len);
      |                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/sbrivio/passt/conf.c:1892:8: note: Calling 'inany_is_unspecified'
 1892 |                         if (inany_is_unspecified(&addr) ||
      |                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/sbrivio/passt/inany.h:197:9: note: Calling 'inany_is_unspecified4'
  197 |         return inany_is_unspecified4(a) || inany_is_unspecified6(a);
      |                ^~~~~~~~~~~~~~~~~~~~~~~~
/home/sbrivio/passt/inany.h:175:29: note: Calling 'inany_v4'
  175 |         const struct in_addr *v4 = inany_v4(a);
      |                                    ^~~~~~~~~~~
/home/sbrivio/passt/inany.h:94:7: note: The left operand of '==' is a garbage value
   94 |         if (!IN6_IS_ADDR_V4MAPPED(&addr->a6))
      |              ^
/usr/include/netinet/in.h:464:35: note: expanded from macro 'IN6_IS_ADDR_V4MAPPED'
  464 |       __a->__in6_u.__u6_addr32[0] == 0                                        \
      |       ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^
Suppressed 473708 warnings (473687 in non-user code, 21 NOLINT).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
1 warning treated as error
make: *** [Makefile:176: clang-tidy] Error 1

> +			    inany_is_multicast(&addr) ||
> +			    inany_is_loopback(&addr) ||
> +			    IN6_IS_ADDR_V4COMPAT(&addr.a6))
> +				die("Invalid address: %s", optarg);

This is already modified though, so:

  $ ./pasta -a 2001:db8::1/129
  Invalid address: 2001:db8::1

which isn't really correct: it's invalid only because of the /129.

> +
> +			if (af == AF_INET6) {
> +				c->ip6.addr = addr.a6;
>  				if (c->mode == MODE_PASTA)
>  					c->ip6.no_copy_addrs = true;
>  				break;
>  			}
>  
> -			if (inet_pton(AF_INET, optarg, &c->ip4.addr)	&&
> -			    !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.addr)	&&
> -			    !IN4_IS_ADDR_BROADCAST(&c->ip4.addr)	&&
> -			    !IN4_IS_ADDR_LOOPBACK(&c->ip4.addr)		&&
> -			    !IN4_IS_ADDR_MULTICAST(&c->ip4.addr)) {
> -				if (c->mode == MODE_PASTA)
> -					c->ip4.no_copy_addrs = true;

Why is c->ip4.no_copy_addrs not set for -a anymore? The IPv6 equivalent
still (correctly) is.

> +			a4 = inany_v4(&addr);
> +			if (af == AF_INET && a4) {
> +				c->ip4.addr = *a4;
> +				if (prefix_len) {
> +					if (prefix_from_opt)
> +						die("Can't mix CIDR with -n");
> +					prefix_from_cidr = true;
> +				} else {
> +					prefix_len = ip4_default_prefix_len(a4);
> +				}
> +				c->ip4.prefix_len = prefix_len;
>  				break;
>  			}
>  
>  			die("Invalid address: %s", optarg);
>  			break;
> +		}
>  		case 'n':
> +			if (prefix_from_cidr)
> +				die("Can't use both -n and CIDR prefix length");
>  			c->ip4.prefix_len = conf_ip4_prefix(optarg);
>  			if (c->ip4.prefix_len < 0)
>  				die("Invalid netmask: %s", optarg);
> -
> +			prefix_from_opt = true;
>  			break;
>  		case 'M':
>  			parse_mac(c->our_tap_mac, optarg);
> diff --git a/inany.c b/inany.c
> index 7680439..f142a76 100644
> --- a/inany.c
> +++ b/inany.c
> @@ -57,3 +57,32 @@ int inany_pton(const char *src, union inany_addr *dst)
>  
>  	return 0;
>  }
> +
> +/** inany_prefix_pton - Parse an IPv[46] address with prefix length adjustment
> + * @src:	IPv[46] address string
> + * @dst:	output buffer, filled with parsed address
> + * @prefix_len: pointer to prefix length

Nit:              ^ this should be a tab, not a space

> + *
> + * Return: on success, 1, if no parseable address is found, 0

I think it's a bit difficult to read like this, what about "1 on
success, 0 if no parsable address is found"?

> + */
> +int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len)
> +{
> +	/* First try parsing as plain IPv4 */
> +	if (inet_pton(AF_INET, src, &dst->v4mapped.a4)) {
> +		memset(&dst->v4mapped.zero, 0, sizeof(dst->v4mapped.zero));
> +		memset(&dst->v4mapped.one, 0xff, sizeof(dst->v4mapped.one));
> +		return 1;
> +	}
> +
> +	/* Try parsing as IPv6, adjust prefix length if mapped IPv4 address */
> +	if (inet_pton(AF_INET6, src, &dst->a6)) {
> +		if (inany_v4(dst) && prefix_len && *prefix_len > 0) {
> +			if (*prefix_len < 96)
> +				return 0;
> +			*prefix_len -= 96;
> +		}
> +		return 1;
> +	}
> +
> +	return 0;
> +}
> diff --git a/inany.h b/inany.h
> index 61b36fb..36865f9 100644
> --- a/inany.h
> +++ b/inany.h
> @@ -295,5 +295,6 @@ static inline void inany_siphash_feed(struct siphash_state *state,
>  
>  const char *inany_ntop(const union inany_addr *src, char *dst, socklen_t size);
>  int inany_pton(const char *src, union inany_addr *dst);
> +int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len);
>  
>  #endif /* INANY_H */
> diff --git a/ip.c b/ip.c
> index 9a7f4c5..2519c71 100644
> --- a/ip.c
> +++ b/ip.c
> @@ -13,6 +13,8 @@
>   */
>  
>  #include <stddef.h>
> +#include <netinet/in.h>
> +
>  #include "util.h"
>  #include "ip.h"
>  
> @@ -67,3 +69,22 @@ found:
>  	*proto = nh;
>  	return true;
>  }
> +
> +/**
> + * ip4_default_prefix_len() - Get default prefix length for IPv4 address
> + * @addr:	IPv4 address
> + *
> + * Return: prefix length based on address class (8/16/24), or 32 for other
> + */
> +int ip4_default_prefix_len(const struct in_addr *addr)
> +{
> +	in_addr_t a = ntohl(addr->s_addr);
> +
> +	if (IN_CLASSA(a))
> +		return 8;

This is defined as IN_CLASSA_NSHIFT, and there are similar constants
defined for the values below. See conf_ip4(), which should now be
changed to use this function instead.

> +	if (IN_CLASSB(a))
> +		return 16;
> +	if (IN_CLASSC(a))
> +		return 24;
> +	return 32;
> +}
> diff --git a/ip.h b/ip.h
> index 5830b92..e5f5198 100644
> --- a/ip.h
> +++ b/ip.h
> @@ -135,4 +135,6 @@ static const struct in_addr in4addr_broadcast = { 0xffffffff };
>  #define IPV6_MIN_MTU		1280
>  #endif
>  
> +int ip4_default_prefix_len(const struct in_addr *addr);
> +
>  #endif /* IP_H */
> diff --git a/passt.1 b/passt.1
> index db0d662..7ca03be 100644
> --- a/passt.1
> +++ b/passt.1
> @@ -156,10 +156,14 @@ By default, the advertised MTU is 65520 bytes, that is, the maximum 802.3 MTU
>  minus the length of a 802.3 header, rounded to 32 bits (IPv4 words).
>  
>  .TP
> -.BR \-a ", " \-\-address " " \fIaddr
> +.BR \-a ", " \-\-address " " \fIaddr\fR[\fB/\fR\fIprefix_len\fR]

\fB\fR\fI:

1. sets a bold typeface

2. resets the formatting (clearing the bold typeface)

3. sets an italic typeface

1. and 2. can be dropped.

>  Assign IPv4 \fIaddr\fR via DHCP (\fByiaddr\fR), or \fIaddr\fR via DHCPv6 (option
>  5) and an \fIaddr\fR-based prefix via NDP Router Advertisement (option type 3)
>  for an IPv6 \fIaddr\fR.
> +An optional \fB/\fR\fIprefix_len\fR (0-32 for IPv4, 0-128 for IPv6) can be

Same here.

/0 is accepted, but the address isn't actually set (just tried with
IPv4).

> +appended in CIDR notation (e.g., 192.168.1.1/24). This is an alternative to

Redundant comma after e.g., example addresses should be from TEST-NET
blocks (RFC 5373, e.g. 192.0.2.1).

> +using the \fB-n\fR, \fB--netmask\fR option. Mixing CIDR notation with
> +\fB-n\fR results in an error.
>  This option can be specified zero (for defaults) to two times (once for IPv4,
>  once for IPv6).
>  By default, assigned IPv4 and IPv6 addresses are taken from the host interfaces
> @@ -172,10 +176,13 @@ is assigned for IPv4, and no additional address will be assigned for IPv6.
>  .TP
>  .BR \-n ", " \-\-netmask " " \fImask
>  Assign IPv4 netmask \fImask\fR, expressed as dot-decimal or number of bits, via
> -DHCP (option 1).
> -By default, the netmask associated to the host address matching the assigned one
> -is used. If there's no matching address on the host, the netmask is determined
> -according to the CIDR block of the assigned address (RFC 4632).
> +DHCP (option 1). Alternatively, the prefix length can be specified using CIDR
> +notation with the \fB-a\fR, \fB--address\fR option (e.g., \fB-a\fR 192.168.1.1/24).
> +Mixing \fB-n\fR with CIDR notation results in an error.
> +If no address is indicated, the netmask associated with the adopted host address,
> +if any, is used. If an address is indicated, but without a prefix length, the
> +netmask is determined based on the corresponding network class. In all other
> +cases, the netmask is determined by using the indicated prefix length.
>  
>  .TP
>  .BR \-M ", " \-\-mac-addr " " \fIaddr

I'll need a bit longer to review the rest of the series.

-- 
Stefano


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

* Re: [PATCH v2 2/9] ip: Introduce unified multi-address data structures
  2026-01-18 22:16 ` [PATCH v2 2/9] ip: Introduce unified multi-address data structures Jon Maloy
  2026-01-19  7:22   ` David Gibson
@ 2026-01-21 13:02   ` Stefano Brivio
  1 sibling, 0 replies; 19+ messages in thread
From: Stefano Brivio @ 2026-01-21 13:02 UTC (permalink / raw)
  To: Jon Maloy; +Cc: dgibson, david, passt-dev

On Sun, 18 Jan 2026 17:16:05 -0500
Jon Maloy <jmaloy@redhat.com> wrote:

> As preparation for supporting multiple addresses per interface, we
> replace the single addr/prefix_len fields with arrays.
> 
> - We add an ip4_addr_entry and an ip6_addr_entry struct containing
>   address and prefix length.
> 
> - We set the array sizes to IP4_MAX_ADDRS=8 and IP6_MAX_ADDRS=16,
>   respectively.

Is there a particular reason behind this choice? Wouldn't it be more
convenient, especially for documentation purposes and to avoid
confusing users, to stick to the same number of addresses for both
versions?

> The only functional change is that the IPv6 prefix length now is
> properly stored instead of being hardcoded to 64 even when set
> via the -a option.
> 
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>
> 
> ---
> v2: Using inany_addr instead of protocol specific addresses as
>     entry address field.

No particular comments here, just three minor nits below, but, in
general: is the plan to have a single array for both IPv4 and IPv6?

If not, does it really make sense to use the inany type for arrays that
only contain IPv4 or IPv6 addresses, separately?

> ---
>  arp.c    |  4 +--
>  conf.c   | 78 +++++++++++++++++++++++++++++++++++---------------------
>  dhcp.c   |  8 +++---
>  dhcpv6.c |  6 ++---
>  fwd.c    | 12 ++++-----
>  inany.h  | 16 ++++++++++++
>  ip.h     |  4 +++
>  ndp.c    |  6 ++---
>  passt.h  | 16 +++++++-----
>  pasta.c  | 12 +++++----
>  tap.c    |  4 +--
>  11 files changed, 106 insertions(+), 60 deletions(-)
> 
> diff --git a/arp.c b/arp.c
> index bb042e9..bc77a9f 100644
> --- a/arp.c
> +++ b/arp.c
> @@ -54,7 +54,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 (!memcmp(am->tip, inany_v4(&c->ip4.addrs[0].addr), sizeof(am->tip)))
>  		return true;
>  
>  	return false;
> @@ -145,7 +145,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(&c->ip4.addrs[0].addr), sizeof(req.am.tip));

Nit: it doesn't look like a table anymore, so either drop all the tabs,
or mix tabs and spaces:

	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, inany_v4(&c->ip4.addrs[0].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 7178a0e..9fc5dca 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -763,33 +763,33 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
>  		}
>  	}
>  
> -	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr)) {
> +	if (!ip4->addr_count) {
> +		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;
>  		}
> +		ip4->addrs[0].addr = inany_from_v4(addr);
> +		ip4->addrs[0].prefix_len = prefix_len;
> +		ip4->addrs[0].flags = INANY_ADDR_HOST;
> +		ip4->addr_count = 1;
>  	}
>  
> -	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;
> +	if (!ip4->addrs[0].prefix_len) {
> +		const struct in_addr *a4 = inany_v4(&ip4->addrs[0].addr);
> +
> +		ip4->addrs[0].prefix_len = ip4_default_prefix_len(a4);
>  	}
>  
> -	ip4->addr_seen = ip4->addr;
> +	ip4->addr_seen = *inany_v4(&ip4->addrs[0].addr);
>  
>  	ip4->our_tap_addr = ip4->guest_gw;
>  
> -	if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr))
> +	if (inany_is_unspecified(&ip4->addrs[0].addr))
>  		return 0;
>  
>  	return ifi;
> @@ -801,9 +801,11 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
>   */
>  static void conf_ip4_local(struct ip4_ctx *ip4)
>  {
> -	ip4->addr_seen = ip4->addr = IP4_LL_GUEST_ADDR;
> +	ip4->addrs[0].addr = inany_from_v4(IP4_LL_GUEST_ADDR);
> +	ip4->addr_seen = *inany_v4(&ip4->addrs[0].addr);
>  	ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
> -	ip4->prefix_len = IP4_LL_PREFIX_LEN;
> +	ip4->addrs[0].prefix_len = IP4_LL_PREFIX_LEN;
> +	ip4->addr_count = 1;
>  
>  	ip4->no_copy_addrs = ip4->no_copy_routes = true;
>  }
> @@ -838,19 +840,25 @@ 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,
> +			 ip6->addr_count ? NULL : &ip6->addrs[0].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 (!ip6->addr_count) {
> +		ip6->addrs[0].prefix_len = prefix_len ? prefix_len : 64;
> +		ip6->addrs[0].flags = INANY_ADDR_HOST;
> +		ip6->addr_count = 1;
> +	}
> +
> +	ip6->addr_seen = ip6->addrs[0].addr.a6;
>  
>  	if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
>  		ip6->our_tap_ll = ip6->guest_gw;
>  
> -	if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ||
> +	if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addrs[0].addr.a6) ||
>  	    IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
>  		return 0;
>  
> @@ -1193,11 +1201,13 @@ static void conf_print(const struct ctx *c)
>  		if (!c->no_dhcp) {
>  			uint32_t mask;
>  
> -			mask = htonl(0xffffffff << (32 - c->ip4.prefix_len));
> +			mask = htonl(0xffffffff <<
> +				     (32 - c->ip4.addrs[0].prefix_len));
>  
>  			info("DHCP:");
>  			info("    assign: %s",
> -			     inet_ntop(AF_INET, &c->ip4.addr, buf4, sizeof(buf4)));
> +			     inet_ntop(AF_INET, inany_v4(&c->ip4.addrs[0].addr),
> +				       buf4, sizeof(buf4)));
>  			info("    mask: %s",
>  			     inet_ntop(AF_INET, &mask,        buf4, sizeof(buf4)));
>  			info("    router: %s",
> @@ -1235,7 +1245,8 @@ static void conf_print(const struct ctx *c)
>  			goto dns6;
>  
>  		info("    assign: %s",
> -		     inet_ntop(AF_INET6, &c->ip6.addr, buf6, sizeof(buf6)));
> +		     inet_ntop(AF_INET6, &c->ip6.addrs[0].addr.a6,
> +			       buf6, sizeof(buf6)));
>  		info("    router: %s",
>  		     inet_ntop(AF_INET6, &c->ip6.guest_gw, buf6, sizeof(buf6)));
>  		info("    our link-local: %s",
> @@ -1870,15 +1881,20 @@ void conf(struct ctx *c, int argc, char **argv)
>  				die("Invalid address: %s", optarg);
>  
>  			if (af == AF_INET6) {
> -				c->ip6.addr = addr.a6;
> +				c->ip6.addrs[0].addr.a6 = addr.a6;
> +				c->ip6.addrs[0].flags |= INANY_ADDR_CONFIGURED;
> +				c->ip6.addr_count = 1;
>  				if (c->mode == MODE_PASTA)
>  					c->ip6.no_copy_addrs = true;
>  				break;
>  			}
>  
>  			a4 = inany_v4(&addr);
> +
>  			if (af == AF_INET && a4) {
> -				c->ip4.addr = *a4;
> +				c->ip4.addrs[0].addr = inany_from_v4(*a4);
> +				c->ip4.addrs[0].flags |= INANY_ADDR_CONFIGURED;
> +				c->ip4.addr_count = 1;
>  				if (prefix_len) {
>  					if (prefix_from_opt)
>  						die("Can't mix CIDR with -n");
> @@ -1886,21 +1902,25 @@ void conf(struct ctx *c, int argc, char **argv)
>  				} else {
>  					prefix_len = ip4_default_prefix_len(a4);
>  				}
> -				c->ip4.prefix_len = prefix_len;
> +				c->ip4.addrs[0].prefix_len = prefix_len;
>  				break;
>  			}
>  
>  			die("Invalid address: %s", optarg);
>  			break;
>  		}
> -		case 'n':
> +		case 'n': {
> +			int plen;
> +
>  			if (prefix_from_cidr)
>  				die("Can't use both -n and CIDR prefix length");
> -			c->ip4.prefix_len = conf_ip4_prefix(optarg);
> -			if (c->ip4.prefix_len < 0)
> +			plen = conf_ip4_prefix(optarg);
> +			if (plen < 0)
>  				die("Invalid netmask: %s", optarg);
> +			c->ip4.addrs[0].prefix_len = plen;
>  			prefix_from_opt = true;
>  			break;
> +		}
>  		case 'M':
>  			parse_mac(c->our_tap_mac, optarg);
>  			break;
> @@ -2185,7 +2205,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 (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addrs[0].addr.a6)) {
>  		c->no_dhcpv6 = 1;
>  	}
>  
> diff --git a/dhcp.c b/dhcp.c
> index 6b9c2e3..d2afc3b 100644
> --- a/dhcp.c
> +++ b/dhcp.c
> @@ -352,7 +352,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		= *inany_v4(&c->ip4.addrs[0].addr);
>  	reply.siaddr		= 0;
>  	reply.giaddr		= m->giaddr;
>  	memcpy(&reply.chaddr,	m->chaddr,	sizeof(reply.chaddr));
> @@ -404,7 +404,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 = htonl(0xffffffff << (32 - c->ip4.addrs[0].prefix_len));
>  	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 +412,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 ((inany_v4(&c->ip4.addrs[0].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;
> @@ -469,7 +469,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 = *inany_v4(&c->ip4.addrs[0].addr);
>  
>  	tap_udp4_send(c, c->ip4.our_tap_addr, 67, dst, 68, &reply, dlen);
>  
> diff --git a/dhcpv6.c b/dhcpv6.c
> index e4df0db..f45dece 100644
> --- a/dhcpv6.c
> +++ b/dhcpv6.c
> @@ -625,7 +625,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 (dhcpv6_ia_notonlink(data, &c->ip6.addrs[0].addr.a6)) {
>  
>  			dhcpv6_send_ia_notonlink(c, data, &client_id_base,
>  						 ntohs(client_id->l), mh->xid);
> @@ -679,7 +679,7 @@ 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;
> +	c->ip6.addr_seen = c->ip6.addrs[0].addr.a6;
>  
>  	return 1;
>  }
> @@ -703,5 +703,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));
>  
> -	resp.ia_addr.addr	= c->ip6.addr;
> +	resp.ia_addr.addr	= c->ip6.addrs[0].addr.a6;
>  }
> diff --git a/fwd.c b/fwd.c
> index 44a0e10..8d8151b 100644
> --- a/fwd.c
> +++ b/fwd.c
> @@ -516,7 +516,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 (IN4_ARE_ADDR_EQUAL(addr, inany_v4(&c->ip4.addrs[0].addr)) ||
>  	    IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
>  		return false;
>  
> @@ -537,7 +537,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 (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addrs[0].addr.a6))
>  		return false;
>  
>  	/* For IPv6, addr_seen starts unspecified, because we don't know what LL
> @@ -587,9 +587,9 @@ static void nat_outbound(const struct ctx *c, const union inany_addr *addr,
>  	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);
> +		*translated = c->ip4.addrs[0].addr;
>  	else if (inany_equals6(addr, &c->ip6.map_guest_addr))
> -		translated->a6 = c->ip6.addr;
> +		translated->a6 = c->ip6.addrs[0].addr.a6;
>  	else
>  		*translated = *addr;
>  }
> @@ -710,10 +710,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)) {
> +		   inany_equals(addr, &c->ip4.addrs[0].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)) {
> +		   inany_equals6(addr, &c->ip6.addrs[0].addr.a6)) {
>  		translated->a6 = c->ip6.map_guest_addr;
>  	} else if (fwd_guest_accessible(c, addr)) {
>  		*translated = *addr;
> diff --git a/inany.h b/inany.h
> index 36865f9..07bfc3d 100644
> --- a/inany.h
> +++ b/inany.h
> @@ -297,4 +297,20 @@ const char *inany_ntop(const union inany_addr *src, char *dst, socklen_t size);
>  int inany_pton(const char *src, union inany_addr *dst);
>  int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len);
>  
> +/* Flags for struct inany_addr_entry */
> +#define INANY_ADDR_CONFIGURED	(1 << 0)	/* User set via -a */
> +#define INANY_ADDR_HOST	(1 << 1)	/* From host interface */
> +
> +/**
> + * struct inany_addr_entry - Unified IPv4/IPv6 address entry
> + * @addr:		IPv4 (as mapped) or IPv6 address
> + * @prefix_len:	Prefix length (0-32 for IPv4, 0-128 for IPv6)

This should be 1-32 and 1-128, unless you find a way to make /0
assignments working (see my comment on 1/9).

> + * @flags:		INANY_ADDR_* flags
> + */
> +struct inany_addr_entry {
> +	union inany_addr	addr;
> +	uint16_t		prefix_len;
> +	uint16_t		flags;

Not that it really matters, but if you're trying to keep it small,
uint8_t is enough for both, and if you're trying to keep it simple,
'int' would be the right choice for 'prefix_len'.

> +};
> +
>  #endif /* INANY_H */
> diff --git a/ip.h b/ip.h
> index e5f5198..78bd43a 100644
> --- a/ip.h
> +++ b/ip.h
> @@ -135,6 +135,10 @@ static const struct in_addr in4addr_broadcast = { 0xffffffff };
>  #define IPV6_MIN_MTU		1280
>  #endif
>  
> +/* Maximum number of addresses per address family */
> +#define IP4_MAX_ADDRS		8
> +#define IP6_MAX_ADDRS		16
> +
>  int ip4_default_prefix_len(const struct in_addr *addr);
>  
>  #endif /* IP_H */
> diff --git a/ndp.c b/ndp.c
> index eb9e313..5248fd6 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 = c->ip6.addrs[0].addr.a6,
>  		.source_ll = {
>  			.header = {
>  				.type		= OPT_SRC_L2_ADDR,
> @@ -466,8 +466,8 @@ void ndp_send_init_req(const struct ctx *c)
>  			.icmp6_solicited	= 0, /* Reserved */
>  			.icmp6_override		= 0, /* Reserved */
>  		},
> -		.target_addr = c->ip6.addr
> +		.target_addr = c->ip6.addrs[0].addr.a6
>  	};
>  	debug("Sending initial NDP NS request for guest MAC address");
> -	ndp_send(c, &c->ip6.addr, &ns, sizeof(ns));
> +	ndp_send(c, &c->ip6.addrs[0].addr.a6, &ns, sizeof(ns));
>  }
> diff --git a/passt.h b/passt.h
> index 79d01dd..9c0c3fe 100644
> --- a/passt.h
> +++ b/passt.h
> @@ -66,9 +66,9 @@ enum passt_modes {
>  
>  /**
>   * struct ip4_ctx - IPv4 execution context
> - * @addr:		IPv4 address assigned to guest
> + * @addrs:		IPv4 addresses assigned to guest
> + * @addr_count:	Number of addresses in addrs[] array
>   * @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
> @@ -85,9 +85,10 @@ enum passt_modes {
>   */
>  struct ip4_ctx {
>  	/* PIF_TAP addresses */
> -	struct in_addr addr;
> +	struct inany_addr_entry addrs[IP4_MAX_ADDRS];
> +	int addr_count;
> +
>  	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 +108,8 @@ struct ip4_ctx {
>  
>  /**
>   * struct ip6_ctx - IPv6 execution context
> - * @addr:		IPv6 address assigned to guest
> + * @addrs:		IPv6 addresses assigned to guest
> + * @addr_count:		Number of addresses in addrs[] array
>   * @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
> @@ -126,7 +128,9 @@ struct ip4_ctx {
>   */
>  struct ip6_ctx {
>  	/* PIF_TAP addresses */
> -	struct in6_addr addr;
> +	struct inany_addr_entry addrs[IP6_MAX_ADDRS];
> +	int addr_count;
> +
>  	struct in6_addr addr_seen;
>  	struct in6_addr addr_ll_seen;
>  	struct in6_addr guest_gw;
> diff --git a/pasta.c b/pasta.c
> index c307b8a..1bb3dd0 100644
> --- a/pasta.c
> +++ b/pasta.c
> @@ -340,8 +340,8 @@ void pasta_ns_conf(struct ctx *c)
>  			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);
> +						 inany_v4(&c->ip4.addrs[0].addr),
> +						 c->ip4.addrs[0].prefix_len);
>  			} else {
>  				rc = nl_addr_dup(nl_sock, c->ifi4,
>  						 nl_sock_ns, c->pasta_ifi,
> @@ -387,10 +387,12 @@ void pasta_ns_conf(struct ctx *c)
>  					  0, IFF_NOARP);
>  
>  			if (c->ip6.no_copy_addrs) {
> -				if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
> +				struct in6_addr *a = &c->ip6.addrs[0].addr.a6;
> +
> +				if (!IN6_IS_ADDR_UNSPECIFIED(a)) {
>  					rc = nl_addr_set(nl_sock_ns,
> -							 c->pasta_ifi, AF_INET6,
> -							 &c->ip6.addr, 64);
> +							 c->pasta_ifi,
> +							 AF_INET6, a, 64);
>  				}
>  			} else {
>  				rc = nl_addr_dup(nl_sock, c->ifi6,
> diff --git a/tap.c b/tap.c
> index 9d1344b..7c50013 100644
> --- a/tap.c
> +++ b/tap.c
> @@ -951,8 +951,8 @@ resume:
>  				c->ip6.addr_seen = *saddr;
>  			}
>  
> -			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
> -				c->ip6.addr = *saddr;
> +			if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addrs[0].addr.a6))
> +				c->ip6.addrs[0].addr.a6 = *saddr;
>  		} else if (!IN6_IS_ADDR_UNSPECIFIED(saddr)){
>  			c->ip6.addr_seen = *saddr;
>  		}

-- 
Stefano


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

* Re: [PATCH v2 3/9] conf: Refactor conf_print() for multi-address support
  2026-01-18 22:16 ` [PATCH v2 3/9] conf: Refactor conf_print() for multi-address support Jon Maloy
  2026-01-19  7:25   ` David Gibson
@ 2026-01-21 13:02   ` Stefano Brivio
  1 sibling, 0 replies; 19+ messages in thread
From: Stefano Brivio @ 2026-01-21 13:02 UTC (permalink / raw)
  To: Jon Maloy; +Cc: dgibson, david, passt-dev

On Sun, 18 Jan 2026 17:16:06 -0500
Jon Maloy <jmaloy@redhat.com> wrote:

> As a preparation for multiple address support, we refactor the
> conf_print() function to handle this properly.
> 
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>
> ---
>  conf.c | 78 +++++++++++++++++++++++++++++++++-------------------------
>  1 file changed, 45 insertions(+), 33 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index 9fc5dca..3ecd1a0 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -1199,20 +1199,28 @@ static void conf_print(const struct ctx *c)
>  				       buf4, sizeof(buf4)));
>  

I didn't really check why that starts being reported now, but, with
this patch, cppcheck now says that:

conf.c:1167:31: style: The scope of the variable 'ifn' can be reduced. [variableScope]
 char bufmac[ETH_ADDRSTRLEN], ifn[IFNAMSIZ];
                              ^

>  		if (!c->no_dhcp) {
> -			uint32_t mask;
> -
> -			mask = htonl(0xffffffff <<
> -				     (32 - c->ip4.addrs[0].prefix_len));
> -
> -			info("DHCP:");
> -			info("    assign: %s",
> -			     inet_ntop(AF_INET, inany_v4(&c->ip4.addrs[0].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 < c->ip4.addr_count; i++) {
> +				const struct inany_addr_entry *e;
> +				uint32_t mask;
> +
> +				e = &c->ip4.addrs[i];
> +				if (!(e->flags & INANY_ADDR_CONFIGURED) &&
> +				    c->ip4.addr_count > 1)
> +					continue;
> +
> +				mask = htonl(0xffffffff << (32 - e->prefix_len));
> +
> +				info("DHCP:");

Should this really be in a loop? We'll assign a single address via 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)));
> +				break;
> +			}
>  		}
>  
>  		for (i = 0; !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns[i]); i++) {
> @@ -1230,30 +1238,34 @@ static void conf_print(const struct ctx *c)
>  	}
>  
>  	if (c->ifi6) {
> +		bool do_slaac = !c->no_ndp || !c->no_dhcpv6;

We can enable DHCPv6 and disable SLAAC though. The "SL" in SLAAC stands
for "stateless", DHCPv6 is stateful. We can enable both, one, or none.

The 'dns6' label was meant to avoid a variable like this one and
another block, maybe it would be more practical to keep it.

> +
>  		if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback))
>  			info("    NAT to host ::1: %s",
>  			     inet_ntop(AF_INET6, &c->ip6.map_host_loopback,
>  				       buf6, sizeof(buf6)));
>  
> -		if (!c->no_ndp && !c->no_dhcpv6)
> -			info("NDP/DHCPv6:");
> -		else if (!c->no_dhcpv6)
> -			info("DHCPv6:");
> -		else if (!c->no_ndp)
> -			info("NDP:");
> -		else
> -			goto dns6;
> -
> -		info("    assign: %s",
> -		     inet_ntop(AF_INET6, &c->ip6.addrs[0].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)));
> -
> -dns6:
> +		if (do_slaac) {
> +			if (!c->no_ndp && !c->no_dhcpv6)
> +				info("NDP/DHCPv6:");
> +			else if (!c->no_dhcpv6)
> +				info("DHCPv6:");
> +			else
> +				info("NDP:");
> +
> +			for (i = 0; i < c->ip6.addr_count; i++) {
> +				info("    assign: %s",
> +				     inet_ntop(AF_INET6, &c->ip6.addrs[i].addr.a6,
> +					       buf6, sizeof(buf6)));

I don't see a matching change for neither NDP nor DHCPv6, so we
shouldn't really print more than one address (at least until this
point).

> +			}
> +			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)));
> +		}
> +
>  		for (i = 0; !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.dns[i]); i++) {
>  			if (!i)
>  				info("DNS:");

I reviewed up to 5/9 so far, no further comments until then.

-- 
Stefano


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

end of thread, other threads:[~2026-01-21 13:02 UTC | newest]

Thread overview: 19+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-01-18 22:16 [PATCH v2 0/9] Introduce multiple addresses Jon Maloy
2026-01-18 22:16 ` [PATCH v2 1/9] conf: Support CIDR notation for -a/--address option Jon Maloy
2026-01-19  5:02   ` David Gibson
2026-01-21  8:15   ` Stefano Brivio
2026-01-18 22:16 ` [PATCH v2 2/9] ip: Introduce unified multi-address data structures Jon Maloy
2026-01-19  7:22   ` David Gibson
2026-01-21 13:02   ` Stefano Brivio
2026-01-18 22:16 ` [PATCH v2 3/9] conf: Refactor conf_print() for multi-address support Jon Maloy
2026-01-19  7:25   ` David Gibson
2026-01-21 13:02   ` Stefano Brivio
2026-01-18 22:16 ` [PATCH v2 4/9] fwd: Check all configured addresses in guest accessibility functions Jon Maloy
2026-01-19  7:29   ` David Gibson
2026-01-18 22:16 ` [PATCH v2 5/9] arp: Check all configured addresses in ARP filtering Jon Maloy
2026-01-19  8:28   ` David Gibson
2026-01-18 22:16 ` [PATCH v2 6/9] conf: Allow multiple -a/--address options per address family Jon Maloy
2026-01-19  8:41   ` David Gibson
2026-01-18 22:16 ` [PATCH v2 7/9] pasta: Unify address configuration paths using address array Jon Maloy
2026-01-18 22:16 ` [PATCH v2 8/9] ip: Track observed guest IPv4 addresses in unified " Jon Maloy
2026-01-18 22:16 ` [PATCH v2 9/9] ip: Track observed guest IPv6 " 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).