* [PATCH v4 00/12] Introduce multiple addresses
@ 2026-02-17 22:18 Jon Maloy
2026-02-17 22:18 ` [PATCH v4 01/12] ip: Introduce unified multi-address data structures Jon Maloy
` (11 more replies)
0 siblings, 12 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 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?
v3:
- Unified the IPv4 and IPv6 arrays into one array
- Changed prefix_len to always be in IPv6/IpV4 mapped format
- Updated migration protocol to v3, handling multiple addresses
- Many other smaller changes, based on feedback from the PASST team
v4:
- Numerous changes based on feedback
- Added several new commits, mostly broken
out of the pre-existing ones.
Jon Maloy (12):
ip: Introduce unified multi-address data structures
ip: Introduce for_each_addr() macro for address iteration
fwd: Unify guest accessibility checks with unified address array
arp: Check all configured addresses in ARP filtering
pasta: Extract pasta_ns_conf_ip4/6() to reduce nesting
netlink: Return prefix length for IPv6 addresses in nl_addr_get()
conf: Allow multiple -a/--address options per address family
ip: Track observed guest IPv4 addresses in unified address array
ip: Track observed guest IPv6 addresses in unified address array
fwd: Unify fwd_set_observed_ip4() and fwd_set_observed_ip6()
migrate: Rename v1 address functions to v2 for clarity
migrate: Update protocol to v3 for multi-address support
arp.c | 15 +++-
conf.c | 147 +++++++++++++++++++++---------------
conf.h | 8 ++
dhcp.c | 13 +++-
dhcpv6.c | 11 ++-
dhcpv6.h | 2 +-
fwd.c | 221 ++++++++++++++++++++++++++++++++++++++----------------
fwd.h | 3 +
inany.h | 3 +
ip.h | 5 ++
migrate.c | 185 ++++++++++++++++++++++++++++++++++++++++-----
ndp.c | 17 ++++-
netlink.c | 4 +-
passt.h | 104 +++++++++++++++++++++----
pasta.c | 200 ++++++++++++++++++++++++++++--------------------
tap.c | 72 +++++++++++++-----
16 files changed, 736 insertions(+), 274 deletions(-)
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 01/12] ip: Introduce unified multi-address data structures
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 02/12] ip: Introduce for_each_addr() macro for address iteration Jon Maloy
` (10 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
As preparation for supporting multiple addresses per interface, we
replace the single addr/prefix_len fields with an array. The
array consists of a new struct inany_addr_entry containing an
address and prefix length, both in inany_addr format.
Despite some necessary code refactoring, there are only two
functional changes:
- The indicated IPv6 prefix length is now properly stored, instead
of being ignored and overridden with the hardcoded value 64, as
as has been the case until now.
- Since even IPv4 addresses now are stored in IPv6 format, we
also store the corresponding prefix length in that format,
i.e. using the range [96,128] instead of [0,32].
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
v2: -Using inany_addr instead of protocol specific addresses as
entry address field.
v3: -Merging into one array, directly in struct ctx
-Changed prefix_len and flags fields in struct inany_addr_entry
to uint8_t, since that makes the struct directly migratable
v4: -Updated according to changes in previous commits
-Updated according to feedback from David G.
-Squashed IP4_MASK macro commit into this one
---
arp.c | 10 ++++-
conf.c | 124 +++++++++++++++++++++++++++++++++----------------------
conf.h | 7 ++++
dhcp.c | 13 ++++--
dhcpv6.c | 9 ++--
fwd.c | 18 ++++----
ip.h | 5 +++
ndp.c | 17 ++++++--
passt.h | 64 ++++++++++++++++++++++++----
pasta.c | 21 ++++++----
tap.c | 11 ++++-
11 files changed, 211 insertions(+), 88 deletions(-)
diff --git a/arp.c b/arp.c
index bb042e9..99a6a67 100644
--- a/arp.c
+++ b/arp.c
@@ -41,6 +41,8 @@
static bool ignore_arp(const struct ctx *c,
const struct arphdr *ah, const struct arpmsg *am)
{
+ struct inany_addr_entry *e = first_v4(c);
+
if (ah->ar_hrd != htons(ARPHRD_ETHER) ||
ah->ar_pro != htons(ETH_P_IP) ||
ah->ar_hln != ETH_ALEN ||
@@ -54,7 +56,7 @@ static bool ignore_arp(const struct ctx *c,
return true;
/* Don't resolve the guest's assigned address, either. */
- if (!memcmp(am->tip, &c->ip4.addr, sizeof(am->tip)))
+ if (e && !memcmp(am->tip, inany_v4(&e->addr), sizeof(am->tip)))
return true;
return false;
@@ -123,12 +125,16 @@ int arp(const struct ctx *c, struct iov_tail *data)
*/
void arp_send_init_req(const struct ctx *c)
{
+ struct inany_addr_entry *e = first_v4(c);
struct {
struct ethhdr eh;
struct arphdr ah;
struct arpmsg am;
} __attribute__((__packed__)) req;
+ if (!e)
+ return;
+
/* Ethernet header */
req.eh.h_proto = htons(ETH_P_ARP);
memcpy(req.eh.h_dest, MAC_BROADCAST, sizeof(req.eh.h_dest));
@@ -145,7 +151,7 @@ void arp_send_init_req(const struct ctx *c)
memcpy(req.am.sha, c->our_tap_mac, sizeof(req.am.sha));
memcpy(req.am.sip, &c->ip4.our_tap_addr, sizeof(req.am.sip));
memcpy(req.am.tha, MAC_BROADCAST, sizeof(req.am.tha));
- memcpy(req.am.tip, &c->ip4.addr, sizeof(req.am.tip));
+ memcpy(req.am.tip, inany_v4(&e->addr), sizeof(req.am.tip));
debug("Sending initial ARP request for guest MAC address");
tap_send_single(c, &req, sizeof(req));
diff --git a/conf.c b/conf.c
index db94f92..ee1b4fe 100644
--- a/conf.c
+++ b/conf.c
@@ -42,6 +42,7 @@
#include "tap.h"
#include "udp.h"
#include "tcp.h"
+#include "conf.h"
#include "pasta.h"
#include "lineread.h"
#include "isolation.h"
@@ -692,13 +693,16 @@ static int conf_ip4_prefix(const char *arg)
/**
* conf_ip4() - Verify or detect IPv4 support, get relevant addresses
+ * @c: Execution context
* @ifi: Host interface to attempt (0 to determine one)
- * @ip4: IPv4 context (will be written)
*
* Return: interface index for IPv4, or 0 on failure.
*/
-static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
+static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
{
+ struct inany_addr_entry *e = first_v4(c);
+ struct ip4_ctx *ip4 = &c->ip4;
+
if (!ifi)
ifi = nl_get_ext_if(nl_sock, AF_INET);
@@ -717,60 +721,60 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
}
}
- if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr)) {
+ if (!e) {
+ struct in_addr addr;
+ int prefix_len = 0;
int rc = nl_addr_get(nl_sock, ifi, AF_INET,
- &ip4->addr, &ip4->prefix_len, NULL);
+ &addr, &prefix_len, NULL);
if (rc < 0) {
debug("Couldn't discover IPv4 address: %s",
strerror_(-rc));
return 0;
}
- }
+ if (IN4_IS_ADDR_UNSPECIFIED(&addr))
+ return 0;
- if (!ip4->prefix_len) {
- in_addr_t addr = ntohl(ip4->addr.s_addr);
- if (IN_CLASSA(addr))
- ip4->prefix_len = (32 - IN_CLASSA_NSHIFT);
- else if (IN_CLASSB(addr))
- ip4->prefix_len = (32 - IN_CLASSB_NSHIFT);
- else if (IN_CLASSC(addr))
- ip4->prefix_len = (32 - IN_CLASSC_NSHIFT);
- else
- ip4->prefix_len = 32;
+ e = &c->addrs[c->addr_count++];
+ e->addr = inany_from_v4(addr);
+ e->prefix_len = prefix_len + 96;
+ e->flags = CONF_ADDR_HOST;
+ ip4->addr_seen = addr;
}
- ip4->addr_seen = ip4->addr;
-
ip4->our_tap_addr = ip4->guest_gw;
- if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr))
- return 0;
-
return ifi;
}
/**
* conf_ip4_local() - Configure IPv4 addresses and attributes for local mode
- * @ip4: IPv4 context (will be written)
+ * @c: Execution context (will be written)
*/
-static void conf_ip4_local(struct ip4_ctx *ip4)
+static void conf_ip4_local(struct ctx *c)
{
- ip4->addr_seen = ip4->addr = IP4_LL_GUEST_ADDR;
- ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
- ip4->prefix_len = IP4_LL_PREFIX_LEN;
+ struct inany_addr_entry *e = &c->addrs[c->addr_count++];
+ struct ip4_ctx *ip4 = &c->ip4;
+ ip4->addr_seen = IP4_LL_GUEST_ADDR;
+ ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
ip4->no_copy_addrs = ip4->no_copy_routes = true;
+ e->addr = inany_from_v4(IP4_LL_GUEST_ADDR);
+ e->prefix_len = IP4_LL_PREFIX_LEN + 96;
+ e->flags = CONF_ADDR_HOST | CONF_ADDR_LINKLOCAL;
}
/**
* conf_ip6() - Verify or detect IPv6 support, get relevant addresses
+ * @c: Execution context
* @ifi: Host interface to attempt (0 to determine one)
- * @ip6: IPv6 context (will be written)
*
* Return: interface index for IPv6, or 0 on failure.
*/
-static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
+static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
{
+ struct inany_addr_entry *e = first_v6(c);
+ struct ip6_ctx *ip6 = &c->ip6;
+ union inany_addr addr;
int prefix_len = 0;
int rc;
@@ -791,21 +795,26 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
}
}
- rc = nl_addr_get(nl_sock, ifi, AF_INET6,
- IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ? &ip6->addr : NULL,
+ rc = nl_addr_get(nl_sock, ifi, AF_INET6, &addr.a6,
&prefix_len, &ip6->our_tap_ll);
if (rc < 0) {
debug("Couldn't discover IPv6 address: %s", strerror_(-rc));
return 0;
}
- ip6->addr_seen = ip6->addr;
+ if (!e) {
+ e = &c->addrs[c->addr_count++];
+ e->addr = addr;
+ e->prefix_len = prefix_len ? prefix_len : 64;
+ e->flags = CONF_ADDR_HOST;
+ }
+
+ ip6->addr_seen = e->addr.a6;
if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
ip6->our_tap_ll = ip6->guest_gw;
- if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ||
- IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
+ if (IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
return 0;
return ifi;
@@ -813,13 +822,13 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
/**
* conf_ip6_local() - Configure IPv6 addresses and attributes for local mode
- * @ip6: IPv6 context (will be written)
+ * @c: Execution context (will be written)
*/
-static void conf_ip6_local(struct ip6_ctx *ip6)
+static void conf_ip6_local(struct ctx *c)
{
- ip6->our_tap_ll = ip6->guest_gw = IP6_LL_GUEST_GW;
+ c->ip6.our_tap_ll = c->ip6.guest_gw = IP6_LL_GUEST_GW;
- ip6->no_copy_addrs = ip6->no_copy_routes = true;
+ c->ip6.no_copy_addrs = c->ip6.no_copy_routes = true;
}
/**
@@ -1101,6 +1110,7 @@ static void conf_print(const struct ctx *c)
{
char buf4[INET_ADDRSTRLEN], buf6[INET6_ADDRSTRLEN];
char bufmac[ETH_ADDRSTRLEN], ifn[IFNAMSIZ];
+ struct inany_addr_entry *e;
int i;
if (c->ifi4 > 0 || c->ifi6 > 0) {
@@ -1144,14 +1154,16 @@ static void conf_print(const struct ctx *c)
inet_ntop(AF_INET, &c->ip4.map_host_loopback,
buf4, sizeof(buf4)));
- if (!c->no_dhcp) {
+ e = first_v4(c);
+ if (e && !c->no_dhcp) {
uint32_t mask;
- mask = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+ mask = IN4_MASK(inany_prefix4(e));
info("DHCP:");
info(" assign: %s",
- inet_ntop(AF_INET, &c->ip4.addr, buf4, sizeof(buf4)));
+ inet_ntop(AF_INET, inany_v4(&e->addr),
+ buf4, sizeof(buf4)));
info(" mask: %s",
inet_ntop(AF_INET, &mask, buf4, sizeof(buf4)));
info(" router: %s",
@@ -1188,8 +1200,9 @@ static void conf_print(const struct ctx *c)
else
goto dns6;
- info(" assign: %s",
- inet_ntop(AF_INET6, &c->ip6.addr, buf6, sizeof(buf6)));
+ e = first_v6(c);
+ info(" assign: %s", !e ? "" :
+ inet_ntop(AF_INET6, &e->addr.a6, buf6, sizeof(buf6)));
info(" router: %s",
inet_ntop(AF_INET6, &c->ip6.guest_gw, buf6, sizeof(buf6)));
info(" our link-local: %s",
@@ -1811,6 +1824,7 @@ void conf(struct ctx *c, int argc, char **argv)
break;
}
case 'a': {
+ struct inany_addr_entry *e;
union inany_addr addr;
uint8_t prefix_len;
@@ -1834,19 +1848,27 @@ void conf(struct ctx *c, int argc, char **argv)
IN6_IS_ADDR_V4COMPAT(&addr.a6))
die("Invalid address: %s", optarg);
+ /* Legacy behaviour: overwrite existing address if any */
if (inany_v4(&addr)) {
- c->ip4.addr = *inany_v4(&addr);
- c->ip4.prefix_len = prefix_len - 96;
+ e = first_v4(c);
+ if (!e)
+ e = &c->addrs[c->addr_count++];
if (c->mode == MODE_PASTA)
c->ip4.no_copy_addrs = true;
} else {
- c->ip6.addr = addr.a6;
+ e = first_v6(c);
+ if (!e)
+ e = &c->addrs[c->addr_count++];
if (c->mode == MODE_PASTA)
c->ip6.no_copy_addrs = true;
}
+ e->prefix_len = prefix_len;
+ e->addr = addr;
+ e->flags = CONF_ADDR_USER;
break;
}
case 'n': {
+ struct inany_addr_entry *e;
int plen;
if (addr_has_prefix_len)
@@ -1857,7 +1879,9 @@ void conf(struct ctx *c, int argc, char **argv)
die("Invalid prefix length: %s", optarg);
prefix_len_from_opt = plen + 96;
- c->ip4.prefix_len = plen;
+ e = first_v4(c);
+ if (e)
+ e->prefix_len = prefix_len_from_opt;
break;
}
case 'M':
@@ -2003,9 +2027,9 @@ void conf(struct ctx *c, int argc, char **argv)
nl_sock_init(c, false);
if (!v6_only)
- c->ifi4 = conf_ip4(ifi4, &c->ip4);
+ c->ifi4 = conf_ip4(c, ifi4);
if (!v4_only)
- c->ifi6 = conf_ip6(ifi6, &c->ip6);
+ c->ifi6 = conf_ip6(c, ifi6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) {
warn("MTU %"PRIu16" is too small for IPv4 (minimum %u)",
@@ -2028,14 +2052,14 @@ void conf(struct ctx *c, int argc, char **argv)
if (!c->ifi4 && !v6_only) {
info("IPv4: no external interface as template, use local mode");
- conf_ip4_local(&c->ip4);
+ conf_ip4_local(c);
c->ifi4 = -1;
}
if (!c->ifi6 && !v4_only) {
info("IPv6: no external interface as template, use local mode");
- conf_ip6_local(&c->ip6);
+ conf_ip6_local(c);
c->ifi6 = -1;
}
@@ -2144,7 +2168,7 @@ void conf(struct ctx *c, int argc, char **argv)
if (!c->ifi6) {
c->no_ndp = 1;
c->no_dhcpv6 = 1;
- } else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
+ } else if (!first_v6(c)) {
c->no_dhcpv6 = 1;
}
diff --git a/conf.h b/conf.h
index b45ad74..bfad36f 100644
--- a/conf.h
+++ b/conf.h
@@ -6,6 +6,13 @@
#ifndef CONF_H
#define CONF_H
+/* Flags indicating origin and role of an address
+ * To be used in struct inany_addr_entry
+ */
+#define CONF_ADDR_USER BIT(0) /* User set via -a */
+#define CONF_ADDR_HOST BIT(1) /* From host interface */
+#define CONF_ADDR_LINKLOCAL BIT(2) /* Link-local address */
+
enum passt_modes conf_mode(int argc, char *argv[]);
void conf(struct ctx *c, int argc, char **argv);
diff --git a/dhcp.c b/dhcp.c
index 6b9c2e3..e4f3107 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -302,6 +302,7 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
*/
int dhcp(const struct ctx *c, struct iov_tail *data)
{
+ struct inany_addr_entry *e = first_v4(c);
char macstr[ETH_ADDRSTRLEN];
size_t mlen, dlen, opt_len;
struct in_addr mask, dst;
@@ -313,6 +314,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
const struct udphdr *uh;
struct msg m_storage;
struct msg const *m;
+ struct in_addr addr;
struct msg reply;
unsigned int i;
@@ -344,6 +346,9 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
m->op != BOOTREQUEST)
return -1;
+ ASSERT(e);
+ addr = *inany_v4(&e->addr);
+
reply.op = BOOTREPLY;
reply.htype = m->htype;
reply.hlen = m->hlen;
@@ -352,7 +357,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
reply.secs = 0;
reply.flags = m->flags;
reply.ciaddr = m->ciaddr;
- reply.yiaddr = c->ip4.addr;
+ reply.yiaddr = addr;
reply.siaddr = 0;
reply.giaddr = m->giaddr;
memcpy(&reply.chaddr, m->chaddr, sizeof(reply.chaddr));
@@ -404,7 +409,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
info(" from %s", eth_ntop(m->chaddr, macstr, sizeof(macstr)));
- mask.s_addr = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+ mask.s_addr = IN4_MASK(inany_prefix4(e));
memcpy(opts[1].s, &mask, sizeof(mask));
memcpy(opts[3].s, &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
memcpy(opts[54].s, &c->ip4.our_tap_addr, sizeof(c->ip4.our_tap_addr));
@@ -412,7 +417,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
/* If the gateway is not on the assigned subnet, send an option 121
* (Classless Static Routing) adding a dummy route to it.
*/
- if ((c->ip4.addr.s_addr & mask.s_addr)
+ if ((addr.s_addr & mask.s_addr)
!= (c->ip4.guest_gw.s_addr & mask.s_addr)) {
/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
opts[121].slen = 14;
@@ -469,7 +474,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
if (m->flags & FLAG_BROADCAST)
dst = in4addr_broadcast;
else
- dst = c->ip4.addr;
+ dst = addr;
tap_udp4_send(c, c->ip4.our_tap_addr, 67, dst, 68, &reply, dlen);
diff --git a/dhcpv6.c b/dhcpv6.c
index e4df0db..801b01b 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -549,6 +549,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
const struct in6_addr *saddr, const struct in6_addr *daddr)
{
const struct opt_server_id *server_id = NULL;
+ struct inany_addr_entry *e = first_v6(c);
const struct opt_hdr *client_id = NULL;
/* The _storage variables can't be local to the blocks they're used in,
* because IOV_*_HEADER() may return pointers to them which are
@@ -625,7 +626,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
if (mh->type == TYPE_CONFIRM && server_id)
return -1;
- if (dhcpv6_ia_notonlink(data, &c->ip6.addr)) {
+ if (e && dhcpv6_ia_notonlink(data, &e->addr.a6)) {
dhcpv6_send_ia_notonlink(c, data, &client_id_base,
ntohs(client_id->l), mh->xid);
@@ -679,7 +680,8 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
tap_udp6_send(c, src, 547, tap_ip6_daddr(c, src), 546,
mh->xid, &resp, n);
- c->ip6.addr_seen = c->ip6.addr;
+ if (e)
+ c->ip6.addr_seen = e->addr.a6;
return 1;
}
@@ -703,5 +705,6 @@ void dhcpv6_init(const struct ctx *c)
memcpy(resp_not_on_link.server_id.duid_lladdr,
c->our_tap_mac, sizeof(c->our_tap_mac));
- resp.ia_addr.addr = c->ip6.addr;
+ if (first_v6(c))
+ resp.ia_addr.addr = first_v6(c)->addr.a6;
}
diff --git a/fwd.c b/fwd.c
index 44a0e10..451c757 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)
{
+ struct inany_addr_entry *e = first_v4(c);
+
if (IN4_IS_ADDR_LOOPBACK(addr))
return false;
@@ -516,7 +518,7 @@ static bool fwd_guest_accessible4(const struct ctx *c,
/* For IPv4, addr_seen is initialised to addr, so is always a valid
* address
*/
- if (IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr) ||
+ if ((e && IN4_ARE_ADDR_EQUAL(addr, inany_v4(&e->addr))) ||
IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
return false;
@@ -537,7 +539,7 @@ static bool fwd_guest_accessible6(const struct ctx *c,
if (IN6_IS_ADDR_LOOPBACK(addr))
return false;
- if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addr))
+ if (first_v6(c) && IN6_ARE_ADDR_EQUAL(addr, &first_v6(c)->addr.a6))
return false;
/* For IPv6, addr_seen starts unspecified, because we don't know what LL
@@ -586,10 +588,10 @@ static void nat_outbound(const struct ctx *c, const union inany_addr *addr,
*translated = inany_loopback4;
else if (inany_equals6(addr, &c->ip6.map_host_loopback))
*translated = inany_loopback6;
- else if (inany_equals4(addr, &c->ip4.map_guest_addr))
- *translated = inany_from_v4(c->ip4.addr);
- else if (inany_equals6(addr, &c->ip6.map_guest_addr))
- translated->a6 = c->ip6.addr;
+ else if (first_v4(c) && inany_equals4(addr, &c->ip4.map_guest_addr))
+ *translated = first_v4(c)->addr;
+ else if (first_v6(c) && inany_equals6(addr, &c->ip6.map_guest_addr))
+ translated->a6 = first_v6(c)->addr.a6;
else
*translated = *addr;
}
@@ -710,10 +712,10 @@ bool nat_inbound(const struct ctx *c, const union inany_addr *addr,
inany_equals6(addr, &in6addr_loopback)) {
translated->a6 = c->ip6.map_host_loopback;
} else if (!IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_guest_addr) &&
- inany_equals4(addr, &c->ip4.addr)) {
+ first_v4(c) && inany_equals(addr, &first_v4(c)->addr)) {
*translated = inany_from_v4(c->ip4.map_guest_addr);
} else if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_guest_addr) &&
- inany_equals6(addr, &c->ip6.addr)) {
+ first_v6(c) && inany_equals(addr, &first_v6(c)->addr)) {
translated->a6 = c->ip6.map_guest_addr;
} else if (fwd_guest_accessible(c, addr)) {
*translated = *addr;
diff --git a/ip.h b/ip.h
index bd28640..0ffd888 100644
--- a/ip.h
+++ b/ip.h
@@ -17,6 +17,8 @@
(ntohl(((struct in_addr *)(a))->s_addr) >> IN_CLASSA_NSHIFT == IN_LOOPBACKNET)
#define IN4_IS_ADDR_MULTICAST(a) \
(IN_MULTICAST(ntohl(((struct in_addr *)(a))->s_addr)))
+#define IN4_MASK(prefix) \
+ ((prefix) <= 0 ? 0 : htonl(0xffffffff << (32 - (prefix))))
#define IN4_ARE_ADDR_EQUAL(a, b) \
(((struct in_addr *)(a))->s_addr == ((struct in_addr *)b)->s_addr)
#define IN4ADDR_LOOPBACK_INIT \
@@ -135,6 +137,9 @@ static const struct in_addr in4addr_broadcast = { 0xffffffff };
#define IPV6_MIN_MTU 1280
#endif
+/* Maximum number of addresses in context address array */
+#define INANY_MAX_ADDRS 32
+
int ip4_class_prefix_len(const struct in_addr *addr);
#endif /* IP_H */
diff --git a/ndp.c b/ndp.c
index eb9e313..03e43f7 100644
--- a/ndp.c
+++ b/ndp.c
@@ -257,7 +257,7 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
.valid_lifetime = ~0U,
.pref_lifetime = ~0U,
},
- .prefix = c->ip6.addr,
+ .prefix = IN6ADDR_ANY_INIT,
.source_ll = {
.header = {
.type = OPT_SRC_L2_ADDR,
@@ -265,8 +265,13 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
},
},
};
+ struct inany_addr_entry *e = first_v6(c);
unsigned char *ptr = NULL;
+ ASSERT(e);
+
+ ra.prefix = e->addr.a6;
+
ptr = &ra.var[0];
if (c->mtu) {
@@ -458,6 +463,7 @@ first:
*/
void ndp_send_init_req(const struct ctx *c)
{
+ struct inany_addr_entry *e = first_v6(c);
struct ndp_ns ns = {
.ih = {
.icmp6_type = NS,
@@ -466,8 +472,13 @@ void ndp_send_init_req(const struct ctx *c)
.icmp6_solicited = 0, /* Reserved */
.icmp6_override = 0, /* Reserved */
},
- .target_addr = c->ip6.addr
+ .target_addr = IN6ADDR_ANY_INIT
};
+
+ if (!e)
+ return;
+
+ ns.target_addr = e->addr.a6;
debug("Sending initial NDP NS request for guest MAC address");
- ndp_send(c, &c->ip6.addr, &ns, sizeof(ns));
+ ndp_send(c, &e->addr.a6, &ns, sizeof(ns));
}
diff --git a/passt.h b/passt.h
index 79d01dd..2a78ff5 100644
--- a/passt.h
+++ b/passt.h
@@ -64,11 +64,21 @@ enum passt_modes {
MODE_VU,
};
+/**
+ * struct inany_addr_entry - Unified IPv4/IPv6 address entry
+ * @addr: IPv4 (as mapped) or IPv6 address
+ * @prefix_len: Prefix length in IPv6/IPv4-mapped [0,128]/[96,128] format
+ * @flags: CONF_ADDR_* flags
+ */
+struct inany_addr_entry {
+ union inany_addr addr;
+ uint8_t prefix_len;
+ uint8_t flags;
+};
+
/**
* struct ip4_ctx - IPv4 execution context
- * @addr: IPv4 address assigned to guest
* @addr_seen: Latest IPv4 address seen as source from tap
- * @prefixlen: IPv4 prefix length (netmask)
* @guest_gw: IPv4 gateway as seen by the guest
* @map_host_loopback: Outbound connections to this address are NATted to the
* host's 127.0.0.1
@@ -84,10 +94,7 @@ enum passt_modes {
* @no_copy_addrs: Don't copy all addresses when configuring namespace
*/
struct ip4_ctx {
- /* PIF_TAP addresses */
- struct in_addr addr;
struct in_addr addr_seen;
- int prefix_len;
struct in_addr guest_gw;
struct in_addr map_host_loopback;
struct in_addr map_guest_addr;
@@ -107,7 +114,6 @@ struct ip4_ctx {
/**
* struct ip6_ctx - IPv6 execution context
- * @addr: IPv6 address assigned to guest
* @addr_seen: Latest IPv6 global/site address seen as source from tap
* @addr_ll_seen: Latest IPv6 link-local address seen as source from tap
* @guest_gw: IPv6 gateway as seen by the guest
@@ -125,8 +131,6 @@ struct ip4_ctx {
* @no_copy_addrs: Don't copy all addresses when configuring namespace
*/
struct ip6_ctx {
- /* PIF_TAP addresses */
- struct in6_addr addr;
struct in6_addr addr_seen;
struct in6_addr addr_ll_seen;
struct in6_addr guest_gw;
@@ -181,6 +185,8 @@ struct ip6_ctx {
* @fqdn: Guest FQDN
* @ifi6: Template interface for IPv6, -1: none, 0: IPv6 disabled
* @ip6: IPv6 configuration
+ * @addrs: Unified address array for both IPv4 (mapped) and IPv6
+ * @addr_count: Number of active entries in @addrs array
* @pasta_ifn: Name of namespace interface for pasta
* @pasta_ifi: Index of namespace interface for pasta
* @pasta_conf_ns: Configure namespace after creating it
@@ -257,6 +263,9 @@ struct ctx {
int ifi6;
struct ip6_ctx ip6;
+ struct inany_addr_entry addrs[INANY_MAX_ADDRS];
+ int addr_count;
+
char pasta_ifn[IF_NAMESIZE];
unsigned int pasta_ifi;
int pasta_conf_ns;
@@ -294,6 +303,45 @@ struct ctx {
bool migrate_exit;
};
+/**
+ * first_v4() - Get first IPv4 address entry
+ * @c: Pointer to struct ctx
+ *
+ * Return: pointer to first IPv4 entry, or NULL if none
+ */
+static inline struct inany_addr_entry *first_v4(const struct ctx *c)
+{
+ for (int i = 0; i < c->addr_count; i++)
+ if (inany_v4(&c->addrs[i].addr))
+ return (struct inany_addr_entry *)&c->addrs[i];
+ return NULL;
+}
+
+/**
+ * first_v6() - Get first IPv6 address entry
+ * @c: Pointer to struct ctx
+ *
+ * Return: pointer to first IPv6 entry, or NULL if none
+ */
+static inline struct inany_addr_entry *first_v6(const struct ctx *c)
+{
+ for (int i = 0; i < c->addr_count; i++)
+ if (!inany_v4(&c->addrs[i].addr))
+ return (struct inany_addr_entry *)&c->addrs[i];
+ return NULL;
+}
+
+/**
+ * inany_prefix4() - Get IPv4 prefix length from address entry
+ * @e: Address entry (must be IPv4)
+ *
+ * Return: prefix length in IPv4 format (0-32)
+ */
+static inline int inany_prefix4(const struct inany_addr_entry *e)
+{
+ return e->prefix_len - 96;
+}
+
void proto_update_l2_buf(const unsigned char *eth_d);
#endif /* PASST_H */
diff --git a/pasta.c b/pasta.c
index c307b8a..4728445 100644
--- a/pasta.c
+++ b/pasta.c
@@ -327,6 +327,7 @@ void pasta_ns_conf(struct ctx *c)
if (c->pasta_conf_ns) {
unsigned int flags = IFF_UP;
+ struct inany_addr_entry *e;
if (c->mtu)
nl_link_set_mtu(nl_sock_ns, c->pasta_ifi, c->mtu);
@@ -338,10 +339,12 @@ void pasta_ns_conf(struct ctx *c)
if (c->ifi4) {
if (c->ip4.no_copy_addrs) {
- rc = nl_addr_set(nl_sock_ns, c->pasta_ifi,
- AF_INET,
- &c->ip4.addr,
- c->ip4.prefix_len);
+ e = first_v4(c);
+ if (e)
+ rc = nl_addr_set(nl_sock_ns,
+ c->pasta_ifi, AF_INET,
+ inany_v4(&e->addr),
+ e->prefix_len - 96);
} else {
rc = nl_addr_dup(nl_sock, c->ifi4,
nl_sock_ns, c->pasta_ifi,
@@ -387,11 +390,13 @@ void pasta_ns_conf(struct ctx *c)
0, IFF_NOARP);
if (c->ip6.no_copy_addrs) {
- if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
+ e = first_v6(c);
+ if (e)
rc = nl_addr_set(nl_sock_ns,
- c->pasta_ifi, AF_INET6,
- &c->ip6.addr, 64);
- }
+ c->pasta_ifi,
+ AF_INET6,
+ &e->addr.a6,
+ e->prefix_len);
} else {
rc = nl_addr_dup(nl_sock, c->ifi6,
nl_sock_ns, c->pasta_ifi,
diff --git a/tap.c b/tap.c
index 9d1344b..4298dcd 100644
--- a/tap.c
+++ b/tap.c
@@ -47,6 +47,7 @@
#include "ip.h"
#include "iov.h"
#include "passt.h"
+#include "conf.h"
#include "arp.h"
#include "dhcp.h"
#include "ndp.h"
@@ -951,8 +952,14 @@ resume:
c->ip6.addr_seen = *saddr;
}
- if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
- c->ip6.addr = *saddr;
+ if (!first_v6(c) && c->addr_count < INANY_MAX_ADDRS) {
+ struct inany_addr_entry *e;
+
+ e = &c->addrs[c->addr_count++];
+ e->addr.a6 = *saddr;
+ e->prefix_len = 64;
+ e->flags = CONF_ADDR_LINKLOCAL;
+ }
} else if (!IN6_IS_ADDR_UNSPECIFIED(saddr)){
c->ip6.addr_seen = *saddr;
}
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 02/12] ip: Introduce for_each_addr() macro for address iteration
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
2026-02-17 22:18 ` [PATCH v4 01/12] ip: Introduce unified multi-address data structures Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 03/12] fwd: Unify guest accessibility checks with unified address array Jon Maloy
` (9 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
Add the for_each_addr() macro to iterate over addresses in the unified
array. The macro supports an address family filter parameter (AF_INET,
AF_INET6, or 0 for all) using a _next_addr_idx() helper function to
skip non-matching entries.
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
v1: - Broke out as separate commit
- I kept the third argument, despite David's comment, since I still
find it practical. If we want to iterate the list without filter
we can just use AF_UNSPEC (== 0)
---
passt.h | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/passt.h b/passt.h
index 2a78ff5..15d6596 100644
--- a/passt.h
+++ b/passt.h
@@ -342,6 +342,40 @@ static inline int inany_prefix4(const struct inany_addr_entry *e)
return e->prefix_len - 96;
}
+/**
+ * _next_addr_idx() - Find next address index matching family filter
+ * @c: Pointer to struct ctx
+ * @i: Starting index
+ * @af: Address family filter: AF_INET, AF_INET6, or 0 for all
+ *
+ * Return: next matching index, or addr_count if none found
+ */
+static inline int _next_addr_idx(const struct ctx *c, int i, sa_family_t af)
+{
+ for (; i < c->addr_count; i++) {
+ sa_family_t entry_af;
+
+ entry_af = inany_v4(&c->addrs[i].addr) ? AF_INET : AF_INET6;
+
+ if (!af || af == entry_af)
+ return i;
+ }
+ return i;
+}
+
+/**
+ * for_each_addr() - Iterate over addresses in unified array
+ * @e: Pointer variable for current entry (struct inany_addr_entry *)
+ * @c: Pointer to struct ctx
+ * @af: Address family filter: AF_INET, AF_INET6, or 0 for all
+ *
+ * Note: @_i is the internal loop counter, uses _next_addr_idx() helper
+ */
+#define for_each_addr(e, c, af) \
+ for (int _i = _next_addr_idx((c), 0, (af)); \
+ _i < (c)->addr_count && ((e) = &(c)->addrs[_i], true); \
+ _i = _next_addr_idx((c), _i + 1, (af)))
+
void proto_update_l2_buf(const unsigned char *eth_d);
#endif /* PASST_H */
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 03/12] fwd: Unify guest accessibility checks with unified address array
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
2026-02-17 22:18 ` [PATCH v4 01/12] ip: Introduce unified multi-address data structures Jon Maloy
2026-02-17 22:18 ` [PATCH v4 02/12] ip: Introduce for_each_addr() macro for address iteration Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 04/12] arp: Check all configured addresses in ARP filtering Jon Maloy
` (8 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
We replace the fwd_guest_accessible4() and fwd_guest_accessible6()
functions with a unified fwd_guest_accessible() function that handles
both address families. With the unified address array, we can check
all configured addresses in a single pass using for_each_addr() with
family filter INADDR_UNSPEC (== 0).
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
fwd.c | 70 +++++++++++++++--------------------------------------------
1 file changed, 18 insertions(+), 52 deletions(-)
diff --git a/fwd.c b/fwd.c
index 451c757..fa5d667 100644
--- a/fwd.c
+++ b/fwd.c
@@ -492,19 +492,19 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
}
/**
- * fwd_guest_accessible4() - Is IPv4 address guest-accessible
+ * fwd_guest_accessible() - Is address guest-accessible
* @c: Execution context
- * @addr: Host visible IPv4 address
+ * @addr: Host visible address (IPv4 or IPv6)
*
* Return: true if @addr on the host is accessible to the guest without
* translation, false otherwise
*/
-static bool fwd_guest_accessible4(const struct ctx *c,
- const struct in_addr *addr)
+static bool fwd_guest_accessible(const struct ctx *c,
+ const union inany_addr *addr)
{
- struct inany_addr_entry *e = first_v4(c);
+ const struct inany_addr_entry *e;
- if (IN4_IS_ADDR_LOOPBACK(addr))
+ if (inany_is_loopback(addr))
return false;
/* In socket interfaces 0.0.0.0 generally means "any" or unspecified,
@@ -512,66 +512,32 @@ static bool fwd_guest_accessible4(const struct ctx *c,
* that has a different meaning for host and guest, we can't let it
* through untranslated.
*/
- if (IN4_IS_ADDR_UNSPECIFIED(addr))
- return false;
-
- /* For IPv4, addr_seen is initialised to addr, so is always a valid
- * address
- */
- if ((e && IN4_ARE_ADDR_EQUAL(addr, inany_v4(&e->addr))) ||
- IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
+ if (inany_is_unspecified4(addr))
return false;
- return true;
-}
-
-/**
- * fwd_guest_accessible6() - Is IPv6 address guest-accessible
- * @c: Execution context
- * @addr: Host visible IPv6 address
- *
- * Return: true if @addr on the host is accessible to the guest without
- * translation, false otherwise
- */
-static bool fwd_guest_accessible6(const struct ctx *c,
- const struct in6_addr *addr)
-{
- if (IN6_IS_ADDR_LOOPBACK(addr))
- return false;
+ /* Check against all configured guest addresses */
+ for_each_addr(e, c, 0)
+ if (inany_equals(addr, &e->addr))
+ return false;
- if (first_v6(c) && IN6_ARE_ADDR_EQUAL(addr, &first_v6(c)->addr.a6))
+ /* Also check addr_seen: it tracks the address the guest is actually
+ * using, which may differ from configured addresses.
+ */
+ if (inany_equals4(addr, &c->ip4.addr_seen))
return false;
/* For IPv6, addr_seen starts unspecified, because we don't know what LL
* address the guest will take until we see it. Only check against it
* if it has been set to a real address.
*/
- if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen) &&
- IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addr_seen))
+ if (!inany_v4(addr) &&
+ !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen) &&
+ inany_equals6(addr, &c->ip6.addr_seen))
return false;
return true;
}
-/**
- * fwd_guest_accessible() - Is IPv[46] address guest-accessible
- * @c: Execution context
- * @addr: Host visible IPv[46] address
- *
- * Return: true if @addr on the host is accessible to the guest without
- * translation, false otherwise
- */
-static bool fwd_guest_accessible(const struct ctx *c,
- const union inany_addr *addr)
-{
- const struct in_addr *a4 = inany_v4(addr);
-
- if (a4)
- return fwd_guest_accessible4(c, a4);
-
- return fwd_guest_accessible6(c, &addr->a6);
-}
-
/**
* nat_outbound() - Apply address translation for outbound (TAP to HOST)
* @c: Execution context
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 04/12] arp: Check all configured addresses in ARP filtering
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
` (2 preceding siblings ...)
2026-02-17 22:18 ` [PATCH v4 03/12] fwd: Unify guest accessibility checks with unified address array Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 05/12] pasta: Extract pasta_ns_conf_ip4/6() to reduce nesting Jon Maloy
` (7 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
As a preparation for handling multiple addresses, we update ignore_arp()
to check against all addresses in the unified addrs[] array using the
for_each_addr() macro.
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
Reviewed-by: David Gibson <david@gibson.dropbear.id.au>
---
v3: Adapted to single-array changes earlier in this series
---
arp.c | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/arp.c b/arp.c
index 99a6a67..8beb2dd 100644
--- a/arp.c
+++ b/arp.c
@@ -41,7 +41,7 @@
static bool ignore_arp(const struct ctx *c,
const struct arphdr *ah, const struct arpmsg *am)
{
- struct inany_addr_entry *e = first_v4(c);
+ const struct inany_addr_entry *e;
if (ah->ar_hrd != htons(ARPHRD_ETHER) ||
ah->ar_pro != htons(ETH_P_IP) ||
@@ -55,9 +55,10 @@ static bool ignore_arp(const struct ctx *c,
!memcmp(am->sip, am->tip, sizeof(am->sip)))
return true;
- /* Don't resolve the guest's assigned address, either. */
- if (e && !memcmp(am->tip, inany_v4(&e->addr), sizeof(am->tip)))
- return true;
+ /* Don't resolve any of the guest's addresses */
+ for_each_addr(e, c, AF_INET)
+ if (!memcmp(am->tip, inany_v4(&e->addr), sizeof(am->tip)))
+ return true;
return false;
}
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 05/12] pasta: Extract pasta_ns_conf_ip4/6() to reduce nesting
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
` (3 preceding siblings ...)
2026-02-17 22:18 ` [PATCH v4 04/12] arp: Check all configured addresses in ARP filtering Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 06/12] netlink: Return prefix length for IPv6 addresses in nl_addr_get() Jon Maloy
` (6 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
Extract the IPv4 and IPv6 namespace configuration code from
pasta_ns_conf() into separate static functions. This reduces
indentation depth and prepares for adding multi-address support.
No functional change.
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
v4: - Leaving them unchanged for now, despite David's comment.
I tried, and the result wasn't pretty. I will try to revisit
this at a later moment.
---
pasta.c | 193 +++++++++++++++++++++++++++++++-------------------------
1 file changed, 107 insertions(+), 86 deletions(-)
diff --git a/pasta.c b/pasta.c
index 4728445..59f5ba8 100644
--- a/pasta.c
+++ b/pasta.c
@@ -303,6 +303,109 @@ void pasta_start_ns(struct ctx *c, uid_t uid, gid_t gid,
die_perror("Failed to join network namespace");
}
+/**
+ * pasta_ns_conf_ip4() - Configure IPv4 in namespace
+ * @c: Execution context
+ */
+static void pasta_ns_conf_ip4(struct ctx *c)
+{
+ int rc = 0;
+
+ if (c->ip4.no_copy_addrs) {
+ const struct inany_addr_entry *e = first_v4(c);
+
+ if (e)
+ rc = nl_addr_set(nl_sock_ns,
+ c->pasta_ifi, AF_INET,
+ inany_v4(&e->addr),
+ inany_prefix4(e));
+ } else {
+ rc = nl_addr_dup(nl_sock, c->ifi4,
+ nl_sock_ns, c->pasta_ifi,
+ AF_INET);
+ }
+
+ if (rc < 0) {
+ die("Couldn't set IPv4 address(es) in namespace: %s",
+ strerror_(-rc));
+ }
+
+ if (c->ip4.no_copy_routes) {
+ rc = nl_route_set_def(nl_sock_ns, c->pasta_ifi,
+ AF_INET,
+ &c->ip4.guest_gw);
+ } else {
+ rc = nl_route_dup(nl_sock, c->ifi4, nl_sock_ns,
+ c->pasta_ifi, AF_INET);
+ }
+
+ if (rc < 0) {
+ die("Couldn't set IPv4 route(s) in guest: %s",
+ strerror_(-rc));
+ }
+}
+
+/**
+ * pasta_ns_conf_ip6() - Configure IPv6 in namespace
+ * @c: Execution context
+ */
+static void pasta_ns_conf_ip6(struct ctx *c)
+{
+ int rc = 0;
+
+ rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi,
+ &c->ip6.addr_ll_seen);
+ if (rc < 0) {
+ warn("Can't get LL address from namespace: %s",
+ strerror_(-rc));
+ }
+
+ rc = nl_addr_set_ll_nodad(nl_sock_ns, c->pasta_ifi);
+ if (rc < 0) {
+ warn("Can't set nodad for LL in namespace: %s",
+ strerror_(-rc));
+ }
+
+ /* We dodged DAD: re-enable neighbour solicitations */
+ nl_link_set_flags(nl_sock_ns, c->pasta_ifi,
+ 0, IFF_NOARP);
+
+ if (c->ip6.no_copy_addrs) {
+ const struct inany_addr_entry *e = first_v6(c);
+
+ if (e)
+ rc = nl_addr_set(nl_sock_ns,
+ c->pasta_ifi,
+ AF_INET6,
+ &e->addr.a6,
+ e->prefix_len);
+ } else {
+ rc = nl_addr_dup(nl_sock, c->ifi6,
+ nl_sock_ns, c->pasta_ifi,
+ AF_INET6);
+ }
+
+ if (rc < 0) {
+ die("Couldn't set IPv6 address(es) in namespace: %s",
+ strerror_(-rc));
+ }
+
+ if (c->ip6.no_copy_routes) {
+ rc = nl_route_set_def(nl_sock_ns, c->pasta_ifi,
+ AF_INET6,
+ &c->ip6.guest_gw);
+ } else {
+ rc = nl_route_dup(nl_sock, c->ifi6,
+ nl_sock_ns, c->pasta_ifi,
+ AF_INET6);
+ }
+
+ if (rc < 0) {
+ die("Couldn't set IPv6 route(s) in guest: %s",
+ strerror_(-rc));
+ }
+}
+
/**
* pasta_ns_conf() - Set up loopback and tap interfaces in namespace as needed
* @c: Execution context
@@ -327,7 +430,6 @@ void pasta_ns_conf(struct ctx *c)
if (c->pasta_conf_ns) {
unsigned int flags = IFF_UP;
- struct inany_addr_entry *e;
if (c->mtu)
nl_link_set_mtu(nl_sock_ns, c->pasta_ifi, c->mtu);
@@ -337,92 +439,11 @@ 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) {
- e = first_v4(c);
- if (e)
- rc = nl_addr_set(nl_sock_ns,
- c->pasta_ifi, AF_INET,
- inany_v4(&e->addr),
- e->prefix_len - 96);
- } else {
- rc = nl_addr_dup(nl_sock, c->ifi4,
- nl_sock_ns, c->pasta_ifi,
- AF_INET);
- }
-
- if (rc < 0) {
- die("Couldn't set IPv4 address(es) in namespace: %s",
- strerror_(-rc));
- }
-
- if (c->ip4.no_copy_routes) {
- rc = nl_route_set_def(nl_sock_ns, c->pasta_ifi,
- AF_INET,
- &c->ip4.guest_gw);
- } else {
- rc = nl_route_dup(nl_sock, c->ifi4, nl_sock_ns,
- c->pasta_ifi, AF_INET);
- }
-
- if (rc < 0) {
- die("Couldn't set IPv4 route(s) in guest: %s",
- strerror_(-rc));
- }
- }
+ if (c->ifi4)
+ pasta_ns_conf_ip4(c);
- if (c->ifi6) {
- rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi,
- &c->ip6.addr_ll_seen);
- if (rc < 0) {
- warn("Can't get LL address from namespace: %s",
- strerror_(-rc));
- }
-
- rc = nl_addr_set_ll_nodad(nl_sock_ns, c->pasta_ifi);
- if (rc < 0) {
- warn("Can't set nodad for LL in namespace: %s",
- strerror_(-rc));
- }
-
- /* We dodged DAD: re-enable neighbour solicitations */
- nl_link_set_flags(nl_sock_ns, c->pasta_ifi,
- 0, IFF_NOARP);
-
- if (c->ip6.no_copy_addrs) {
- e = first_v6(c);
- if (e)
- rc = nl_addr_set(nl_sock_ns,
- c->pasta_ifi,
- AF_INET6,
- &e->addr.a6,
- e->prefix_len);
- } else {
- rc = nl_addr_dup(nl_sock, c->ifi6,
- nl_sock_ns, c->pasta_ifi,
- AF_INET6);
- }
-
- if (rc < 0) {
- die("Couldn't set IPv6 address(es) in namespace: %s",
- strerror_(-rc));
- }
-
- if (c->ip6.no_copy_routes) {
- rc = nl_route_set_def(nl_sock_ns, c->pasta_ifi,
- AF_INET6,
- &c->ip6.guest_gw);
- } else {
- rc = nl_route_dup(nl_sock, c->ifi6,
- nl_sock_ns, c->pasta_ifi,
- AF_INET6);
- }
-
- if (rc < 0) {
- die("Couldn't set IPv6 route(s) in guest: %s",
- strerror_(-rc));
- }
- }
+ if (c->ifi6)
+ pasta_ns_conf_ip6(c);
}
proto_update_l2_buf(c->guest_mac);
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 06/12] netlink: Return prefix length for IPv6 addresses in nl_addr_get()
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
` (4 preceding siblings ...)
2026-02-17 22:18 ` [PATCH v4 05/12] pasta: Extract pasta_ns_conf_ip4/6() to reduce nesting Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 07/12] conf: Allow multiple -a/--address options per address family Jon Maloy
` (5 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
nl_addr_get() was not setting the prefix_len output parameter for
IPv6 addresses, only for IPv4. This meant callers always got 0 for
IPv6, forcing them to use a hardcoded default (64).
Fix by assigning *prefix_len in the IPv6 case, matching the IPv4
behavior.
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
conf.c | 2 +-
netlink.c | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/conf.c b/conf.c
index ee1b4fe..b894ec5 100644
--- a/conf.c
+++ b/conf.c
@@ -805,7 +805,7 @@ static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
if (!e) {
e = &c->addrs[c->addr_count++];
e->addr = addr;
- e->prefix_len = prefix_len ? prefix_len : 64;
+ e->prefix_len = prefix_len;
e->flags = CONF_ADDR_HOST;
}
diff --git a/netlink.c b/netlink.c
index 82a2f0c..769cb23 100644
--- a/netlink.c
+++ b/netlink.c
@@ -752,7 +752,7 @@ int nl_addr_set_ll_nodad(int s, unsigned int ifi)
* @ifi: Interface index in outer network namespace
* @af: Address family
* @addr: Global address to fill
- * @prefix_len: Mask or prefix length, to fill (for IPv4)
+ * @prefix_len: Mask or prefix length, to fill
* @addr_l: Link-scoped address to fill (for IPv6)
*
* Return: 0 on success, negative error code on failure
@@ -797,7 +797,7 @@ int nl_addr_get(int s, unsigned int ifi, sa_family_t af,
ifa->ifa_prefixlen > prefix_max) {
memcpy(addr, RTA_DATA(rta), RTA_PAYLOAD(rta));
- prefix_max = ifa->ifa_prefixlen;
+ prefix_max = *prefix_len = ifa->ifa_prefixlen;
}
if (addr_l &&
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 07/12] conf: Allow multiple -a/--address options per address family
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
` (5 preceding siblings ...)
2026-02-17 22:18 ` [PATCH v4 06/12] netlink: Return prefix length for IPv6 addresses in nl_addr_get() Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 08/12] ip: Track observed guest IPv4 addresses in unified address array Jon Maloy
` (4 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
Allow specifying multiple addresses per family with -a/--address.
The first address of each family is used for DHCP/DHCPv6 assignment.
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
v2: - Adapted to previous code changes
v3: - Adapted to single-array strategy
- Changes according to feedback from S. Brivio and G Gibson.
v4: - Stripped down and adapted after feedback from David G.
---
conf.c | 41 ++++++++++++++++++++++++-----------------
pasta.c | 29 +++++++++++++++--------------
2 files changed, 39 insertions(+), 31 deletions(-)
diff --git a/conf.c b/conf.c
index b894ec5..ca0a764 100644
--- a/conf.c
+++ b/conf.c
@@ -906,9 +906,10 @@ static void usage(const char *name, FILE *f, int status)
" default: 65520: maximum 802.3 MTU minus 802.3 header\n"
" length, rounded to 32 bits (IPv4 words)\n"
" -a, --address ADDR Assign IPv4 or IPv6 address ADDR[/PREFIXLEN]\n"
- " can be specified zero to two times (for IPv4 and IPv6)\n"
+ " can be specified up to a maximum of 32 times\n"
" default: use addresses from interface with default route\n"
- " -n, --netmask MASK Assign IPv4 MASK, dot-decimal or bits\n"
+ " -n, --netmask MASK Assign IPv4 MASK, dot-decimal or bits\n");
+ FPRINTF(f,
" default: netmask from matching address on the host\n"
" -M, --mac-addr ADDR Use source MAC address ADDR\n"
" default: 9a:55:9a:55:9a:55 (locally administered)\n"
@@ -1848,32 +1849,38 @@ void conf(struct ctx *c, int argc, char **argv)
IN6_IS_ADDR_V4COMPAT(&addr.a6))
die("Invalid address: %s", optarg);
- /* Legacy behaviour: overwrite existing address if any */
- if (inany_v4(&addr)) {
- e = first_v4(c);
- if (!e)
- e = &c->addrs[c->addr_count++];
- if (c->mode == MODE_PASTA)
- c->ip4.no_copy_addrs = true;
- } else {
- e = first_v6(c);
- if (!e)
- e = &c->addrs[c->addr_count++];
- if (c->mode == MODE_PASTA)
- c->ip6.no_copy_addrs = true;
- }
+ for_each_addr(e, c, AF_UNSPEC)
+ if (inany_equals(&addr, &e->addr))
+ die("Address exists: %s", optarg);
+
+ if (c->addr_count >= INANY_MAX_ADDRS)
+ die("Max addresses is %d", INANY_MAX_ADDRS);
+
+ e = &c->addrs[c->addr_count++];
e->prefix_len = prefix_len;
e->addr = addr;
e->flags = CONF_ADDR_USER;
+
+ if (c->mode != MODE_PASTA)
+ break;
+
+ if (inany_v4(&addr))
+ c->ip4.no_copy_addrs = true;
+ else
+ c->ip6.no_copy_addrs = true;
break;
}
case 'n': {
struct inany_addr_entry *e;
- int plen;
+ int plen, i = 0;
if (addr_has_prefix_len)
die("Redundant prefix length specification");
+ for_each_addr(e, c, AF_INET)
+ if (++i > 1)
+ die("Use -n only when one address");
+
plen = conf_ip4_prefix(optarg);
if (plen < 0)
die("Invalid prefix length: %s", optarg);
diff --git a/pasta.c b/pasta.c
index 59f5ba8..dd23a14 100644
--- a/pasta.c
+++ b/pasta.c
@@ -312,13 +312,14 @@ static void pasta_ns_conf_ip4(struct ctx *c)
int rc = 0;
if (c->ip4.no_copy_addrs) {
- const struct inany_addr_entry *e = first_v4(c);
+ const struct inany_addr_entry *e;
- if (e)
- rc = nl_addr_set(nl_sock_ns,
- c->pasta_ifi, AF_INET,
- inany_v4(&e->addr),
- inany_prefix4(e));
+ for_each_addr(e, c, AF_INET) {
+ rc = nl_addr_set(nl_sock_ns, c->pasta_ifi, AF_INET,
+ inany_v4(&e->addr), inany_prefix4(e));
+ if (rc < 0)
+ break;
+ }
} else {
rc = nl_addr_dup(nl_sock, c->ifi4,
nl_sock_ns, c->pasta_ifi,
@@ -371,14 +372,14 @@ static void pasta_ns_conf_ip6(struct ctx *c)
0, IFF_NOARP);
if (c->ip6.no_copy_addrs) {
- const struct inany_addr_entry *e = first_v6(c);
-
- if (e)
- rc = nl_addr_set(nl_sock_ns,
- c->pasta_ifi,
- AF_INET6,
- &e->addr.a6,
- e->prefix_len);
+ const struct inany_addr_entry *e;
+
+ for_each_addr(e, c, AF_INET6) {
+ rc = nl_addr_set(nl_sock_ns, c->pasta_ifi,
+ AF_INET6, &e->addr.a6, e->prefix_len);
+ if (rc < 0)
+ break;
+ }
} else {
rc = nl_addr_dup(nl_sock, c->ifi6,
nl_sock_ns, c->pasta_ifi,
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 08/12] ip: Track observed guest IPv4 addresses in unified address array
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
` (6 preceding siblings ...)
2026-02-17 22:18 ` [PATCH v4 07/12] conf: Allow multiple -a/--address options per address family Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-18 14:14 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 09/12] ip: Track observed guest IPv6 " Jon Maloy
` (3 subsequent siblings)
11 siblings, 1 reply; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
We remove the addr_seen field in struct ip4_ctx and replace it by
setting a new CONF_ADDR_OBSERVED flag in the corresponding entry
in the unified address array.
The observed IPv4 address is always put at position 0 in the array,
allowing for very fast lookup. Only one IPv4 address can have the
OBSERVED flag at a time.
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
v4: - Removed migration protocol update, to be added in later commit
- Allow only one OBSERVED address at a time
- Some other changes based on feedback from David G
---
conf.c | 2 -
conf.h | 1 +
fwd.c | 114 ++++++++++++++++++++++++++++++++++++++++++++++++------
fwd.h | 3 ++
migrate.c | 15 ++++++-
passt.h | 2 -
tap.c | 15 ++++++-
7 files changed, 133 insertions(+), 19 deletions(-)
diff --git a/conf.c b/conf.c
index ca0a764..0172dcd 100644
--- a/conf.c
+++ b/conf.c
@@ -738,7 +738,6 @@ static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
e->addr = inany_from_v4(addr);
e->prefix_len = prefix_len + 96;
e->flags = CONF_ADDR_HOST;
- ip4->addr_seen = addr;
}
ip4->our_tap_addr = ip4->guest_gw;
@@ -755,7 +754,6 @@ static void conf_ip4_local(struct ctx *c)
struct inany_addr_entry *e = &c->addrs[c->addr_count++];
struct ip4_ctx *ip4 = &c->ip4;
- ip4->addr_seen = IP4_LL_GUEST_ADDR;
ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
ip4->no_copy_addrs = ip4->no_copy_routes = true;
e->addr = inany_from_v4(IP4_LL_GUEST_ADDR);
diff --git a/conf.h b/conf.h
index bfad36f..8b10ac6 100644
--- a/conf.h
+++ b/conf.h
@@ -12,6 +12,7 @@
#define CONF_ADDR_USER BIT(0) /* User set via -a */
#define CONF_ADDR_HOST BIT(1) /* From host interface */
#define CONF_ADDR_LINKLOCAL BIT(2) /* Link-local address */
+#define CONF_ADDR_OBSERVED BIT(3) /* Seen in guest traffic */
enum passt_modes conf_mode(int argc, char *argv[]);
void conf(struct ctx *c, int argc, char **argv);
diff --git a/fwd.c b/fwd.c
index fa5d667..ca704c2 100644
--- a/fwd.c
+++ b/fwd.c
@@ -24,6 +24,7 @@
#include "ip.h"
#include "fwd.h"
#include "passt.h"
+#include "conf.h"
#include "lineread.h"
#include "flow_table.h"
#include "netlink.h"
@@ -491,6 +492,85 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
((ini->oport == 53) || (ini->oport == 853));
}
+/**
+ * fwd_guest_addr() - Get guest address matching criteria
+ * @c: Execution context
+ * @af: Address family (AF_INET, AF_INET6, or 0 for any)
+ * @incl: Flags that must be present (any-match)
+ * @excl: Flags that must not be present
+ *
+ * Return: first address matching criteria, or NULL
+ */
+const union inany_addr *fwd_guest_addr(const struct ctx *c, sa_family_t af,
+ uint8_t incl, uint8_t excl)
+{
+ const struct inany_addr_entry *e;
+
+ for_each_addr(e, c, af) {
+ if (incl && !(e->flags & incl))
+ continue;
+ if (e->flags & excl)
+ continue;
+ return &e->addr;
+ }
+
+ return NULL;
+}
+
+/**
+ * fwd_set_observed_ip4() - Set observed IPv4 guest address
+ * @c: Execution context
+ * @addr: IPv4 address observed in guest traffic
+ *
+ * Mark @addr as the observed guest address. The observed address is always
+ * kept at position 0 for O(1) lookup. Only one address can have the OBSERVED
+ * flag at a time.
+ */
+void fwd_set_observed_ip4(struct ctx *c, const struct in_addr *addr)
+{
+ struct inany_addr_entry *e = &c->addrs[0];
+ int i;
+
+ if (!addr->s_addr)
+ return;
+
+ /* Fast path: check if already observed at position 0 */
+ if (c->addr_count > 0 && (e->flags & CONF_ADDR_OBSERVED) &&
+ inany_equals4(&e->addr, addr))
+ return;
+
+ /* Slow path: new observed address - insert at position 0 */
+ if (c->addr_count >= INANY_MAX_ADDRS) {
+ debug("Address table full, can't add observed IPv4");
+ return;
+ }
+
+ /* Make room and insert at position 0 */
+ memmove(&c->addrs[1], e, c->addr_count * sizeof(*e));
+ c->addr_count++;
+ inany_from_af(&e->addr, AF_INET, addr);
+ e->prefix_len = 0;
+ e->flags = CONF_ADDR_OBSERVED;
+
+ /* Handle old observed IPv4 address, if any */
+ for (i = 1; i < c->addr_count; i++) {
+ e = &c->addrs[i];
+
+ if (!inany_v4(&e->addr) || !(e->flags & CONF_ADDR_OBSERVED))
+ continue;
+
+ e->flags &= ~CONF_ADDR_OBSERVED;
+
+ /* Remove if no other flags, or if addr is duplicate */
+ if (!e->flags || inany_equals4(&e->addr, addr)) {
+ memmove(&c->addrs[i], &c->addrs[i + 1],
+ (c->addr_count - i - 1) * sizeof(*e));
+ c->addr_count--;
+ }
+ break;
+ }
+}
+
/**
* fwd_guest_accessible() - Is address guest-accessible
* @c: Execution context
@@ -515,17 +595,11 @@ static bool fwd_guest_accessible(const struct ctx *c,
if (inany_is_unspecified4(addr))
return false;
- /* Check against all configured guest addresses */
+ /* Check against all configured and observed guest addresses */
for_each_addr(e, c, 0)
if (inany_equals(addr, &e->addr))
return false;
- /* Also check addr_seen: it tracks the address the guest is actually
- * using, which may differ from configured addresses.
- */
- if (inany_equals4(addr, &c->ip4.addr_seen))
- return false;
-
/* For IPv6, addr_seen starts unspecified, because we don't know what LL
* address the guest will take until we see it. Only check against it
* if it has been set to a real address.
@@ -726,10 +800,23 @@ 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)
+ if (c->host_lo_to_ns_lo) {
tgt->eaddr = inany_loopback4;
- else
- tgt->eaddr = inany_from_v4(c->ip4.addr_seen);
+ } else {
+ const union inany_addr *guest_addr;
+
+ guest_addr = fwd_guest_addr(c, AF_INET,
+ CONF_ADDR_OBSERVED,
+ 0);
+ if (!guest_addr)
+ guest_addr = fwd_guest_addr(c, AF_INET,
+ CONF_ADDR_USER | CONF_ADDR_HOST,
+ 0);
+ if (!guest_addr)
+ return PIF_NONE;
+
+ tgt->eaddr = *guest_addr;
+ }
tgt->oaddr = inany_any4;
} else {
if (c->host_lo_to_ns_lo)
@@ -761,7 +848,12 @@ 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 union inany_addr *guest_addr;
+
+ guest_addr = fwd_guest_addr(c, AF_INET, CONF_ADDR_OBSERVED, 0);
+ if (!guest_addr)
+ return PIF_NONE;
+ tgt->eaddr = *guest_addr;
} else {
if (inany_is_linklocal6(&tgt->oaddr))
tgt->eaddr.a6 = c->ip6.addr_ll_seen;
diff --git a/fwd.h b/fwd.h
index 7792582..38f4e60 100644
--- a/fwd.h
+++ b/fwd.h
@@ -15,6 +15,9 @@ struct flowside;
void fwd_probe_ephemeral(void);
bool fwd_port_is_ephemeral(in_port_t port);
+const union inany_addr *fwd_guest_addr(const struct ctx *c, sa_family_t af,
+ uint8_t incl, uint8_t excl);
+void fwd_set_observed_ip4(struct ctx *c, const struct in_addr *addr);
enum fwd_ports_mode {
FWD_UNSET = 0,
diff --git a/migrate.c b/migrate.c
index 48d63a0..d223857 100644
--- a/migrate.c
+++ b/migrate.c
@@ -18,6 +18,8 @@
#include "util.h"
#include "ip.h"
#include "passt.h"
+#include "conf.h"
+#include "fwd.h"
#include "inany.h"
#include "flow.h"
#include "flow_table.h"
@@ -57,11 +59,15 @@ static int seen_addrs_source_v1(struct ctx *c,
struct migrate_seen_addrs_v1 addrs = {
.addr6 = c->ip6.addr_seen,
.addr6_ll = c->ip6.addr_ll_seen,
- .addr4 = c->ip4.addr_seen,
};
+ const union inany_addr *obs4;
(void)stage;
+ obs4 = fwd_guest_addr(c, AF_INET, CONF_ADDR_OBSERVED, 0);
+ if (obs4)
+ addrs.addr4 = *inany_v4(obs4);
+
memcpy(addrs.mac, c->guest_mac, sizeof(addrs.mac));
if (write_all_buf(fd, &addrs, sizeof(addrs)))
@@ -82,6 +88,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 +97,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;
+
+ /* Copy to avoid unaligned access from packed struct */
+ addr4 = addrs.addr4;
+ fwd_set_observed_ip4(c, &addr4);
+
memcpy(c->guest_mac, addrs.mac, sizeof(c->guest_mac));
return 0;
diff --git a/passt.h b/passt.h
index 15d6596..fa747c6 100644
--- a/passt.h
+++ b/passt.h
@@ -78,7 +78,6 @@ struct inany_addr_entry {
/**
* struct ip4_ctx - IPv4 execution context
- * @addr_seen: Latest IPv4 address seen as source from tap
* @guest_gw: IPv4 gateway as seen by the guest
* @map_host_loopback: Outbound connections to this address are NATted to the
* host's 127.0.0.1
@@ -94,7 +93,6 @@ struct inany_addr_entry {
* @no_copy_addrs: Don't copy all addresses when configuring namespace
*/
struct ip4_ctx {
- struct in_addr addr_seen;
struct in_addr guest_gw;
struct in_addr map_host_loopback;
struct in_addr map_guest_addr;
diff --git a/tap.c b/tap.c
index 4298dcd..8c1ed35 100644
--- a/tap.c
+++ b/tap.c
@@ -48,6 +48,7 @@
#include "iov.h"
#include "passt.h"
#include "conf.h"
+#include "fwd.h"
#include "arp.h"
#include "dhcp.h"
#include "ndp.h"
@@ -162,6 +163,16 @@ void tap_send_single(const struct ctx *c, const void *data, size_t l2len)
}
}
+/**
+ * tap_check_src_addr4() - Note an IPv4 address seen in guest traffic
+ * @c: Execution context
+ * @addr: IPv4 address seen as source from guest
+ */
+static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
+{
+ fwd_set_observed_ip4(c, addr);
+}
+
/**
* tap_ip6_daddr() - Normal IPv6 destination address for inbound packets
* @c: Execution context
@@ -772,8 +783,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;
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 09/12] ip: Track observed guest IPv6 addresses in unified address array
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
` (7 preceding siblings ...)
2026-02-17 22:18 ` [PATCH v4 08/12] ip: Track observed guest IPv4 addresses in unified address array Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 10/12] fwd: Unify fwd_set_observed_ip4() and fwd_set_observed_ip6() Jon Maloy
` (2 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
We remove the addr_seen and addr_ll_seen fields in struct ip6_ctx
and replace them by setting CONF_ADDR_OBSERVED and CONF_ADDR_LINKLOCAL
flags in the corresponding entry in the unified address array.
The observed IPv6 address is always kept at position 1 (position 0
is reserved for IPv4), allowing very fast lookup. Only one IPv6 address
can have the OBSERVED flag at a time.
A new fwd_set_observed_ip6() function handles observed IPv6 addresses,
mirroring the IPv4 fwd_set_observed_ip4() function. Both tap.c and
migrate.c now use this common function.
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
conf.c | 2 -
dhcpv6.c | 6 +--
dhcpv6.h | 2 +-
fwd.c | 108 ++++++++++++++++++++++++++++++++++++++++++++++--------
fwd.h | 1 +
migrate.c | 33 +++++++++++++----
passt.h | 4 --
pasta.c | 19 ++++++++--
tap.c | 62 ++++++++++++++++++++-----------
9 files changed, 175 insertions(+), 62 deletions(-)
diff --git a/conf.c b/conf.c
index 0172dcd..450a15e 100644
--- a/conf.c
+++ b/conf.c
@@ -807,8 +807,6 @@ static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
e->flags = CONF_ADDR_HOST;
}
- ip6->addr_seen = e->addr.a6;
-
if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
ip6->our_tap_ll = ip6->guest_gw;
diff --git a/dhcpv6.c b/dhcpv6.c
index 801b01b..33a1161 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -546,7 +546,7 @@ static size_t dhcpv6_client_fqdn_fill(const struct iov_tail *data,
* Return: 0 if it's not a DHCPv6 message, 1 if handled, -1 on failure
*/
int dhcpv6(struct ctx *c, struct iov_tail *data,
- const struct in6_addr *saddr, const struct in6_addr *daddr)
+ const struct in6_addr *daddr)
{
const struct opt_server_id *server_id = NULL;
struct inany_addr_entry *e = first_v6(c);
@@ -588,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);
@@ -680,8 +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);
- if (e)
- c->ip6.addr_seen = e->addr.a6;
return 1;
}
diff --git a/dhcpv6.h b/dhcpv6.h
index c706dfd..8cbc769 100644
--- a/dhcpv6.h
+++ b/dhcpv6.h
@@ -7,7 +7,7 @@
#define DHCPV6_H
int dhcpv6(struct ctx *c, struct iov_tail *data,
- struct in6_addr *saddr, struct in6_addr *daddr);
+ struct in6_addr *daddr);
void dhcpv6_init(const struct ctx *c);
#endif /* DHCPV6_H */
diff --git a/fwd.c b/fwd.c
index ca704c2..8598fff 100644
--- a/fwd.c
+++ b/fwd.c
@@ -571,6 +571,74 @@ void fwd_set_observed_ip4(struct ctx *c, const struct in_addr *addr)
}
}
+/**
+ * fwd_set_observed_ip6() - Set observed IPv6 guest address
+ * @c: Execution context
+ * @addr: IPv6 address observed in guest traffic
+ *
+ * Mark @addr as the observed guest address. The observed address is always
+ * kept at position 1 for O(1) lookup. Only one IPv6 address can have the
+ * OBSERVED flag at a time. Link-local addresses also get LINKLOCAL flag.
+ */
+void fwd_set_observed_ip6(struct ctx *c, const struct in6_addr *addr)
+{
+ struct inany_addr_entry *e;
+ uint8_t flags = CONF_ADDR_OBSERVED;
+ int i;
+
+ if (IN6_IS_ADDR_UNSPECIFIED(addr))
+ return;
+
+ if (IN6_IS_ADDR_LINKLOCAL(addr))
+ flags |= CONF_ADDR_LINKLOCAL;
+
+ /* Fast path: check if already observed at position 1 */
+ if (c->addr_count > 1) {
+ e = &c->addrs[1];
+ if ((e->flags & CONF_ADDR_OBSERVED) && !inany_v4(&e->addr) &&
+ IN6_ARE_ADDR_EQUAL(&e->addr.a6, addr))
+ return;
+ }
+
+ /* Slow path: new observed address - insert at position 1 */
+ if (c->addr_count >= INANY_MAX_ADDRS) {
+ debug("Address table full, can't add observed IPv6");
+ return;
+ }
+
+ /* Make room at position 1 */
+ if (c->addr_count > 1) {
+ e = &c->addrs[1];
+ memmove(&c->addrs[2], e, (c->addr_count - 1) * sizeof(*e));
+ }
+ c->addr_count++;
+
+ /* Insert new observed address at position 1 */
+ e = &c->addrs[1];
+ e->addr.a6 = *addr;
+ e->prefix_len = 64;
+ e->flags = flags;
+
+ /* Clean up: find and handle old observed IPv6 address */
+ for (i = 2; i < c->addr_count; i++) {
+ e = &c->addrs[i];
+
+ if (inany_v4(&e->addr) || !(e->flags & CONF_ADDR_OBSERVED))
+ continue;
+
+ /* Found old observed - clear flag */
+ e->flags &= ~CONF_ADDR_OBSERVED;
+
+ /* Remove if no other flags, or if addr is duplicate */
+ if (!e->flags || IN6_ARE_ADDR_EQUAL(&e->addr.a6, addr)) {
+ memmove(&c->addrs[i], &c->addrs[i + 1],
+ (c->addr_count - i - 1) * sizeof(*e));
+ c->addr_count--;
+ }
+ break;
+ }
+}
+
/**
* fwd_guest_accessible() - Is address guest-accessible
* @c: Execution context
@@ -600,15 +668,6 @@ static bool fwd_guest_accessible(const struct ctx *c,
if (inany_equals(addr, &e->addr))
return false;
- /* For IPv6, addr_seen starts unspecified, because we don't know what LL
- * address the guest will take until we see it. Only check against it
- * if it has been set to a real address.
- */
- if (!inany_v4(addr) &&
- !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen) &&
- inany_equals6(addr, &c->ip6.addr_seen))
- return false;
-
return true;
}
@@ -819,10 +878,21 @@ 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)
+ if (c->host_lo_to_ns_lo) {
tgt->eaddr = inany_loopback6;
- else
- tgt->eaddr.a6 = c->ip6.addr_seen;
+ } else {
+ const union inany_addr *guest_addr;
+
+ guest_addr = fwd_guest_addr(c, AF_INET6,
+ CONF_ADDR_OBSERVED,
+ CONF_ADDR_LINKLOCAL);
+ if (!guest_addr)
+ guest_addr = fwd_guest_addr(c, AF_INET6,
+ 0, CONF_ADDR_LINKLOCAL);
+ if (!guest_addr)
+ return PIF_NONE;
+ tgt->eaddr = *guest_addr;
+ }
tgt->oaddr = inany_any6;
}
@@ -855,10 +925,16 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto,
return PIF_NONE;
tgt->eaddr = *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 union inany_addr *guest_addr;
+ uint8_t excl = linklocal ? 0 : CONF_ADDR_LINKLOCAL;
+
+ guest_addr = fwd_guest_addr(c, AF_INET6, CONF_ADDR_OBSERVED, excl);
+ if (!guest_addr)
+ guest_addr = fwd_guest_addr(c, AF_INET6, 0, excl);
+ if (!guest_addr)
+ return PIF_NONE;
+ tgt->eaddr = *guest_addr;
}
return PIF_TAP;
diff --git a/fwd.h b/fwd.h
index 38f4e60..4de3890 100644
--- a/fwd.h
+++ b/fwd.h
@@ -18,6 +18,7 @@ bool fwd_port_is_ephemeral(in_port_t port);
const union inany_addr *fwd_guest_addr(const struct ctx *c, sa_family_t af,
uint8_t incl, uint8_t excl);
void fwd_set_observed_ip4(struct ctx *c, const struct in_addr *addr);
+void fwd_set_observed_ip6(struct ctx *c, const struct in6_addr *addr);
enum fwd_ports_mode {
FWD_UNSET = 0,
diff --git a/migrate.c b/migrate.c
index d223857..6577334 100644
--- a/migrate.c
+++ b/migrate.c
@@ -56,18 +56,28 @@ struct migrate_seen_addrs_v1 {
static int seen_addrs_source_v1(struct ctx *c,
const struct migrate_stage *stage, int fd)
{
- struct migrate_seen_addrs_v1 addrs = {
- .addr6 = c->ip6.addr_seen,
- .addr6_ll = c->ip6.addr_ll_seen,
- };
+ struct migrate_seen_addrs_v1 addrs = { 0 };
+ const struct inany_addr_entry *e;
const union inany_addr *obs4;
(void)stage;
+ /* IPv4 observed address at position 0 */
obs4 = fwd_guest_addr(c, AF_INET, CONF_ADDR_OBSERVED, 0);
if (obs4)
addrs.addr4 = *inany_v4(obs4);
+ /* IPv6 observed address at position 1 */
+ if (c->addr_count > 1) {
+ e = &c->addrs[1];
+ if (!inany_v4(&e->addr) && (e->flags & CONF_ADDR_OBSERVED)) {
+ if (e->flags & CONF_ADDR_LINKLOCAL)
+ addrs.addr6_ll = e->addr.a6;
+ else
+ addrs.addr6 = e->addr.a6;
+ }
+ }
+
memcpy(addrs.mac, c->guest_mac, sizeof(addrs.mac));
if (write_all_buf(fd, &addrs, sizeof(addrs)))
@@ -88,6 +98,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;
@@ -95,13 +106,19 @@ static int seen_addrs_target_v1(struct ctx *c,
if (read_all_buf(fd, &addrs, sizeof(addrs)))
return errno;
- c->ip6.addr_seen = addrs.addr6;
- c->ip6.addr_ll_seen = addrs.addr6_ll;
-
- /* Copy to avoid unaligned access from packed struct */
+ /* Copy from packed struct to avoid alignment issues */
addr4 = addrs.addr4;
+ addr6 = addrs.addr6;
+ addr6_ll = addrs.addr6_ll;
+
fwd_set_observed_ip4(c, &addr4);
+ /* Prefer global over link-local if both present (only one slot) */
+ if (!IN6_IS_ADDR_UNSPECIFIED(&addr6))
+ fwd_set_observed_ip6(c, &addr6);
+ else
+ fwd_set_observed_ip6(c, &addr6_ll);
+
memcpy(c->guest_mac, addrs.mac, sizeof(c->guest_mac));
return 0;
diff --git a/passt.h b/passt.h
index fa747c6..a9f63b5 100644
--- a/passt.h
+++ b/passt.h
@@ -112,8 +112,6 @@ struct ip4_ctx {
/**
* struct ip6_ctx - IPv6 execution context
- * @addr_seen: Latest IPv6 global/site address seen as source from tap
- * @addr_ll_seen: Latest IPv6 link-local address seen as source from tap
* @guest_gw: IPv6 gateway as seen by the guest
* @map_host_loopback: Outbound connections to this address are NATted to the
* host's [::1]
@@ -129,8 +127,6 @@ struct ip4_ctx {
* @no_copy_addrs: Don't copy all addresses when configuring namespace
*/
struct ip6_ctx {
- struct in6_addr addr_seen;
- struct in6_addr addr_ll_seen;
struct in6_addr guest_gw;
struct in6_addr map_host_loopback;
struct in6_addr map_guest_addr;
diff --git a/pasta.c b/pasta.c
index dd23a14..f16d508 100644
--- a/pasta.c
+++ b/pasta.c
@@ -46,6 +46,8 @@
#include "util.h"
#include "passt.h"
+#include "conf.h"
+#include "fwd.h"
#include "isolation.h"
#include "netlink.h"
#include "log.h"
@@ -352,14 +354,15 @@ static void pasta_ns_conf_ip4(struct ctx *c)
*/
static void pasta_ns_conf_ip6(struct ctx *c)
{
+ struct in6_addr addr_ll;
int rc = 0;
- rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi,
- &c->ip6.addr_ll_seen);
- if (rc < 0) {
+ rc = nl_addr_get_ll(nl_sock_ns, c->pasta_ifi, &addr_ll);
+ if (rc < 0)
warn("Can't get LL address from namespace: %s",
strerror_(-rc));
- }
+ else
+ fwd_set_observed_ip6(c, &addr_ll);
rc = nl_addr_set_ll_nodad(nl_sock_ns, c->pasta_ifi);
if (rc < 0) {
@@ -375,6 +378,13 @@ static void pasta_ns_conf_ip6(struct ctx *c)
const struct inany_addr_entry *e;
for_each_addr(e, c, AF_INET6) {
+ if (IN6_IS_ADDR_UNSPECIFIED(&e->addr.a6))
+ continue;
+
+ /* Skip link-local - kernel auto-configures */
+ if (e->flags & CONF_ADDR_LINKLOCAL)
+ continue;
+
rc = nl_addr_set(nl_sock_ns, c->pasta_ifi,
AF_INET6, &e->addr.a6, e->prefix_len);
if (rc < 0)
@@ -445,6 +455,7 @@ void pasta_ns_conf(struct ctx *c)
if (c->ifi6)
pasta_ns_conf_ip6(c);
+
}
proto_update_l2_buf(c->guest_mac);
diff --git a/tap.c b/tap.c
index 8c1ed35..be698eb 100644
--- a/tap.c
+++ b/tap.c
@@ -173,6 +173,16 @@ static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
fwd_set_observed_ip4(c, addr);
}
+/**
+ * tap_check_src_addr6() - Note an IPv6 address seen in guest traffic
+ * @c: Execution context
+ * @addr: IPv6 address seen as source from guest
+ */
+static void tap_check_src_addr6(struct ctx *c, const struct in6_addr *addr)
+{
+ fwd_set_observed_ip6(c, addr);
+}
+
/**
* tap_ip6_daddr() - Normal IPv6 destination address for inbound packets
* @c: Execution context
@@ -183,9 +193,33 @@ 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);
+ const struct inany_addr_entry *e;
+
+ /* Find first observed address of matching scope */
+ for_each_addr(e, c, AF_INET6) {
+ bool is_ll = !!(e->flags & CONF_ADDR_LINKLOCAL);
+
+ if (is_ll != want_ll)
+ continue;
+ if (e->flags & CONF_ADDR_OBSERVED)
+ return &e->addr.a6;
+ }
+
+ /* Fallback to first address of matching scope */
+ for_each_addr(e, c, AF_INET6) {
+ bool is_ll = !!(e->flags & CONF_ADDR_LINKLOCAL);
+
+ if (is_ll == want_ll)
+ return &e->addr.a6;
+ }
+
+ /* Last resort: return first IPv6 address */
+ e = first_v6(c);
+ if (e)
+ return &e->addr.a6;
+
+ return &in6addr_any;
}
/**
@@ -956,24 +990,8 @@ resume:
continue;
}
- if (IN6_IS_ADDR_LINKLOCAL(saddr)) {
- c->ip6.addr_ll_seen = *saddr;
-
- if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr_seen)) {
- c->ip6.addr_seen = *saddr;
- }
-
- if (!first_v6(c) && c->addr_count < INANY_MAX_ADDRS) {
- struct inany_addr_entry *e;
-
- e = &c->addrs[c->addr_count++];
- e->addr.a6 = *saddr;
- e->prefix_len = 64;
- e->flags = CONF_ADDR_LINKLOCAL;
- }
- } else if (!IN6_IS_ADDR_UNSPECIFIED(saddr)){
- c->ip6.addr_seen = *saddr;
- }
+ if (!IN6_IS_ADDR_UNSPECIFIED(saddr))
+ tap_check_src_addr6(c, saddr);
if (proto == IPPROTO_ICMPV6) {
struct iov_tail ndp_data;
@@ -1004,7 +1022,7 @@ resume:
if (proto == IPPROTO_UDP) {
struct iov_tail uh_data = data;
- if (dhcpv6(c, &uh_data, saddr, daddr))
+ if (dhcpv6(c, &uh_data, daddr))
continue;
}
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 10/12] fwd: Unify fwd_set_observed_ip4() and fwd_set_observed_ip6()
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
` (8 preceding siblings ...)
2026-02-17 22:18 ` [PATCH v4 09/12] ip: Track observed guest IPv6 " Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 11/12] migrate: Rename v1 address functions to v2 for clarity Jon Maloy
2026-02-17 22:18 ` [PATCH v4 12/12] migrate: Update protocol to v3 for multi-address support Jon Maloy
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
Create a common fwd_set_observed() function that handles both IPv4 and
IPv6 observed addresses. The function determines the address family from
the input and uses the appropriate reserved position (0 for IPv4, 1 for
IPv6) for O(1) lookup.
Call sites are updated to use fwd_set_observed() directly with
union inany_addr.
This reduces code duplication and ensures consistent behavior between
IPv4 and IPv6 address tracking.
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
fwd.c | 129 ++++++++++++++++++------------------------------------
fwd.h | 3 +-
inany.h | 3 ++
migrate.c | 6 +--
pasta.c | 2 +-
tap.c | 4 +-
6 files changed, 53 insertions(+), 94 deletions(-)
diff --git a/fwd.c b/fwd.c
index 8598fff..d5f4932 100644
--- a/fwd.c
+++ b/fwd.c
@@ -518,119 +518,76 @@ const union inany_addr *fwd_guest_addr(const struct ctx *c, sa_family_t af,
}
/**
- * fwd_set_observed_ip4() - Set observed IPv4 guest address
+ * fwd_set_observed() - Set observed guest address (IPv4 or IPv6)
* @c: Execution context
- * @addr: IPv4 address observed in guest traffic
+ * @addr: Address observed in guest traffic
*
- * Mark @addr as the observed guest address. The observed address is always
- * kept at position 0 for O(1) lookup. Only one address can have the OBSERVED
- * flag at a time.
+ * Mark @addr as the observed guest address. Observed addresses are kept at
+ * reserved positions for O(1) lookup: position 0 for IPv4, position 1 for
+ * IPv6. Only one address per family can have the OBSERVED flag at a time.
+ * Link-local IPv6 addresses also get the LINKLOCAL flag.
*/
-void fwd_set_observed_ip4(struct ctx *c, const struct in_addr *addr)
+void fwd_set_observed(struct ctx *c, const union inany_addr *addr)
{
- struct inany_addr_entry *e = &c->addrs[0];
- int i;
-
- if (!addr->s_addr)
- return;
-
- /* Fast path: check if already observed at position 0 */
- if (c->addr_count > 0 && (e->flags & CONF_ADDR_OBSERVED) &&
- inany_equals4(&e->addr, addr))
- return;
-
- /* Slow path: new observed address - insert at position 0 */
- if (c->addr_count >= INANY_MAX_ADDRS) {
- debug("Address table full, can't add observed IPv4");
- return;
- }
-
- /* Make room and insert at position 0 */
- memmove(&c->addrs[1], e, c->addr_count * sizeof(*e));
- c->addr_count++;
- inany_from_af(&e->addr, AF_INET, addr);
- e->prefix_len = 0;
- e->flags = CONF_ADDR_OBSERVED;
-
- /* Handle old observed IPv4 address, if any */
- for (i = 1; i < c->addr_count; i++) {
- e = &c->addrs[i];
-
- if (!inany_v4(&e->addr) || !(e->flags & CONF_ADDR_OBSERVED))
- continue;
-
- e->flags &= ~CONF_ADDR_OBSERVED;
-
- /* Remove if no other flags, or if addr is duplicate */
- if (!e->flags || inany_equals4(&e->addr, addr)) {
- memmove(&c->addrs[i], &c->addrs[i + 1],
- (c->addr_count - i - 1) * sizeof(*e));
- c->addr_count--;
- }
- break;
- }
-}
-
-/**
- * fwd_set_observed_ip6() - Set observed IPv6 guest address
- * @c: Execution context
- * @addr: IPv6 address observed in guest traffic
- *
- * Mark @addr as the observed guest address. The observed address is always
- * kept at position 1 for O(1) lookup. Only one IPv6 address can have the
- * OBSERVED flag at a time. Link-local addresses also get LINKLOCAL flag.
- */
-void fwd_set_observed_ip6(struct ctx *c, const struct in6_addr *addr)
-{
- struct inany_addr_entry *e;
uint8_t flags = CONF_ADDR_OBSERVED;
+ bool is_v4 = !!inany_v4(addr);
+ struct inany_addr_entry *e;
+ int pos = is_v4 ? 0 : 1;
int i;
- if (IN6_IS_ADDR_UNSPECIFIED(addr))
- return;
-
- if (IN6_IS_ADDR_LINKLOCAL(addr))
- flags |= CONF_ADDR_LINKLOCAL;
+ /* Check for unspecified address */
+ if (is_v4) {
+ if (inany_is_unspecified4(addr))
+ return;
+ } else {
+ if (IN6_IS_ADDR_UNSPECIFIED(&addr->a6))
+ return;
+ if (IN6_IS_ADDR_LINKLOCAL(&addr->a6))
+ flags |= CONF_ADDR_LINKLOCAL;
+ }
- /* Fast path: check if already observed at position 1 */
- if (c->addr_count > 1) {
- e = &c->addrs[1];
- if ((e->flags & CONF_ADDR_OBSERVED) && !inany_v4(&e->addr) &&
- IN6_ARE_ADDR_EQUAL(&e->addr.a6, addr))
+ /* Fast path: check if already observed at reserved position */
+ if (c->addr_count > pos) {
+ e = &c->addrs[pos];
+ if ((e->flags & CONF_ADDR_OBSERVED) &&
+ (is_v4 == !!inany_v4(&e->addr)) &&
+ inany_equals(&e->addr, addr))
return;
}
- /* Slow path: new observed address - insert at position 1 */
+ /* Slow path: new observed address */
if (c->addr_count >= INANY_MAX_ADDRS) {
- debug("Address table full, can't add observed IPv6");
+ debug("Address table full, can't add observed %s",
+ is_v4 ? "IPv4" : "IPv6");
return;
}
- /* Make room at position 1 */
- if (c->addr_count > 1) {
- e = &c->addrs[1];
- memmove(&c->addrs[2], e, (c->addr_count - 1) * sizeof(*e));
+ /* Make room at reserved position */
+ if (c->addr_count > pos) {
+ e = &c->addrs[pos];
+ memmove(&c->addrs[pos + 1], e,
+ (c->addr_count - pos) * sizeof(*e));
}
c->addr_count++;
- /* Insert new observed address at position 1 */
- e = &c->addrs[1];
- e->addr.a6 = *addr;
- e->prefix_len = 64;
+ /* Insert new observed address */
+ e = &c->addrs[pos];
+ e->addr = *addr;
+ e->prefix_len = is_v4 ? 0 : 64;
e->flags = flags;
- /* Clean up: find and handle old observed IPv6 address */
- for (i = 2; i < c->addr_count; i++) {
+ /* Clean up: find and handle old observed address of same family */
+ for (i = pos + 1; i < c->addr_count; i++) {
e = &c->addrs[i];
- if (inany_v4(&e->addr) || !(e->flags & CONF_ADDR_OBSERVED))
+ if ((is_v4 != !!inany_v4(&e->addr)) ||
+ !(e->flags & CONF_ADDR_OBSERVED))
continue;
- /* Found old observed - clear flag */
e->flags &= ~CONF_ADDR_OBSERVED;
/* Remove if no other flags, or if addr is duplicate */
- if (!e->flags || IN6_ARE_ADDR_EQUAL(&e->addr.a6, addr)) {
+ if (!e->flags || inany_equals(&e->addr, addr)) {
memmove(&c->addrs[i], &c->addrs[i + 1],
(c->addr_count - i - 1) * sizeof(*e));
c->addr_count--;
diff --git a/fwd.h b/fwd.h
index 4de3890..7bdf462 100644
--- a/fwd.h
+++ b/fwd.h
@@ -17,8 +17,7 @@ void fwd_probe_ephemeral(void);
bool fwd_port_is_ephemeral(in_port_t port);
const union inany_addr *fwd_guest_addr(const struct ctx *c, sa_family_t af,
uint8_t incl, uint8_t excl);
-void fwd_set_observed_ip4(struct ctx *c, const struct in_addr *addr);
-void fwd_set_observed_ip6(struct ctx *c, const struct in6_addr *addr);
+void fwd_set_observed(struct ctx *c, const union inany_addr *addr);
enum fwd_ports_mode {
FWD_UNSET = 0,
diff --git a/inany.h b/inany.h
index c7d6027..8b7caa5 100644
--- a/inany.h
+++ b/inany.h
@@ -54,6 +54,9 @@ extern const union inany_addr inany_any4;
#define inany_from_v4(a4) \
((union inany_addr)INANY_INIT4((a4)))
+#define inany_from_v6(addr) \
+ ((union inany_addr){ .a6 = (addr) })
+
/** union sockaddr_inany - Either a sockaddr_in or a sockaddr_in6
* @sa_family: Address family, AF_INET or AF_INET6
* @sa: Plain struct sockaddr (useful to avoid casts)
diff --git a/migrate.c b/migrate.c
index 6577334..2dc5c8a 100644
--- a/migrate.c
+++ b/migrate.c
@@ -111,13 +111,13 @@ static int seen_addrs_target_v1(struct ctx *c,
addr6 = addrs.addr6;
addr6_ll = addrs.addr6_ll;
- fwd_set_observed_ip4(c, &addr4);
+ fwd_set_observed(c, &inany_from_v4(addr4));
/* Prefer global over link-local if both present (only one slot) */
if (!IN6_IS_ADDR_UNSPECIFIED(&addr6))
- fwd_set_observed_ip6(c, &addr6);
+ fwd_set_observed(c, &inany_from_v6(addr6));
else
- fwd_set_observed_ip6(c, &addr6_ll);
+ fwd_set_observed(c, &inany_from_v6(addr6_ll));
memcpy(c->guest_mac, addrs.mac, sizeof(c->guest_mac));
diff --git a/pasta.c b/pasta.c
index f16d508..a134af8 100644
--- a/pasta.c
+++ b/pasta.c
@@ -362,7 +362,7 @@ static void pasta_ns_conf_ip6(struct ctx *c)
warn("Can't get LL address from namespace: %s",
strerror_(-rc));
else
- fwd_set_observed_ip6(c, &addr_ll);
+ fwd_set_observed(c, &inany_from_v6(addr_ll));
rc = nl_addr_set_ll_nodad(nl_sock_ns, c->pasta_ifi);
if (rc < 0) {
diff --git a/tap.c b/tap.c
index be698eb..643c139 100644
--- a/tap.c
+++ b/tap.c
@@ -170,7 +170,7 @@ void tap_send_single(const struct ctx *c, const void *data, size_t l2len)
*/
static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
{
- fwd_set_observed_ip4(c, addr);
+ fwd_set_observed(c, &inany_from_v4(*addr));
}
/**
@@ -180,7 +180,7 @@ static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
*/
static void tap_check_src_addr6(struct ctx *c, const struct in6_addr *addr)
{
- fwd_set_observed_ip6(c, addr);
+ fwd_set_observed(c, &inany_from_v6(*addr));
}
/**
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 11/12] migrate: Rename v1 address functions to v2 for clarity
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
` (9 preceding siblings ...)
2026-02-17 22:18 ` [PATCH v4 10/12] fwd: Unify fwd_set_observed_ip4() and fwd_set_observed_ip6() Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 12/12] migrate: Update protocol to v3 for multi-address support Jon Maloy
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
Some migration address structures and functions have a _v1 suffix.
This is confusing, since they are currently handling version 2 of
the migration protocol. We are now going to introduce a new version
3 of the protocol, so we choose to give these functions the correct
suffix _v2 instead. This is in correspondence with current reality,
and will help make a clearer distinction between the old and the new
versions of those functions.
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
Reviewed-by: David Gibson <david@gibson.dropbear.id.au>
---
migrate.c | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/migrate.c b/migrate.c
index 2dc5c8a..f937793 100644
--- a/migrate.c
+++ b/migrate.c
@@ -31,13 +31,13 @@
#define MIGRATE_MAGIC 0xB1BB1D1B0BB1D1B0
/**
- * struct migrate_seen_addrs_v1 - Migratable guest addresses for v1 state stream
+ * struct migrate_seen_addrs_v2 - Migratable guest addresses for v2 protocol
* @addr6: Observed guest IPv6 address
* @addr6_ll: Observed guest IPv6 link-local address
* @addr4: Observed guest IPv4 address
* @mac: Observed guest MAC address
*/
-struct migrate_seen_addrs_v1 {
+struct migrate_seen_addrs_v2 {
struct in6_addr addr6;
struct in6_addr addr6_ll;
struct in_addr addr4;
@@ -45,7 +45,7 @@ struct migrate_seen_addrs_v1 {
} __attribute__((packed));
/**
- * seen_addrs_source_v1() - Copy and send guest observed addresses from source
+ * seen_addrs_source_v2() - Copy and send guest observed addresses from source
* @c: Execution context
* @stage: Migration stage, unused
* @fd: File descriptor for state transfer
@@ -53,10 +53,10 @@ struct migrate_seen_addrs_v1 {
* Return: 0 on success, positive error code on failure
*/
/* cppcheck-suppress [constParameterCallback, unmatchedSuppression] */
-static int seen_addrs_source_v1(struct ctx *c,
+static int seen_addrs_source_v2(struct ctx *c,
const struct migrate_stage *stage, int fd)
{
- struct migrate_seen_addrs_v1 addrs = { 0 };
+ struct migrate_seen_addrs_v2 addrs = { 0 };
const struct inany_addr_entry *e;
const union inany_addr *obs4;
@@ -87,17 +87,17 @@ static int seen_addrs_source_v1(struct ctx *c,
}
/**
- * seen_addrs_target_v1() - Receive and use guest observed addresses on target
+ * seen_addrs_target_v2() - Receive and use guest observed addresses on target
* @c: Execution context
* @stage: Migration stage, unused
* @fd: File descriptor for state transfer
*
* Return: 0 on success, positive error code on failure
*/
-static int seen_addrs_target_v1(struct ctx *c,
+static int seen_addrs_target_v2(struct ctx *c,
const struct migrate_stage *stage, int fd)
{
- struct migrate_seen_addrs_v1 addrs;
+ struct migrate_seen_addrs_v2 addrs;
struct in6_addr addr6, addr6_ll;
struct in_addr addr4;
@@ -128,8 +128,8 @@ static int seen_addrs_target_v1(struct ctx *c,
static const struct migrate_stage stages_v2[] = {
{
.name = "observed addresses",
- .source = seen_addrs_source_v1,
- .target = seen_addrs_target_v1,
+ .source = seen_addrs_source_v2,
+ .target = seen_addrs_target_v2,
},
{
.name = "prepare flows",
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v4 12/12] migrate: Update protocol to v3 for multi-address support
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
` (10 preceding siblings ...)
2026-02-17 22:18 ` [PATCH v4 11/12] migrate: Rename v1 address functions to v2 for clarity Jon Maloy
@ 2026-02-17 22:18 ` Jon Maloy
11 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-17 22:18 UTC (permalink / raw)
To: sbrivio, dgibson, david, jmaloy, passt-dev
We update the migration protocol to version 3 to support distributing
multiple addresses from the unified address array. The new protocol
migrates all address entries in the array, along with their prefix
lengths and flags, and leaves it to the receiver to filter which
ones he wants to apply.
Signed-off-by: Jon Maloy <jmaloy@redhat.com>
---
v4: - Broke out as separate commit
- Made number of transferrable addresses variable
---
migrate.c | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 123 insertions(+)
diff --git a/migrate.c b/migrate.c
index f937793..13a5d6f 100644
--- a/migrate.c
+++ b/migrate.c
@@ -124,6 +124,108 @@ static int seen_addrs_target_v2(struct ctx *c,
return 0;
}
+/**
+ * addrs_source_v3() - Send all addresses with flags from source
+ * @c: Execution context
+ * @stage: Migration stage, unused
+ * @fd: File descriptor for state transfer
+ *
+ * Send all address entries with their flags. The receiver can filter
+ * based on flags as needed. This provides forward compatibility if
+ * future versions need different address types.
+ *
+ * Return: 0 on success, positive error code on failure
+ */
+/* cppcheck-suppress [constParameterCallback, unmatchedSuppression] */
+static int addrs_source_v3(struct ctx *c,
+ const struct migrate_stage *stage, int fd)
+{
+ uint8_t addr_count = c->addr_count;
+
+ (void)stage;
+
+ /* Send count, then all addresses with flags, then MAC */
+ if (write_all_buf(fd, &addr_count, sizeof(addr_count)))
+ return errno;
+
+ if (addr_count && write_all_buf(fd, c->addrs,
+ addr_count * sizeof(c->addrs[0])))
+ return errno;
+
+ if (write_all_buf(fd, c->guest_mac, ETH_ALEN))
+ return errno;
+
+ return 0;
+}
+
+/**
+ * migrate_merge_addr() - Merge migrated address into local array
+ * @c: Execution context
+ * @addr: Address entry from migration source
+ *
+ * If the address already exists locally, merge the flags (preserving
+ * local flags and adding migrated ones). Otherwise add as new entry.
+ */
+static void migrate_merge_addr(struct ctx *c,
+ const struct inany_addr_entry *addr)
+{
+ struct inany_addr_entry *e;
+
+ if (inany_is_unspecified(&addr->addr))
+ return;
+
+ /* Check if address already exists, merge flags */
+ for_each_addr(e, c, 0) {
+ if (inany_equals(&e->addr, &addr->addr)) {
+ e->flags |= addr->flags;
+ return;
+ }
+ }
+
+ /* Add new entry if there's room */
+ if (c->addr_count < INANY_MAX_ADDRS)
+ c->addrs[c->addr_count++] = *addr;
+}
+
+/**
+ * addrs_target_v3() - Receive addresses on target
+ * @c: Execution context
+ * @stage: Migration stage, unused
+ * @fd: File descriptor for state transfer
+ *
+ * Receive all address entries and merge only observed addresses into local
+ * array. Source sends all addresses for forward compatibility, but target
+ * only applies those marked as observed by guest traffic.
+ *
+ * Return: 0 on success, positive error code on failure
+ */
+static int addrs_target_v3(struct ctx *c,
+ const struct migrate_stage *stage, int fd)
+{
+ struct inany_addr_entry addrs[INANY_MAX_ADDRS];
+ uint8_t addr_count, i;
+
+ (void)stage;
+
+ if (read_all_buf(fd, &addr_count, sizeof(addr_count)))
+ return errno;
+
+ if (addr_count > INANY_MAX_ADDRS)
+ addr_count = INANY_MAX_ADDRS;
+
+ if (addr_count && read_all_buf(fd, addrs, addr_count * sizeof(addrs[0])))
+ return errno;
+
+ if (read_all_buf(fd, c->guest_mac, ETH_ALEN))
+ return errno;
+
+ for (i = 0; i < addr_count; i++)
+ if (addrs[i].flags & CONF_ADDR_OBSERVED)
+ migrate_merge_addr(c, &addrs[i]);
+
+ return 0;
+}
+
/* Stages for version 2 */
static const struct migrate_stage stages_v2[] = {
{
@@ -144,8 +246,29 @@ static const struct migrate_stage stages_v2[] = {
{ 0 },
};
+/* Stages for version 3 (multiple observed IPv4 addresses) */
+static const struct migrate_stage stages_v3[] = {
+ {
+ .name = "addresses",
+ .source = addrs_source_v3,
+ .target = addrs_target_v3,
+ },
+ {
+ .name = "prepare flows",
+ .source = flow_migrate_source_pre,
+ .target = NULL,
+ },
+ {
+ .name = "transfer flows",
+ .source = flow_migrate_source,
+ .target = flow_migrate_target,
+ },
+ { 0 },
+};
+
/* Supported encoding versions, from latest (most preferred) to oldest */
static const struct migrate_version versions[] = {
+ { 3, stages_v3, },
{ 2, stages_v2, },
/* v1 was released, but not widely used. It had bad endianness for the
* MSS and omitted timestamps, which meant it usually wouldn't work.
--
2.52.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v4 08/12] ip: Track observed guest IPv4 addresses in unified address array
2026-02-17 22:18 ` [PATCH v4 08/12] ip: Track observed guest IPv4 addresses in unified address array Jon Maloy
@ 2026-02-18 14:14 ` Jon Maloy
0 siblings, 0 replies; 14+ messages in thread
From: Jon Maloy @ 2026-02-18 14:14 UTC (permalink / raw)
To: sbrivio, dgibson, david, passt-dev
On 2026-02-17 17:18, Jon Maloy wrote:
> We remove the addr_seen field in struct ip4_ctx and replace it by
> setting a new CONF_ADDR_OBSERVED flag in the corresponding entry
> in the unified address array.
>
> The observed IPv4 address is always put at position 0 in the array,
> allowing for very fast lookup. Only one IPv4 address can have the
> OBSERVED flag at a time.
>
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>
>
> ---
> v4: - Removed migration protocol update, to be added in later commit
> - Allow only one OBSERVED address at a time
> - Some other changes based on feedback from David G
> ---
> conf.c | 2 -
> conf.h | 1 +
> fwd.c | 114 ++++++++++++++++++++++++++++++++++++++++++++++++------
> fwd.h | 3 ++
> migrate.c | 15 ++++++-
> passt.h | 2 -
> tap.c | 15 ++++++-
> 7 files changed, 133 insertions(+), 19 deletions(-)
>
> diff --git a/conf.c b/conf.c
> index ca0a764..0172dcd 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -738,7 +738,6 @@ static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
> e->addr = inany_from_v4(addr);
> e->prefix_len = prefix_len + 96;
> e->flags = CONF_ADDR_HOST;
> - ip4->addr_seen = addr;
> }
>
> ip4->our_tap_addr = ip4->guest_gw;
> @@ -755,7 +754,6 @@ static void conf_ip4_local(struct ctx *c)
> struct inany_addr_entry *e = &c->addrs[c->addr_count++];
> struct ip4_ctx *ip4 = &c->ip4;
>
> - ip4->addr_seen = IP4_LL_GUEST_ADDR;
> ip4->our_tap_addr = ip4->guest_gw = IP4_LL_GUEST_GW;
> ip4->no_copy_addrs = ip4->no_copy_routes = true;
> e->addr = inany_from_v4(IP4_LL_GUEST_ADDR);
> diff --git a/conf.h b/conf.h
> index bfad36f..8b10ac6 100644
> --- a/conf.h
> +++ b/conf.h
> @@ -12,6 +12,7 @@
> #define CONF_ADDR_USER BIT(0) /* User set via -a */
> #define CONF_ADDR_HOST BIT(1) /* From host interface */
> #define CONF_ADDR_LINKLOCAL BIT(2) /* Link-local address */
> +#define CONF_ADDR_OBSERVED BIT(3) /* Seen in guest traffic */
>
> enum passt_modes conf_mode(int argc, char *argv[]);
> void conf(struct ctx *c, int argc, char **argv);
> diff --git a/fwd.c b/fwd.c
> index fa5d667..ca704c2 100644
> --- a/fwd.c
> +++ b/fwd.c
> @@ -24,6 +24,7 @@
> #include "ip.h"
> #include "fwd.h"
> #include "passt.h"
> +#include "conf.h"
> #include "lineread.h"
> #include "flow_table.h"
> #include "netlink.h"
> @@ -491,6 +492,85 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
> ((ini->oport == 53) || (ini->oport == 853));
> }
>
> +/**
> + * fwd_guest_addr() - Get guest address matching criteria
> + * @c: Execution context
> + * @af: Address family (AF_INET, AF_INET6, or 0 for any)
> + * @incl: Flags that must be present (any-match)
> + * @excl: Flags that must not be present
> + *
> + * Return: first address matching criteria, or NULL
> + */
> +const union inany_addr *fwd_guest_addr(const struct ctx *c, sa_family_t af,
> + uint8_t incl, uint8_t excl)
> +{
> + const struct inany_addr_entry *e;
> +
> + for_each_addr(e, c, af) {
> + if (incl && !(e->flags & incl))
> + continue;
> + if (e->flags & excl)
> + continue;
> + return &e->addr;
> + }
> +
> + return NULL;
> +}
> +
> +/**
> + * fwd_set_observed_ip4() - Set observed IPv4 guest address
> + * @c: Execution context
> + * @addr: IPv4 address observed in guest traffic
> + *
> + * Mark @addr as the observed guest address. The observed address is always
> + * kept at position 0 for O(1) lookup. Only one address can have the OBSERVED
> + * flag at a time.
> + */
> +void fwd_set_observed_ip4(struct ctx *c, const struct in_addr *addr)
> +{
> + struct inany_addr_entry *e = &c->addrs[0];
> + int i;
> +
> + if (!addr->s_addr)
> + return;
> +
> + /* Fast path: check if already observed at position 0 */
> + if (c->addr_count > 0 && (e->flags & CONF_ADDR_OBSERVED) &&
> + inany_equals4(&e->addr, addr))
> + return;
> +
> + /* Slow path: new observed address - insert at position 0 */
> + if (c->addr_count >= INANY_MAX_ADDRS) {
> + debug("Address table full, can't add observed IPv4");
> + return;
> + }
> +
> + /* Make room and insert at position 0 */
> + memmove(&c->addrs[1], e, c->addr_count * sizeof(*e));
> + c->addr_count++;
> + inany_from_af(&e->addr, AF_INET, addr);
> + e->prefix_len = 0;
> + e->flags = CONF_ADDR_OBSERVED;
> +
> + /* Handle old observed IPv4 address, if any */
> + for (i = 1; i < c->addr_count; i++) {
> + e = &c->addrs[i];
> +
> + if (!inany_v4(&e->addr) || !(e->flags & CONF_ADDR_OBSERVED))
> + continue;
> +
> + e->flags &= ~CONF_ADDR_OBSERVED;
> +
> + /* Remove if no other flags, or if addr is duplicate */
> + if (!e->flags || inany_equals4(&e->addr, addr)) {
> + memmove(&c->addrs[i], &c->addrs[i + 1],
> + (c->addr_count - i - 1) * sizeof(*e));
> + c->addr_count--;
> + }
> + break;
> + }
> +}
I did this on suggestion from David, but thinking more about this I find
it is not optimal. There is no reason to limit the number of
OBSERVED addresses to just one per protocol. If a guest a happens to
switch frequencly between addresses this wil come with a measurable
performance penalty, because w always will have to addthe current
address at the head of the array and potentially remove another one.
Alternative approach: We still add all new OBSERVED addresses to the tip
of the array, but leave the others as they are.
This means that all OBSERVED addresses will accumulate at the beginning,
and all lookups will be very quick. Statistically, it is overwhelmingly
likely there will be a hit already at position 0 or 1, and we really
have lost nothing in comparison to the one-OBSERVED-address policy.
/jon
> +
> /**
> * fwd_guest_accessible() - Is address guest-accessible
> * @c: Execution context
> @@ -515,17 +595,11 @@ static bool fwd_guest_accessible(const struct ctx *c,
> if (inany_is_unspecified4(addr))
> return false;
>
> - /* Check against all configured guest addresses */
> + /* Check against all configured and observed guest addresses */
> for_each_addr(e, c, 0)
> if (inany_equals(addr, &e->addr))
> return false;
>
> - /* Also check addr_seen: it tracks the address the guest is actually
> - * using, which may differ from configured addresses.
> - */
> - if (inany_equals4(addr, &c->ip4.addr_seen))
> - return false;
> -
> /* For IPv6, addr_seen starts unspecified, because we don't know what LL
> * address the guest will take until we see it. Only check against it
> * if it has been set to a real address.
> @@ -726,10 +800,23 @@ 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)
> + if (c->host_lo_to_ns_lo) {
> tgt->eaddr = inany_loopback4;
> - else
> - tgt->eaddr = inany_from_v4(c->ip4.addr_seen);
> + } else {
> + const union inany_addr *guest_addr;
> +
> + guest_addr = fwd_guest_addr(c, AF_INET,
> + CONF_ADDR_OBSERVED,
> + 0);
> + if (!guest_addr)
> + guest_addr = fwd_guest_addr(c, AF_INET,
> + CONF_ADDR_USER | CONF_ADDR_HOST,
> + 0);
> + if (!guest_addr)
> + return PIF_NONE;
> +
> + tgt->eaddr = *guest_addr;
> + }
> tgt->oaddr = inany_any4;
> } else {
> if (c->host_lo_to_ns_lo)
> @@ -761,7 +848,12 @@ 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 union inany_addr *guest_addr;
> +
> + guest_addr = fwd_guest_addr(c, AF_INET, CONF_ADDR_OBSERVED, 0);
> + if (!guest_addr)
> + return PIF_NONE;
> + tgt->eaddr = *guest_addr;
> } else {
> if (inany_is_linklocal6(&tgt->oaddr))
> tgt->eaddr.a6 = c->ip6.addr_ll_seen;
> diff --git a/fwd.h b/fwd.h
> index 7792582..38f4e60 100644
> --- a/fwd.h
> +++ b/fwd.h
> @@ -15,6 +15,9 @@ struct flowside;
>
> void fwd_probe_ephemeral(void);
> bool fwd_port_is_ephemeral(in_port_t port);
> +const union inany_addr *fwd_guest_addr(const struct ctx *c, sa_family_t af,
> + uint8_t incl, uint8_t excl);
> +void fwd_set_observed_ip4(struct ctx *c, const struct in_addr *addr);
>
> enum fwd_ports_mode {
> FWD_UNSET = 0,
> diff --git a/migrate.c b/migrate.c
> index 48d63a0..d223857 100644
> --- a/migrate.c
> +++ b/migrate.c
> @@ -18,6 +18,8 @@
> #include "util.h"
> #include "ip.h"
> #include "passt.h"
> +#include "conf.h"
> +#include "fwd.h"
> #include "inany.h"
> #include "flow.h"
> #include "flow_table.h"
> @@ -57,11 +59,15 @@ static int seen_addrs_source_v1(struct ctx *c,
> struct migrate_seen_addrs_v1 addrs = {
> .addr6 = c->ip6.addr_seen,
> .addr6_ll = c->ip6.addr_ll_seen,
> - .addr4 = c->ip4.addr_seen,
> };
> + const union inany_addr *obs4;
>
> (void)stage;
>
> + obs4 = fwd_guest_addr(c, AF_INET, CONF_ADDR_OBSERVED, 0);
> + if (obs4)
> + addrs.addr4 = *inany_v4(obs4);
> +
> memcpy(addrs.mac, c->guest_mac, sizeof(addrs.mac));
>
> if (write_all_buf(fd, &addrs, sizeof(addrs)))
> @@ -82,6 +88,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 +97,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;
> +
> + /* Copy to avoid unaligned access from packed struct */
> + addr4 = addrs.addr4;
> + fwd_set_observed_ip4(c, &addr4);
> +
> memcpy(c->guest_mac, addrs.mac, sizeof(c->guest_mac));
>
> return 0;
> diff --git a/passt.h b/passt.h
> index 15d6596..fa747c6 100644
> --- a/passt.h
> +++ b/passt.h
> @@ -78,7 +78,6 @@ struct inany_addr_entry {
>
> /**
> * struct ip4_ctx - IPv4 execution context
> - * @addr_seen: Latest IPv4 address seen as source from tap
> * @guest_gw: IPv4 gateway as seen by the guest
> * @map_host_loopback: Outbound connections to this address are NATted to the
> * host's 127.0.0.1
> @@ -94,7 +93,6 @@ struct inany_addr_entry {
> * @no_copy_addrs: Don't copy all addresses when configuring namespace
> */
> struct ip4_ctx {
> - struct in_addr addr_seen;
> struct in_addr guest_gw;
> struct in_addr map_host_loopback;
> struct in_addr map_guest_addr;
> diff --git a/tap.c b/tap.c
> index 4298dcd..8c1ed35 100644
> --- a/tap.c
> +++ b/tap.c
> @@ -48,6 +48,7 @@
> #include "iov.h"
> #include "passt.h"
> #include "conf.h"
> +#include "fwd.h"
> #include "arp.h"
> #include "dhcp.h"
> #include "ndp.h"
> @@ -162,6 +163,16 @@ void tap_send_single(const struct ctx *c, const void *data, size_t l2len)
> }
> }
>
> +/**
> + * tap_check_src_addr4() - Note an IPv4 address seen in guest traffic
> + * @c: Execution context
> + * @addr: IPv4 address seen as source from guest
> + */
> +static void tap_check_src_addr4(struct ctx *c, const struct in_addr *addr)
> +{
> + fwd_set_observed_ip4(c, addr);
> +}
> +
> /**
> * tap_ip6_daddr() - Normal IPv6 destination address for inbound packets
> * @c: Execution context
> @@ -772,8 +783,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;
^ permalink raw reply [flat|nested] 14+ messages in thread
end of thread, other threads:[~2026-02-18 14:14 UTC | newest]
Thread overview: 14+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-02-17 22:18 [PATCH v4 00/12] Introduce multiple addresses Jon Maloy
2026-02-17 22:18 ` [PATCH v4 01/12] ip: Introduce unified multi-address data structures Jon Maloy
2026-02-17 22:18 ` [PATCH v4 02/12] ip: Introduce for_each_addr() macro for address iteration Jon Maloy
2026-02-17 22:18 ` [PATCH v4 03/12] fwd: Unify guest accessibility checks with unified address array Jon Maloy
2026-02-17 22:18 ` [PATCH v4 04/12] arp: Check all configured addresses in ARP filtering Jon Maloy
2026-02-17 22:18 ` [PATCH v4 05/12] pasta: Extract pasta_ns_conf_ip4/6() to reduce nesting Jon Maloy
2026-02-17 22:18 ` [PATCH v4 06/12] netlink: Return prefix length for IPv6 addresses in nl_addr_get() Jon Maloy
2026-02-17 22:18 ` [PATCH v4 07/12] conf: Allow multiple -a/--address options per address family Jon Maloy
2026-02-17 22:18 ` [PATCH v4 08/12] ip: Track observed guest IPv4 addresses in unified address array Jon Maloy
2026-02-18 14:14 ` Jon Maloy
2026-02-17 22:18 ` [PATCH v4 09/12] ip: Track observed guest IPv6 " Jon Maloy
2026-02-17 22:18 ` [PATCH v4 10/12] fwd: Unify fwd_set_observed_ip4() and fwd_set_observed_ip6() Jon Maloy
2026-02-17 22:18 ` [PATCH v4 11/12] migrate: Rename v1 address functions to v2 for clarity Jon Maloy
2026-02-17 22:18 ` [PATCH v4 12/12] migrate: Update protocol to v3 for multi-address support Jon Maloy
Code repositories for project(s) associated with this public inbox
https://passt.top/passt
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).