public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
* [PATCH v3 00/25] RFC: Read-only dynamic update implementation
@ 2026-03-23  7:37 David Gibson
  2026-03-23  7:37 ` [PATCH v3 01/25] conf: runas can be const David Gibson
                   ` (26 more replies)
  0 siblings, 27 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Here's a new draft of dynamic updates.  This now can successfully
update rules, though I've not tested it very extensively.  Essentially
this is just barely enough to work, it still could do with rather a
lot of polish.

Patches 1..12/22 are preliminary reworks that make moderate sense even
without pesto - feel free to apply if you're happy with them.

Changes in v3:
 * Removed already applied ASSERT() rename
 * Renamed serialisation functions
 * Incorporated Stefano's extensions, reworked and fixed
 * Several additional cleanups / preliminary reworks
Changes in v2:
 * Removed already applied cleanups
 * Reworked assert() patch to handle -DNDEBUG properly
 * Numerous extra patches:
   * Factored out serialisation helpers and use them for migration as
     well
   * Reworked to allow ip.[ch] and inany.[ch] to be shared with pesto
   * Reworks to share some forwarding rule datatypes with pesto
   * Implemented sending pif names and current ruleset to pesto

David Gibson (22):
  conf: runas can be const
  vhost_user: Fix assorted minor cppcheck warnings
  serialise: Split functions user for serialisation from util.c
  serialise: Add helpers for serialising unsigned integers
  fwd: Move selecting correct scan bitmap into fwd_sync_one()
  fwd: Look up rule index in fwd_sync_one()
  fwd: Store forwarding tables indexed by (origin) pif
  fwd: Allow FWD_DUAL_STACK_ANY flag to be passed directly to
    fwd_rule_add()
  fwd, conf: Expose ephemeral ports as bitmap rather than function
  conf: Don't bother complaining about overlapping excluded ranges
  conf: Move check for mapping port 0 to caller
  conf: Move check for disabled interfaces earlier
  pesto: Introduce stub configuration interface and tool
  pesto: Add command line option parsing and debug messages
  pesto: Expose list of pifs to pesto
  ip: Prepare ip.[ch] for sharing with pesto tool
  inany: Prepare inany.[ch] for sharing with pesto tool
  fwd: Split forwading rule specification from its implementation state
  ip: Define a bound for the string returned by ipproto_name()
  fwd_rule: Move forwarding rule text formatting to common code
  pesto: Read current ruleset from passt/pasta and display it
  conf, fwd: Allow switching to new rules received from pesto

Stefano Brivio (3):
  conf: Move port parsing functions to own file, ports.c
  conf, fwd, ports, util: Move things around for pesto
  pesto, conf: Parse, send and receive new rules

 .gitignore   |   2 +
 Makefile     |  42 +--
 common.h     | 178 +++++++++++++
 conf.c       | 740 ++++++++++++++++++++++++---------------------------
 conf.h       |   2 +
 epoll_type.h |   4 +
 flow.c       |  37 ++-
 fwd.c        | 347 +++++++-----------------
 fwd.h        |  82 +-----
 fwd_rule.c   | 204 ++++++++++++++
 fwd_rule.h   | 112 ++++++++
 inany.c      |  16 +-
 inany.h      |  16 +-
 iov.c        |   1 +
 ip.c         |  74 ++----
 ip.h         |   4 +-
 lineread.c   |   1 -
 log.c        |   1 +
 log.h        |  33 +++
 migrate.c    |   1 +
 passt.1      |   5 +
 passt.c      |   9 +
 passt.h      |  14 +-
 pcap.c       |   1 +
 pesto.1      |  46 ++++
 pesto.c      | 412 ++++++++++++++++++++++++++++
 pesto.h      |  34 +++
 pif.h        |   2 +
 ports.c      | 444 +++++++++++++++++++++++++++++++
 ports.h      |  48 ++++
 serialise.c  | 147 ++++++++++
 serialise.h  |  24 ++
 siphash.h    |  13 +
 tap.c        |  52 ++++
 tcp.c        |   1 +
 util.c       | 154 +----------
 util.h       |  87 ------
 vhost_user.c |  16 +-
 virtio.c     |   1 +
 virtio.h     |   2 +-
 vu_common.c  |   2 +-
 41 files changed, 2334 insertions(+), 1077 deletions(-)
 create mode 100644 common.h
 create mode 100644 fwd_rule.c
 create mode 100644 fwd_rule.h
 create mode 100644 pesto.1
 create mode 100644 pesto.c
 create mode 100644 pesto.h
 create mode 100644 ports.c
 create mode 100644 ports.h
 create mode 100644 serialise.c
 create mode 100644 serialise.h

-- 
2.53.0


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

* [PATCH v3 01/25] conf: runas can be const
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 02/25] vhost_user: Fix assorted minor cppcheck warnings David Gibson
                   ` (25 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Both the runas variable in conf() and the parameter to conf_ugid() can be
a const pointer.  For some reason cppcheck doesn't catch this now, but does
after some upcoming rearrangements for sharing code with the dynamic update
tool.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 conf.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/conf.c b/conf.c
index 940fb9e9..b1ebb4a4 100644
--- a/conf.c
+++ b/conf.c
@@ -1320,7 +1320,7 @@ static int conf_runas(const char *opt, unsigned int *uid, unsigned int *gid)
  * @uid:	User ID, set on success
  * @gid:	Group ID, set on success
  */
-static void conf_ugid(char *runas, uid_t *uid, gid_t *gid)
+static void conf_ugid(const char *runas, uid_t *uid, gid_t *gid)
 {
 	/* If user has specified --runas, that takes precedence... */
 	if (runas) {
@@ -1561,8 +1561,8 @@ void conf(struct ctx *c, int argc, char **argv)
 	uint8_t prefix_len_from_opt = 0;
 	unsigned int ifi4 = 0, ifi6 = 0;
 	const char *logfile = NULL;
+	const char *runas = NULL;
 	size_t logsize = 0;
-	char *runas = NULL;
 	long fd_tap_opt;
 	int name, ret;
 	uid_t uid;
-- 
2.53.0


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

* [PATCH v3 02/25] vhost_user: Fix assorted minor cppcheck warnings
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
  2026-03-23  7:37 ` [PATCH v3 01/25] conf: runas can be const David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 03/25] serialise: Split functions user for serialisation from util.c David Gibson
                   ` (24 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

This fixes a batch of minor incorrect format specifier and "could be const"
warnings in the vhost_user code.  For some very strange reason, cppcheck
doesn't catch these errors right now, but does after code rearrangements
we're making for the dynamic forwarding update stuff.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 vhost_user.c | 16 +++++++++-------
 virtio.h     |  2 +-
 vu_common.c  |  2 +-
 3 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/vhost_user.c b/vhost_user.c
index 75665ec6..f062badd 100644
--- a/vhost_user.c
+++ b/vhost_user.c
@@ -440,11 +440,13 @@ static bool vu_set_mem_table_exec(struct vu_dev *vdev,
 
 	debug("vhost-user nregions: %u", memory->nregions);
 	for (i = 0; i < vdev->memory.nregions; i++) {
-		struct vhost_user_memory_region *msg_region = &memory->regions[i];
 		struct vu_dev_region *dev_region = &vdev->memory.regions[i];
+		const struct vhost_user_memory_region *msg_region;
 		const void *mmap_addr;
 
-		debug("vhost-user region %d", i);
+		msg_region = &memory->regions[i];
+
+		debug("vhost-user region %u", i);
 		debug("    guest_phys_addr: 0x%016"PRIx64,
 		      msg_region->guest_phys_addr);
 		debug("    memory_size:     0x%016"PRIx64,
@@ -479,7 +481,7 @@ static bool vu_set_mem_table_exec(struct vu_dev *vdev,
 	for (i = 0; i < VHOST_USER_MAX_VQS; i++) {
 		if (vdev->vq[i].vring.desc) {
 			if (map_ring(vdev, &vdev->vq[i]))
-				die("remapping queue %d during setmemtable", i);
+				die("remapping queue %u during setmemtable", i);
 		}
 	}
 
@@ -763,8 +765,8 @@ static void vu_set_watch(const struct vu_dev *vdev, int idx)
  */
 static void vu_check_queue_msg_file(struct vhost_user_msg *vmsg)
 {
+	unsigned idx = vmsg->payload.u64 & VHOST_USER_VRING_IDX_MASK;
 	bool nofd = vmsg->payload.u64 & VHOST_USER_VRING_NOFD_MASK;
-	int idx = vmsg->payload.u64 & VHOST_USER_VRING_IDX_MASK;
 
 	if (idx >= VHOST_USER_MAX_VQS)
 		die("Invalid vhost-user queue index: %u", idx);
@@ -1010,11 +1012,11 @@ static bool vu_set_device_state_fd_exec(struct vu_dev *vdev,
 		die("Invalid device_state_fd message");
 
 	if (phase != VHOST_USER_TRANSFER_STATE_PHASE_STOPPED)
-		die("Invalid device_state_fd phase: %d", phase);
+		die("Invalid device_state_fd phase: %u", phase);
 
 	if (direction != VHOST_USER_TRANSFER_STATE_DIRECTION_SAVE &&
 	    direction != VHOST_USER_TRANSFER_STATE_DIRECTION_LOAD)
-		die("Invalid device_state_fd direction: %d", direction);
+		die("Invalid device_state_fd direction: %u", direction);
 
 	migrate_request(vdev->context, vmsg->fds[0],
 			direction == VHOST_USER_TRANSFER_STATE_DIRECTION_LOAD);
@@ -1047,7 +1049,7 @@ static bool vu_check_device_state_exec(struct vu_dev *vdev,
  */
 void vu_init(struct ctx *c)
 {
-	int i;
+	unsigned i;
 
 	c->vdev = &vdev_storage;
 	c->vdev->context = c;
diff --git a/virtio.h b/virtio.h
index c7e447d5..8f2ae068 100644
--- a/virtio.h
+++ b/virtio.h
@@ -90,7 +90,7 @@ struct vu_dev_region {
 	uint64_t mmap_addr;
 };
 
-#define VHOST_USER_MAX_VQS 2
+#define VHOST_USER_MAX_VQS 2U
 
 /*
  * Set a reasonable maximum number of ram slots, which will be supported by
diff --git a/vu_common.c b/vu_common.c
index 92381cd3..13b1e510 100644
--- a/vu_common.c
+++ b/vu_common.c
@@ -256,7 +256,7 @@ int vu_send_single(const struct ctx *c, const void *buf, size_t size)
 			      ARRAY_SIZE(in_sg), &in_total, size, &total);
 	if (elem_cnt == 0 || total < size) {
 		debug("vu_send_single: no space to send the data "
-		      "elem_cnt %d size %zd", elem_cnt, total);
+		      "elem_cnt %d size %zu", elem_cnt, total);
 		goto err;
 	}
 
-- 
2.53.0


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

* [PATCH v3 03/25] serialise: Split functions user for serialisation from util.c
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
  2026-03-23  7:37 ` [PATCH v3 01/25] conf: runas can be const David Gibson
  2026-03-23  7:37 ` [PATCH v3 02/25] vhost_user: Fix assorted minor cppcheck warnings David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-25  0:54   ` Stefano Brivio
  2026-03-23  7:37 ` [PATCH v3 04/25] serialise: Add helpers for serialising unsigned integers David Gibson
                   ` (23 subsequent siblings)
  26 siblings, 1 reply; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

The read_all_buf() and write_all_buf() functions in util.c are
primarily used for serialising data structures to a stream during
migraiton.  We're going to have further use for such serialisation
when we add dynamic configuration updates, where we'll want to share
the code with the client program.

To make that easier move the functions into a new serialise.c
file, and rename thematically.  While we're there add some macros for
the common idiom of sending all of a given variable using sizeof().

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 Makefile    | 11 ++++---
 flow.c      |  1 +
 migrate.c   |  1 +
 pcap.c      |  1 +
 serialise.c | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 serialise.h | 14 +++++++++
 tcp.c       |  1 +
 util.c      | 70 +-----------------------------------------
 util.h      |  2 --
 9 files changed, 113 insertions(+), 76 deletions(-)
 create mode 100644 serialise.c
 create mode 100644 serialise.h

diff --git a/Makefile b/Makefile
index 513dc6c6..5b6891d7 100644
--- a/Makefile
+++ b/Makefile
@@ -40,8 +40,8 @@ FLAGS += -DDUAL_STACK_SOCKETS=$(DUAL_STACK_SOCKETS)
 PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
 	flow.c fwd.c icmp.c igmp.c inany.c iov.c ip.c isolation.c lineread.c \
 	log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c pasta.c pcap.c \
-	pif.c repair.c tap.c tcp.c tcp_buf.c tcp_splice.c tcp_vu.c udp.c \
-	udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
+	pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c tcp_splice.c tcp_vu.c \
+	udp.c udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
 QRAP_SRCS = qrap.c
 PASST_REPAIR_SRCS = passt-repair.c
 SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS)
@@ -51,9 +51,10 @@ MANPAGES = passt.1 pasta.1 qrap.1 passt-repair.1
 PASST_HEADERS = arch.h arp.h checksum.h conf.h dhcp.h dhcpv6.h epoll_ctl.h \
 	flow.h fwd.h flow_table.h icmp.h icmp_flow.h inany.h iov.h ip.h \
 	isolation.h lineread.h log.h migrate.h ndp.h netlink.h packet.h \
-	passt.h pasta.h pcap.h pif.h repair.h siphash.h tap.h tcp.h tcp_buf.h \
-	tcp_conn.h tcp_internal.h tcp_splice.h tcp_vu.h udp.h udp_flow.h \
-	udp_internal.h udp_vu.h util.h vhost_user.h virtio.h vu_common.h
+	passt.h pasta.h pcap.h pif.h repair.h serialise.h siphash.h tap.h tcp.h \
+	tcp_buf.h tcp_conn.h tcp_internal.h tcp_splice.h tcp_vu.h udp.h \
+	udp_flow.h udp_internal.h udp_vu.h util.h vhost_user.h virtio.h \
+	vu_common.h
 HEADERS = $(PASST_HEADERS) seccomp.h
 
 C := \#include <sys/random.h>\nint main(){int a=getrandom(0, 0, 0);}
diff --git a/flow.c b/flow.c
index 4282da2e..25a6f1a3 100644
--- a/flow.c
+++ b/flow.c
@@ -21,6 +21,7 @@
 #include "flow_table.h"
 #include "repair.h"
 #include "epoll_ctl.h"
+#include "serialise.h"
 
 const char *flow_state_str[] = {
 	[FLOW_STATE_FREE]	= "FREE",
diff --git a/migrate.c b/migrate.c
index 1e8858a3..8937b85f 100644
--- a/migrate.c
+++ b/migrate.c
@@ -24,6 +24,7 @@
 
 #include "migrate.h"
 #include "repair.h"
+#include "serialise.h"
 
 /* Magic identifier for migration data */
 #define MIGRATE_MAGIC		0xB1BB1D1B0BB1D1B0
diff --git a/pcap.c b/pcap.c
index 54fba5c4..a026f17e 100644
--- a/pcap.c
+++ b/pcap.c
@@ -34,6 +34,7 @@
 #include "pcap.h"
 #include "iov.h"
 #include "tap.h"
+#include "serialise.h"
 
 #define PCAP_VERSION_MINOR 4
 
diff --git a/serialise.c b/serialise.c
new file mode 100644
index 00000000..f162eeb6
--- /dev/null
+++ b/serialise.c
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/* PASST - Plug A Simple Socket Transport
+ *  for qemu/UNIX domain socket mode
+ *
+ * PASTA - Pack A Subtle Tap Abstraction
+ *  for network namespace/tap device mode
+ *
+ * serialise.c - Serialisation of data structures over bytestreams
+ *
+ * Copyright Red Hat
+ * Author: David Gibson <david@gibson.dropbear.id.au>
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <unistd.h>
+
+#include "serialise.h"
+
+/**
+ * read_all_buf() - Fill a whole buffer from a file descriptor
+ * @fd:		File descriptor
+ * @buf:	Pointer to base of buffer
+ * @len:	Length of buffer
+ *
+ * Return: 0 on success, -1 on error (with errno set)
+ *
+ * #syscalls read
+ */
+int read_all_buf(int fd, void *buf, size_t len)
+{
+	size_t left = len;
+	char *p = buf;
+
+	while (left) {
+		ssize_t rc;
+
+		assert(left <= len);
+
+		do
+			rc = read(fd, p, left);
+		while ((rc < 0) && errno == EINTR);
+
+		if (rc < 0)
+			return -1;
+
+		if (rc == 0) {
+			errno = ENODATA;
+			return -1;
+		}
+
+		p += rc;
+		left -= rc;
+	}
+	return 0;
+}
+
+/**
+ * write_all_buf() - write all of a buffer to an fd
+ * @fd:		File descriptor
+ * @buf:	Pointer to base of buffer
+ * @len:	Length of buffer
+ *
+ * Return: 0 on success, -1 on error (with errno set)
+ *
+ * #syscalls write
+ */
+int write_all_buf(int fd, const void *buf, size_t len)
+{
+	const char *p = buf;
+	size_t left = len;
+
+	while (left) {
+		ssize_t rc;
+
+		do
+			rc = write(fd, p, left);
+		while ((rc < 0) && errno == EINTR);
+
+		if (rc < 0)
+			return -1;
+
+		p += rc;
+		left -= rc;
+	}
+	return 0;
+}
diff --git a/serialise.h b/serialise.h
new file mode 100644
index 00000000..92a63e6b
--- /dev/null
+++ b/serialise.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright Red Hat
+ * Author: David Gibson <david@gibson.dropbear.id.au>
+ */
+
+#ifndef SERIALISE_H
+#define SERIALISE_H
+
+#include <stddef.h>
+
+int read_all_buf(int fd, void *buf, size_t len);
+int write_all_buf(int fd, const void *buf, size_t len);
+
+#endif /* _SERIALISE_H */
diff --git a/tcp.c b/tcp.c
index b1458624..8ea9be84 100644
--- a/tcp.c
+++ b/tcp.c
@@ -308,6 +308,7 @@
 #include "flow.h"
 #include "repair.h"
 #include "linux_dep.h"
+#include "serialise.h"
 
 #include "flow_table.h"
 #include "tcp_internal.h"
diff --git a/util.c b/util.c
index 22318c00..faa2c6a4 100644
--- a/util.c
+++ b/util.c
@@ -37,6 +37,7 @@
 #include "pcap.h"
 #include "epoll_ctl.h"
 #include "pasta.h"
+#include "serialise.h"
 #ifdef HAS_GETRANDOM
 #include <sys/random.h>
 #endif
@@ -799,37 +800,6 @@ int do_clone(int (*fn)(void *), char *stack_area, size_t stack_size, int flags,
 #endif
 }
 
-/**
- * write_all_buf() - write all of a buffer to an fd
- * @fd:		File descriptor
- * @buf:	Pointer to base of buffer
- * @len:	Length of buffer
- *
- * Return: 0 on success, -1 on error (with errno set)
- *
- * #syscalls write
- */
-int write_all_buf(int fd, const void *buf, size_t len)
-{
-	const char *p = buf;
-	size_t left = len;
-
-	while (left) {
-		ssize_t rc;
-
-		do
-			rc = write(fd, p, left);
-		while ((rc < 0) && errno == EINTR);
-
-		if (rc < 0)
-			return -1;
-
-		p += rc;
-		left -= rc;
-	}
-	return 0;
-}
-
 /**
  * write_remainder() - write the tail of an IO vector to an fd
  * @fd:		File descriptor
@@ -866,44 +836,6 @@ int write_remainder(int fd, const struct iovec *iov, size_t iovcnt, size_t skip)
 	return 0;
 }
 
-/**
- * read_all_buf() - Fill a whole buffer from a file descriptor
- * @fd:		File descriptor
- * @buf:	Pointer to base of buffer
- * @len:	Length of buffer
- *
- * Return: 0 on success, -1 on error (with errno set)
- *
- * #syscalls read
- */
-int read_all_buf(int fd, void *buf, size_t len)
-{
-	size_t left = len;
-	char *p = buf;
-
-	while (left) {
-		ssize_t rc;
-
-		assert(left <= len);
-
-		do
-			rc = read(fd, p, left);
-		while ((rc < 0) && errno == EINTR);
-
-		if (rc < 0)
-			return -1;
-
-		if (rc == 0) {
-			errno = ENODATA;
-			return -1;
-		}
-
-		p += rc;
-		left -= rc;
-	}
-	return 0;
-}
-
 /**
  * read_remainder() - Read the tail of an IO vector from a file descriptor
  * @fd:		File descriptor
diff --git a/util.h b/util.h
index 2fc5cd74..cb669105 100644
--- a/util.h
+++ b/util.h
@@ -245,9 +245,7 @@ int fls(unsigned long x);
 int ilog2(unsigned long x);
 int write_file(const char *path, const char *buf);
 intmax_t read_file_integer(const char *path, intmax_t fallback);
-int write_all_buf(int fd, const void *buf, size_t len);
 int write_remainder(int fd, const struct iovec *iov, size_t iovcnt, size_t skip);
-int read_all_buf(int fd, void *buf, size_t len);
 int read_remainder(int fd, const struct iovec *iov, size_t cnt, size_t skip);
 void close_open_files(int argc, char **argv);
 bool snprintf_check(char *str, size_t size, const char *format, ...);
-- 
2.53.0


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

* [PATCH v3 04/25] serialise: Add helpers for serialising unsigned integers
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (2 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 03/25] serialise: Split functions user for serialisation from util.c David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 05/25] fwd: Move selecting correct scan bitmap into fwd_sync_one() David Gibson
                   ` (22 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Add helpers to serialise/deserialise unsigned integers, handling endian
conversion so that the stream format is consistent regardless of host
endiannness.  Currently we only use this on one place: sending the number
of flows during migration.  We're going to have more use for this as we
add dynamic configuration updates, so these will become more useful.

For now we only need a 32-bit version, however define the functions with
a macro so we can easily add other integer widths when we need them.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 flow.c      | 10 ++++------
 serialise.c | 35 +++++++++++++++++++++++++++++++++++
 serialise.h |  7 +++++++
 3 files changed, 46 insertions(+), 6 deletions(-)

diff --git a/flow.c b/flow.c
index 25a6f1a3..c84857b2 100644
--- a/flow.c
+++ b/flow.c
@@ -1135,10 +1135,9 @@ int flow_migrate_source(struct ctx *c, const struct migrate_stage *stage,
 			count++;
 	}
 
-	count = htonl(count);
-	if (write_all_buf(fd, &count, sizeof(count))) {
+	if (write_u32(fd, count)) {
 		rc = errno;
-		err_perror("Can't send flow count (%u)", ntohl(count));
+		err_perror("Can't send flow count (%u)", count);
 		return flow_migrate_source_rollback(c, FLOW_MAX, rc);
 	}
 
@@ -1151,7 +1150,7 @@ int flow_migrate_source(struct ctx *c, const struct migrate_stage *stage,
 	debug("Stop listen()s");
 	fwd_listen_close(&c->fwd_in);
 
-	debug("Sending %u flows", ntohl(count));
+	debug("Sending %u flows", count);
 
 	if (!count)
 		return 0;
@@ -1221,10 +1220,9 @@ int flow_migrate_target(struct ctx *c, const struct migrate_stage *stage,
 
 	(void)stage;
 
-	if (read_all_buf(fd, &count, sizeof(count)))
+	if (read_u32(fd, &count))
 		return errno;
 
-	count = ntohl(count);
 	debug("Receiving %u flows", count);
 
 	if (!count)
diff --git a/serialise.c b/serialise.c
index f162eeb6..d6c3396a 100644
--- a/serialise.c
+++ b/serialise.c
@@ -13,7 +13,9 @@
  */
 
 #include <assert.h>
+#include <endian.h>
 #include <errno.h>
+#include <stdint.h>
 #include <unistd.h>
 
 #include "serialise.h"
@@ -86,3 +88,36 @@ int write_all_buf(int fd, const void *buf, size_t len)
 	}
 	return 0;
 }
+
+/**
+ * read_uXXX() - Receive a uXXX value from an fd
+ * @fd:		File descriptor to read from
+ * @valp:	Pointer to variable to update with read value
+ *
+ * Return: 0 on success, -1 on error
+ */
+/**
+ * write_uXXX() - Send a uXXX value to an fd
+ * @fd:		File descriptor to write to
+ * @val:	Value to send
+ *
+ * Return: 0 on success, -1 on error
+ */
+#define SERIALISE_UINT(bits)						\
+	int read_u##bits(int fd, uint##bits##_t *val)			\
+	{								\
+		uint##bits##_t beval;					\
+		if (read_all_buf(fd, &beval, sizeof(beval)) < 0)	\
+			return -1;					\
+		*val = be##bits##toh(beval);				\
+		return 0;						\
+	}								\
+	int write_u##bits(int fd, uint##bits##_t val)			\
+	{								\
+		uint##bits##_t beval = htobe##bits(val);		\
+		return write_all_buf(fd, &beval, sizeof(beval));	\
+	}
+
+SERIALISE_UINT(32)
+
+#undef SERIALISE_UNIT
diff --git a/serialise.h b/serialise.h
index 92a63e6b..0a4ed086 100644
--- a/serialise.h
+++ b/serialise.h
@@ -7,8 +7,15 @@
 #define SERIALISE_H
 
 #include <stddef.h>
+#include <stdint.h>
 
 int read_all_buf(int fd, void *buf, size_t len);
 int write_all_buf(int fd, const void *buf, size_t len);
 
+#define SERIALISE_UINT_DECL(bits)					\
+	int read_u##bits(int fd, uint##bits##_t *val);			\
+	int write_u##bits(int fd, uint##bits##_t val);
+
+SERIALISE_UINT_DECL(32)
+
 #endif /* _SERIALISE_H */
-- 
2.53.0


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

* [PATCH v3 05/25] fwd: Move selecting correct scan bitmap into fwd_sync_one()
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (3 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 04/25] serialise: Add helpers for serialising unsigned integers David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 06/25] fwd: Look up rule index in fwd_sync_one() David Gibson
                   ` (21 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Currently fwd_listen_sync_() determines which of the two port scanned
bitmaps is the correct one for a rule before passing it into
fwd_sync_one().  However, fwd_sync_one() is already looking at the rule
internals so is better placed to do this.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 fwd.c | 28 +++++++++++++++-------------
 1 file changed, 15 insertions(+), 13 deletions(-)

diff --git a/fwd.c b/fwd.c
index f3b4bf2a..a306b409 100644
--- a/fwd.c
+++ b/fwd.c
@@ -508,16 +508,18 @@ void fwd_rules_print(const struct fwd_table *fwd)
  * @fwd:	Forwarding table
  * @rule:	Forwarding rule
  * @pif:	Interface to create listening sockets for
- * @scanmap:	Bitmap of ports to listen for on FWD_SCAN entries
+ * @tcp:	Bitmap of TCP ports to listen for on FWD_SCAN entries
+ * @udp:	Bitmap of UDP ports to listen for on FWD_SCAN entries
  *
  * Return: 0 on success, -1 on failure
  */
 static int fwd_sync_one(const struct ctx *c, const struct fwd_table *fwd,
 			const struct fwd_rule *rule, uint8_t pif,
-			const uint8_t *scanmap)
+			const uint8_t *tcp, const uint8_t *udp)
 {
 	const union inany_addr *addr = fwd_rule_addr(rule);
 	const char *ifname = rule->ifname;
+	const uint8_t *map = NULL;
 	bool bound_one = false;
 	unsigned port, idx;
 
@@ -528,12 +530,19 @@ static int fwd_sync_one(const struct ctx *c, const struct fwd_table *fwd,
 
 	idx = rule - fwd->rules;
 	assert(idx < MAX_FWD_RULES);
-	assert(!(rule->flags & FWD_SCAN && !scanmap));
-	
+
+	if (rule->flags & FWD_SCAN) {
+		if (rule->proto == IPPROTO_TCP)
+			map = tcp;
+		else if (rule->proto == IPPROTO_UDP)
+			map = udp;
+		assert(map);
+	}
+
 	for (port = rule->first; port <= rule->last; port++) {
 		int fd = rule->socks[port - rule->first];
 
-		if ((rule->flags & FWD_SCAN) && !bitmap_isset(scanmap, port)) {
+		if (map && !bitmap_isset(map, port)) {
 			/* We don't want to listen on this port */
 			if (fd >= 0) {
 				/* We already are, so stop */
@@ -619,15 +628,8 @@ static int fwd_listen_sync_(void *arg)
 		ns_enter(a->c);
 
 	for (i = 0; i < a->fwd->count; i++) {
-		const uint8_t *scanmap = NULL;
-
-		if (a->fwd->rules[i].proto == IPPROTO_TCP)
-			scanmap = a->tcpmap;
-		else if (a->fwd->rules[i].proto == IPPROTO_UDP)
-			scanmap = a->udpmap;
-
 		a->ret = fwd_sync_one(a->c, a->fwd, &a->fwd->rules[i],
-				      a->pif, scanmap);
+				      a->pif, a->tcpmap, a->udpmap);
 		if (a->ret < 0)
 			break;
 	}
-- 
2.53.0


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

* [PATCH v3 06/25] fwd: Look up rule index in fwd_sync_one()
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (4 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 05/25] fwd: Move selecting correct scan bitmap into fwd_sync_one() David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 07/25] fwd: Store forwarding tables indexed by (origin) pif David Gibson
                   ` (20 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Currently we have the slightly silly setup that fwd_listen_sync_() looks
up the pointer to a rule based on its index, then fwd_sync_one() turns it
back into an index.  Avoid this by passing the index directly into
fwd_sync_one().

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 fwd.c | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/fwd.c b/fwd.c
index a306b409..7844a674 100644
--- a/fwd.c
+++ b/fwd.c
@@ -506,7 +506,7 @@ void fwd_rules_print(const struct fwd_table *fwd)
 /** fwd_sync_one() - Create or remove listening sockets for a forward entry
  * @c:		Execution context
  * @fwd:	Forwarding table
- * @rule:	Forwarding rule
+ * @idx:	Rule index
  * @pif:	Interface to create listening sockets for
  * @tcp:	Bitmap of TCP ports to listen for on FWD_SCAN entries
  * @udp:	Bitmap of UDP ports to listen for on FWD_SCAN entries
@@ -514,23 +514,21 @@ void fwd_rules_print(const struct fwd_table *fwd)
  * Return: 0 on success, -1 on failure
  */
 static int fwd_sync_one(const struct ctx *c, const struct fwd_table *fwd,
-			const struct fwd_rule *rule, uint8_t pif,
+			unsigned idx, uint8_t pif,
 			const uint8_t *tcp, const uint8_t *udp)
 {
+	const struct fwd_rule *rule = &fwd->rules[idx];
 	const union inany_addr *addr = fwd_rule_addr(rule);
 	const char *ifname = rule->ifname;
 	const uint8_t *map = NULL;
 	bool bound_one = false;
-	unsigned port, idx;
+	unsigned port;
 
 	assert(pif_is_socket(pif));
 
 	if (!*ifname)
 		ifname = NULL;
 
-	idx = rule - fwd->rules;
-	assert(idx < MAX_FWD_RULES);
-
 	if (rule->flags & FWD_SCAN) {
 		if (rule->proto == IPPROTO_TCP)
 			map = tcp;
@@ -628,8 +626,8 @@ static int fwd_listen_sync_(void *arg)
 		ns_enter(a->c);
 
 	for (i = 0; i < a->fwd->count; i++) {
-		a->ret = fwd_sync_one(a->c, a->fwd, &a->fwd->rules[i],
-				      a->pif, a->tcpmap, a->udpmap);
+		a->ret = fwd_sync_one(a->c, a->fwd, i, a->pif,
+				      a->tcpmap, a->udpmap);
 		if (a->ret < 0)
 			break;
 	}
-- 
2.53.0


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

* [PATCH v3 07/25] fwd: Store forwarding tables indexed by (origin) pif
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (5 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 06/25] fwd: Look up rule index in fwd_sync_one() David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-25  0:54   ` Stefano Brivio
  2026-03-23  7:37 ` [PATCH v3 08/25] fwd: Allow FWD_DUAL_STACK_ANY flag to be passed directly to fwd_rule_add() David Gibson
                   ` (19 subsequent siblings)
  26 siblings, 1 reply; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Currently we store the inbound (PIF_HOST) and outbound (PIF_SPLICE)
forwarding tables in separate fields of struct ctx.  In a number of places
this requires somewhat awkward if or switch constructs to select the
right table for updates.  Conceptually simplify that by using an index of
forwarding tables by pif, which as a bonus keeps track generically which
pifs have implemented forwarding tables so far.

For now this doesn't simplify a lot textually, because many places that
need this also have other special cases to apply by pif.  It does simplify
a few crucial places though, and we expect it will become more useful as
the flexibility of the forwarding table is improved.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 conf.c  | 58 +++++++++++++++++++++++++++++++-------------------
 flow.c  | 22 +++++++------------
 fwd.c   | 65 ++++++++++++++++++++++++++++++---------------------------
 fwd.h   |  4 ++--
 passt.h |  6 ++----
 5 files changed, 83 insertions(+), 72 deletions(-)

diff --git a/conf.c b/conf.c
index b1ebb4a4..6ca61b74 100644
--- a/conf.c
+++ b/conf.c
@@ -1252,11 +1252,17 @@ dns6:
 		}
 	}
 
-	info("Inbound forwarding:");
-	fwd_rules_print(&c->fwd_in);
-	if (c->mode == MODE_PASTA) {
-		info("Outbound forwarding:");
-		fwd_rules_print(&c->fwd_out);
+	for (i = 0; i < PIF_NUM_TYPES; i++) {
+		const char *dir = "Outbound";
+
+		if (!c->fwd[i])
+			continue;
+
+		if (i == PIF_HOST)
+			dir = "Inbound";
+
+		info("%s forwarding rules (%s):", dir, pif_name(i));
+		fwd_rules_print(c->fwd[i]);
 	}
 }
 
@@ -2154,18 +2160,24 @@ void conf(struct ctx *c, int argc, char **argv)
 
 	/* Forwarding options can be parsed now, after IPv4/IPv6 settings */
 	fwd_probe_ephemeral();
+	fwd_rule_init(c);
 	optind = 0;
 	do {
 		name = getopt_long(argc, argv, optstring, options, NULL);
 
-		if (name == 't')
-			conf_ports(c, name, optarg, &c->fwd_in, &tcp_in_mode);
-		else if (name == 'u')
-			conf_ports(c, name, optarg, &c->fwd_in, &udp_in_mode);
-		else if (name == 'T')
-			conf_ports(c, name, optarg, &c->fwd_out, &tcp_out_mode);
-		else if (name == 'U')
-			conf_ports(c, name, optarg, &c->fwd_out, &udp_out_mode);
+		if (name == 't') {
+			conf_ports(c, name, optarg, c->fwd[PIF_HOST],
+				   &tcp_in_mode);
+		} else if (name == 'u') {
+			conf_ports(c, name, optarg, c->fwd[PIF_HOST],
+				   &udp_in_mode);
+		} else if (name == 'T') {
+			conf_ports(c, name, optarg, c->fwd[PIF_SPLICE],
+				   &tcp_out_mode);
+		} else if (name == 'U') {
+			conf_ports(c, name, optarg, c->fwd[PIF_SPLICE],
+				   &udp_out_mode);
+		}
 	} while (name != -1);
 
 	if (c->mode == MODE_PASTA)
@@ -2224,20 +2236,24 @@ void conf(struct ctx *c, int argc, char **argv)
 		udp_out_mode = fwd_default;
 
 	if (tcp_in_mode == FWD_MODE_AUTO) {
-		conf_ports_range_except(c, 't', "auto", &c->fwd_in, NULL, NULL,
-					1, NUM_PORTS - 1, NULL, 1, FWD_SCAN);
+		conf_ports_range_except(c, 't', "auto", c->fwd[PIF_HOST],
+					NULL, NULL, 1, NUM_PORTS - 1, NULL, 1,
+					FWD_SCAN);
 	}
 	if (tcp_out_mode == FWD_MODE_AUTO) {
-		conf_ports_range_except(c, 'T', "auto", &c->fwd_out, NULL, "lo",
-					1, NUM_PORTS - 1, NULL, 1, FWD_SCAN);
+		conf_ports_range_except(c, 'T', "auto", c->fwd[PIF_SPLICE],
+					NULL, "lo", 1, NUM_PORTS - 1, NULL, 1,
+					FWD_SCAN);
 	}
 	if (udp_in_mode == FWD_MODE_AUTO) {
-		conf_ports_range_except(c, 'u', "auto", &c->fwd_in, NULL, NULL,
-					1, NUM_PORTS - 1, NULL, 1, FWD_SCAN);
+		conf_ports_range_except(c, 'u', "auto", c->fwd[PIF_HOST],
+					NULL, NULL, 1, NUM_PORTS - 1, NULL, 1,
+					FWD_SCAN);
 	}
 	if (udp_out_mode == FWD_MODE_AUTO) {
-		conf_ports_range_except(c, 'U', "auto", &c->fwd_out, NULL, "lo",
-					1, NUM_PORTS - 1, NULL, 1, FWD_SCAN);
+		conf_ports_range_except(c, 'U', "auto", c->fwd[PIF_SPLICE],
+					NULL, "lo", 1, NUM_PORTS - 1, NULL, 1,
+					FWD_SCAN);
 	}
 
 	if (!c->quiet)
diff --git a/flow.c b/flow.c
index c84857b2..2972ab87 100644
--- a/flow.c
+++ b/flow.c
@@ -503,10 +503,10 @@ struct flowside *flow_target(const struct ctx *c, union flow *flow,
 {
 	char estr[INANY_ADDRSTRLEN], ostr[INANY_ADDRSTRLEN];
 	struct flow_common *f = &flow->f;
+	const struct fwd_table *fwd = c->fwd[f->pif[INISIDE]];
 	const struct flowside *ini = &f->side[INISIDE];
 	struct flowside *tgt = &f->side[TGTSIDE];
 	const struct fwd_rule *rule = NULL;
-	const struct fwd_table *fwd;
 	uint8_t tgtpif = PIF_NONE;
 
 	assert(flow_new_entry == flow && f->state == FLOW_STATE_INI);
@@ -514,6 +514,11 @@ struct flowside *flow_target(const struct ctx *c, union flow *flow,
 	assert(f->pif[INISIDE] != PIF_NONE && f->pif[TGTSIDE] == PIF_NONE);
 	assert(flow->f.state == FLOW_STATE_INI);
 
+	if (fwd) {
+		if (!(rule = fwd_rule_search(fwd, ini, proto, rule_hint)))
+			goto norule;
+	}
+
 	switch (f->pif[INISIDE]) {
 	case PIF_TAP:
 		memcpy(f->tap_omac, MAC_UNDEF, ETH_ALEN);
@@ -521,20 +526,10 @@ struct flowside *flow_target(const struct ctx *c, union flow *flow,
 		break;
 
 	case PIF_SPLICE:
-		fwd = &c->fwd_out;
-
-		if (!(rule = fwd_rule_search(fwd, ini, proto, rule_hint)))
-			goto norule;
-
 		tgtpif = fwd_nat_from_splice(rule, proto, ini, tgt);
 		break;
 
 	case PIF_HOST:
-		fwd = &c->fwd_in;
-
-		if (!(rule = fwd_rule_search(fwd, ini, proto, rule_hint)))
-			goto norule;
-
 		tgtpif = fwd_nat_from_host(c, rule, proto, ini, tgt);
 		fwd_neigh_mac_get(c, &tgt->oaddr, f->tap_omac);
 		break;
@@ -1014,8 +1009,7 @@ static int flow_migrate_source_rollback(struct ctx *c, unsigned bound, int ret)
 
 	debug("...roll back migration");
 
-	if (fwd_listen_sync(c, &c->fwd_in, PIF_HOST,
-			    &c->tcp.scan_in, &c->udp.scan_in) < 0)
+	if (fwd_listen_sync(c, PIF_HOST, &c->tcp.scan_in, &c->udp.scan_in) < 0)
 		die("Failed to re-establish listening sockets");
 
 	foreach_established_tcp_flow(flow) {
@@ -1148,7 +1142,7 @@ int flow_migrate_source(struct ctx *c, const struct migrate_stage *stage,
 	 * fix that is to not allow local to local migration, which arguably we
 	 * should (use namespaces for testing instead). */
 	debug("Stop listen()s");
-	fwd_listen_close(&c->fwd_in);
+	fwd_listen_close(c->fwd[PIF_HOST]);
 
 	debug("Sending %u flows", count);
 
diff --git a/fwd.c b/fwd.c
index 7844a674..3395a28e 100644
--- a/fwd.c
+++ b/fwd.c
@@ -331,6 +331,21 @@ bool fwd_port_is_ephemeral(in_port_t port)
 	return (port >= fwd_ephemeral_min) && (port <= fwd_ephemeral_max);
 }
 
+/* Forwarding table storage, generally accessed via pointers in struct ctx */
+static struct fwd_table fwd_in;
+static struct fwd_table fwd_out;
+
+/**
+ * fwd_rule_init() - Initialise forwarding tables
+ * @c:		Execution context
+ */
+void fwd_rule_init(struct ctx *c)
+{
+	c->fwd[PIF_HOST] = &fwd_in;
+	if (c->mode == MODE_PASTA)
+		c->fwd[PIF_SPLICE] = &fwd_out;
+}
+
 /**
  * fwd_rule_add() - Add a rule to a forwarding table
  * @fwd:	Table to add to
@@ -505,19 +520,17 @@ void fwd_rules_print(const struct fwd_table *fwd)
 
 /** fwd_sync_one() - Create or remove listening sockets for a forward entry
  * @c:		Execution context
- * @fwd:	Forwarding table
- * @idx:	Rule index
  * @pif:	Interface to create listening sockets for
+ * @idx:	Rule index
  * @tcp:	Bitmap of TCP ports to listen for on FWD_SCAN entries
  * @udp:	Bitmap of UDP ports to listen for on FWD_SCAN entries
  *
  * Return: 0 on success, -1 on failure
  */
-static int fwd_sync_one(const struct ctx *c, const struct fwd_table *fwd,
-			unsigned idx, uint8_t pif,
+static int fwd_sync_one(const struct ctx *c, uint8_t pif, unsigned idx,
 			const uint8_t *tcp, const uint8_t *udp)
 {
-	const struct fwd_rule *rule = &fwd->rules[idx];
+	const struct fwd_rule *rule = &c->fwd[pif]->rules[idx];
 	const union inany_addr *addr = fwd_rule_addr(rule);
 	const char *ifname = rule->ifname;
 	const uint8_t *map = NULL;
@@ -598,7 +611,6 @@ static int fwd_sync_one(const struct ctx *c, const struct fwd_table *fwd,
 
 /** struct fwd_listen_args - arguments for fwd_listen_init_()
  * @c:		Execution context
- * @fwd:	Forwarding table
  * @tcpmap:	Bitmap of TCP ports to auto-forward
  * @udpmap:	Bitmap of TCP ports to auto-forward
  * @pif:	Interface to create listening sockets for
@@ -606,7 +618,6 @@ static int fwd_sync_one(const struct ctx *c, const struct fwd_table *fwd,
  */
 struct fwd_listen_args {
 	const struct ctx *c;
-	const struct fwd_table *fwd;
 	const uint8_t *tcpmap, *udpmap;
 	uint8_t pif;
 	int ret;
@@ -625,9 +636,8 @@ static int fwd_listen_sync_(void *arg)
 	if (a->pif == PIF_SPLICE)
 		ns_enter(a->c);
 
-	for (i = 0; i < a->fwd->count; i++) {
-		a->ret = fwd_sync_one(a->c, a->fwd, i, a->pif,
-				      a->tcpmap, a->udpmap);
+	for (i = 0; i < a->c->fwd[a->pif]->count; i++) {
+		a->ret = fwd_sync_one(a->c, a->pif, i, a->tcpmap, a->udpmap);
 		if (a->ret < 0)
 			break;
 	}
@@ -637,21 +647,17 @@ static int fwd_listen_sync_(void *arg)
 
 /** fwd_listen_sync() - Call fwd_listen_sync_() in correct namespace
  * @c:		Execution context
- * @fwd:	Forwarding information
  * @pif:	Interface to create listening sockets for
  * @tcp:	Scanning state for TCP
  * @udp:	Scanning state for UDP
  *
  * Return: 0 on success, -1 on failure
  */
-int fwd_listen_sync(const struct ctx *c, const struct fwd_table *fwd,
-		    uint8_t pif,
+int fwd_listen_sync(const struct ctx *c, uint8_t pif,
 		    const struct fwd_scan *tcp, const struct fwd_scan *udp)
 {
 	struct fwd_listen_args a = {
-		.c = c, .fwd = fwd,
-		.tcpmap = tcp->map, .udpmap = udp->map,
-		.pif = pif,
+		.c = c, .tcpmap = tcp->map, .udpmap = udp->map, .pif = pif,
 	};
 
 	if (pif == PIF_SPLICE)
@@ -695,12 +701,11 @@ void fwd_listen_close(const struct fwd_table *fwd)
  */
 int fwd_listen_init(const struct ctx *c)
 {
-	if (fwd_listen_sync(c, &c->fwd_in, PIF_HOST,
-			    &c->tcp.scan_in, &c->udp.scan_in) < 0)
+	if (fwd_listen_sync(c, PIF_HOST, &c->tcp.scan_in, &c->udp.scan_in) < 0)
 		return -1;
 
 	if (c->mode == MODE_PASTA) {
-		if (fwd_listen_sync(c, &c->fwd_out, PIF_SPLICE,
+		if (fwd_listen_sync(c, PIF_SPLICE,
 				    &c->tcp.scan_out, &c->udp.scan_out) < 0)
 			return -1;
 	}
@@ -851,16 +856,16 @@ static void fwd_scan_ports(struct ctx *c)
 	uint8_t excl_tcp_out[PORT_BITMAP_SIZE], excl_udp_out[PORT_BITMAP_SIZE];
 	uint8_t excl_tcp_in[PORT_BITMAP_SIZE], excl_udp_in[PORT_BITMAP_SIZE];
 
-	current_listen_map(excl_tcp_out, &c->fwd_in, IPPROTO_TCP);
-	current_listen_map(excl_tcp_in, &c->fwd_out, IPPROTO_TCP);
-	current_listen_map(excl_udp_out, &c->fwd_in, IPPROTO_UDP);
-	current_listen_map(excl_udp_in, &c->fwd_out, IPPROTO_UDP);
+	current_listen_map(excl_tcp_out, c->fwd[PIF_HOST], IPPROTO_TCP);
+	current_listen_map(excl_tcp_in, c->fwd[PIF_SPLICE], IPPROTO_TCP);
+	current_listen_map(excl_udp_out, c->fwd[PIF_HOST], IPPROTO_UDP);
+	current_listen_map(excl_udp_in, c->fwd[PIF_SPLICE], IPPROTO_UDP);
 
-	fwd_scan_ports_tcp(&c->fwd_out, &c->tcp.scan_out, excl_tcp_out);
-	fwd_scan_ports_tcp(&c->fwd_in, &c->tcp.scan_in, excl_tcp_in);
-	fwd_scan_ports_udp(&c->fwd_out, &c->udp.scan_out,
+	fwd_scan_ports_tcp(c->fwd[PIF_SPLICE], &c->tcp.scan_out, excl_tcp_out);
+	fwd_scan_ports_tcp(c->fwd[PIF_HOST], &c->tcp.scan_in, excl_tcp_in);
+	fwd_scan_ports_udp(c->fwd[PIF_SPLICE], &c->udp.scan_out,
 			   &c->tcp.scan_out, excl_udp_out);
-	fwd_scan_ports_udp(&c->fwd_in, &c->udp.scan_in,
+	fwd_scan_ports_udp(c->fwd[PIF_HOST], &c->udp.scan_in,
 			   &c->tcp.scan_in, excl_udp_in);
 }
 
@@ -912,10 +917,8 @@ void fwd_scan_ports_timer(struct ctx *c, const struct timespec *now)
 
 	fwd_scan_ports(c);
 
-	fwd_listen_sync(c, &c->fwd_in, PIF_HOST,
-			&c->tcp.scan_in, &c->udp.scan_in);
-	fwd_listen_sync(c, &c->fwd_out, PIF_SPLICE,
-			&c->tcp.scan_out, &c->udp.scan_out);
+	fwd_listen_sync(c, PIF_HOST, &c->tcp.scan_in, &c->udp.scan_in);
+	fwd_listen_sync(c, PIF_SPLICE, &c->tcp.scan_out, &c->udp.scan_out);
 }
 
 /**
diff --git a/fwd.h b/fwd.h
index 958eee25..b387d926 100644
--- a/fwd.h
+++ b/fwd.h
@@ -108,6 +108,7 @@ struct fwd_scan {
 
 #define FWD_PORT_SCAN_INTERVAL		1000	/* ms */
 
+void fwd_rule_init(struct ctx *c);
 void fwd_rule_add(struct fwd_table *fwd, uint8_t proto, uint8_t flags,
 		  const union inany_addr *addr, const char *ifname,
 		  in_port_t first, in_port_t last, in_port_t to);
@@ -119,8 +120,7 @@ void fwd_rules_print(const struct fwd_table *fwd);
 void fwd_scan_ports_init(struct ctx *c);
 void fwd_scan_ports_timer(struct ctx * c, const struct timespec *now);
 
-int fwd_listen_sync(const struct ctx *c, const struct fwd_table *fwd,
-		    uint8_t pif,
+int fwd_listen_sync(const struct ctx *c, uint8_t pif,
 		    const struct fwd_scan *tcp, const struct fwd_scan *udp);
 void fwd_listen_close(const struct fwd_table *fwd);
 int fwd_listen_init(const struct ctx *c);
diff --git a/passt.h b/passt.h
index b614bdf0..62b8dcdf 100644
--- a/passt.h
+++ b/passt.h
@@ -184,8 +184,7 @@ struct ip6_ctx {
  * @pasta_ifn:		Name of namespace interface for pasta
  * @pasta_ifi:		Index of namespace interface for pasta
  * @pasta_conf_ns:	Configure namespace after creating it
- * @fwd_in:		Forwarding table for inbound flows
- * @fwd_out:		Forwarding table for outbound flows
+ * @fwd:		Forwarding tables
  * @no_tcp:		Disable TCP operation
  * @tcp:		Context for TCP protocol handler
  * @no_udp:		Disable UDP operation
@@ -264,8 +263,7 @@ struct ctx {
 	unsigned int pasta_ifi;
 	int pasta_conf_ns;
 
-	struct fwd_table fwd_in;
-	struct fwd_table fwd_out;
+	struct fwd_table *fwd[PIF_NUM_TYPES];
 
 	int no_tcp;
 	struct tcp_ctx tcp;
-- 
2.53.0


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

* [PATCH v3 08/25] fwd: Allow FWD_DUAL_STACK_ANY flag to be passed directly to fwd_rule_add()
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (6 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 07/25] fwd: Store forwarding tables indexed by (origin) pif David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-25  0:54   ` Stefano Brivio
  2026-03-23  7:37 ` [PATCH v3 09/25] fwd, conf: Expose ephemeral ports as bitmap rather than function David Gibson
                   ` (18 subsequent siblings)
  26 siblings, 1 reply; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

fwd_rule_add() takes a flags parameter, but it only allows the FWD_WEAK
and FWD_SCAN flags to be specified there.  It doesn't allow the
FWD_DUAL_STACK_ANY flag to be set, instead expecting a [*] address to be
indicated by passing NULL as @addr.

However, for upcoming dynamic rule updates, it's more convenient to be able
to explicitly pass FWD_DUAL_STACK_ANY along with an address of ::.  Allow
that mode of calling.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 fwd.c | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/fwd.c b/fwd.c
index 3395a28e..d73b7ca7 100644
--- a/fwd.c
+++ b/fwd.c
@@ -362,18 +362,21 @@ void fwd_rule_add(struct fwd_table *fwd, uint8_t proto, uint8_t flags,
 		  in_port_t first, in_port_t last, in_port_t to)
 {
 	/* Flags which can be set from the caller */
-	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN;
+	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
 	unsigned num = (unsigned)last - first + 1;
 	struct fwd_rule *new;
 	unsigned i, port;
 
 	assert(!(flags & ~allowed_flags));
-
 	if (fwd->count >= ARRAY_SIZE(fwd->rules))
 		die("Too many port forwarding ranges");
 	if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks))
 		die("Too many listening sockets");
 
+	/* Passing a non-wildcard address with DUAL_STACK_ANY  is a bug */
+	assert(!(flags & FWD_DUAL_STACK_ANY) || !addr ||
+	       inany_equals(addr, &inany_any6));
+
 	/* Check for any conflicting entries */
 	for (i = 0; i < fwd->count; i++) {
 		char newstr[INANY_ADDRSTRLEN], rulestr[INANY_ADDRSTRLEN];
-- 
2.53.0


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

* [PATCH v3 09/25] fwd, conf: Expose ephemeral ports as bitmap rather than function
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (7 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 08/25] fwd: Allow FWD_DUAL_STACK_ANY flag to be passed directly to fwd_rule_add() David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 10/25] conf: Don't bother complaining about overlapping excluded ranges David Gibson
                   ` (17 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

It turns out the only callers of fwd_port_is_ephemeral() use it to build a
bitmap of ephemeral ports.  So, replace it with fwd_port_map_ephemeral(),
which directly builds that bitmap.  As a bonus this allows a slightly
cheaper implementation of building the map, since inside fwd.c we know that
the ephemeral ports form a single range.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 conf.c |  8 ++------
 fwd.c  | 15 +++++++--------
 fwd.h  |  2 +-
 3 files changed, 10 insertions(+), 15 deletions(-)

diff --git a/conf.c b/conf.c
index 6ca61b74..7a3085b7 100644
--- a/conf.c
+++ b/conf.c
@@ -282,9 +282,7 @@ static void conf_ports(const struct ctx *c, char optname, const char *optarg,
 		*mode = FWD_MODE_ALL;
 
 		/* Exclude ephemeral ports */
-		for (i = 0; i < NUM_PORTS; i++)
-			if (fwd_port_is_ephemeral(i))
-				bitmap_set(exclude, i);
+		fwd_port_map_ephemeral(exclude);
 
 		conf_ports_range_except(c, optname, optarg, fwd,
 					NULL, NULL,
@@ -376,9 +374,7 @@ static void conf_ports(const struct ctx *c, char optname, const char *optarg,
 
 	if (exclude_only) {
 		/* Exclude ephemeral ports */
-		for (i = 0; i < NUM_PORTS; i++)
-			if (fwd_port_is_ephemeral(i))
-				bitmap_set(exclude, i);
+		fwd_port_map_ephemeral(exclude);
 
 		conf_ports_range_except(c, optname, optarg, fwd,
 					addr, ifname,
diff --git a/fwd.c b/fwd.c
index d73b7ca7..cfe89362 100644
--- a/fwd.c
+++ b/fwd.c
@@ -319,16 +319,15 @@ static const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
 }
 
 /**
- * fwd_port_is_ephemeral() - Is port number ephemeral?
- * @port:	Port number
- *
- * Return: true if @port is ephemeral, that is may be allocated by the kernel as
- *         a local port for outgoing connections or datagrams, but should not be
- *         used for binding services to.
+ * fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap
+ * @map:	Bitmap to update
  */
-bool fwd_port_is_ephemeral(in_port_t port)
+void fwd_port_map_ephemeral(uint8_t *map)
 {
-	return (port >= fwd_ephemeral_min) && (port <= fwd_ephemeral_max);
+	unsigned port;
+
+	for (port = fwd_ephemeral_min; port <= fwd_ephemeral_max; port++)
+		bitmap_set(map, port);
 }
 
 /* Forwarding table storage, generally accessed via pointers in struct ctx */
diff --git a/fwd.h b/fwd.h
index b387d926..f111e139 100644
--- a/fwd.h
+++ b/fwd.h
@@ -22,7 +22,7 @@ struct flowside;
 #define	NUM_PORTS	(1U << 16)
 
 void fwd_probe_ephemeral(void);
-bool fwd_port_is_ephemeral(in_port_t port);
+void fwd_port_map_ephemeral(uint8_t *map);
 
 /**
  * struct fwd_rule - Forwarding rule governing a range of ports
-- 
2.53.0


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

* [PATCH v3 10/25] conf: Don't bother complaining about overlapping excluded ranges
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (8 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 09/25] fwd, conf: Expose ephemeral ports as bitmap rather than function David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 11/25] conf: Move check for mapping port 0 to caller David Gibson
                   ` (16 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

In conf_ports() we die with an error if a port specification includes
overlapping excluded ranges.  This is contrary to our usual convention of
handling conflicting options (last option wins, rather than error).  Plus,
these options don't even conflict, they're just redundant.

This behaviour potentially makes life harder for scripts or other tools
invoking pasta - if they might have the same port excluded for multiple
different reasons, they have to explicitly deduplicate the list, rather
than just list everything on the command line.  So, don't give this error,
let a port be excluded as many times as you like.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 conf.c | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/conf.c b/conf.c
index 7a3085b7..e5baf8ee 100644
--- a/conf.c
+++ b/conf.c
@@ -355,12 +355,8 @@ static void conf_ports(const struct ctx *c, char optname, const char *optarg,
 		if ((*p != '\0')  && (*p != ',')) /* Garbage after the range */
 			goto bad;
 
-		for (i = xrange.first; i <= xrange.last; i++) {
-			if (bitmap_isset(exclude, i))
-				die("Overlapping excluded ranges %s", optarg);
-
+		for (i = xrange.first; i <= xrange.last; i++)
 			bitmap_set(exclude, i);
-		}
 	} while ((p = next_chunk(p, ',')));
 
 	if (ifname && c->no_bindtodevice) {
-- 
2.53.0


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

* [PATCH v3 11/25] conf: Move check for mapping port 0 to caller
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (9 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 10/25] conf: Don't bother complaining about overlapping excluded ranges David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 12/25] conf: Move check for disabled interfaces earlier David Gibson
                   ` (15 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

conf_ports_range_except() checks that it hasn't been asked to map port 0,
which generally won't work.  However, there's only one callsite that
doesn't statically pass a non-zero value here.  Change the check to an
assert() and move the actual error message to that callsite.  This will
allow further cleanups later.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 conf.c | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/conf.c b/conf.c
index e5baf8ee..dc9468c2 100644
--- a/conf.c
+++ b/conf.c
@@ -151,10 +151,7 @@ static void conf_ports_range_except(const struct ctx *c, char optname,
 	unsigned base, i;
 	uint8_t proto;
 
-	if (first == 0) {
-		die("Can't forward port 0 for option '-%c %s'",
-		    optname, optarg);
-	}
+	assert(first != 0);
 
 	if (optname == 't' || optname == 'T')
 		proto = IPPROTO_TCP;
@@ -404,6 +401,11 @@ static void conf_ports(const struct ctx *c, char optname, const char *optarg,
 		if ((*p != '\0')  && (*p != ',')) /* Garbage after the ranges */
 			goto bad;
 
+		if (orig_range.first == 0) {
+			die("Can't forward port 0 for option '-%c %s'",
+			    optname, optarg);
+		}
+
 		conf_ports_range_except(c, optname, optarg, fwd,
 					addr, ifname,
 					orig_range.first, orig_range.last,
-- 
2.53.0


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

* [PATCH v3 12/25] conf: Move check for disabled interfaces earlier
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (10 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 11/25] conf: Move check for mapping port 0 to caller David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 13/25] pesto: Introduce stub configuration interface and tool David Gibson
                   ` (14 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

conf_ports_range_except() generates errors if asked to map an address of
a family that's not available.  This is quite late to perform the check,
most callsites pass a NULL address, making the test moot, the one remaining
can check the address earlier.  Move the check there, simplifying the
lower level function.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 conf.c | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/conf.c b/conf.c
index dc9468c2..d4c2a013 100644
--- a/conf.c
+++ b/conf.c
@@ -160,16 +160,6 @@ static void conf_ports_range_except(const struct ctx *c, char optname,
 	else
 		assert(0);
 
-	if (addr) {
-		if (!c->ifi4 && inany_v4(addr)) {
-			die("IPv4 is disabled, can't use -%c %s",
-			    optname, optarg);
-		} else if (!c->ifi6 && !inany_v4(addr)) {
-			die("IPv6 is disabled, can't use -%c %s",
-			    optname, optarg);
-		}
-	}
-
 	for (base = first; base <= last; base++) {
 		if (exclude && bitmap_isset(exclude, base))
 			continue;
@@ -335,6 +325,16 @@ static void conf_ports(const struct ctx *c, char optname, const char *optarg,
 		addr = NULL;
 	}
 
+	if (addr) {
+		if (!c->ifi4 && inany_v4(addr)) {
+			die("IPv4 is disabled, can't use -%c %s",
+			    optname, optarg);
+		} else if (!c->ifi6 && !inany_v4(addr)) {
+			die("IPv6 is disabled, can't use -%c %s",
+			    optname, optarg);
+		}
+	}
+
 	/* Mark all exclusions first, they might be given after base ranges */
 	p = spec;
 	do {
-- 
2.53.0


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

* [PATCH v3 13/25] pesto: Introduce stub configuration interface and tool
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (11 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 12/25] conf: Move check for disabled interfaces earlier David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-25  0:54   ` Stefano Brivio
  2026-03-23  7:37 ` [PATCH v3 14/25] pesto: Add command line option parsing and debug messages David Gibson
                   ` (13 subsequent siblings)
  26 siblings, 1 reply; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Add a control socket to passt/pasta for updating configuration at runtime.
Add a tool (pesto) for communicating with the control socket.

For now this is a stub implementation that forms the connection and sends
some version information, but nothing more.
Example:
  ./pasta --debug --config-net -c /tmp/pasta.conf -t none
  ./pesto /tmp/pasta.conf

Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
[dwg: Based on an early draft from Stefano]
Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 .gitignore   |   2 +
 Makefile     |  30 +++++++----
 common.h     |  14 +++++
 conf.c       | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 conf.h       |   2 +
 epoll_type.h |   4 ++
 log.c        |   1 +
 passt.1      |   5 ++
 passt.c      |   9 ++++
 passt.h      |   6 +++
 pesto.1      |  46 ++++++++++++++++
 pesto.c      | 113 ++++++++++++++++++++++++++++++++++++++
 pesto.h      |  34 ++++++++++++
 serialise.c  |   3 ++
 util.h       |   3 --
 15 files changed, 407 insertions(+), 15 deletions(-)
 create mode 100644 common.h
 create mode 100644 pesto.1
 create mode 100644 pesto.c
 create mode 100644 pesto.h

diff --git a/.gitignore b/.gitignore
index 3c16adc7..3e40d9f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,9 +4,11 @@
 /pasta
 /pasta.avx2
 /passt-repair
+/pesto
 /qrap
 /pasta.1
 /seccomp.h
+/seccomp_pesto.h
 /seccomp_repair.h
 /c*.json
 README.plain.md
diff --git a/Makefile b/Makefile
index 5b6891d7..d085c9c1 100644
--- a/Makefile
+++ b/Makefile
@@ -44,17 +44,19 @@ PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
 	udp.c udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
 QRAP_SRCS = qrap.c
 PASST_REPAIR_SRCS = passt-repair.c
-SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS)
+PESTO_SRCS = pesto.c serialise.c
+SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
 
-MANPAGES = passt.1 pasta.1 qrap.1 passt-repair.1
+MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
 
+PESTO_HEADERS = common.h pesto.h serialise.h
 PASST_HEADERS = arch.h arp.h checksum.h conf.h dhcp.h dhcpv6.h epoll_ctl.h \
 	flow.h fwd.h flow_table.h icmp.h icmp_flow.h inany.h iov.h ip.h \
 	isolation.h lineread.h log.h migrate.h ndp.h netlink.h packet.h \
-	passt.h pasta.h pcap.h pif.h repair.h serialise.h siphash.h tap.h tcp.h \
-	tcp_buf.h tcp_conn.h tcp_internal.h tcp_splice.h tcp_vu.h udp.h \
-	udp_flow.h udp_internal.h udp_vu.h util.h vhost_user.h virtio.h \
-	vu_common.h
+	passt.h pasta.h pcap.h pif.h repair.h siphash.h tap.h tcp.h tcp_buf.h \
+	tcp_conn.h tcp_internal.h tcp_splice.h tcp_vu.h udp.h udp_flow.h \
+	udp_internal.h udp_vu.h util.h vhost_user.h virtio.h vu_common.h \
+	$(PESTO_HEADERS)
 HEADERS = $(PASST_HEADERS) seccomp.h
 
 C := \#include <sys/random.h>\nint main(){int a=getrandom(0, 0, 0);}
@@ -75,9 +77,9 @@ mandir		?= $(datarootdir)/man
 man1dir		?= $(mandir)/man1
 
 ifeq ($(TARGET_ARCH),x86_64)
-BIN := passt passt.avx2 pasta pasta.avx2 qrap passt-repair
+BIN := passt passt.avx2 pasta pasta.avx2 qrap passt-repair pesto
 else
-BIN := passt pasta qrap passt-repair
+BIN := passt pasta qrap passt-repair pesto
 endif
 
 all: $(BIN) $(MANPAGES) docs
@@ -91,6 +93,9 @@ seccomp.h: seccomp.sh $(PASST_SRCS) $(PASST_HEADERS)
 seccomp_repair.h: seccomp.sh $(PASST_REPAIR_SRCS)
 	@ ARCH="$(TARGET_ARCH)" CC="$(CC)" ./seccomp.sh seccomp_repair.h $(PASST_REPAIR_SRCS)
 
+seccomp_pesto.h: seccomp.sh $(PESTO_SRCS)
+	@ ARCH="$(TARGET_ARCH)" CC="$(CC)" ./seccomp.sh seccomp_pesto.h $(PESTO_SRCS)
+
 passt: $(PASST_SRCS) $(HEADERS)
 	$(CC) $(FLAGS) $(CFLAGS) $(CPPFLAGS) $(PASST_SRCS) -o passt $(LDFLAGS)
 
@@ -110,6 +115,9 @@ qrap: $(QRAP_SRCS) passt.h
 passt-repair: $(PASST_REPAIR_SRCS) seccomp_repair.h
 	$(CC) $(FLAGS) $(CFLAGS) $(CPPFLAGS) $(PASST_REPAIR_SRCS) -o passt-repair $(LDFLAGS)
 
+pesto: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
+	$(CC) $(FLAGS) $(CFLAGS) $(CPPFLAGS) $(PESTO_SRCS) -o pesto $(LDFLAGS)
+
 valgrind: EXTRA_SYSCALLS += rt_sigprocmask rt_sigtimedwait rt_sigaction	\
 			    rt_sigreturn getpid gettid kill clock_gettime \
 			    mmap|mmap2 munmap open unlink gettimeofday futex \
@@ -119,7 +127,7 @@ valgrind: all
 
 .PHONY: clean
 clean:
-	$(RM) $(BIN) *~ *.o seccomp.h seccomp_repair.h pasta.1 \
+	$(RM) $(BIN) *~ *.o seccomp.h seccomp_repair.h seccomp_pesto.h pasta.1 \
 		passt.tar passt.tar.gz *.deb *.rpm \
 		passt.pid README.plain.md
 
@@ -173,11 +181,11 @@ docs: README.md
 		done < README.md;					\
 	) > README.plain.md
 
-clang-tidy: $(PASST_SRCS)
+clang-tidy: $(PASST_SRCS) $(PESTO_SRCS)
 	clang-tidy $^ -- $(filter-out -pie,$(FLAGS) $(CFLAGS) $(CPPFLAGS)) \
 	           -DCLANG_TIDY_58992
 
-cppcheck: $(PASST_SRCS) $(HEADERS)
+cppcheck: $(PASST_SRCS) $(PESTO_SRCS) $(HEADERS)
 	if cppcheck --check-level=exhaustive /dev/null > /dev/null 2>&1; then \
 		CPPCHECK_EXHAUSTIVE="--check-level=exhaustive";		\
 	else								\
diff --git a/common.h b/common.h
new file mode 100644
index 00000000..76a95609
--- /dev/null
+++ b/common.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright Red Hat
+ * Author: David Gibson <david@gibson.dropbear.id.au>
+ *
+ * Definitions used by both passt/pasta and other tools
+ */
+
+#ifndef COMMON_H
+#define COMMON_H
+
+/* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
+#define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
+
+#endif /* _COMMON_H */
diff --git a/conf.c b/conf.c
index d4c2a013..5e8bc665 100644
--- a/conf.c
+++ b/conf.c
@@ -35,6 +35,7 @@
 #include <netinet/in.h>
 #include <netinet/if_ether.h>
 
+#include "common.h"
 #include "util.h"
 #include "ip.h"
 #include "passt.h"
@@ -47,6 +48,10 @@
 #include "isolation.h"
 #include "log.h"
 #include "vhost_user.h"
+#include "epoll_ctl.h"
+#include "conf.h"
+#include "pesto.h"
+#include "serialise.h"
 
 #define NETNS_RUN_DIR	"/run/netns"
 
@@ -888,6 +893,7 @@ static void usage(const char *name, FILE *f, int status)
 		"  --runas UID|UID:GID 	Run as given UID, GID, which can be\n"
 		"    numeric, or login and group names\n"
 		"    default: drop to user \"nobody\"\n"
+		"  -c, --conf-path PATH	Configuration socket path\n"
 		"  -h, --help		Display this help message and exit\n"
 		"  --version		Show version and exit\n");
 
@@ -1425,6 +1431,17 @@ static void conf_open_files(struct ctx *c)
 		if (c->pidfile_fd < 0)
 			die_perror("Couldn't open PID file %s", c->pidfile);
 	}
+
+	c->fd_conf = -1;
+	if (*c->conf_path) {
+		c->fd_conf_listen = sock_unix(c->conf_path);
+		if (c->fd_conf_listen < 0) {
+			die_perror("Couldn't open control socket %s",
+				   c->conf_path);
+		}
+	} else {
+		c->fd_conf_listen = -1;
+	}
 }
 
 /**
@@ -1460,6 +1477,25 @@ fail:
 	die("Invalid MAC address: %s", str);
 }
 
+/**
+ * conf_sock_listen() - Start listening for connections on configuration socket
+ * @c:		Execution context
+ */
+static void conf_sock_listen(const struct ctx *c)
+{
+	union epoll_ref ref = { .type = EPOLL_TYPE_CONF_LISTEN };
+
+	if (c->fd_conf_listen < 0)
+		return;
+
+	if (listen(c->fd_conf_listen, 0))
+		die_perror("Couldn't listen on configuration socket");
+
+	ref.fd = c->fd_conf_listen;
+	if (epoll_add(c->epollfd, EPOLLIN | EPOLLET, ref))
+		die_perror("Couldn't add configuration socket to epoll");
+}
+
 /**
  * conf() - Process command-line arguments and set configuration
  * @c:		Execution context
@@ -1542,9 +1578,10 @@ void conf(struct ctx *c, int argc, char **argv)
 		{"migrate-exit", no_argument,		NULL,		29 },
 		{"migrate-no-linger", no_argument,	NULL,		30 },
 		{"stats", required_argument,		NULL,		31 },
+		{"conf-path",	required_argument,	NULL,		'c' },
 		{ 0 },
 	};
-	const char *optstring = "+dqfel:hs:F:I:p:P:m:a:n:M:g:i:o:D:S:H:461t:u:T:U:";
+	const char *optstring = "+dqfel:hs:c:F:I:p:P:m:a:n:M:g:i:o:D:S:H:461t:u:T:U:";
 	const char *logname = (c->mode == MODE_PASTA) ? "pasta" : "passt";
 	char userns[PATH_MAX] = { 0 }, netns[PATH_MAX] = { 0 };
 	bool copy_addrs_opt = false, copy_routes_opt = false;
@@ -1808,6 +1845,13 @@ void conf(struct ctx *c, int argc, char **argv)
 
 			c->fd_tap = -1;
 			break;
+		case 'c':
+			ret = snprintf(c->conf_path, sizeof(c->conf_path), "%s",
+				       optarg);
+			if (ret <= 0 || ret >= (int)sizeof(c->sock_path))
+				die("Invalid configuration path: %s", optarg);
+			c->fd_conf_listen = c->fd_conf = -1;
+			break;
 		case 'F':
 			errno = 0;
 			fd_tap_opt = strtol(optarg, NULL, 0);
@@ -2252,4 +2296,108 @@ void conf(struct ctx *c, int argc, char **argv)
 
 	if (!c->quiet)
 		conf_print(c);
+
+	conf_sock_listen(c);
+}
+
+/**
+ * conf_listen_handler() - Handle events on configuration listening socket
+ * @c:		Execution context
+ * @events:	epoll events
+ */
+void conf_listen_handler(struct ctx *c, uint32_t events)
+{
+	struct pesto_hello hello = {
+		.magic = PESTO_SERVER_MAGIC,
+		.version = htonl(PESTO_PROTOCOL_VERSION),
+	};
+	union epoll_ref ref = { .type = EPOLL_TYPE_CONF };
+	struct ucred uc = { 0 };
+	socklen_t len = sizeof(uc);
+	int fd, rc;
+
+	if (events != EPOLLIN) {
+		err("Unexpected event 0x%04x on configuration socket", events);
+		return;
+	}
+
+	fd = accept4(c->fd_conf_listen, NULL, NULL, SOCK_NONBLOCK);
+	if (fd < 0) {
+		warn_perror("accept4() on configuration listening socket");
+		return;
+	}
+
+	if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &uc, &len) < 0)
+		warn_perror("Can't get configuration client credentials");
+
+	/* Another client is already connected: accept and close right away. */
+	if (c->fd_conf != -1) {
+		info("Discarding configuration client, PID %i", uc.pid);
+		goto fail;
+	}
+
+	ref.fd = fd;
+	rc = epoll_add(c->epollfd, EPOLLIN | EPOLLET, ref);
+	if (rc < 0) {
+		warn_perror("epoll_ctl() on configuration socket");
+		goto fail;
+	}
+
+	rc = write_all_buf(fd, &hello, sizeof(hello));
+	if (rc < 0) {
+		warn_perror("Error writing configuration protocol hello");
+		goto fail;
+	}
+
+	c->fd_conf = fd;
+	info("Accepted configuration client, PID %i", uc.pid);
+	if (!PESTO_PROTOCOL_VERSION) {
+		warn(
+"Warning: Using experimental unsupported configuration protocol");
+	}
+
+	return;
+
+fail:
+	close(fd);
+}
+
+/**
+ * conf_handler() - Handle events on configuration socket
+ * @c:		Execution context
+ * @events:	epoll events
+ */
+void conf_handler(struct ctx *c, uint32_t events)
+{
+	if (events & EPOLLIN) {
+		char discard[BUFSIZ];
+		ssize_t n;
+
+		do {
+			n = read(c->fd_conf, discard, sizeof(discard));
+			if (n > 0)
+				debug("Discarded %zd bytes of config data", n);
+		} while (n > 0);
+		if (n == 0) {
+			debug("Configuration client EOF");
+			goto close;
+		}
+		if (errno != EAGAIN && errno != EWOULDBLOCK) {
+			err_perror("Error reading config data");
+			goto close;
+		}
+	}
+
+	if (events & EPOLLHUP) {
+		debug("Configuration client hangup");
+		goto close;
+	}
+
+	return;
+
+close:
+	debug("Closing configuration socket");
+	epoll_ctl(c->epollfd, EPOLL_CTL_DEL, c->fd_conf, NULL);
+	close(c->fd_conf);
+	c->fd_conf = -1;
 }
diff --git a/conf.h b/conf.h
index b45ad746..16f97189 100644
--- a/conf.h
+++ b/conf.h
@@ -8,5 +8,7 @@
 
 enum passt_modes conf_mode(int argc, char *argv[]);
 void conf(struct ctx *c, int argc, char **argv);
+void conf_listen_handler(struct ctx *c, uint32_t events);
+void conf_handler(struct ctx *c, uint32_t events);
 
 #endif /* CONF_H */
diff --git a/epoll_type.h b/epoll_type.h
index a90ffb67..061325aa 100644
--- a/epoll_type.h
+++ b/epoll_type.h
@@ -46,6 +46,10 @@ enum epoll_type {
 	EPOLL_TYPE_REPAIR,
 	/* Netlink neighbour subscription socket */
 	EPOLL_TYPE_NL_NEIGH,
+	/* Configuration listening socket */
+	EPOLL_TYPE_CONF_LISTEN,
+	/* Configuration socket */
+	EPOLL_TYPE_CONF,
 
 	EPOLL_NUM_TYPES,
 };
diff --git a/log.c b/log.c
index 21e3673e..99404e25 100644
--- a/log.c
+++ b/log.c
@@ -26,6 +26,7 @@
 #include <stdarg.h>
 #include <sys/socket.h>
 
+#include "common.h"
 #include "linux_dep.h"
 #include "log.h"
 #include "util.h"
diff --git a/passt.1 b/passt.1
index 13e8df9d..32d70c8d 100644
--- a/passt.1
+++ b/passt.1
@@ -127,6 +127,11 @@ login name and group name can be passed. This requires privileges (either
 initial effective UID 0 or CAP_SETUID capability) to work.
 Default is to change to user \fInobody\fR if started as root.
 
+.TP
+.BR \-c ", " \-\-conf-path " " \fIpath " " (EXPERIMENTAL)
+Path for configuration and control socket used by \fBpesto\fR(1) to
+dynamically update passt or pasta's configuration.
+
 .TP
 .BR \-h ", " \-\-help
 Display a help message and exit.
diff --git a/passt.c b/passt.c
index f84419c7..d37beef3 100644
--- a/passt.c
+++ b/passt.c
@@ -36,6 +36,7 @@
 #include <netinet/if_ether.h>
 #include <libgen.h>
 
+#include "common.h"
 #include "util.h"
 #include "passt.h"
 #include "dhcp.h"
@@ -80,6 +81,8 @@ char *epoll_type_str[] = {
 	[EPOLL_TYPE_REPAIR_LISTEN]	= "TCP_REPAIR helper listening socket",
 	[EPOLL_TYPE_REPAIR]		= "TCP_REPAIR helper socket",
 	[EPOLL_TYPE_NL_NEIGH]		= "netlink neighbour notifier socket",
+	[EPOLL_TYPE_CONF_LISTEN]	= "configuration listening socket",
+	[EPOLL_TYPE_CONF]		= "configuration socket",
 };
 static_assert(ARRAY_SIZE(epoll_type_str) == EPOLL_NUM_TYPES,
 	      "epoll_type_str[] doesn't match enum epoll_type");
@@ -303,6 +306,12 @@ static void passt_worker(void *opaque, int nfds, struct epoll_event *events)
 		case EPOLL_TYPE_NL_NEIGH:
 			nl_neigh_notify_handler(c);
 			break;
+		case EPOLL_TYPE_CONF_LISTEN:
+			conf_listen_handler(c, eventmask);
+			break;
+		case EPOLL_TYPE_CONF:
+			conf_handler(c, eventmask);
+			break;
 		default:
 			/* Can't happen */
 			assert(0);
diff --git a/passt.h b/passt.h
index 62b8dcdf..c38bb5ae 100644
--- a/passt.h
+++ b/passt.h
@@ -158,6 +158,7 @@ struct ip6_ctx {
  * @foreground:		Run in foreground, don't log to stderr by default
  * @nofile:		Maximum number of open files (ulimit -n)
  * @sock_path:		Path for UNIX domain socket
+ * @conf_path:		Path for configuration UNIX domain socket
  * @repair_path:	TCP_REPAIR helper path, can be "none", empty for default
  * @pcap:		Path for packet capture file
  * @pidfile:		Path to PID file, empty string if not configured
@@ -169,6 +170,8 @@ struct ip6_ctx {
  * @epollfd:		File descriptor for epoll instance
  * @fd_tap_listen:	File descriptor for listening AF_UNIX socket, if any
  * @fd_tap:		AF_UNIX socket, tuntap device, or pre-opened socket
+ * @fd_conf_listen:	Listening configuration socket, if any
+ * @fd_conf:		Configuration socket, if any
  * @fd_repair_listen:	File descriptor for listening TCP_REPAIR socket, if any
  * @fd_repair:		Connected AF_UNIX socket for TCP_REPAIR helper
  * @our_tap_mac:	Pasta/passt's MAC on the tap link
@@ -223,6 +226,7 @@ struct ctx {
 	int foreground;
 	int nofile;
 	char sock_path[UNIX_PATH_MAX];
+	char conf_path[UNIX_PATH_MAX];
 	char repair_path[UNIX_PATH_MAX];
 	char pcap[PATH_MAX];
 
@@ -240,6 +244,8 @@ struct ctx {
 	int epollfd;
 	int fd_tap_listen;
 	int fd_tap;
+	int fd_conf_listen;
+	int fd_conf;
 	int fd_repair_listen;
 	int fd_repair;
 	unsigned char our_tap_mac[ETH_ALEN];
diff --git a/pesto.1 b/pesto.1
new file mode 100644
index 00000000..338fb8a6
--- /dev/null
+++ b/pesto.1
@@ -0,0 +1,46 @@
+.\" SPDX-License-Identifier: GPL-2.0-or-later
+.\" Copyright Red Hat
+.\" Author: David Gibson <david@gibson.dropbear.id.au>
+.TH pesto 1
+
+.SH NAME
+.B pesto
+\- Configure a running \fBpasst\fR(1) or \fBpasta\fR(1) instance.
+
+.SH SYNOPSIS
+.B pesto
+\fIPATH\fR
+
+.SH DESCRIPTION
+
+.B pesto
+is an experimental client to view and update the port forwarding
+configuration of a running \fBpasst\fR(1) or \fBpasta\fR(1) instance.
+
+\fIPATH\fR gives the path to the UNIX domain socket created by
+\fBpasst\fR or \fBpasta\fR.  It should match the \fB-c\fR command line
+option given to that instance.
+
+.SH AUTHORS
+
+Stefano Brivio <sbrivio@redhat.com>,
+David Gibson <david@gibson.dropbear.id.au>.
+
+.SH REPORTING BUGS
+
+Please report issues on the bug tracker at https://bugs.passt.top/, or
+send a message to the passt-user@passt.top mailing list, see
+https://lists.passt.top/.
+
+.SH COPYRIGHT
+
+Copyright Red Hat
+
+\fBpesto\fR is free software: you can redistribute them and/or modify
+them under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or (at
+your option) any later version.
+
+.SH SEE ALSO
+
+\fBpasst\fR(1), \fBpasta\fR(1), \fBunix\fR(7).
diff --git a/pesto.c b/pesto.c
new file mode 100644
index 00000000..6d066084
--- /dev/null
+++ b/pesto.c
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/* PESTO - Programmable Extensible Socket Translation Orchestrator
+ *  front-end for passt(1) and pasta(1) forwarding configuration
+ *
+ * pesto.c - Main program (it's not actually extensible)
+ *
+ * Copyright (c) 2026 Red Hat GmbH
+ * Author: Stefano Brivio <sbrivio@redhat.com>
+ */
+
+#include <arpa/inet.h>
+#include <sys/prctl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <unistd.h>
+
+#include <linux/audit.h>
+#include <linux/capability.h>
+#include <linux/filter.h>
+#include <linux/seccomp.h>
+
+#include "common.h"
+#include "seccomp_pesto.h"
+#include "serialise.h"
+#include "pesto.h"
+
+#define die(...)							\
+	do {								\
+		FPRINTF(stderr, __VA_ARGS__);				\
+		FPRINTF(stderr, "\n");					\
+		exit(EXIT_FAILURE);					\
+	} while (0)
+
+/**
+ * main() - Entry point and whole program with loop
+ * @argc:	Argument count
+ * @argv:	Arguments: socket path, operation, port specifiers
+ *
+ * Return: 0 on success, won't return on failure
+ *
+ * #syscalls:pesto connect write close exit_group fstat brk
+ * #syscalls:pesto socket s390x:socketcall i686:socketcall
+ * #syscalls:pesto recvfrom recvmsg arm:recv ppc64le:recv
+ * #syscalls:pesto sendto sendmsg arm:send ppc64le:send
+ */
+int main(int argc, char **argv)
+{
+	struct sockaddr_un a = { AF_UNIX, "" };
+	struct pesto_hello hello;
+	struct sock_fprog prog;
+	uint32_t s_version;
+	int ret, s;
+
+	prctl(PR_SET_DUMPABLE, 0);
+
+	prog.len = (unsigned short)sizeof(filter_pesto) /
+				   sizeof(filter_pesto[0]);
+	prog.filter = filter_pesto;
+	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) ||
+	    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
+		die("Failed to apply seccomp filter");
+
+	if (argc < 2)
+		die("Usage: %s CONTROLPATH", argv[0]);
+
+	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
+		die("Failed to create AF_UNIX socket: %s", strerror(errno));
+
+	ret = snprintf(a.sun_path, sizeof(a.sun_path), "%s", argv[1]);
+	if (ret <= 0 || ret >= (int)sizeof(a.sun_path))
+		die("Invalid socket path \"%s\"", argv[1]);
+
+	ret = connect(s, (struct sockaddr *)&a, sizeof(a));
+	if (ret < 0) {
+		die("Failed to connect to %s: %s",
+		    a.sun_path, strerror(errno));
+	}
+
+	ret = read_all_buf(s, &hello, sizeof(hello));
+	if (ret < 0)
+		die("Couldn't read server greeting: %s", strerror(errno));
+
+	if (memcmp(hello.magic, PESTO_SERVER_MAGIC, sizeof(hello.magic)))
+		die("Bad magic number from server");
+
+	s_version = ntohl(hello.version);
+
+	if (s_version > PESTO_PROTOCOL_VERSION) {
+		die("Unknown server protocol version %"PRIu32" > %"PRIu32"\n",
+		    s_version, PESTO_PROTOCOL_VERSION);
+	}
+
+	/* cppcheck-suppress knownConditionTrueFalse */
+	if (!s_version) {
+		if (PESTO_PROTOCOL_VERSION)
+			die("Unsupported experimental server protocol");
+		FPRINTF(stderr,
+"Warning: Using experimental protocol version, client and server must match\n");
+	}
+
+	exit(0);
+}
diff --git a/pesto.h b/pesto.h
new file mode 100644
index 00000000..92d4df3a
--- /dev/null
+++ b/pesto.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright Red Hat
+ * Author: David Gibson <david@gibson.dropbear.id.au>
+ *
+ * Definitions and functions used by both client and server of the configuration
+ * update protocol (pesto).
+ */
+
+#ifndef PESTO_H
+#define PESTO_H
+
+#include <assert.h>
+#include <stdint.h>
+
+#define PESTO_SERVER_MAGIC	"pesto:s"
+
+/* Version 0 is reserved for unreleased / unsupported experimental versions */
+#define PESTO_PROTOCOL_VERSION	0
+
+/**
+ * struct pesto_hello - Server introduction message
+ * @magic:	PESTO_SERVER_MAGIC
+ * @version:	Version number
+ */
+struct pesto_hello {
+	char magic[8];
+	uint32_t version;
+} __attribute__ ((__packed__));
+
+static_assert(sizeof(PESTO_SERVER_MAGIC)
+	      == sizeof(((struct pesto_hello *)0)->magic),
+	      "PESTO_SERVER_MAGIC has wrong size");
+
+#endif /* PESTO_H */
diff --git a/serialise.c b/serialise.c
index d6c3396a..e3ea86e3 100644
--- a/serialise.c
+++ b/serialise.c
@@ -6,6 +6,9 @@
  * PASTA - Pack A Subtle Tap Abstraction
  *  for network namespace/tap device mode
  *
+ * PESTO - Programmable Extensible Socket Translation Orchestrator
+ *  front-end for passt(1) and pasta(1) forwarding configuration
+ *
  * serialise.c - Serialisation of data structures over bytestreams
  *
  * Copyright Red Hat
diff --git a/util.h b/util.h
index cb669105..e7993f4d 100644
--- a/util.h
+++ b/util.h
@@ -317,9 +317,6 @@ static inline bool mod_between(unsigned x, unsigned i, unsigned j, unsigned m)
 	return mod_sub(x, i, m) < mod_sub(j, i, m);
 }
 
-/* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
-#define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
-
 void raw_random(void *buf, size_t buflen);
 
 /*
-- 
2.53.0


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

* [PATCH v3 14/25] pesto: Add command line option parsing and debug messages
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (12 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 13/25] pesto: Introduce stub configuration interface and tool David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-25  0:55   ` Stefano Brivio
  2026-03-23  7:37 ` [PATCH v3 15/25] pesto: Expose list of pifs to pesto David Gibson
                   ` (12 subsequent siblings)
  26 siblings, 1 reply; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Add basic command line option parsing using getopt_long() to pesto.
Implement --help, --version, --quiet and --verbose options, along with a
debug() macro to print debugging messages when given the verbose option.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 common.h |  8 ++++++
 conf.c   | 45 +++++++++++++++++----------------
 passt.h  | 12 ++++-----
 pesto.c  | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++------
 util.h   |  8 ------
 5 files changed, 108 insertions(+), 42 deletions(-)

diff --git a/common.h b/common.h
index 76a95609..927d20a4 100644
--- a/common.h
+++ b/common.h
@@ -8,6 +8,14 @@
 #ifndef COMMON_H
 #define COMMON_H
 
+#define VERSION_BLOB							       \
+	VERSION "\n"							       \
+	"Copyright Red Hat\n"						       \
+	"GNU General Public License, version 2 or later\n"		       \
+	"  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>\n"	       \
+	"This is free software: you are free to change and redistribute it.\n" \
+	"There is NO WARRANTY, to the extent permitted by law.\n\n"
+
 /* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
 #define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
 
diff --git a/conf.c b/conf.c
index 5e8bc665..7b960fe9 100644
--- a/conf.c
+++ b/conf.c
@@ -1140,6 +1140,9 @@ static void conf_print(const struct ctx *c)
 	char bufmac[ETH_ADDRSTRLEN], ifn[IFNAMSIZ];
 	int i;
 
+	if (c->fd_control_listen >= 0)
+		info("Configuration socket: %s", c->control_path);
+
 	if (c->ifi4 > 0 || c->ifi6 > 0) {
 		info("Template interface: %s%s%s%s%s",
 		     c->ifi4 > 0 ? if_indextoname(c->ifi4, ifn) : "",
@@ -1432,15 +1435,15 @@ static void conf_open_files(struct ctx *c)
 			die_perror("Couldn't open PID file %s", c->pidfile);
 	}
 
-	c->fd_conf = -1;
-	if (*c->conf_path) {
-		c->fd_conf_listen = sock_unix(c->conf_path);
-		if (c->fd_conf_listen < 0) {
+	c->fd_control = -1;
+	if (*c->control_path) {
+		c->fd_control_listen = sock_unix(c->control_path);
+		if (c->fd_control_listen < 0) {
 			die_perror("Couldn't open control socket %s",
-				   c->conf_path);
+				   c->control_path);
 		}
 	} else {
-		c->fd_conf_listen = -1;
+		c->fd_control_listen = -1;
 	}
 }
 
@@ -1485,13 +1488,13 @@ static void conf_sock_listen(const struct ctx *c)
 {
 	union epoll_ref ref = { .type = EPOLL_TYPE_CONF_LISTEN };
 
-	if (c->fd_conf_listen < 0)
+	if (c->fd_control_listen < 0)
 		return;
 
-	if (listen(c->fd_conf_listen, 0))
+	if (listen(c->fd_control_listen, 0))
 		die_perror("Couldn't listen on configuration socket");
 
-	ref.fd = c->fd_conf_listen;
+	ref.fd = c->fd_control_listen;
 	if (epoll_add(c->epollfd, EPOLLIN | EPOLLET, ref))
 		die_perror("Couldn't add configuration socket to epoll");
 }
@@ -1846,11 +1849,11 @@ void conf(struct ctx *c, int argc, char **argv)
 			c->fd_tap = -1;
 			break;
 		case 'c':
-			ret = snprintf(c->conf_path, sizeof(c->conf_path), "%s",
-				       optarg);
+			ret = snprintf(c->control_path, sizeof(c->control_path),
+				       "%s", optarg);
 			if (ret <= 0 || ret >= (int)sizeof(c->sock_path))
 				die("Invalid configuration path: %s", optarg);
-			c->fd_conf_listen = c->fd_conf = -1;
+			c->fd_control_listen = c->fd_control = -1;
 			break;
 		case 'F':
 			errno = 0;
@@ -2294,10 +2297,10 @@ void conf(struct ctx *c, int argc, char **argv)
 					FWD_SCAN);
 	}
 
+	conf_sock_listen(c);
+
 	if (!c->quiet)
 		conf_print(c);
-
-	conf_sock_listen(c);
 }
 
 /**
@@ -2321,7 +2324,7 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
 		return;
 	}
 
-	fd = accept4(c->fd_conf_listen, NULL, NULL, SOCK_NONBLOCK);
+	fd = accept4(c->fd_control_listen, NULL, NULL, SOCK_NONBLOCK);
 	if (fd < 0) {
 		warn_perror("accept4() on configuration listening socket");
 		return;
@@ -2331,7 +2334,7 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
 		warn_perror("Can't get configuration client credentials");
 
 	/* Another client is already connected: accept and close right away. */
-	if (c->fd_conf != -1) {
+	if (c->fd_control != -1) {
 		info("Discarding configuration client, PID %i", uc.pid);
 		goto fail;
 	}
@@ -2349,7 +2352,7 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
 		goto fail;
 	}
 
-	c->fd_conf = fd;
+	c->fd_control = fd;
 	info("Accepted configuration client, PID %i", uc.pid);
 	if (!PESTO_PROTOCOL_VERSION) {
 		warn(
@@ -2374,7 +2377,7 @@ void conf_handler(struct ctx *c, uint32_t events)
 		ssize_t n;
 
 		do {
-			n = read(c->fd_conf, discard, sizeof(discard));
+			n = read(c->fd_control, discard, sizeof(discard));
 			if (n > 0)
 				debug("Discarded %zd bytes of config data", n);
 		} while (n > 0);
@@ -2397,7 +2400,7 @@ void conf_handler(struct ctx *c, uint32_t events)
 
 close:
 	debug("Closing configuration socket");
-	epoll_ctl(c->epollfd, EPOLL_CTL_DEL, c->fd_conf, NULL);
-	close(c->fd_conf);
-	c->fd_conf = -1;
+	epoll_ctl(c->epollfd, EPOLL_CTL_DEL, c->fd_control, NULL);
+	close(c->fd_control);
+	c->fd_control = -1;
 }
diff --git a/passt.h b/passt.h
index c38bb5ae..b3f049de 100644
--- a/passt.h
+++ b/passt.h
@@ -158,7 +158,7 @@ struct ip6_ctx {
  * @foreground:		Run in foreground, don't log to stderr by default
  * @nofile:		Maximum number of open files (ulimit -n)
  * @sock_path:		Path for UNIX domain socket
- * @conf_path:		Path for configuration UNIX domain socket
+ * @control_path:	Path for control/configuration UNIX domain socket
  * @repair_path:	TCP_REPAIR helper path, can be "none", empty for default
  * @pcap:		Path for packet capture file
  * @pidfile:		Path to PID file, empty string if not configured
@@ -170,8 +170,8 @@ struct ip6_ctx {
  * @epollfd:		File descriptor for epoll instance
  * @fd_tap_listen:	File descriptor for listening AF_UNIX socket, if any
  * @fd_tap:		AF_UNIX socket, tuntap device, or pre-opened socket
- * @fd_conf_listen:	Listening configuration socket, if any
- * @fd_conf:		Configuration socket, if any
+ * @fd_control_listen:	Listening control/configuration socket, if any
+ * @fd_control:		Control/configuration socket, if any
  * @fd_repair_listen:	File descriptor for listening TCP_REPAIR socket, if any
  * @fd_repair:		Connected AF_UNIX socket for TCP_REPAIR helper
  * @our_tap_mac:	Pasta/passt's MAC on the tap link
@@ -226,7 +226,7 @@ struct ctx {
 	int foreground;
 	int nofile;
 	char sock_path[UNIX_PATH_MAX];
-	char conf_path[UNIX_PATH_MAX];
+	char control_path[UNIX_PATH_MAX];
 	char repair_path[UNIX_PATH_MAX];
 	char pcap[PATH_MAX];
 
@@ -244,8 +244,8 @@ struct ctx {
 	int epollfd;
 	int fd_tap_listen;
 	int fd_tap;
-	int fd_conf_listen;
-	int fd_conf;
+	int fd_control_listen;
+	int fd_control;
 	int fd_repair_listen;
 	int fd_repair;
 	unsigned char our_tap_mac[ETH_ALEN];
diff --git a/pesto.c b/pesto.c
index 6d066084..f8c9e01d 100644
--- a/pesto.c
+++ b/pesto.c
@@ -15,6 +15,7 @@
 #include <sys/socket.h>
 #include <sys/un.h>
 #include <errno.h>
+#include <getopt.h>
 #include <inttypes.h>
 #include <stdbool.h>
 #include <stddef.h>
@@ -42,6 +43,32 @@
 		exit(EXIT_FAILURE);					\
 	} while (0)
 
+#define debug(...)							\
+	do {								\
+		if (verbosity > 1) {					\
+			FPRINTF(stderr, __VA_ARGS__);			\
+			FPRINTF(stderr, "\n");				\
+		}							\
+	} while (0)
+
+/**
+ * usage() - Print usage, exit with given status code
+ * @name:	Executable name
+ * @f:		Stream to print usage info to
+ * @status:	Status code for exit(2)
+ */
+static void usage(const char *name, FILE *f, int status)
+{
+	FPRINTF(f, "Usage: %s [OPTION]... PATH\n", name);
+	FPRINTF(f,
+		"\n"
+		"  -v, --verbose		Be more verbose\n"
+		"  -q, --quiet		Be less verbose\n"
+		"  -h, --help		Display this help message and exit\n"
+		"  --version		Show version and exit\n");
+	exit(status);
+}
+
 /**
  * main() - Entry point and whole program with loop
  * @argc:	Argument count
@@ -56,13 +83,20 @@
  */
 int main(int argc, char **argv)
 {
+	const struct option options[] = {
+		{"quiet",	no_argument,		NULL,		'q' },
+		{"verbose",	no_argument,		NULL,		'v' },
+		{"help",	no_argument,		NULL,		'h' },
+		{"version",	no_argument,		NULL,		1 },
+		{ 0 },
+	};
 	struct sockaddr_un a = { AF_UNIX, "" };
+	const char *optstring = "vh";
 	struct pesto_hello hello;
 	struct sock_fprog prog;
+	int optname, ret, s;
 	uint32_t s_version;
-	int ret, s;
-
-	prctl(PR_SET_DUMPABLE, 0);
+	int verbosity = 1;
 
 	prog.len = (unsigned short)sizeof(filter_pesto) /
 				   sizeof(filter_pesto[0]);
@@ -71,15 +105,40 @@ int main(int argc, char **argv)
 	    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
 		die("Failed to apply seccomp filter");
 
-	if (argc < 2)
-		die("Usage: %s CONTROLPATH", argv[0]);
+	do {
+		optname = getopt_long(argc, argv, optstring, options, NULL);
+
+		switch (optname) {
+		case -1:
+		case 0:
+			break;
+		case 'h':
+			usage(argv[0], stdout, EXIT_SUCCESS);
+			break;
+		case 'q':
+			verbosity--;
+			break;
+		case 'v':
+			verbosity++;
+			break;
+		case 1:
+			FPRINTF(stdout, "pesto ");
+			FPRINTF(stdout, VERSION_BLOB);
+			exit(EXIT_SUCCESS);
+		default:
+			usage(argv[0], stderr, EXIT_FAILURE);
+		}
+	} while (optname != -1);
+
+	if (argc - optind != 1)
+		usage(argv[0], stderr, EXIT_FAILURE);
 
 	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
 		die("Failed to create AF_UNIX socket: %s", strerror(errno));
 
-	ret = snprintf(a.sun_path, sizeof(a.sun_path), "%s", argv[1]);
+	ret = snprintf(a.sun_path, sizeof(a.sun_path), "%s", argv[optind]);
 	if (ret <= 0 || ret >= (int)sizeof(a.sun_path))
-		die("Invalid socket path \"%s\"", argv[1]);
+		die("Invalid socket path \"%s\"", argv[optind]);
 
 	ret = connect(s, (struct sockaddr *)&a, sizeof(a));
 	if (ret < 0) {
@@ -87,6 +146,8 @@ int main(int argc, char **argv)
 		    a.sun_path, strerror(errno));
 	}
 
+	debug("Connected to passt/pasta control socket");
+
 	ret = read_all_buf(s, &hello, sizeof(hello));
 	if (ret < 0)
 		die("Couldn't read server greeting: %s", strerror(errno));
@@ -96,6 +157,8 @@ int main(int argc, char **argv)
 
 	s_version = ntohl(hello.version);
 
+	debug("Server protocol version: %"PRIu32, s_version);
+
 	if (s_version > PESTO_PROTOCOL_VERSION) {
 		die("Unknown server protocol version %"PRIu32" > %"PRIu32"\n",
 		    s_version, PESTO_PROTOCOL_VERSION);
diff --git a/util.h b/util.h
index e7993f4d..d7c397f6 100644
--- a/util.h
+++ b/util.h
@@ -21,14 +21,6 @@
 
 #include "log.h"
 
-#define VERSION_BLOB							       \
-	VERSION "\n"							       \
-	"Copyright Red Hat\n"						       \
-	"GNU General Public License, version 2 or later\n"		       \
-	"  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>\n"	       \
-	"This is free software: you are free to change and redistribute it.\n" \
-	"There is NO WARRANTY, to the extent permitted by law.\n\n"
-
 #ifndef SECCOMP_RET_KILL_PROCESS
 #define SECCOMP_RET_KILL_PROCESS	SECCOMP_RET_KILL
 #endif
-- 
2.53.0


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

* [PATCH v3 15/25] pesto: Expose list of pifs to pesto
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (13 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 14/25] pesto: Add command line option parsing and debug messages David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-25  0:56   ` Stefano Brivio
  2026-03-23  7:37 ` [PATCH v3 16/25] ip: Prepare ip.[ch] for sharing with pesto tool David Gibson
                   ` (11 subsequent siblings)
  26 siblings, 1 reply; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Extend the dynamic update protocol to expose the pif indices and names
from a running passt/pasta to the pesto tool.  pesto records that data
and (for now) prints it out.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 conf.c      | 38 +++++++++++++++++++++
 pesto.c     | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 serialise.c | 21 ++++++++++++
 serialise.h |  3 ++
 4 files changed, 159 insertions(+), 1 deletion(-)

diff --git a/conf.c b/conf.c
index 7b960fe9..b878db94 100644
--- a/conf.c
+++ b/conf.c
@@ -2303,6 +2303,41 @@ void conf(struct ctx *c, int argc, char **argv)
 		conf_print(c);
 }
 
+/**
+ * conf_send_pifs() - Send list of pifs to dynamic update client (pesto)
+ * @c:		Execution context
+ * @fd:		Socket to the client
+ *
+ * Return: 0 on success, -1 on failure
+ */
+static int conf_send_pifs(const struct ctx *c, int fd)
+{
+	uint32_t num = 0;
+	unsigned pif;
+
+	/* First count the number of pifs with tables */
+	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
+		if (c->fwd[pif])
+			num++;
+	}
+
+	if (write_u32(fd, num))
+		return -1;
+
+	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
+		if (!c->fwd[pif])
+			continue;
+
+		if (write_u8(fd, pif))
+			return -1;
+
+		if (write_str(fd, pif_name(pif)) < 0)
+			return -1;
+	}
+
+	return 0;
+}
+
 /**
  * conf_listen_handler() - Handle events on configuration listening socket
  * @c:		Execution context
@@ -2359,6 +2394,9 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
 "Warning: Using experimental unsupported configuration protocol");
 	}
 
+	if (conf_send_pifs(c, fd) < 0)
+		goto fail;
+
 	return;
 
 fail:
diff --git a/pesto.c b/pesto.c
index f8c9e01d..ed4f2aab 100644
--- a/pesto.c
+++ b/pesto.c
@@ -36,6 +36,8 @@
 #include "serialise.h"
 #include "pesto.h"
 
+static int verbosity = 1;
+
 #define die(...)							\
 	do {								\
 		FPRINTF(stderr, __VA_ARGS__);				\
@@ -51,6 +53,19 @@
 		}							\
 	} while (0)
 
+/**
+ * xmalloc() - Allocate memory, with fatal error on failure
+ * @size:	Number of bytes to allocate
+ */
+static void *xmalloc(size_t size)
+{
+	void *p = malloc(size);
+
+	if (!p)
+		die("Memory allocation failure");
+	return p;
+}
+
 /**
  * usage() - Print usage, exit with given status code
  * @name:	Executable name
@@ -69,6 +84,83 @@ static void usage(const char *name, FILE *f, int status)
 	exit(status);
 }
 
+/**
+ * pesto_recv_str() - Receive a string from passt/pasta
+ * @fd:		Control socket
+ *
+ * Return: pointer to malloc()ed string
+ */
+static const char *pesto_recv_str(int fd)
+{
+	uint32_t len;
+	char *buf;
+
+	if (read_u32(fd, &len) < 0)
+		die("Error reading from control socket");
+
+	buf = xmalloc(len);
+	if (read_all_buf(fd, buf, len) < 0)
+		die("Error reading from control socket");
+
+	return buf;
+}
+
+struct pif_state {
+	uint8_t pif;
+	const char *name;
+};
+
+struct conf_state {
+	uint32_t npifs;
+	struct pif_state pif[];
+};
+
+/**
+ * pesto_read_pifs() - Read pif names and IDs from passt/pasta
+ * @fd:		Control socket
+ */
+static const struct conf_state *pesto_read_pifs(int fd)
+{
+	uint32_t num;
+	struct conf_state *state;
+	unsigned i;
+
+	if (read_u32(fd, &num) < 0)
+		die("Error reading from control socket");
+
+	debug("Receiving %"PRIu32" interface names", num);
+
+	state = xmalloc(sizeof(*state) + num * sizeof(struct pif_state));
+	state->npifs = num;
+
+	for (i = 0; i < num; i++) {
+		struct pif_state *ps = &state->pif[i];
+
+		if (read_u8(fd, &ps->pif) < 0)
+			die("Error reading from control socket");
+		ps->name = pesto_recv_str(fd);
+
+		debug("%u: %s", ps->pif, ps->name);
+	}
+
+	return state;
+}
+
+/**
+ * show_state() - Show current rule state obtained from passt/pasta
+ * @pifs:	PIF name information
+ */
+static void show_state(const struct conf_state *state)
+{
+	unsigned i;
+
+	for (i = 0; i < state->npifs; i++) {
+		const struct pif_state *ps = &state->pif[i];
+		printf("Forwarding rules for %s interface\n", ps->name);
+		printf("\tTBD\n");
+	}
+}
+
 /**
  * main() - Entry point and whole program with loop
  * @argc:	Argument count
@@ -91,12 +183,12 @@ int main(int argc, char **argv)
 		{ 0 },
 	};
 	struct sockaddr_un a = { AF_UNIX, "" };
+	const struct conf_state *state;
 	const char *optstring = "vh";
 	struct pesto_hello hello;
 	struct sock_fprog prog;
 	int optname, ret, s;
 	uint32_t s_version;
-	int verbosity = 1;
 
 	prog.len = (unsigned short)sizeof(filter_pesto) /
 				   sizeof(filter_pesto[0]);
@@ -172,5 +264,9 @@ int main(int argc, char **argv)
 "Warning: Using experimental protocol version, client and server must match\n");
 	}
 
+	state = pesto_read_pifs(s);
+
+	show_state(state);
+
 	exit(0);
 }
diff --git a/serialise.c b/serialise.c
index e3ea86e3..94c34d3c 100644
--- a/serialise.c
+++ b/serialise.c
@@ -19,6 +19,7 @@
 #include <endian.h>
 #include <errno.h>
 #include <stdint.h>
+#include <string.h>
 #include <unistd.h>
 
 #include "serialise.h"
@@ -121,6 +122,26 @@ int write_all_buf(int fd, const void *buf, size_t len)
 		return write_all_buf(fd, &beval, sizeof(beval));	\
 	}
 
+#define be8toh(x)	(x)
+#define htobe8(x)	(x)
+
+SERIALISE_UINT(8)
 SERIALISE_UINT(32)
 
 #undef SERIALISE_UNIT
+
+/**
+ * write_str() - Write a string to an fd in length/value format
+ * @fd:		Socket to the client
+ * @s:		String to send
+ *
+ * Return: 0 on success, -1 on error
+ */
+int write_str(int fd, const char *s)
+{
+	uint32_t len = strlen(s) + 1; /* Include \0 */
+
+	if (write_u32(fd, len) < 0)
+		return -1;
+	return write_all_buf(fd, s, len);
+}
diff --git a/serialise.h b/serialise.h
index 0a4ed086..7ef35786 100644
--- a/serialise.h
+++ b/serialise.h
@@ -16,6 +16,9 @@ int write_all_buf(int fd, const void *buf, size_t len);
 	int read_u##bits(int fd, uint##bits##_t *val);			\
 	int write_u##bits(int fd, uint##bits##_t val);
 
+SERIALISE_UINT_DECL(8)
 SERIALISE_UINT_DECL(32)
 
+int write_str(int fd, const char *s);
+
 #endif /* _SERIALISE_H */
-- 
2.53.0


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

* [PATCH v3 16/25] ip: Prepare ip.[ch] for sharing with pesto tool
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (14 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 15/25] pesto: Expose list of pifs to pesto David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 17/25] inany: Prepare inany.[ch] " David Gibson
                   ` (10 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Most things in ip.[ch] related purely to IP addresses and headers with
no dependency on other passt/pasta internals.  A number of these will be
useful to re-use in pesto.  The exception is ipv6_l4hdr() which uses
iov_tail.

The only caller of this is in tap.c, so move the function there.  Along
with moving the constant byteswapping functions to common.h, that lets
ip.[ch] to be linked into pesto as well as passt/pasta.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 Makefile | 15 +++++++--------
 common.h | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
 ip.c     | 56 +++-----------------------------------------------------
 ip.h     |  3 +--
 tap.c    | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 util.h   | 48 ------------------------------------------------
 6 files changed, 113 insertions(+), 111 deletions(-)

diff --git a/Makefile b/Makefile
index d085c9c1..b9f20bb5 100644
--- a/Makefile
+++ b/Makefile
@@ -44,19 +44,18 @@ PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
 	udp.c udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
 QRAP_SRCS = qrap.c
 PASST_REPAIR_SRCS = passt-repair.c
-PESTO_SRCS = pesto.c serialise.c
+PESTO_SRCS = pesto.c ip.c serialise.c
 SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
 
 MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
 
-PESTO_HEADERS = common.h pesto.h serialise.h
+PESTO_HEADERS = common.h ip.h pesto.h serialise.h
 PASST_HEADERS = arch.h arp.h checksum.h conf.h dhcp.h dhcpv6.h epoll_ctl.h \
-	flow.h fwd.h flow_table.h icmp.h icmp_flow.h inany.h iov.h ip.h \
-	isolation.h lineread.h log.h migrate.h ndp.h netlink.h packet.h \
-	passt.h pasta.h pcap.h pif.h repair.h siphash.h tap.h tcp.h tcp_buf.h \
-	tcp_conn.h tcp_internal.h tcp_splice.h tcp_vu.h udp.h udp_flow.h \
-	udp_internal.h udp_vu.h util.h vhost_user.h virtio.h vu_common.h \
-	$(PESTO_HEADERS)
+	flow.h fwd.h flow_table.h icmp.h icmp_flow.h inany.h iov.h isolation.h \
+	lineread.h log.h migrate.h ndp.h netlink.h packet.h passt.h pasta.h \
+	pcap.h pif.h repair.h siphash.h tap.h tcp.h tcp_buf.h tcp_conn.h \
+	tcp_internal.h tcp_splice.h tcp_vu.h udp.h udp_flow.h udp_internal.h \
+	udp_vu.h util.h vhost_user.h virtio.h vu_common.h $(PESTO_HEADERS)
 HEADERS = $(PASST_HEADERS) seccomp.h
 
 C := \#include <sys/random.h>\nint main(){int a=getrandom(0, 0, 0);}
diff --git a/common.h b/common.h
index 927d20a4..7d6ba5c1 100644
--- a/common.h
+++ b/common.h
@@ -19,4 +19,54 @@
 /* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
 #define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
 
+#define BIT(n)			(1UL << (n))
+
+#ifndef __bswap_constant_16
+#define __bswap_constant_16(x)						\
+	((uint16_t) ((((x) >> 8) & 0xff) | (((x) & 0xff) << 8)))
+#endif
+
+#ifndef __bswap_constant_32
+#define __bswap_constant_32(x)						\
+	((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >>  8) |	\
+	 (((x) & 0x0000ff00) <<  8) | (((x) & 0x000000ff) << 24))
+#endif
+
+#ifndef __bswap_constant_32
+#define __bswap_constant_32(x)						\
+	((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >>  8) |	\
+	 (((x) & 0x0000ff00) <<  8) | (((x) & 0x000000ff) << 24))
+#endif
+
+#ifndef __bswap_constant_64
+#define __bswap_constant_64(x) \
+	((((x) & 0xff00000000000000ULL) >> 56) |			\
+	 (((x) & 0x00ff000000000000ULL) >> 40) |			\
+	 (((x) & 0x0000ff0000000000ULL) >> 24) |			\
+	 (((x) & 0x000000ff00000000ULL) >> 8)  |			\
+	 (((x) & 0x00000000ff000000ULL) << 8)  |			\
+	 (((x) & 0x0000000000ff0000ULL) << 24) |			\
+	 (((x) & 0x000000000000ff00ULL) << 40) |			\
+	 (((x) & 0x00000000000000ffULL) << 56))
+#endif
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#define	htons_constant(x)	(x)
+#define	htonl_constant(x)	(x)
+#define htonll_constant(x)	(x)
+#define	ntohs_constant(x)	(x)
+#define	ntohl_constant(x)	(x)
+#define ntohll_constant(x)	(x)
+#else
+#define	htons_constant(x)	(__bswap_constant_16(x))
+#define	htonl_constant(x)	(__bswap_constant_32(x))
+#define	htonll_constant(x)	(__bswap_constant_64(x))
+#define	ntohs_constant(x)	(__bswap_constant_16(x))
+#define	ntohl_constant(x)	(__bswap_constant_32(x))
+#define	ntohll_constant(x)	(__bswap_constant_64(x))
+#endif
+
+#define ntohll(x)		(be64toh((x)))
+#define htonll(x)		(htobe64((x)))
+
 #endif /* _COMMON_H */
diff --git a/ip.c b/ip.c
index 0ea62998..4e4e0bf5 100644
--- a/ip.c
+++ b/ip.c
@@ -6,6 +6,9 @@
  * PASTA - Pack A Subtle Tap Abstraction
  *  for network namespace/tap device mode
  *
+ * PESTO - Programmable Extensible Socket Translation Orchestrator
+ *  front-end for passt(1) and pasta(1) forwarding configuration
+ *
  * ip.c - IP related functions
  *
  * Copyright (c) 2020-2021 Red Hat GmbH
@@ -15,61 +18,8 @@
 #include <stddef.h>
 #include <netinet/in.h>
 
-#include "util.h"
 #include "ip.h"
 
-#define IPV6_NH_OPT(nh)							\
-	((nh) == 0   || (nh) == 43  || (nh) == 44  || (nh) == 50  ||	\
-	 (nh) == 51  || (nh) == 60  || (nh) == 135 || (nh) == 139 ||	\
-	 (nh) == 140 || (nh) == 253 || (nh) == 254)
-
-/**
- * ipv6_l4hdr() - Find pointer to L4 header in IPv6 packet and extract protocol
- * @data:	IPv6 packet
- * @proto:	Filled with L4 protocol number
- * @dlen:	Data length (payload excluding header extensions), set on return
- *
- * Return: true if the L4 header is found and @data, @proto, @dlen are set,
- * 	   false on error. Outputs are indeterminate on failure.
- */
-bool ipv6_l4hdr(struct iov_tail *data, uint8_t *proto, size_t *dlen)
-{
-	struct ipv6_opt_hdr o_storage;
-	const struct ipv6_opt_hdr *o;
-	struct ipv6hdr ip6h_storage;
-	const struct ipv6hdr *ip6h;
-	int hdrlen;
-	uint8_t nh;
-
-	ip6h = IOV_REMOVE_HEADER(data, ip6h_storage);
-	if (!ip6h)
-		return false;
-
-	nh = ip6h->nexthdr;
-	if (!IPV6_NH_OPT(nh))
-		goto found;
-
-	while ((o = IOV_PEEK_HEADER(data, o_storage))) {
-		nh = o->nexthdr;
-		hdrlen = (o->hdrlen + 1) * 8;
-
-		if (IPV6_NH_OPT(nh))
-			iov_drop_header(data, hdrlen);
-		else
-			goto found;
-	}
-
-	return false;
-
-found:
-	if (nh == IPPROTO_NONE)
-		return false;
-
-	*dlen = iov_tail_size(data);
-	*proto = nh;
-	return true;
-}
-
 /**
  * ipproto_name() - Get IP protocol name from number
  * @proto:	IP protocol number
diff --git a/ip.h b/ip.h
index d0de6c8d..f6c29e00 100644
--- a/ip.h
+++ b/ip.h
@@ -9,7 +9,7 @@
 #include <netinet/ip.h>
 #include <netinet/ip6.h>
 
-#include "util.h"
+#include "common.h"
 
 #define IN4_IS_ADDR_UNSPECIFIED(a) \
 	(((struct in_addr *)(a))->s_addr == htonl_constant(INADDR_ANY))
@@ -117,7 +117,6 @@ static inline uint32_t ip6_get_flow_lbl(const struct ipv6hdr *ip6h)
 		ip6h->flow_lbl[2];
 }
 
-bool ipv6_l4hdr(struct iov_tail *data, uint8_t *proto, size_t *dlen);
 const char *ipproto_name(uint8_t proto);
 
 /* IPv6 link-local all-nodes multicast address, ff02::1 */
diff --git a/tap.c b/tap.c
index 1049e023..185ab471 100644
--- a/tap.c
+++ b/tap.c
@@ -881,6 +881,58 @@ append:
 	return in->count;
 }
 
+#define IPV6_NH_OPT(nh)							\
+	((nh) == 0   || (nh) == 43  || (nh) == 44  || (nh) == 50  ||	\
+	 (nh) == 51  || (nh) == 60  || (nh) == 135 || (nh) == 139 ||	\
+	 (nh) == 140 || (nh) == 253 || (nh) == 254)
+
+/**
+ * ipv6_l4hdr() - Find pointer to L4 header in IPv6 packet and extract protocol
+ * @data:	IPv6 packet
+ * @proto:	Filled with L4 protocol number
+ * @dlen:	Data length (payload excluding header extensions), set on return
+ *
+ * Return: true if the L4 header is found and @data, @proto, @dlen are set,
+ * 	   false on error. Outputs are indeterminate on failure.
+ */
+static bool ipv6_l4hdr(struct iov_tail *data, uint8_t *proto, size_t *dlen)
+{
+	struct ipv6_opt_hdr o_storage;
+	const struct ipv6_opt_hdr *o;
+	struct ipv6hdr ip6h_storage;
+	const struct ipv6hdr *ip6h;
+	int hdrlen;
+	uint8_t nh;
+
+	ip6h = IOV_REMOVE_HEADER(data, ip6h_storage);
+	if (!ip6h)
+		return false;
+
+	nh = ip6h->nexthdr;
+	if (!IPV6_NH_OPT(nh))
+		goto found;
+
+	while ((o = IOV_PEEK_HEADER(data, o_storage))) {
+		nh = o->nexthdr;
+		hdrlen = (o->hdrlen + 1) * 8;
+
+		if (IPV6_NH_OPT(nh))
+			iov_drop_header(data, hdrlen);
+		else
+			goto found;
+	}
+
+	return false;
+
+found:
+	if (nh == IPPROTO_NONE)
+		return false;
+
+	*dlen = iov_tail_size(data);
+	*proto = nh;
+	return true;
+}
+
 /**
  * tap6_handler() - IPv6 packet handler for tap file descriptor
  * @c:		Execution context
diff --git a/util.h b/util.h
index d7c397f6..5357814c 100644
--- a/util.h
+++ b/util.h
@@ -106,54 +106,6 @@ void abort_with_msg(const char *fmt, ...)
 #define MAC_UNDEF		MAC_BROADCAST
 #define MAC_IS_UNDEF(addr)	(!memcmp((addr), MAC_UNDEF, ETH_ALEN))
 
-#ifndef __bswap_constant_16
-#define __bswap_constant_16(x)						\
-	((uint16_t) ((((x) >> 8) & 0xff) | (((x) & 0xff) << 8)))
-#endif
-
-#ifndef __bswap_constant_32
-#define __bswap_constant_32(x)						\
-	((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >>  8) |	\
-	 (((x) & 0x0000ff00) <<  8) | (((x) & 0x000000ff) << 24))
-#endif
-
-#ifndef __bswap_constant_32
-#define __bswap_constant_32(x)						\
-	((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >>  8) |	\
-	 (((x) & 0x0000ff00) <<  8) | (((x) & 0x000000ff) << 24))
-#endif
-
-#ifndef __bswap_constant_64
-#define __bswap_constant_64(x) \
-	((((x) & 0xff00000000000000ULL) >> 56) |			\
-	 (((x) & 0x00ff000000000000ULL) >> 40) |			\
-	 (((x) & 0x0000ff0000000000ULL) >> 24) |			\
-	 (((x) & 0x000000ff00000000ULL) >> 8)  |			\
-	 (((x) & 0x00000000ff000000ULL) << 8)  |			\
-	 (((x) & 0x0000000000ff0000ULL) << 24) |			\
-	 (((x) & 0x000000000000ff00ULL) << 40) |			\
-	 (((x) & 0x00000000000000ffULL) << 56))
-#endif
-
-#if __BYTE_ORDER == __BIG_ENDIAN
-#define	htons_constant(x)	(x)
-#define	htonl_constant(x)	(x)
-#define htonll_constant(x)	(x)
-#define	ntohs_constant(x)	(x)
-#define	ntohl_constant(x)	(x)
-#define ntohll_constant(x)	(x)
-#else
-#define	htons_constant(x)	(__bswap_constant_16(x))
-#define	htonl_constant(x)	(__bswap_constant_32(x))
-#define	htonll_constant(x)	(__bswap_constant_64(x))
-#define	ntohs_constant(x)	(__bswap_constant_16(x))
-#define	ntohl_constant(x)	(__bswap_constant_32(x))
-#define	ntohll_constant(x)	(__bswap_constant_64(x))
-#endif
-
-#define ntohll(x)		(be64toh((x)))
-#define htonll(x)		(htobe64((x)))
-
 extern uint8_t eth_pad[ETH_ZLEN];
 
 /**
-- 
2.53.0


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

* [PATCH v3 17/25] inany: Prepare inany.[ch] for sharing with pesto tool
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (15 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 16/25] ip: Prepare ip.[ch] for sharing with pesto tool David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 18/25] fwd: Split forwading rule specification from its implementation state David Gibson
                   ` (9 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

inany contains a number of helpful functions for dealing with addresses
which might be IPv4 or IPv6.  We're going to want to use that in pesto.
For the most part inany doesn't depend on other passt/pasta internals,
however it does depend on siphash.h, which pesto doesn't need.

Move the single dependent function, inany_siphash_feed() to siphash.h,
renaming to match.  Use that include inany.[ch] into pesto as well as
passt/pasta.  While we're there reformat pesto.c's header comment to match
the convention used in most other files.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 Makefile  |  6 +++---
 common.h  |  7 +++++++
 flow.c    |  4 ++--
 fwd.c     |  2 +-
 inany.c   | 16 +++++++++++++---
 inany.h   | 16 ++--------------
 iov.c     |  1 +
 siphash.h | 13 +++++++++++++
 util.h    |  7 -------
 virtio.c  |  1 +
 10 files changed, 43 insertions(+), 30 deletions(-)

diff --git a/Makefile b/Makefile
index b9f20bb5..92809813 100644
--- a/Makefile
+++ b/Makefile
@@ -44,14 +44,14 @@ PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
 	udp.c udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
 QRAP_SRCS = qrap.c
 PASST_REPAIR_SRCS = passt-repair.c
-PESTO_SRCS = pesto.c ip.c serialise.c
+PESTO_SRCS = pesto.c inany.c ip.c serialise.c
 SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
 
 MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
 
-PESTO_HEADERS = common.h ip.h pesto.h serialise.h
+PESTO_HEADERS = common.h inany.h ip.h pesto.h serialise.h
 PASST_HEADERS = arch.h arp.h checksum.h conf.h dhcp.h dhcpv6.h epoll_ctl.h \
-	flow.h fwd.h flow_table.h icmp.h icmp_flow.h inany.h iov.h isolation.h \
+	flow.h fwd.h flow_table.h icmp.h icmp_flow.h iov.h isolation.h \
 	lineread.h log.h migrate.h ndp.h netlink.h packet.h passt.h pasta.h \
 	pcap.h pif.h repair.h siphash.h tap.h tcp.h tcp_buf.h tcp_conn.h \
 	tcp_internal.h tcp_splice.h tcp_vu.h udp.h udp_flow.h udp_internal.h \
diff --git a/common.h b/common.h
index 7d6ba5c1..7d37b926 100644
--- a/common.h
+++ b/common.h
@@ -19,6 +19,13 @@
 /* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
 #define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
 
+#ifndef MIN
+#define MIN(x, y)		(((x) < (y)) ? (x) : (y))
+#endif
+#ifndef MAX
+#define MAX(x, y)		(((x) > (y)) ? (x) : (y))
+#endif
+
 #define BIT(n)			(1UL << (n))
 
 #ifndef __bswap_constant_16
diff --git a/flow.c b/flow.c
index 2972ab87..c2d36882 100644
--- a/flow.c
+++ b/flow.c
@@ -678,8 +678,8 @@ static uint64_t flow_hash(const struct ctx *c, uint8_t proto, uint8_t pif,
 {
 	struct siphash_state state = SIPHASH_INIT(c->hash_secret);
 
-	inany_siphash_feed(&state, &side->oaddr);
-	inany_siphash_feed(&state, &side->eaddr);
+	siphash_feed_inany(&state, &side->oaddr);
+	siphash_feed_inany(&state, &side->eaddr);
 
 	return siphash_final(&state, 38, (uint64_t)proto << 40 |
 			     (uint64_t)pif << 32 |
diff --git a/fwd.c b/fwd.c
index cfe89362..05c8e378 100644
--- a/fwd.c
+++ b/fwd.c
@@ -86,7 +86,7 @@ static size_t neigh_table_slot(const struct ctx *c,
 	struct siphash_state st = SIPHASH_INIT(c->hash_secret);
 	uint32_t i;
 
-	inany_siphash_feed(&st, key);
+	siphash_feed_inany(&st, key);
 	i = siphash_final(&st, sizeof(*key), 0);
 
 	return ((size_t)i) & (NEIGH_TABLE_SIZE - 1);
diff --git a/inany.c b/inany.c
index 2a586ed1..426d2a64 100644
--- a/inany.c
+++ b/inany.c
@@ -1,9 +1,19 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later
- * Copyright Red Hat
- * Author: David Gibson <david@gibson.dropbear.id.au>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/* PASST - Plug A Simple Socket Transport
+ *  for qemu/UNIX domain socket mode
+ *
+ * PASTA - Pack A Subtle Tap Abstraction
+ *  for network namespace/tap device mode
+ *
+ * PESTO - Programmable Extensible Socket Translation Orchestrator
+ *  front-end for passt(1) and pasta(1) forwarding configuration
  *
  * inany.c - Types and helpers for handling addresses which could be
  *           IPv6 or IPv4 (encoded as IPv4-mapped IPv6 addresses)
+ *
+ * Copyright Red Hat
+ * Author: David Gibson <david@gibson.dropbear.id.au>
  */
 
 #include <stdlib.h>
diff --git a/inany.h b/inany.h
index 30e24164..6a1ff3c0 100644
--- a/inany.h
+++ b/inany.h
@@ -10,12 +10,11 @@
 #define INANY_H
 
 #include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
 #include <string.h>
 
 #include "ip.h"
-#include "siphash.h"
-
-struct siphash_state;
 
 /** union inany_addr - Represents either an IPv4 or IPv6 address
  * @a6:			Address as an IPv6 address, may be IPv4-mapped
@@ -299,17 +298,6 @@ static inline int inany_from_sockaddr(union inany_addr *dst, in_port_t *port,
 	return -1;
 }
 
-/** inany_siphash_feed- Fold IPv[46] address into an in-progress siphash
- * @state:	siphash state
- * @aa:		inany to hash
- */
-static inline void inany_siphash_feed(struct siphash_state *state,
-				      const union inany_addr *aa)
-{
-	siphash_feed(state, (uint64_t)aa->u32[0] << 32 | aa->u32[1]);
-	siphash_feed(state, (uint64_t)aa->u32[2] << 32 | aa->u32[3]);
-}
-
 #define INANY_ADDRSTRLEN	MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)
 
 bool inany_matches(const union inany_addr *a, const union inany_addr *b);
diff --git a/iov.c b/iov.c
index ae074393..d291ee51 100644
--- a/iov.c
+++ b/iov.c
@@ -24,6 +24,7 @@
 #include <assert.h>
 #include <sys/socket.h>
 
+#include "common.h"
 #include "util.h"
 #include "iov.h"
 
diff --git a/siphash.h b/siphash.h
index bbddcac0..313b8947 100644
--- a/siphash.h
+++ b/siphash.h
@@ -47,6 +47,8 @@
 #include <stddef.h>
 #include <stdint.h>
 
+#include "inany.h"
+
 /**
  * struct siphash_state - Internal state of siphash calculation
  */
@@ -101,6 +103,17 @@ static inline void siphash_feed(struct siphash_state *state, uint64_t in)
 	state->v[0] ^= in;
 }
 
+/** siphash_feed_inany() - Fold IPv[46] address into an in-progress siphash
+ * @state:	siphash state
+ * @aa:		inany to hash
+ */
+static inline void siphash_feed_inany(struct siphash_state *state,
+				      const union inany_addr *aa)
+{
+	siphash_feed(state, (uint64_t)aa->u32[0] << 32 | aa->u32[1]);
+	siphash_feed(state, (uint64_t)aa->u32[2] << 32 | aa->u32[3]);
+}
+
 /**
  * siphash_final() - Finalize SipHash calculations
  * @v:		siphash state (4 x 64-bit integers)
diff --git a/util.h b/util.h
index 5357814c..45792f46 100644
--- a/util.h
+++ b/util.h
@@ -28,13 +28,6 @@
 #define IP_MAX_MTU			USHRT_MAX
 #endif
 
-#ifndef MIN
-#define MIN(x, y)		(((x) < (y)) ? (x) : (y))
-#endif
-#ifndef MAX
-#define MAX(x, y)		(((x) > (y)) ? (x) : (y))
-#endif
-
 #define DIV_ROUND_UP(n, d)	(((n) + (d) - 1) / (d))
 #define DIV_ROUND_CLOSEST(n, d)	(((n) + (d) / 2) / (d))
 #define ROUND_DOWN(x, y)	((x) & ~((y) - 1))
diff --git a/virtio.c b/virtio.c
index b283de66..bc0bb2fc 100644
--- a/virtio.c
+++ b/virtio.c
@@ -80,6 +80,7 @@
 #include <sys/eventfd.h>
 #include <sys/socket.h>
 
+#include "common.h"
 #include "util.h"
 #include "virtio.h"
 #include "vhost_user.h"
-- 
2.53.0


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

* [PATCH v3 18/25] fwd: Split forwading rule specification from its implementation state
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (16 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 17/25] inany: Prepare inany.[ch] " David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 19/25] ip: Define a bound for the string returned by ipproto_name() David Gibson
                   ` (8 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Most of the fields in struct fwd_rule give the parameters of the forwarding
rule itself.  The @socks field gives the list of listening sockets we use
to implement it.  In order to share code with the configuraiton update tool
pesto, we want to split these.  Move the rule specification itself into
fwd_rule.h, which can be shared with pesto.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 Makefile   |  2 +-
 fwd.c      | 77 ++++++++++++++++++++++++++++--------------------------
 fwd.h      | 31 +++++-----------------
 fwd_rule.h | 45 +++++++++++++++++++++++++++++++
 util.h     |  1 -
 5 files changed, 92 insertions(+), 64 deletions(-)
 create mode 100644 fwd_rule.h

diff --git a/Makefile b/Makefile
index 92809813..bc325482 100644
--- a/Makefile
+++ b/Makefile
@@ -49,7 +49,7 @@ SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
 
 MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
 
-PESTO_HEADERS = common.h inany.h ip.h pesto.h serialise.h
+PESTO_HEADERS = common.h fwd_rule.h inany.h ip.h pesto.h serialise.h
 PASST_HEADERS = arch.h arp.h checksum.h conf.h dhcp.h dhcpv6.h epoll_ctl.h \
 	flow.h fwd.h flow_table.h icmp.h icmp_flow.h iov.h isolation.h \
 	lineread.h log.h migrate.h ndp.h netlink.h packet.h passt.h pasta.h \
diff --git a/fwd.c b/fwd.c
index 05c8e378..a32d0a20 100644
--- a/fwd.c
+++ b/fwd.c
@@ -363,7 +363,7 @@ void fwd_rule_add(struct fwd_table *fwd, uint8_t proto, uint8_t flags,
 	/* Flags which can be set from the caller */
 	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
 	unsigned num = (unsigned)last - first + 1;
-	struct fwd_rule *new;
+	struct fwd_rule_state *new;
 	unsigned i, port;
 
 	assert(!(flags & ~allowed_flags));
@@ -379,57 +379,59 @@ void fwd_rule_add(struct fwd_table *fwd, uint8_t proto, uint8_t flags,
 	/* Check for any conflicting entries */
 	for (i = 0; i < fwd->count; i++) {
 		char newstr[INANY_ADDRSTRLEN], rulestr[INANY_ADDRSTRLEN];
-		struct fwd_rule *rule = &fwd->rules[i];
+		struct fwd_rule_state *rule = &fwd->rules[i];
 
-		if (proto != rule->proto)
+		if (proto != rule->rule.proto)
 			/* Non-conflicting protocols */
 			continue;
 
-		if (!inany_matches(addr, fwd_rule_addr(rule)))
+		if (!inany_matches(addr, fwd_rule_addr(&rule->rule)))
 			/* Non-conflicting addresses */
 			continue;
 
-		if (last < rule->first || rule->last < first)
+		if (last < rule->rule.first || rule->rule.last < first)
 			/* Port ranges don't overlap */
 			continue;
 
 		die("Forwarding configuration conflict: %s/%u-%u versus %s/%u-%u",
 		    inany_ntop(addr, newstr, sizeof(newstr)), first, last,
-		    inany_ntop(fwd_rule_addr(rule), rulestr, sizeof(rulestr)),
-		    rule->first, rule->last);
+		    inany_ntop(fwd_rule_addr(&rule->rule),
+			       rulestr, sizeof(rulestr)),
+		    rule->rule.first, rule->rule.last);
 	}
 
 	new = &fwd->rules[fwd->count++];
-	new->proto = proto;
-	new->flags = flags;
+	new->rule.proto = proto;
+	new->rule.flags = flags;
 
 	if (addr) {
-		new->addr = *addr;
+		new->rule.addr = *addr;
 	} else {
-		new->addr = inany_any6;
-		new->flags |= FWD_DUAL_STACK_ANY;
+		new->rule.addr = inany_any6;
+		new->rule.flags |= FWD_DUAL_STACK_ANY;
 	}
 
-	memset(new->ifname, 0, sizeof(new->ifname));
+	memset(new->rule.ifname, 0, sizeof(new->rule.ifname));
 	if (ifname) {
 		int ret;
 
-		ret = snprintf(new->ifname, sizeof(new->ifname), "%s", ifname);
-		if (ret <= 0 || (size_t)ret >= sizeof(new->ifname))
+		ret = snprintf(new->rule.ifname, sizeof(new->rule.ifname),
+			       "%s", ifname);
+		if (ret <= 0 || (size_t)ret >= sizeof(new->rule.ifname))
 			die("Invalid interface name: %s", ifname);
 	}
 
 	assert(first <= last);
-	new->first = first;
-	new->last = last;
+	new->rule.first = first;
+	new->rule.last = last;
 
-	new->to = to;
+	new->rule.to = to;
 
 	new->socks = &fwd->socks[fwd->sock_count];
 	fwd->sock_count += num;
 
-	for (port = new->first; port <= new->last; port++)
-		new->socks[port - new->first] = -1;
+	for (port = new->rule.first; port <= new->rule.last; port++)
+		new->socks[port - new->rule.first] = -1;
 }
 
 /**
@@ -465,7 +467,7 @@ const struct fwd_rule *fwd_rule_search(const struct fwd_table *fwd,
 
 	if (hint >= 0) {
 		char ostr[INANY_ADDRSTRLEN], rstr[INANY_ADDRSTRLEN];
-		const struct fwd_rule *rule = &fwd->rules[hint];
+		const struct fwd_rule *rule = &fwd->rules[hint].rule;
 
 		assert((unsigned)hint < fwd->count);
 		if (fwd_rule_match(rule, ini, proto))
@@ -479,8 +481,8 @@ const struct fwd_rule *fwd_rule_search(const struct fwd_table *fwd,
 	}
 
 	for (i = 0; i < fwd->count; i++) {
-		if (fwd_rule_match(&fwd->rules[i], ini, proto))
-			return &fwd->rules[i];
+		if (fwd_rule_match(&fwd->rules[i].rule, ini, proto))
+			return &fwd->rules[i].rule;
 	}
 
 	return NULL;
@@ -495,7 +497,7 @@ void fwd_rules_print(const struct fwd_table *fwd)
 	unsigned i;
 
 	for (i = 0; i < fwd->count; i++) {
-		const struct fwd_rule *rule = &fwd->rules[i];
+		const struct fwd_rule *rule = &fwd->rules[i].rule;
 		const char *percent = *rule->ifname ? "%" : "";
 		const char *weak = "", *scan = "";
 		char addr[INANY_ADDRSTRLEN];
@@ -532,7 +534,8 @@ void fwd_rules_print(const struct fwd_table *fwd)
 static int fwd_sync_one(const struct ctx *c, uint8_t pif, unsigned idx,
 			const uint8_t *tcp, const uint8_t *udp)
 {
-	const struct fwd_rule *rule = &c->fwd[pif]->rules[idx];
+	const struct fwd_rule_state *rs = &c->fwd[pif]->rules[idx];
+	const struct fwd_rule *rule = &rs->rule;
 	const union inany_addr *addr = fwd_rule_addr(rule);
 	const char *ifname = rule->ifname;
 	const uint8_t *map = NULL;
@@ -553,7 +556,7 @@ static int fwd_sync_one(const struct ctx *c, uint8_t pif, unsigned idx,
 	}
 
 	for (port = rule->first; port <= rule->last; port++) {
-		int fd = rule->socks[port - rule->first];
+		int fd = rs->socks[port - rule->first];
 
 		if (map && !bitmap_isset(map, port)) {
 			/* We don't want to listen on this port */
@@ -561,7 +564,7 @@ static int fwd_sync_one(const struct ctx *c, uint8_t pif, unsigned idx,
 				/* We already are, so stop */
 				epoll_del(c->epollfd, fd);
 				close(fd);
-				rule->socks[port - rule->first] = -1;
+				rs->socks[port - rule->first] = -1;
 			}
 			continue;
 		}
@@ -593,7 +596,7 @@ static int fwd_sync_one(const struct ctx *c, uint8_t pif, unsigned idx,
 			continue;
 		}
 
-		rule->socks[port - rule->first] = fd;
+		rs->socks[port - rule->first] = fd;
 		bound_one = true;
 	}
 
@@ -683,11 +686,11 @@ void fwd_listen_close(const struct fwd_table *fwd)
 	unsigned i;
 
 	for (i = 0; i < fwd->count; i++) {
-		const struct fwd_rule *rule = &fwd->rules[i];
+		const struct fwd_rule_state *rs = &fwd->rules[i];
 		unsigned port;
 
-		for (port = rule->first; port <= rule->last; port++) {
-			int *fdp = &rule->socks[port - rule->first];
+		for (port = rs->rule.first; port <= rs->rule.last; port++) {
+			int *fdp = &rs->socks[port - rs->rule.first];
 			if (*fdp >= 0) {
 				close(*fdp);
 				*fdp = -1;
@@ -767,8 +770,8 @@ static bool has_scan_rules(const struct fwd_table *fwd, uint8_t proto)
 	unsigned i;
 
 	for (i = 0; i < fwd->count; i++) {
-		if (fwd->rules[i].proto == proto &&
-		    fwd->rules[i].flags & FWD_SCAN)
+		if (fwd->rules[i].rule.proto == proto &&
+		    fwd->rules[i].rule.flags & FWD_SCAN)
 			return true;
 	}
 	return false;
@@ -836,14 +839,14 @@ static void current_listen_map(uint8_t *map, const struct fwd_table *fwd,
 	memset(map, 0, PORT_BITMAP_SIZE);
 
 	for (i = 0; i < fwd->count; i++) {
-		const struct fwd_rule *rule = &fwd->rules[i];
+		const struct fwd_rule_state *rs = &fwd->rules[i];
 		unsigned port;
 
-		if (rule->proto != proto)
+		if (rs->rule.proto != proto)
 			continue;
 
-		for (port = rule->first; port <= rule->last; port++) {
-			if (rule->socks[port - rule->first] >= 0)
+		for (port = rs->rule.first; port <= rs->rule.last; port++) {
+			if (rs->socks[port - rs->rule.first] >= 0)
 				bitmap_set(map, port);
 		}
 	}
diff --git a/fwd.h b/fwd.h
index f111e139..d37f2cdc 100644
--- a/fwd.h
+++ b/fwd.h
@@ -15,6 +15,7 @@
 #include <netinet/in.h>
 
 #include "inany.h"
+#include "fwd_rule.h"
 
 struct flowside;
 
@@ -25,32 +26,12 @@ void fwd_probe_ephemeral(void);
 void fwd_port_map_ephemeral(uint8_t *map);
 
 /**
- * struct fwd_rule - Forwarding rule governing a range of ports
- * @addr:	Address to forward from
- * @ifname:	Interface to forward from
- * @first:	First port number to forward
- * @last:	Last port number to forward
- * @to:		Target port for @first, port n goes to @to + (n - @first)
- * @proto:	Protocol to forward
- * @flags:	Flag mask
- * 	FWD_DUAL_STACK_ANY - match any IPv4 or IPv6 address (@addr should be ::)
- *	FWD_WEAK - Don't give an error if binds fail for some forwards
- *	FWD_SCAN - Only forward if the matching port in the target is listening
+ * struct fwd_rule_state - Forwarding rule and associated state
+ * @rule:	Rule specification
  * @socks:	Array of listening sockets for this entry
- *
- * FIXME: @addr and @ifname currently ignored for outbound tables
  */
-struct fwd_rule {
-	union inany_addr addr;
-	char ifname[IFNAMSIZ];
-	in_port_t first;
-	in_port_t last;
-	in_port_t to;
-	uint8_t proto;
-#define FWD_DUAL_STACK_ANY	BIT(0)
-#define FWD_WEAK		BIT(1)
-#define FWD_SCAN		BIT(2)
-	uint8_t flags;
+struct fwd_rule_state {
+	struct fwd_rule rule;
 	int *socks;
 };
 
@@ -87,7 +68,7 @@ struct fwd_listen_ref {
  */
 struct fwd_table {
 	unsigned count;
-	struct fwd_rule rules[MAX_FWD_RULES];
+	struct fwd_rule_state rules[MAX_FWD_RULES];
 	unsigned sock_count;
 	int socks[MAX_LISTEN_SOCKS];
 };
diff --git a/fwd_rule.h b/fwd_rule.h
new file mode 100644
index 00000000..84ec5cbe
--- /dev/null
+++ b/fwd_rule.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright Red Hat
+ * Author: Stefano Brivio <sbrivio@redhat.com>
+ * Author: David Gibson <david@gibson.dropbear.id.au>
+ *
+ * Forwarding rule definitions shared between passt/pasta and pesto
+ */
+
+#ifndef FWD_RULE_H
+#define FWD_RULE_H
+
+#include <stdint.h>
+#include <net/if.h>
+#include <netinet/in.h>
+
+#include "common.h"
+#include "inany.h"
+
+/**
+ * struct fwd_rule - Forwarding rule governing a range of ports
+ * @addr:	Address to forward from
+ * @ifname:	Interface to forward from
+ * @first:	First port number to forward
+ * @last:	Last port number to forward
+ * @to:		Target port for @first, port n goes to @to + (n - @first)
+ * @proto:	Protocol to forward
+ * @flags:	Flag mask
+ * 	FWD_DUAL_STACK_ANY - match any IPv4 or IPv6 address (@addr should be ::)
+ *	FWD_WEAK - Don't give an error if binds fail for some forwards
+ *	FWD_SCAN - Only forward if the matching port in the target is listening
+ */
+struct fwd_rule {
+	union inany_addr addr;
+	char ifname[IFNAMSIZ];
+	in_port_t first;
+	in_port_t last;
+	in_port_t to;
+	uint8_t proto;
+#define FWD_DUAL_STACK_ANY	BIT(0)
+#define FWD_WEAK		BIT(1)
+#define FWD_SCAN		BIT(2)
+	uint8_t flags;
+};
+
+#endif /* FWD_RULE_H */
diff --git a/util.h b/util.h
index 45792f46..8495ed9e 100644
--- a/util.h
+++ b/util.h
@@ -35,7 +35,6 @@
 
 #define MAX_FROM_BITS(n)	(((1U << (n)) - 1))
 
-#define BIT(n)			(1UL << (n))
 #define BITMAP_BIT(n)		(BIT((n) % (sizeof(long) * 8)))
 #define BITMAP_WORD(n)		(n / (sizeof(long) * 8))
 
-- 
2.53.0


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

* [PATCH v3 19/25] ip: Define a bound for the string returned by ipproto_name()
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (17 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 18/25] fwd: Split forwading rule specification from its implementation state David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 20/25] fwd_rule: Move forwarding rule text formatting to common code David Gibson
                   ` (7 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

ipproto_name() returns a static string of theoretically unbounded length.
That's going to be inconvenient in future, so introduce IPPROTO_STRLEN
giving an explicit bound on the length.  Use static_assert() and some
macros to ensure nothing we return can exceed this.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 ip.c | 18 ++++++++++++------
 ip.h |  1 +
 2 files changed, 13 insertions(+), 6 deletions(-)

diff --git a/ip.c b/ip.c
index 4e4e0bf5..f2506bb1 100644
--- a/ip.c
+++ b/ip.c
@@ -15,6 +15,7 @@
  * Author: Stefano Brivio <sbrivio@redhat.com>
  */
 
+#include <assert.h>
 #include <stddef.h>
 #include <netinet/in.h>
 
@@ -24,7 +25,7 @@
  * ipproto_name() - Get IP protocol name from number
  * @proto:	IP protocol number
  *
- * Return: pointer to name of protocol @proto
+ * Return: pointer to name of protocol @proto (<= IPPROTO_STRLEN bytes)
  *
  * Usually this would be done with getprotobynumber(3) but that reads
  * /etc/protocols and might allocate, which isn't possible for us once
@@ -33,16 +34,21 @@
 const char *ipproto_name(uint8_t proto)
 {
 	switch (proto) {
+#define CASE(s)								\
+		static_assert(sizeof(s) <= IPPROTO_STRLEN,		\
+			      "Increase IPPROTO_STRLEN to contain " #s); \
+		return s;
 	case IPPROTO_ICMP:
-		return "ICMP";
+		CASE("ICMP");
 	case IPPROTO_TCP:
-		return "TCP";
+		CASE("TCP");
 	case IPPROTO_UDP:
-		return "UDP";
+		CASE("UDP");
 	case IPPROTO_ICMPV6:
-		return "ICMPv6";
+		CASE("ICMPv6");
 	default:
-		return "<unknown protocol>";
+		CASE("<unknown protocol>");
+#undef CASE
 	}
 }
 
diff --git a/ip.h b/ip.h
index f6c29e00..aab9b86a 100644
--- a/ip.h
+++ b/ip.h
@@ -117,6 +117,7 @@ static inline uint32_t ip6_get_flow_lbl(const struct ipv6hdr *ip6h)
 		ip6h->flow_lbl[2];
 }
 
+#define IPPROTO_STRLEN		(sizeof("<unknown protocol>"))
 const char *ipproto_name(uint8_t proto);
 
 /* IPv6 link-local all-nodes multicast address, ff02::1 */
-- 
2.53.0


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

* [PATCH v3 20/25] fwd_rule: Move forwarding rule text formatting to common code
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (18 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 19/25] ip: Define a bound for the string returned by ipproto_name() David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-25  0:56   ` Stefano Brivio
  2026-03-23  7:37 ` [PATCH v3 21/25] pesto: Read current ruleset from passt/pasta and display it David Gibson
                   ` (6 subsequent siblings)
  26 siblings, 1 reply; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Move the logic for formatting forwarding rules into strings from
fwd_rules_print() into fwd_rule.c where it can be shared with pesto.
We also make the function explicitly construct a string, rather than
directly printing with info(), for greater flexibility.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 Makefile   | 11 ++++----
 fwd.c      | 40 +++---------------------------
 fwd_rule.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 fwd_rule.h | 11 ++++++++
 4 files changed, 94 insertions(+), 41 deletions(-)
 create mode 100644 fwd_rule.c

diff --git a/Makefile b/Makefile
index bc325482..44c396e7 100644
--- a/Makefile
+++ b/Makefile
@@ -38,13 +38,14 @@ FLAGS += -DVERSION=\"$(VERSION)\"
 FLAGS += -DDUAL_STACK_SOCKETS=$(DUAL_STACK_SOCKETS)
 
 PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
-	flow.c fwd.c icmp.c igmp.c inany.c iov.c ip.c isolation.c lineread.c \
-	log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c pasta.c pcap.c \
-	pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c tcp_splice.c tcp_vu.c \
-	udp.c udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
+	flow.c fwd.c fwd_rule.c icmp.c igmp.c inany.c iov.c ip.c isolation.c \
+	lineread.c log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c \
+	pasta.c pcap.c pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c \
+	tcp_splice.c tcp_vu.c udp.c udp_flow.c udp_vu.c util.c vhost_user.c \
+	virtio.c vu_common.c
 QRAP_SRCS = qrap.c
 PASST_REPAIR_SRCS = passt-repair.c
-PESTO_SRCS = pesto.c inany.c ip.c serialise.c
+PESTO_SRCS = pesto.c fwd_rule.c inany.c ip.c serialise.c
 SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
 
 MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
diff --git a/fwd.c b/fwd.c
index a32d0a20..20409c62 100644
--- a/fwd.c
+++ b/fwd.c
@@ -304,20 +304,6 @@ parse_err:
 	warn("Unable to parse %s", PORT_RANGE_SYSCTL);
 }
 
-/**
- * fwd_rule_addr() - Return match address for a rule
- * @rule:	Forwarding rule
- *
- * Return: matching address for rule, NULL if it matches all addresses
- */
-static const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
-{
-	if (rule->flags & FWD_DUAL_STACK_ANY)
-		return NULL;
-
-	return &rule->addr;
-}
-
 /**
  * fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap
  * @map:	Bitmap to update
@@ -497,28 +483,10 @@ void fwd_rules_print(const struct fwd_table *fwd)
 	unsigned i;
 
 	for (i = 0; i < fwd->count; i++) {
-		const struct fwd_rule *rule = &fwd->rules[i].rule;
-		const char *percent = *rule->ifname ? "%" : "";
-		const char *weak = "", *scan = "";
-		char addr[INANY_ADDRSTRLEN];
-
-		inany_ntop(fwd_rule_addr(rule), addr, sizeof(addr));
-		if (rule->flags & FWD_WEAK)
-			weak = " (best effort)";
-		if (rule->flags & FWD_SCAN)
-			scan = " (auto-scan)";
-
-		if (rule->first == rule->last) {
-			info("    %s [%s]%s%s:%hu  =>  %hu %s%s",
-			     ipproto_name(rule->proto), addr, percent,
-			     rule->ifname, rule->first, rule->to, weak, scan);
-		} else {
-			info("    %s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s",
-			     ipproto_name(rule->proto), addr, percent,
-			     rule->ifname, rule->first, rule->last,
-			     rule->to, rule->last - rule->first + rule->to,
-			     weak, scan);
-		}
+		char rulestr[FWD_RULE_STRLEN];
+
+		info("    %s", fwd_rule_ntop(&fwd->rules[i].rule,
+					     rulestr, sizeof(rulestr)));
 	}
 }
 
diff --git a/fwd_rule.c b/fwd_rule.c
new file mode 100644
index 00000000..dfbdf683
--- /dev/null
+++ b/fwd_rule.c
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/* PASST - Plug A Simple Socket Transport
+ *  for qemu/UNIX domain socket mode
+ *
+ * PASTA - Pack A Subtle Tap Abstraction
+ *  for network namespace/tap device mode
+ *
+ * PESTO - Programmable Extensible Socket Translation Orchestrator
+ *  front-end for passt(1) and pasta(1) forwarding configuration
+ *
+ * fwd_rule.c - Helpers for working with forwarding rule specifications
+ *
+ * Copyright Red Hat
+ * Author: David Gibson <david@gibson.dropbear.id.au>
+ */
+
+#include <stdio.h>
+
+#include "fwd_rule.h"
+
+/**
+ * fwd_rule_addr() - Return match address for a rule
+ * @rule:	Forwarding rule
+ *
+ * Return: matching address for rule, NULL if it matches all addresses
+ */
+const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
+{
+	if (rule->flags & FWD_DUAL_STACK_ANY)
+		return NULL;
+
+	return &rule->addr;
+}
+
+/**
+ * fwd_rule_ntop() - Format forwarding rule as a string
+ * @rule:	Rule to format
+ * @dst:	Buffer to store output (should have FWD_RULE_STRLEN bytes)
+ * @size:	Size of @dst
+ */
+const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size)
+{
+	const char *percent = *rule->ifname ? "%" : "";
+	const char *weak = "", *scan = "";
+	char addr[INANY_ADDRSTRLEN];
+	int len;
+
+	inany_ntop(fwd_rule_addr(rule), addr, sizeof(addr));
+	if (rule->flags & FWD_WEAK)
+		weak = " (best effort)";
+	if (rule->flags & FWD_SCAN)
+		scan = " (auto-scan)";
+
+	if (rule->first == rule->last) {
+		len = snprintf(dst, size,
+			       "%s [%s]%s%s:%hu  =>  %hu %s%s",
+			       ipproto_name(rule->proto), addr, percent,
+			       rule->ifname, rule->first, rule->to, weak, scan);
+	} else {
+		in_port_t tolast = rule->last - rule->first + rule->to;
+		len = snprintf(dst, size,
+			       "%s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s",
+			       ipproto_name(rule->proto), addr, percent,
+			       rule->ifname, rule->first, rule->last,
+			       rule->to, tolast, weak, scan);
+	}
+
+	if (len < 0 || (size_t)len >= size)
+		return NULL;
+
+	return dst;
+}
diff --git a/fwd_rule.h b/fwd_rule.h
index 84ec5cbe..59db0e95 100644
--- a/fwd_rule.h
+++ b/fwd_rule.h
@@ -42,4 +42,15 @@ struct fwd_rule {
 	uint8_t flags;
 };
 
+const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
+
+#define FWD_RULE_STRLEN					    \
+	(IPPROTO_STRLEN - 1				    \
+	 + INANY_ADDRSTRLEN - 1				    \
+	 + IFNAMSIZ - 1					    \
+	 + sizeof(" (best effort)") - 1			    \
+	 + sizeof(" (auto-scan)") - 1			    \
+	 + 15)
+const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size);
+
 #endif /* FWD_RULE_H */
-- 
2.53.0


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

* [PATCH v3 21/25] pesto: Read current ruleset from passt/pasta and display it
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (19 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 20/25] fwd_rule: Move forwarding rule text formatting to common code David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-25  0:56   ` Stefano Brivio
  2026-03-23  7:37 ` [PATCH v3 22/25] conf: Move port parsing functions to own file, ports.c David Gibson
                   ` (5 subsequent siblings)
  26 siblings, 1 reply; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

Implement serialisation of our current forwarding rules in conf.c,
deserialising it to display in the pesto client.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 conf.c     | 44 +++++++++++++++++++++++++++++++
 fwd_rule.c | 40 ++++++++++++++++++++++++++++
 fwd_rule.h |  3 +++
 pesto.c    | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 4 files changed, 160 insertions(+), 4 deletions(-)

diff --git a/conf.c b/conf.c
index b878db94..7f311914 100644
--- a/conf.c
+++ b/conf.c
@@ -2338,6 +2338,47 @@ static int conf_send_pifs(const struct ctx *c, int fd)
 	return 0;
 }
 
+/**
+ * conf_send_rules() - Send current forwarding rules to dynamic update client (pesto)
+ * @c:		Execution context
+ * @fd:		Socket to the client
+ *
+ * Return: 0 on success, -1 on failure
+ */
+static int conf_send_rules(const struct ctx *c, int fd)
+{
+	unsigned pif;
+
+	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
+		const struct fwd_table *fwd = c->fwd[pif];
+		unsigned i;
+
+		if (!fwd)
+			continue;
+
+		assert(pif);
+
+		/* PIF id */
+		if (write_u8(fd, pif))
+			return -1;
+
+		/* Number of rules */
+		if (write_u32(fd, fwd->count))
+			return -1;
+
+		for (i = 0; i < fwd->count; i++) {
+			if (fwd_rule_write(fd, &fwd->rules[i].rule))
+				return -1;
+		}
+	}
+
+	/* Write 0 PIF id to finish */
+	if (write_u8(fd, 0))
+		return -1;
+
+	return 0;
+}
+
 /**
  * conf_listen_handler() - Handle events on configuration listening socket
  * @c:		Execution context
@@ -2397,6 +2438,9 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
 	if (conf_send_pifs(c, fd) < 0)
 		goto fail;
 
+	if (conf_send_rules(c, fd) < 0)
+		goto fail;
+
 	return;
 
 fail:
diff --git a/fwd_rule.c b/fwd_rule.c
index dfbdf683..b2ed9b3b 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -17,6 +17,8 @@
 
 #include <stdio.h>
 
+#include "serialise.h"
+
 #include "fwd_rule.h"
 
 /**
@@ -71,3 +73,41 @@ const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size)
 
 	return dst;
 }
+
+/**
+ * fwd_rule_read() - Read serialised rule from an fd
+ * @fd:		fd to serialise to
+ * @rule:	Buffer to store rule into
+ *
+ * Return: 0 on success, -1 on error (with errno set)
+ */
+int fwd_rule_read(int fd, struct fwd_rule *rule)
+{
+	if (read_all_buf(fd, rule, sizeof(*rule)))
+		return -1;
+
+	/* Byteswap for host */
+	rule->first = ntohs(rule->first);
+	rule->last = ntohs(rule->last);
+	rule->to = htons(rule->to);
+	return 0;
+}
+
+/**
+ * fwd_rule_write() - Serialise rule to an fd
+ * @fd:		fd to serialise to
+ * @rule:	Rule to send
+ *
+ * Return: 0 on success, -1 on error (with errno set)
+ */
+int fwd_rule_write(int fd, const struct fwd_rule *rule)
+{
+	struct fwd_rule tmp = *rule;
+
+	/* Byteswap for transport */
+	tmp.first = htons(tmp.first);
+	tmp.last = htons(tmp.last);
+	tmp.to = htons(tmp.to);
+
+	return write_all_buf(fd, &tmp, sizeof(tmp));
+}
diff --git a/fwd_rule.h b/fwd_rule.h
index 59db0e95..a4d54da0 100644
--- a/fwd_rule.h
+++ b/fwd_rule.h
@@ -53,4 +53,7 @@ const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
 	 + 15)
 const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size);
 
+int fwd_rule_read(int fd, struct fwd_rule *rule);
+int fwd_rule_write(int fd, const struct fwd_rule *rule);
+
 #endif /* FWD_RULE_H */
diff --git a/pesto.c b/pesto.c
index ed4f2aab..11447575 100644
--- a/pesto.c
+++ b/pesto.c
@@ -34,6 +34,7 @@
 #include "common.h"
 #include "seccomp_pesto.h"
 #include "serialise.h"
+#include "fwd_rule.h"
 #include "pesto.h"
 
 static int verbosity = 1;
@@ -108,6 +109,8 @@ static const char *pesto_recv_str(int fd)
 struct pif_state {
 	uint8_t pif;
 	const char *name;
+	uint32_t count;
+	struct fwd_rule *rule;
 };
 
 struct conf_state {
@@ -119,7 +122,7 @@ struct conf_state {
  * pesto_read_pifs() - Read pif names and IDs from passt/pasta
  * @fd:		Control socket
  */
-static const struct conf_state *pesto_read_pifs(int fd)
+static struct conf_state *pesto_read_pifs(int fd)
 {
 	uint32_t num;
 	struct conf_state *state;
@@ -146,18 +149,81 @@ static const struct conf_state *pesto_read_pifs(int fd)
 	return state;
 }
 
+/**
+ * find_pif_state() - Find the pif state structure for a given pif id
+ * @state:	Rule state information
+ * @pif:	pif id
+ *
+ * Return: pointer to the pif_state for @pif, or NULL if not found
+ */
+static struct pif_state *find_pif_state(struct conf_state *state, uint8_t pif)
+{
+	unsigned i;
+
+	for (i = 0; i < state->npifs; i++) {
+		if (state->pif[i].pif == pif)
+			return &state->pif[i];
+	}
+
+	return NULL;
+}
+
+/**
+ * pesto_read_rules() - Read a set of rules for one pif
+ * @fd:		Control socket
+ * @state:	Rule state information to update
+ *
+ * Return: true if there may be more rules to read, false if finished
+ */
+static bool pesto_read_rules(int fd, struct conf_state *state)
+{
+	struct pif_state *ps;
+	uint8_t pif;
+	unsigned i;
+
+	if (read_u8(fd, &pif) < 0)
+		die("Error reading from control socket");
+
+	if (!pif)
+		return false;
+
+	ps = find_pif_state(state, pif);
+	if (!ps)
+		die("Received rules for an unknown pif");
+
+	if (read_u32(fd, &ps->count) < 0)
+		die("Error reading from control socket");
+
+	debug("Receiving rules %"PRIu32" rules for %s", ps->count, ps->name);
+
+	ps->rule = xmalloc(sizeof(*ps->rule) * ps->count);
+
+	for (i = 0; i < ps->count; i++) {
+		if (fwd_rule_read(fd, &ps->rule[i]) < 0)
+			die("Error reading from control socket");
+	}
+
+	return true;
+}
+
 /**
  * show_state() - Show current rule state obtained from passt/pasta
  * @pifs:	PIF name information
  */
 static void show_state(const struct conf_state *state)
 {
-	unsigned i;
+	unsigned i, j;
 
 	for (i = 0; i < state->npifs; i++) {
 		const struct pif_state *ps = &state->pif[i];
 		printf("Forwarding rules for %s interface\n", ps->name);
-		printf("\tTBD\n");
+		for (j = 0; j < ps->count; j++) {
+			const struct fwd_rule *rule = &ps->rule[j];
+			char rulestr[FWD_RULE_STRLEN];
+
+			printf("    %s\n", fwd_rule_ntop(rule, rulestr,
+							 sizeof(rulestr)));
+		}
 	}
 }
 
@@ -183,9 +249,9 @@ int main(int argc, char **argv)
 		{ 0 },
 	};
 	struct sockaddr_un a = { AF_UNIX, "" };
-	const struct conf_state *state;
 	const char *optstring = "vh";
 	struct pesto_hello hello;
+	struct conf_state *state;
 	struct sock_fprog prog;
 	int optname, ret, s;
 	uint32_t s_version;
@@ -266,6 +332,9 @@ int main(int argc, char **argv)
 
 	state = pesto_read_pifs(s);
 
+	while (pesto_read_rules(s, state))
+		;
+
 	show_state(state);
 
 	exit(0);
-- 
2.53.0


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

* [PATCH v3 22/25] conf: Move port parsing functions to own file, ports.c
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (20 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 21/25] pesto: Read current ruleset from passt/pasta and display it David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 23/25] conf, fwd, ports, util: Move things around for pesto David Gibson
                   ` (4 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

From: Stefano Brivio <sbrivio@redhat.com>

Move conf_ports_range_except(), conf_ports(), and related to helpers
to ports.c, so that they can be used from pesto in the future. We'll
need to make those independent from passt-specific bits, but this
patch just moves them out, first.

Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
Message-ID: <20260322141843.4095972-1-sbrivio@redhat.com>
Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 Makefile |  13 +-
 conf.c   | 357 +---------------------------------------------------
 ports.c  | 375 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 ports.h  |  35 ++++++
 4 files changed, 418 insertions(+), 362 deletions(-)
 create mode 100644 ports.c
 create mode 100644 ports.h

diff --git a/Makefile b/Makefile
index 44c396e7..47d4c950 100644
--- a/Makefile
+++ b/Makefile
@@ -40,9 +40,9 @@ FLAGS += -DDUAL_STACK_SOCKETS=$(DUAL_STACK_SOCKETS)
 PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
 	flow.c fwd.c fwd_rule.c icmp.c igmp.c inany.c iov.c ip.c isolation.c \
 	lineread.c log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c \
-	pasta.c pcap.c pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c \
-	tcp_splice.c tcp_vu.c udp.c udp_flow.c udp_vu.c util.c vhost_user.c \
-	virtio.c vu_common.c
+	pasta.c pcap.c pif.c ports.c repair.c serialise.c tap.c tcp.c \
+	tcp_buf.c tcp_splice.c tcp_vu.c udp.c udp_flow.c udp_vu.c util.c \
+	vhost_user.c virtio.c vu_common.c
 QRAP_SRCS = qrap.c
 PASST_REPAIR_SRCS = passt-repair.c
 PESTO_SRCS = pesto.c fwd_rule.c inany.c ip.c serialise.c
@@ -54,9 +54,10 @@ PESTO_HEADERS = common.h fwd_rule.h inany.h ip.h pesto.h serialise.h
 PASST_HEADERS = arch.h arp.h checksum.h conf.h dhcp.h dhcpv6.h epoll_ctl.h \
 	flow.h fwd.h flow_table.h icmp.h icmp_flow.h iov.h isolation.h \
 	lineread.h log.h migrate.h ndp.h netlink.h packet.h passt.h pasta.h \
-	pcap.h pif.h repair.h siphash.h tap.h tcp.h tcp_buf.h tcp_conn.h \
-	tcp_internal.h tcp_splice.h tcp_vu.h udp.h udp_flow.h udp_internal.h \
-	udp_vu.h util.h vhost_user.h virtio.h vu_common.h $(PESTO_HEADERS)
+	pcap.h pif.h ports.h repair.h siphash.h tap.h tcp.h tcp_buf.h \
+	tcp_conn.h tcp_internal.h tcp_splice.h tcp_vu.h udp.h udp_flow.h \
+	udp_internal.h udp_vu.h util.h vhost_user.h virtio.h vu_common.h \
+	$(PESTO_HEADERS)
 HEADERS = $(PASST_HEADERS) seccomp.h
 
 C := \#include <sys/random.h>\nint main(){int a=getrandom(0, 0, 0);}
diff --git a/conf.c b/conf.c
index 7f311914..1ed1ee54 100644
--- a/conf.c
+++ b/conf.c
@@ -52,6 +52,7 @@
 #include "conf.h"
 #include "pesto.h"
 #include "serialise.h"
+#include "ports.h"
 
 #define NETNS_RUN_DIR	"/run/netns"
 
@@ -69,362 +70,6 @@
 
 const char *pasta_default_ifn = "tap0";
 
-/**
- * next_chunk() - Return the next piece of a string delimited by a character
- * @s:		String to search
- * @c:		Delimiter character
- *
- * Return: if another @c is found in @s, returns a pointer to the
- *	   character *after* the delimiter, if no further @c is in @s,
- *	   return NULL
- */
-static char *next_chunk(const char *s, char c)
-{
-	char *sep = strchr(s, c);
-	return sep ? sep + 1 : NULL;
-}
-
-/**
- * port_range() - Represents a non-empty range of ports
- * @first:	First port number in the range
- * @last:	Last port number in the range (inclusive)
- *
- * Invariant:	@last >= @first
- */
-struct port_range {
-	in_port_t first, last;
-};
-
-/**
- * parse_port_range() - Parse a range of port numbers '<first>[-<last>]'
- * @s:		String to parse
- * @endptr:	Update to the character after the parsed range (similar to
- *		strtol() etc.)
- * @range:	Update with the parsed values on success
- *
- * Return: -EINVAL on parsing error, -ERANGE on out of range port
- *	   numbers, 0 on success
- */
-static int parse_port_range(const char *s, char **endptr,
-			    struct port_range *range)
-{
-	unsigned long first, last;
-
-	last = first = strtoul(s, endptr, 10);
-	if (*endptr == s) /* Parsed nothing */
-		return -EINVAL;
-	if (**endptr == '-') { /* we have a last value too */
-		const char *lasts = *endptr + 1;
-		last = strtoul(lasts, endptr, 10);
-		if (*endptr == lasts) /* Parsed nothing */
-			return -EINVAL;
-	}
-
-	if ((last < first) || (last >= NUM_PORTS))
-		return -ERANGE;
-
-	range->first = first;
-	range->last = last;
-
-	return 0;
-}
-
-/**
- * conf_ports_range_except() - Set up forwarding for a range of ports minus a
- *                             bitmap of exclusions
- * @c:		Execution context
- * @optname:	Short option name, t, T, u, or U
- * @optarg:	Option argument (port specification)
- * @fwd:	Forwarding table to be updated
- * @addr:	Listening address
- * @ifname:	Listening interface
- * @first:	First port to forward
- * @last:	Last port to forward
- * @exclude:	Bitmap of ports to exclude (may be NULL)
- * @to:		Port to translate @first to when forwarding
- * @flags:	Flags for forwarding entries
- */
-static void conf_ports_range_except(const struct ctx *c, char optname,
-				    const char *optarg, struct fwd_table *fwd,
-				    const union inany_addr *addr,
-				    const char *ifname,
-				    uint16_t first, uint16_t last,
-				    const uint8_t *exclude, uint16_t to,
-				    uint8_t flags)
-{
-	unsigned delta = to - first;
-	unsigned base, i;
-	uint8_t proto;
-
-	assert(first != 0);
-
-	if (optname == 't' || optname == 'T')
-		proto = IPPROTO_TCP;
-	else if (optname == 'u' || optname == 'U')
-		proto = IPPROTO_UDP;
-	else
-		assert(0);
-
-	for (base = first; base <= last; base++) {
-		if (exclude && bitmap_isset(exclude, base))
-			continue;
-
-		for (i = base; i <= last; i++) {
-			if (exclude && bitmap_isset(exclude, i))
-				break;
-		}
-
-		if ((optname == 'T' || optname == 'U') && c->no_bindtodevice) {
-			/* FIXME: Once the fwd bitmaps are removed, move this
-			 * workaround to the caller
-			 */
-			assert(!addr && ifname && !strcmp(ifname, "lo"));
-			warn(
-"SO_BINDTODEVICE unavailable, forwarding only 127.0.0.1 and ::1 for '-%c %s'",
-			     optname, optarg);
-
-			if (c->ifi4) {
-				fwd_rule_add(fwd, proto, flags,
-					     &inany_loopback4, NULL,
-					     base, i - 1, base + delta);
-			}
-			if (c->ifi6) {
-				fwd_rule_add(fwd, proto, flags,
-					     &inany_loopback6, NULL,
-					     base, i - 1, base + delta);
-			}
-		} else {
-			fwd_rule_add(fwd, proto, flags, addr, ifname,
-				     base, i - 1, base + delta);
-		}
-		base = i - 1;
-	}
-}
-
-/**
- * enum fwd_mode - Overall forwarding mode for a direction and protocol
- * @FWD_MODE_UNSET	Initial value, not parsed/configured yet
- * @FWD_MODE_SPEC	Forward specified ports
- * @FWD_MODE_NONE	No forwarded ports
- * @FWD_MODE_AUTO	Automatic detection and forwarding based on bound ports
- * @FWD_MODE_ALL	Bind all free ports
- */
-enum fwd_mode {
-	FWD_MODE_UNSET = 0,
-	FWD_MODE_SPEC,
-	FWD_MODE_NONE,
-	FWD_MODE_AUTO,
-	FWD_MODE_ALL,
-};
-
-/**
- * conf_ports() - Parse port configuration options, initialise UDP/TCP sockets
- * @c:		Execution context
- * @optname:	Short option name, t, T, u, or U
- * @optarg:	Option argument (port specification)
- * @fwd:	Forwarding table to be updated
- * @mode:	Overall port forwarding mode (updated)
- */
-static void conf_ports(const struct ctx *c, char optname, const char *optarg,
-		       struct fwd_table *fwd, enum fwd_mode *mode)
-{
-	union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
-	char buf[BUFSIZ], *spec, *ifname = NULL, *p;
-	uint8_t exclude[PORT_BITMAP_SIZE] = { 0 };
-	bool exclude_only = true;
-	unsigned i;
-
-	if (!strcmp(optarg, "none")) {
-		if (*mode)
-			goto mode_conflict;
-
-		*mode = FWD_MODE_NONE;
-		return;
-	}
-
-	if ((optname == 't' || optname == 'T') && c->no_tcp)
-		die("TCP port forwarding requested but TCP is disabled");
-	if ((optname == 'u' || optname == 'U') && c->no_udp)
-		die("UDP port forwarding requested but UDP is disabled");
-
-	if (!strcmp(optarg, "auto")) {
-		if (*mode)
-			goto mode_conflict;
-
-		if (c->mode != MODE_PASTA)
-			die("'auto' port forwarding is only allowed for pasta");
-
-		if ((optname == 'T' || optname == 'U') && c->no_bindtodevice) {
-			warn(
-"'-%c auto' enabled without unprivileged SO_BINDTODEVICE", optname);
-			warn(
-"Forwarding from addresses other than 127.0.0.1 will not work");
-		}
-		*mode = FWD_MODE_AUTO;
-		return;
-	}
-
-	if (!strcmp(optarg, "all")) {
-		if (*mode)
-			goto mode_conflict;
-
-		if (c->mode == MODE_PASTA)
-			die("'all' port forwarding is only allowed for passt");
-
-		*mode = FWD_MODE_ALL;
-
-		/* Exclude ephemeral ports */
-		fwd_port_map_ephemeral(exclude);
-
-		conf_ports_range_except(c, optname, optarg, fwd,
-					NULL, NULL,
-					1, NUM_PORTS - 1, exclude,
-					1, FWD_WEAK);
-		return;
-	}
-
-	if (*mode > FWD_MODE_SPEC)
-		die("Specific ports cannot be specified together with all/none/auto");
-
-	*mode = FWD_MODE_SPEC;
-
-	strncpy(buf, optarg, sizeof(buf) - 1);
-
-	if ((spec = strchr(buf, '/'))) {
-		*spec = 0;
-		spec++;
-
-		if (optname != 't' && optname != 'u')
-			goto bad;
-
-		if ((ifname = strchr(buf, '%'))) {
-			*ifname = 0;
-			ifname++;
-
-			/* spec is already advanced one past the '/',
-			 * so the length of the given ifname is:
-			 * (spec - ifname - 1)
-			 */
-			if (spec - ifname - 1 >= IFNAMSIZ)
-				goto bad;
-
-		}
-
-		if (ifname == buf + 1) {	/* Interface without address */
-			addr = NULL;
-		} else {
-			p = buf;
-
-			/* Allow square brackets for IPv4 too for convenience */
-			if (*p == '[' && p[strlen(p) - 1] == ']') {
-				p[strlen(p) - 1] = '\0';
-				p++;
-			}
-
-			if (!inany_pton(p, addr))
-				goto bad;
-		}
-	} else {
-		spec = buf;
-
-		addr = NULL;
-	}
-
-	if (addr) {
-		if (!c->ifi4 && inany_v4(addr)) {
-			die("IPv4 is disabled, can't use -%c %s",
-			    optname, optarg);
-		} else if (!c->ifi6 && !inany_v4(addr)) {
-			die("IPv6 is disabled, can't use -%c %s",
-			    optname, optarg);
-		}
-	}
-
-	/* Mark all exclusions first, they might be given after base ranges */
-	p = spec;
-	do {
-		struct port_range xrange;
-
-		if (*p != '~') {
-			/* Not an exclude range, parse later */
-			exclude_only = false;
-			continue;
-		}
-		p++;
-
-		if (parse_port_range(p, &p, &xrange))
-			goto bad;
-		if ((*p != '\0')  && (*p != ',')) /* Garbage after the range */
-			goto bad;
-
-		for (i = xrange.first; i <= xrange.last; i++)
-			bitmap_set(exclude, i);
-	} while ((p = next_chunk(p, ',')));
-
-	if (ifname && c->no_bindtodevice) {
-		die(
-"Device binding for '-%c %s' unsupported (requires kernel 5.7+)",
-		    optname, optarg);
-	}
-	/* Outbound forwards come from guest loopback */
-	if ((optname == 'T' || optname == 'U') && !ifname)
-		ifname = "lo";
-
-	if (exclude_only) {
-		/* Exclude ephemeral ports */
-		fwd_port_map_ephemeral(exclude);
-
-		conf_ports_range_except(c, optname, optarg, fwd,
-					addr, ifname,
-					1, NUM_PORTS - 1, exclude,
-					1, FWD_WEAK);
-		return;
-	}
-
-	/* Now process base ranges, skipping exclusions */
-	p = spec;
-	do {
-		struct port_range orig_range, mapped_range;
-
-		if (*p == '~')
-			/* Exclude range, already parsed */
-			continue;
-
-		if (parse_port_range(p, &p, &orig_range))
-			goto bad;
-
-		if (*p == ':') { /* There's a range to map to as well */
-			if (parse_port_range(p + 1, &p, &mapped_range))
-				goto bad;
-			if ((mapped_range.last - mapped_range.first) !=
-			    (orig_range.last - orig_range.first))
-				goto bad;
-		} else {
-			mapped_range = orig_range;
-		}
-
-		if ((*p != '\0')  && (*p != ',')) /* Garbage after the ranges */
-			goto bad;
-
-		if (orig_range.first == 0) {
-			die("Can't forward port 0 for option '-%c %s'",
-			    optname, optarg);
-		}
-
-		conf_ports_range_except(c, optname, optarg, fwd,
-					addr, ifname,
-					orig_range.first, orig_range.last,
-					exclude,
-					mapped_range.first, 0);
-	} while ((p = next_chunk(p, ',')));
-
-	return;
-bad:
-	die("Invalid port specifier %s", optarg);
-mode_conflict:
-	die("Port forwarding mode '%s' conflicts with previous mode", optarg);
-}
-
 /**
  * add_dns4() - Possibly add the IPv4 address of a DNS resolver to configuration
  * @c:		Execution context
diff --git a/ports.c b/ports.c
new file mode 100644
index 00000000..79abafc9
--- /dev/null
+++ b/ports.c
@@ -0,0 +1,375 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/* PASST - Plug A Simple Socket Transport
+ *  for qemu/UNIX domain socket mode
+ *
+ * PASTA - Pack A Subtle Tap Abstraction
+ *  for network namespace/tap device mode
+ *
+ * PESTO - Programmable Extensible Socket Translation Orchestrator
+ *  front-end for passt(1) and pasta(1) forwarding configuration
+ *
+ * ports.c - Parse port options
+ *
+ * Copyright (c) 2026 Red Hat GmbH
+ * Author: Stefano Brivio <sbrivio@redhat.com>
+ * Author: David Gibson <david@gibson.dropbear.id.au>
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <string.h>
+#include <sched.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <limits.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <netinet/in.h>
+#include <netinet/if_ether.h>
+
+#include "common.h"
+#include "util.h"
+#include "ip.h"
+#include "passt.h"
+#include "common.h"
+#include "pesto.h"
+#include "ports.h"
+
+/**
+ * next_chunk() - Return the next piece of a string delimited by a character
+ * @s:		String to search
+ * @c:		Delimiter character
+ *
+ * Return: if another @c is found in @s, returns a pointer to the
+ *	   character *after* the delimiter, if no further @c is in @s,
+ *	   return NULL
+ */
+static char *next_chunk(const char *s, char c)
+{
+	char *sep = strchr(s, c);
+	return sep ? sep + 1 : NULL;
+}
+
+/**
+ * port_range() - Represents a non-empty range of ports
+ * @first:	First port number in the range
+ * @last:	Last port number in the range (inclusive)
+ *
+ * Invariant:	@last >= @first
+ */
+struct port_range {
+	in_port_t first, last;
+};
+
+/**
+ * parse_port_range() - Parse a range of port numbers '<first>[-<last>]'
+ * @s:		String to parse
+ * @endptr:	Update to the character after the parsed range (similar to
+ *		strtol() etc.)
+ * @range:	Update with the parsed values on success
+ *
+ * Return: -EINVAL on parsing error, -ERANGE on out of range port
+ *	   numbers, 0 on success
+ */
+static int parse_port_range(const char *s, char **endptr,
+			    struct port_range *range)
+{
+	unsigned long first, last;
+
+	last = first = strtoul(s, endptr, 10);
+	if (*endptr == s) /* Parsed nothing */
+		return -EINVAL;
+	if (**endptr == '-') { /* we have a last value too */
+		const char *lasts = *endptr + 1;
+		last = strtoul(lasts, endptr, 10);
+		if (*endptr == lasts) /* Parsed nothing */
+			return -EINVAL;
+	}
+
+	if ((last < first) || (last >= NUM_PORTS))
+		return -ERANGE;
+
+	range->first = first;
+	range->last = last;
+
+	return 0;
+}
+
+/**
+ * conf_ports_range_except() - Set up forwarding for a range of ports minus a
+ *                             bitmap of exclusions
+ * @c:		Execution context
+ * @optname:	Short option name, t, T, u, or U
+ * @optarg:	Option argument (port specification)
+ * @fwd:	Forwarding table to be updated
+ * @addr:	Listening address
+ * @ifname:	Listening interface
+ * @first:	First port to forward
+ * @last:	Last port to forward
+ * @exclude:	Bitmap of ports to exclude (may be NULL)
+ * @to:		Port to translate @first to when forwarding
+ * @flags:	Flags for forwarding entries
+ */
+void conf_ports_range_except(const struct ctx *c, char optname,
+			     const char *optarg, struct fwd_table *fwd,
+			     const union inany_addr *addr,
+			     const char *ifname, uint16_t first, uint16_t last,
+			     const uint8_t *exclude, uint16_t to, uint8_t flags)
+{
+	unsigned delta = to - first;
+	unsigned base, i;
+	uint8_t proto;
+
+	assert(first != 0);
+
+	if (optname == 't' || optname == 'T')
+		proto = IPPROTO_TCP;
+	else if (optname == 'u' || optname == 'U')
+		proto = IPPROTO_UDP;
+	else
+		assert(0);
+
+	for (base = first; base <= last; base++) {
+		if (exclude && bitmap_isset(exclude, base))
+			continue;
+
+		for (i = base; i <= last; i++) {
+			if (exclude && bitmap_isset(exclude, i))
+				break;
+		}
+
+		if ((optname == 'T' || optname == 'U') && c->no_bindtodevice) {
+			/* FIXME: Once the fwd bitmaps are removed, move this
+			 * workaround to the caller
+			 */
+			assert(!addr && ifname && !strcmp(ifname, "lo"));
+			warn(
+"SO_BINDTODEVICE unavailable, forwarding only 127.0.0.1 and ::1 for '-%c %s'",
+			     optname, optarg);
+
+			if (c->ifi4) {
+				fwd_rule_add(fwd, proto, flags,
+					     &inany_loopback4, NULL,
+					     base, i - 1, base + delta);
+			}
+			if (c->ifi6) {
+				fwd_rule_add(fwd, proto, flags,
+					     &inany_loopback6, NULL,
+					     base, i - 1, base + delta);
+			}
+		} else {
+			fwd_rule_add(fwd, proto, flags, addr, ifname,
+				     base, i - 1, base + delta);
+		}
+		base = i - 1;
+	}
+}
+
+/**
+ * conf_ports() - Parse port configuration options, initialise UDP/TCP sockets
+ * @c:		Execution context
+ * @optname:	Short option name, t, T, u, or U
+ * @optarg:	Option argument (port specification)
+ * @fwd:	Forwarding table to be updated
+ * @mode:	Overall port forwarding mode (updated)
+ */
+void conf_ports(const struct ctx *c, char optname, const char *optarg,
+		struct fwd_table *fwd, enum fwd_mode *mode)
+{
+	union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
+	char buf[BUFSIZ], *spec, *ifname = NULL, *p;
+	uint8_t exclude[PORT_BITMAP_SIZE] = { 0 };
+	bool exclude_only = true;
+
+	if (!strcmp(optarg, "none")) {
+		if (*mode)
+			goto mode_conflict;
+
+		*mode = FWD_MODE_NONE;
+		return;
+	}
+
+	if ((optname == 't' || optname == 'T') && c->no_tcp)
+		die("TCP port forwarding requested but TCP is disabled");
+	if ((optname == 'u' || optname == 'U') && c->no_udp)
+		die("UDP port forwarding requested but UDP is disabled");
+
+	if (!strcmp(optarg, "auto")) {
+		if (*mode)
+			goto mode_conflict;
+
+		if (c->mode != MODE_PASTA)
+			die("'auto' port forwarding is only allowed for pasta");
+
+		if ((optname == 'T' || optname == 'U') && c->no_bindtodevice) {
+			warn(
+"'-%c auto' enabled without unprivileged SO_BINDTODEVICE", optname);
+			warn(
+"Forwarding from addresses other than 127.0.0.1 will not work");
+		}
+		*mode = FWD_MODE_AUTO;
+		return;
+	}
+
+	if (!strcmp(optarg, "all")) {
+		if (*mode)
+			goto mode_conflict;
+
+		if (c->mode == MODE_PASTA)
+			die("'all' port forwarding is only allowed for passt");
+
+		*mode = FWD_MODE_ALL;
+
+		/* Exclude ephemeral ports */
+		fwd_port_map_ephemeral(exclude);
+
+		conf_ports_range_except(c, optname, optarg, fwd,
+					NULL, NULL,
+					1, NUM_PORTS - 1, exclude,
+					1, FWD_WEAK);
+		return;
+	}
+
+	if (*mode > FWD_MODE_SPEC)
+		die("Specific ports cannot be specified together with all/none/auto");
+
+	*mode = FWD_MODE_SPEC;
+
+	strncpy(buf, optarg, sizeof(buf) - 1);
+
+	if ((spec = strchr(buf, '/'))) {
+		*spec = 0;
+		spec++;
+
+		if (optname != 't' && optname != 'u')
+			goto bad;
+
+		if ((ifname = strchr(buf, '%'))) {
+			*ifname = 0;
+			ifname++;
+
+			/* spec is already advanced one past the '/',
+			 * so the length of the given ifname is:
+			 * (spec - ifname - 1)
+			 */
+			if (spec - ifname - 1 >= IFNAMSIZ)
+				goto bad;
+
+		}
+
+		if (ifname == buf + 1) {	/* Interface without address */
+			addr = NULL;
+		} else {
+			p = buf;
+
+			/* Allow square brackets for IPv4 too for convenience */
+			if (*p == '[' && p[strlen(p) - 1] == ']') {
+				p[strlen(p) - 1] = '\0';
+				p++;
+			}
+
+			if (!inany_pton(p, addr))
+				goto bad;
+		}
+	} else {
+		spec = buf;
+
+		addr = NULL;
+	}
+
+	if (addr) {
+		if (!c->ifi4 && inany_v4(addr)) {
+			die("IPv4 is disabled, can't use -%c %s",
+			    optname, optarg);
+		} else if (!c->ifi6 && !inany_v4(addr)) {
+			die("IPv6 is disabled, can't use -%c %s",
+			    optname, optarg);
+		}
+	}
+
+	/* Mark all exclusions first, they might be given after base ranges */
+	p = spec;
+	do {
+		struct port_range xrange;
+
+		if (*p != '~') {
+			/* Not an exclude range, parse later */
+			exclude_only = false;
+			continue;
+		}
+		p++;
+
+		if (parse_port_range(p, &p, &xrange))
+			goto bad;
+		if ((*p != '\0')  && (*p != ',')) /* Garbage after the range */
+			goto bad;
+	} while ((p = next_chunk(p, ',')));
+
+	if (ifname && c->no_bindtodevice) {
+		die(
+"Device binding for '-%c %s' unsupported (requires kernel 5.7+)",
+		    optname, optarg);
+	}
+	/* Outbound forwards come from guest loopback */
+	if ((optname == 'T' || optname == 'U') && !ifname)
+		ifname = "lo";
+
+	if (exclude_only) {
+		/* Exclude ephemeral ports */
+		fwd_port_map_ephemeral(exclude);
+
+		conf_ports_range_except(c, optname, optarg, fwd,
+					addr, ifname,
+					1, NUM_PORTS - 1, exclude,
+					1, FWD_WEAK);
+		return;
+	}
+
+	/* Now process base ranges, skipping exclusions */
+	p = spec;
+	do {
+		struct port_range orig_range, mapped_range;
+
+		if (*p == '~')
+			/* Exclude range, already parsed */
+			continue;
+
+		if (parse_port_range(p, &p, &orig_range))
+			goto bad;
+
+		if (*p == ':') { /* There's a range to map to as well */
+			if (parse_port_range(p + 1, &p, &mapped_range))
+				goto bad;
+			if ((mapped_range.last - mapped_range.first) !=
+			    (orig_range.last - orig_range.first))
+				goto bad;
+		} else {
+			mapped_range = orig_range;
+		}
+
+		if ((*p != '\0')  && (*p != ',')) /* Garbage after the ranges */
+			goto bad;
+
+		if (orig_range.first == 0) {
+			die("Can't forward port 0 for option '-%c %s'",
+			    optname, optarg);
+		}
+
+		conf_ports_range_except(c, optname, optarg, fwd,
+					addr, ifname,
+					orig_range.first, orig_range.last,
+					exclude,
+					mapped_range.first, 0);
+	} while ((p = next_chunk(p, ',')));
+
+	return;
+bad:
+	die("Invalid port specifier %s", optarg);
+mode_conflict:
+	die("Port forwarding mode '%s' conflicts with previous mode", optarg);
+}
diff --git a/ports.h b/ports.h
new file mode 100644
index 00000000..3ef50e69
--- /dev/null
+++ b/ports.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright (c) 2026 Red Hat GmbH
+ * Author: Stefano Brivio <sbrivio@redhat.com>
+ */
+
+#ifndef PORTS_H
+#define PORTS_H
+
+/**
+ * enum fwd_mode - Overall forwarding mode for a direction and protocol
+ * @FWD_MODE_UNSET	Initial value, not parsed/configured yet
+ * @FWD_MODE_SPEC	Forward specified ports
+ * @FWD_MODE_NONE	No forwarded ports
+ * @FWD_MODE_AUTO	Automatic detection and forwarding based on bound ports
+ * @FWD_MODE_ALL	Bind all free ports
+ */
+enum fwd_mode {
+	FWD_MODE_UNSET = 0,
+	FWD_MODE_SPEC,
+	FWD_MODE_NONE,
+	FWD_MODE_AUTO,
+	FWD_MODE_ALL,
+};
+
+void conf_ports_range_except(const struct ctx *c, char optname,
+			     const char *optarg, struct fwd_table *fwd,
+			     const union inany_addr *addr,
+			     const char *ifname, uint16_t first, uint16_t last,
+			     const uint8_t *exclude, uint16_t to,
+			     uint8_t flags);
+void conf_ports(const struct ctx *c, char optname, const char *optarg,
+		struct fwd_table *fwd, enum fwd_mode *mode);
+
+
+#endif /* PORTS_H */
-- 
2.53.0


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

* [PATCH v3 23/25] conf, fwd, ports, util: Move things around for pesto
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (21 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 22/25] conf: Move port parsing functions to own file, ports.c David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 24/25] pesto, conf: Parse, send and receive new rules David Gibson
                   ` (3 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

From: Stefano Brivio <sbrivio@redhat.com>

...so that pesto can reuse functions parsing port forwarding
specifications from ports.c:

- checks on validity of some forwarding options (auto with pasta only
  all with passt only) move to the caller, conf()

- some other checks (availability of IPv4, IPv6, SO_BINDTODEVICE) are
  now based on specific parameters passed to conf_ports() and
  conf_ports_range_except(), so that we don't need struct ctx there

- bitmap operations and some convenience macros move to common.h

- fwd_probe_ephemeral(), fwd_port_is_ephemeral(), and fwd_rule_add()
  move to ports.c and fwd_rule.c, without any renaming for the moment

- forwarding table definitions move to fwd_rule.h

- selection of the definition of some logging functions now depends on
  a gross but unintrusive hack, a PESTO define (this will needs a
  cleaner solution later)

Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
Message-ID: <20260322141843.4095972-2-sbrivio@redhat.com>
Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 Makefile   |   8 ++-
 common.h   |  99 ++++++++++++++++++++++++++++
 conf.c     |  54 +++++++++++-----
 fwd.c      | 162 ----------------------------------------------
 fwd.h      |  54 +---------------
 fwd_rule.c |  95 ++++++++++++++++++++++++++-
 fwd_rule.h |  53 +++++++++++++++
 lineread.c |   1 -
 log.h      |  33 ++++++++++
 pesto.c    |  21 ++----
 ports.c    | 185 ++++++++++++++++++++++++++++++++++++-----------------
 ports.h    |  29 ++++++---
 util.c     |  84 ------------------------
 util.h     |  18 ------
 14 files changed, 474 insertions(+), 422 deletions(-)

diff --git a/Makefile b/Makefile
index 47d4c950..b6b6a823 100644
--- a/Makefile
+++ b/Makefile
@@ -45,12 +45,13 @@ PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
 	vhost_user.c virtio.c vu_common.c
 QRAP_SRCS = qrap.c
 PASST_REPAIR_SRCS = passt-repair.c
-PESTO_SRCS = pesto.c fwd_rule.c inany.c ip.c serialise.c
+PESTO_SRCS = pesto.c fwd_rule.c inany.c ip.c serialise.c ports.c lineread.c
 SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
 
 MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
 
-PESTO_HEADERS = common.h fwd_rule.h inany.h ip.h pesto.h serialise.h
+PESTO_HEADERS = common.h fwd_rule.h inany.h ip.h pesto.h serialise.h ports.h \
+	lineread.h
 PASST_HEADERS = arch.h arp.h checksum.h conf.h dhcp.h dhcpv6.h epoll_ctl.h \
 	flow.h fwd.h flow_table.h icmp.h icmp_flow.h iov.h isolation.h \
 	lineread.h log.h migrate.h ndp.h netlink.h packet.h passt.h pasta.h \
@@ -117,7 +118,8 @@ passt-repair: $(PASST_REPAIR_SRCS) seccomp_repair.h
 	$(CC) $(FLAGS) $(CFLAGS) $(CPPFLAGS) $(PASST_REPAIR_SRCS) -o passt-repair $(LDFLAGS)
 
 pesto: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
-	$(CC) $(FLAGS) $(CFLAGS) $(CPPFLAGS) $(PESTO_SRCS) -o pesto $(LDFLAGS)
+	$(CC) $(FLAGS) $(CFLAGS) $(CPPFLAGS) $(PESTO_SRCS) -DPESTO -o pesto \
+	$(LDFLAGS)
 
 valgrind: EXTRA_SYSCALLS += rt_sigprocmask rt_sigtimedwait rt_sigaction	\
 			    rt_sigreturn getpid gettid kill clock_gettime \
diff --git a/common.h b/common.h
index 7d37b926..43b14558 100644
--- a/common.h
+++ b/common.h
@@ -8,6 +8,9 @@
 #ifndef COMMON_H
 #define COMMON_H
 
+#include <stdbool.h>
+#include <stdint.h>
+
 #define VERSION_BLOB							       \
 	VERSION "\n"							       \
 	"Copyright Red Hat\n"						       \
@@ -19,6 +22,8 @@
 /* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
 #define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
 
+#define ARRAY_SIZE(a)		((int)(sizeof(a) / sizeof((a)[0])))
+
 #ifndef MIN
 #define MIN(x, y)		(((x) < (y)) ? (x) : (y))
 #endif
@@ -26,7 +31,13 @@
 #define MAX(x, y)		(((x) > (y)) ? (x) : (y))
 #endif
 
+#define DIV_ROUND_UP(n, d)	(((n) + (d) - 1) / (d))
+#define DIV_ROUND_CLOSEST(n, d)	(((n) + (d) / 2) / (d))
+#define ROUND_DOWN(x, y)	((x) & ~((y) - 1))
+#define ROUND_UP(x, y)		(((x) + (y) - 1) & ~((y) - 1))
+
 #define BIT(n)			(1UL << (n))
+#define MAX_FROM_BITS(n)	(((1U << (n)) - 1))
 
 #ifndef __bswap_constant_16
 #define __bswap_constant_16(x)						\
@@ -76,4 +87,92 @@
 #define ntohll(x)		(be64toh((x)))
 #define htonll(x)		(htobe64((x)))
 
+#define BITMAP_BIT(n)		(BIT((n) % (sizeof(long) * 8)))
+#define BITMAP_WORD(n)		(n / (sizeof(long) * 8))
+
+/**
+ * bitmap_set() - Set single bit in bitmap
+ * @map:	Pointer to bitmap
+ * @bit:	Bit number to set
+ */
+static inline void bitmap_set(uint8_t *map, unsigned bit)
+{
+	unsigned long *word = (unsigned long *)map + BITMAP_WORD(bit);
+
+	*word |= BITMAP_BIT(bit);
+}
+
+/**
+ * bitmap_clear() - Clear single bit in bitmap
+ * @map:	Pointer to bitmap
+ * @bit:	Bit number to clear
+ */
+/* cppcheck-suppress unusedFunction */
+static inline void bitmap_clear(uint8_t *map, unsigned bit)
+{
+	unsigned long *word = (unsigned long *)map + BITMAP_WORD(bit);
+
+	*word &= ~BITMAP_BIT(bit);
+}
+
+/**
+ * bitmap_isset() - Check for set bit in bitmap
+ * @map:	Pointer to bitmap
+ * @bit:	Bit number to check
+ *
+ * Return: true if given bit is set, false if it's not
+ */
+static inline bool bitmap_isset(const uint8_t *map, unsigned bit)
+{
+	const unsigned long *word
+		= (const unsigned long *)map + BITMAP_WORD(bit);
+
+	return !!(*word & BITMAP_BIT(bit));
+}
+
+/**
+ * bitmap_or() - Logical disjunction (OR) of two bitmaps
+ * @dst:	Pointer to result bitmap
+ * @size:	Size of bitmaps, in bytes
+ * @a:		First operand
+ * @b:		Second operand
+ */
+/* cppcheck-suppress unusedFunction */
+static inline void bitmap_or(uint8_t *dst, size_t size,
+			     const uint8_t *a, const uint8_t *b)
+{
+	unsigned long *dw = (unsigned long *)dst;
+	unsigned long *aw = (unsigned long *)a;
+	unsigned long *bw = (unsigned long *)b;
+	size_t i;
+
+	for (i = 0; i < size / sizeof(long); i++, dw++, aw++, bw++)
+		*dw = *aw | *bw;
+
+	for (i = size / sizeof(long) * sizeof(long); i < size; i++)
+		dst[i] = a[i] | b[i];
+}
+
+/**
+ * bitmap_and_not() - Logical conjunction with complement (AND NOT) of bitmap
+ * @dst:	Pointer to result bitmap
+ * @size:	Size of bitmaps, in bytes
+ * @a:		First operand
+ * @b:		Second operand
+ */
+static inline void bitmap_and_not(uint8_t *dst, size_t size,
+				  const uint8_t *a, const uint8_t *b)
+{
+	unsigned long *dw = (unsigned long *)dst;
+	unsigned long *aw = (unsigned long *)a;
+	unsigned long *bw = (unsigned long *)b;
+	size_t i;
+
+	for (i = 0; i < size / sizeof(long); i++, dw++, aw++, bw++)
+		*dw = *aw & ~*bw;
+
+	for (i = size / sizeof(long) * sizeof(long); i < size; i++)
+		dst[i] = a[i] & ~b[i];
+}
+
 #endif /* _COMMON_H */
diff --git a/conf.c b/conf.c
index 1ed1ee54..7f624db6 100644
--- a/conf.c
+++ b/conf.c
@@ -1851,18 +1851,36 @@ void conf(struct ctx *c, int argc, char **argv)
 	do {
 		name = getopt_long(argc, argv, optstring, options, NULL);
 
+		if (name != 't' && name != 'T' && name != 'u' && name != 'U')
+			continue;
+
+		if ((name == 't' || name == 'T') && c->no_tcp)
+			die("TCP forwarding requested but TCP is disabled");
+		if ((name == 'u' || name == 'U') && c->no_udp)
+			die("UDP forwarding requested but UDP is disabled");
+
+		if (!strcmp(optarg, "auto") && c->mode != MODE_PASTA)
+			die("'auto' port forwarding is only allowed for pasta");
+
+		if (!strcmp(optarg, "all") && c->mode == MODE_PASTA)
+			die("'all' port forwarding is only allowed for passt");
+
 		if (name == 't') {
-			conf_ports(c, name, optarg, c->fwd[PIF_HOST],
-				   &tcp_in_mode);
+			conf_ports(name, optarg, c->fwd[PIF_HOST],
+				   &tcp_in_mode, !c->no_bindtodevice,
+				   !!c->ifi4, !!c->ifi6);
 		} else if (name == 'u') {
-			conf_ports(c, name, optarg, c->fwd[PIF_HOST],
-				   &udp_in_mode);
+			conf_ports(name, optarg, c->fwd[PIF_HOST],
+				   &udp_in_mode, !c->no_bindtodevice,
+				   !!c->ifi4, !!c->ifi6);
 		} else if (name == 'T') {
-			conf_ports(c, name, optarg, c->fwd[PIF_SPLICE],
-				   &tcp_out_mode);
+			conf_ports(name, optarg, c->fwd[PIF_SPLICE],
+				   &tcp_out_mode, !c->no_bindtodevice,
+				   !!c->ifi4, !!c->ifi6);
 		} else if (name == 'U') {
-			conf_ports(c, name, optarg, c->fwd[PIF_SPLICE],
-				   &udp_out_mode);
+			conf_ports(name, optarg, c->fwd[PIF_SPLICE],
+				   &udp_out_mode, !c->no_bindtodevice,
+				   !!c->ifi4, !!c->ifi6);
 		}
 	} while (name != -1);
 
@@ -1922,24 +1940,28 @@ void conf(struct ctx *c, int argc, char **argv)
 		udp_out_mode = fwd_default;
 
 	if (tcp_in_mode == FWD_MODE_AUTO) {
-		conf_ports_range_except(c, 't', "auto", c->fwd[PIF_HOST],
+		conf_ports_range_except('t', "auto", c->fwd[PIF_HOST],
 					NULL, NULL, 1, NUM_PORTS - 1, NULL, 1,
-					FWD_SCAN);
+					FWD_SCAN, !c->no_bindtodevice,
+					!!c->ifi4, !!c->ifi6);
 	}
 	if (tcp_out_mode == FWD_MODE_AUTO) {
-		conf_ports_range_except(c, 'T', "auto", c->fwd[PIF_SPLICE],
+		conf_ports_range_except('T', "auto", c->fwd[PIF_SPLICE],
 					NULL, "lo", 1, NUM_PORTS - 1, NULL, 1,
-					FWD_SCAN);
+					FWD_SCAN, !c->no_bindtodevice,
+					!!c->ifi4, !!c->ifi6);
 	}
 	if (udp_in_mode == FWD_MODE_AUTO) {
-		conf_ports_range_except(c, 'u', "auto", c->fwd[PIF_HOST],
+		conf_ports_range_except('u', "auto", c->fwd[PIF_HOST],
 					NULL, NULL, 1, NUM_PORTS - 1, NULL, 1,
-					FWD_SCAN);
+					FWD_SCAN, !c->no_bindtodevice,
+					!!c->ifi4, !!c->ifi6);
 	}
 	if (udp_out_mode == FWD_MODE_AUTO) {
-		conf_ports_range_except(c, 'U', "auto", c->fwd[PIF_SPLICE],
+		conf_ports_range_except('U', "auto", c->fwd[PIF_SPLICE],
 					NULL, "lo", 1, NUM_PORTS - 1, NULL, 1,
-					FWD_SCAN);
+					FWD_SCAN, !c->no_bindtodevice,
+					!!c->ifi4, !!c->ifi6);
 	}
 
 	conf_sock_listen(c);
diff --git a/fwd.c b/fwd.c
index 20409c62..4592af25 100644
--- a/fwd.c
+++ b/fwd.c
@@ -34,12 +34,6 @@
 #include "arp.h"
 #include "ndp.h"
 
-/* Ephemeral port range: values from RFC 6335 */
-static in_port_t fwd_ephemeral_min = (1 << 15) + (1 << 14);
-static in_port_t fwd_ephemeral_max = NUM_PORTS - 1;
-
-#define PORT_RANGE_SYSCTL	"/proc/sys/net/ipv4/ip_local_port_range"
-
 #define NEIGH_TABLE_SLOTS    1024
 #define NEIGH_TABLE_SIZE     (NEIGH_TABLE_SLOTS / 2)
 static_assert((NEIGH_TABLE_SLOTS & (NEIGH_TABLE_SLOTS - 1)) == 0,
@@ -249,73 +243,6 @@ void fwd_neigh_table_init(const struct ctx *c)
 		fwd_neigh_table_update(c, &mga, c->our_tap_mac, true);
 }
 
-/** fwd_probe_ephemeral() - Determine what ports this host considers ephemeral
- *
- * Work out what ports the host thinks are emphemeral and record it for later
- * use by fwd_port_is_ephemeral().  If we're unable to probe, assume the range
- * recommended by RFC 6335.
- */
-void fwd_probe_ephemeral(void)
-{
-	char *line, *tab, *end;
-	struct lineread lr;
-	long min, max;
-	ssize_t len;
-	int fd;
-
-	fd = open(PORT_RANGE_SYSCTL, O_RDONLY | O_CLOEXEC);
-	if (fd < 0) {
-		warn_perror("Unable to open %s", PORT_RANGE_SYSCTL);
-		return;
-	}
-
-	lineread_init(&lr, fd);
-	len = lineread_get(&lr, &line);
-	close(fd);
-
-	if (len < 0)
-		goto parse_err;
-
-	tab = strchr(line, '\t');
-	if (!tab)
-		goto parse_err;
-	*tab = '\0';
-
-	errno = 0;
-	min = strtol(line, &end, 10);
-	if (*end || errno)
-		goto parse_err;
-
-	errno = 0;
-	max = strtol(tab + 1, &end, 10);
-	if (*end || errno)
-		goto parse_err;
-
-	if (min < 0 || min >= (long)NUM_PORTS ||
-	    max < 0 || max >= (long)NUM_PORTS)
-		goto parse_err;
-
-	fwd_ephemeral_min = min;
-	fwd_ephemeral_max = max;
-
-	return;
-
-parse_err:
-	warn("Unable to parse %s", PORT_RANGE_SYSCTL);
-}
-
-/**
- * fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap
- * @map:	Bitmap to update
- */
-void fwd_port_map_ephemeral(uint8_t *map)
-{
-	unsigned port;
-
-	for (port = fwd_ephemeral_min; port <= fwd_ephemeral_max; port++)
-		bitmap_set(map, port);
-}
-
 /* Forwarding table storage, generally accessed via pointers in struct ctx */
 static struct fwd_table fwd_in;
 static struct fwd_table fwd_out;
@@ -331,95 +258,6 @@ void fwd_rule_init(struct ctx *c)
 		c->fwd[PIF_SPLICE] = &fwd_out;
 }
 
-/**
- * fwd_rule_add() - Add a rule to a forwarding table
- * @fwd:	Table to add to
- * @proto:	Protocol to forward
- * @flags:	Flags for this entry
- * @addr:	Our address to forward (NULL for both 0.0.0.0 and ::)
- * @ifname:	Only forward from this interface name, if non-empty
- * @first:	First port number to forward
- * @last:	Last port number to forward
- * @to:		First port of target port range to map to
- */
-void fwd_rule_add(struct fwd_table *fwd, uint8_t proto, uint8_t flags,
-		  const union inany_addr *addr, const char *ifname,
-		  in_port_t first, in_port_t last, in_port_t to)
-{
-	/* Flags which can be set from the caller */
-	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
-	unsigned num = (unsigned)last - first + 1;
-	struct fwd_rule_state *new;
-	unsigned i, port;
-
-	assert(!(flags & ~allowed_flags));
-	if (fwd->count >= ARRAY_SIZE(fwd->rules))
-		die("Too many port forwarding ranges");
-	if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks))
-		die("Too many listening sockets");
-
-	/* Passing a non-wildcard address with DUAL_STACK_ANY  is a bug */
-	assert(!(flags & FWD_DUAL_STACK_ANY) || !addr ||
-	       inany_equals(addr, &inany_any6));
-
-	/* Check for any conflicting entries */
-	for (i = 0; i < fwd->count; i++) {
-		char newstr[INANY_ADDRSTRLEN], rulestr[INANY_ADDRSTRLEN];
-		struct fwd_rule_state *rule = &fwd->rules[i];
-
-		if (proto != rule->rule.proto)
-			/* Non-conflicting protocols */
-			continue;
-
-		if (!inany_matches(addr, fwd_rule_addr(&rule->rule)))
-			/* Non-conflicting addresses */
-			continue;
-
-		if (last < rule->rule.first || rule->rule.last < first)
-			/* Port ranges don't overlap */
-			continue;
-
-		die("Forwarding configuration conflict: %s/%u-%u versus %s/%u-%u",
-		    inany_ntop(addr, newstr, sizeof(newstr)), first, last,
-		    inany_ntop(fwd_rule_addr(&rule->rule),
-			       rulestr, sizeof(rulestr)),
-		    rule->rule.first, rule->rule.last);
-	}
-
-	new = &fwd->rules[fwd->count++];
-	new->rule.proto = proto;
-	new->rule.flags = flags;
-
-	if (addr) {
-		new->rule.addr = *addr;
-	} else {
-		new->rule.addr = inany_any6;
-		new->rule.flags |= FWD_DUAL_STACK_ANY;
-	}
-
-	memset(new->rule.ifname, 0, sizeof(new->rule.ifname));
-	if (ifname) {
-		int ret;
-
-		ret = snprintf(new->rule.ifname, sizeof(new->rule.ifname),
-			       "%s", ifname);
-		if (ret <= 0 || (size_t)ret >= sizeof(new->rule.ifname))
-			die("Invalid interface name: %s", ifname);
-	}
-
-	assert(first <= last);
-	new->rule.first = first;
-	new->rule.last = last;
-
-	new->rule.to = to;
-
-	new->socks = &fwd->socks[fwd->sock_count];
-	fwd->sock_count += num;
-
-	for (port = new->rule.first; port <= new->rule.last; port++)
-		new->socks[port - new->rule.first] = -1;
-}
-
 /**
  * fwd_rule_match() - Does a prospective flow match a given forwarding rule?
  * @rule:	Forwarding rule
diff --git a/fwd.h b/fwd.h
index d37f2cdc..a00fe52d 100644
--- a/fwd.h
+++ b/fwd.h
@@ -15,66 +15,14 @@
 #include <netinet/in.h>
 
 #include "inany.h"
+#include "ports.h"
 #include "fwd_rule.h"
 
 struct flowside;
 
-/* Number of ports for both TCP and UDP */
-#define	NUM_PORTS	(1U << 16)
-
 void fwd_probe_ephemeral(void);
 void fwd_port_map_ephemeral(uint8_t *map);
 
-/**
- * struct fwd_rule_state - Forwarding rule and associated state
- * @rule:	Rule specification
- * @socks:	Array of listening sockets for this entry
- */
-struct fwd_rule_state {
-	struct fwd_rule rule;
-	int *socks;
-};
-
-#define FWD_RULE_BITS	8
-#define MAX_FWD_RULES	MAX_FROM_BITS(FWD_RULE_BITS)
-#define FWD_NO_HINT	(-1)
-
-/**
- * struct fwd_listen_ref - information about a single listening socket
- * @port:	Bound port number of the socket
- * @pif:	pif in which the socket is listening
- * @rule:	Index of forwarding rule
- */
-struct fwd_listen_ref {
-	in_port_t	port;
-	uint8_t		pif;
-	unsigned	rule :FWD_RULE_BITS;
-};
-
-/* Maximum number of listening sockets (per pif)
- *
- * Rationale: This lets us listen on every port for two addresses and two
- * protocols (which we need for -T auto -U auto without SO_BINDTODEVICE), plus a
- * comfortable number of extras.
- */
-#define MAX_LISTEN_SOCKS	(NUM_PORTS * 5)
-
-/**
- * struct fwd_table - Table of forwarding rules (per initiating pif)
- * @count:	Number of forwarding rules
- * @rules:	Array of forwarding rules
- * @sock_count:	Number of entries used in @socks
- * @socks:	Listening sockets for forwarding
- */
-struct fwd_table {
-	unsigned count;
-	struct fwd_rule_state rules[MAX_FWD_RULES];
-	unsigned sock_count;
-	int socks[MAX_LISTEN_SOCKS];
-};
-
-#define PORT_BITMAP_SIZE	DIV_ROUND_UP(NUM_PORTS, 8)
-
 /**
  * struct fwd_scan - Port scanning state for a protocol+direction
  * @scan4:	/proc/net fd to scan for IPv4 ports when in AUTO mode
diff --git a/fwd_rule.c b/fwd_rule.c
index b2ed9b3b..5eabd0ad 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -18,8 +18,9 @@
 #include <stdio.h>
 
 #include "serialise.h"
-
+#include "ports.h"
 #include "fwd_rule.h"
+#include "log.h"
 
 /**
  * fwd_rule_addr() - Return match address for a rule
@@ -75,7 +76,97 @@ const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size)
 }
 
 /**
- * fwd_rule_read() - Read serialised rule from an fd
+ * fwd_rule_add() - Add a rule to a forwarding table
+ * @fwd:	Table to add to
+ * @proto:	Protocol to forward
+ * @flags:	Flags for this entry
+ * @addr:	Our address to forward (NULL for both 0.0.0.0 and ::)
+ * @ifname:	Only forward from this interface name, if non-empty
+ * @first:	First port number to forward
+ * @last:	Last port number to forward
+ * @to:		First port of target port range to map to
+ */
+void fwd_rule_add(struct fwd_table *fwd, uint8_t proto, uint8_t flags,
+		  const union inany_addr *addr, const char *ifname,
+		  in_port_t first, in_port_t last, in_port_t to)
+{
+	/* Flags which can be set from the caller */
+	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
+	unsigned num = (unsigned)last - first + 1;
+	struct fwd_rule_state *new;
+	unsigned i, port;
+
+	assert(!(flags & ~allowed_flags));
+
+	if (fwd->count >= ARRAY_SIZE(fwd->rules))
+		die("Too many port forwarding ranges");
+	if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks))
+		die("Too many listening sockets");
+
+	/* Passing a non-wildcard address with DUAL_STACK_ANY  is a bug */
+	assert(!(flags & FWD_DUAL_STACK_ANY) || !addr ||
+	       inany_equals(addr, &inany_any6));
+
+	/* Check for any conflicting entries */
+	for (i = 0; i < fwd->count; i++) {
+		char newstr[INANY_ADDRSTRLEN], rulestr[INANY_ADDRSTRLEN];
+		struct fwd_rule_state *rule = &fwd->rules[i];
+
+		if (proto != rule->rule.proto)
+			/* Non-conflicting protocols */
+			continue;
+
+		if (!inany_matches(addr, fwd_rule_addr(&rule->rule)))
+			/* Non-conflicting addresses */
+			continue;
+
+		if (last < rule->rule.first || rule->rule.last < first)
+			/* Port ranges don't overlap */
+			continue;
+
+		die("Forwarding configuration conflict: %s/%u-%u versus %s/%u-%u",
+		    inany_ntop(addr, newstr, sizeof(newstr)), first, last,
+		    inany_ntop(fwd_rule_addr(&rule->rule),
+			       rulestr, sizeof(rulestr)),
+		    rule->rule.first, rule->rule.last);
+	}
+
+	new = &fwd->rules[fwd->count++];
+	new->rule.proto = proto;
+	new->rule.flags = flags;
+
+	if (addr) {
+		new->rule.addr = *addr;
+	} else {
+		new->rule.addr = inany_any6;
+		new->rule.flags |= FWD_DUAL_STACK_ANY;
+	}
+
+	memset(new->rule.ifname, 0, sizeof(new->rule.ifname));
+	if (ifname) {
+		int ret;
+
+		ret = snprintf(new->rule.ifname, sizeof(new->rule.ifname),
+			       "%s", ifname);
+		if (ret <= 0 || (size_t)ret >= sizeof(new->rule.ifname))
+			die("Invalid interface name: %s", ifname);
+	}
+
+	assert(first <= last);
+	new->rule.first = first;
+	new->rule.last = last;
+
+	new->rule.to = to;
+
+	new->socks = &fwd->socks[fwd->sock_count];
+	fwd->sock_count += num;
+
+	for (port = new->rule.first; port <= new->rule.last; port++)
+		new->socks[port - new->rule.first] = -1;
+}
+
+/**
+ * fwd_rule_read() - Read erialised rule from an fd
  * @fd:		fd to serialise to
  * @rule:	Buffer to store rule into
  *
diff --git a/fwd_rule.h b/fwd_rule.h
index a4d54da0..879ff19b 100644
--- a/fwd_rule.h
+++ b/fwd_rule.h
@@ -42,7 +42,60 @@ struct fwd_rule {
 	uint8_t flags;
 };
 
+/**
+ * struct fwd_rule_state - Forwarding rule and associated state
+ * @rule:	Rule specification
+ * @socks:	Array of listening sockets for this entry
+ */
+struct fwd_rule_state {
+	struct fwd_rule rule;
+	int *socks;
+};
+
+#define FWD_RULE_BITS	8
+#define MAX_FWD_RULES	MAX_FROM_BITS(FWD_RULE_BITS)
+#define FWD_NO_HINT	(-1)
+
+/**
+ * struct fwd_listen_ref - information about a single listening socket
+ * @port:	Bound port number of the socket
+ * @pif:	pif in which the socket is listening
+ * @rule:	Index of forwarding rule
+ */
+struct fwd_listen_ref {
+	in_port_t	port;
+	uint8_t		pif;
+	unsigned	rule :FWD_RULE_BITS;
+};
+
+/* Maximum number of listening sockets (per pif)
+ *
+ * Rationale: This lets us listen on every port for two addresses and two
+ * protocols (which we need for -T auto -U auto without SO_BINDTODEVICE), plus a
+ * comfortable number of extras.
+ */
+#define MAX_LISTEN_SOCKS	(NUM_PORTS * 5)
+
+/**
+ * struct fwd_table - Table of forwarding rules (per initiating pif)
+ * @count:	Number of forwarding rules
+ * @rules:	Array of forwarding rules
+ * @sock_count:	Number of entries used in @socks
+ * @socks:	Listening sockets for forwarding
+ */
+struct fwd_table {
+	unsigned count;
+	struct fwd_rule_state rules[MAX_FWD_RULES];
+	unsigned sock_count;
+	int socks[MAX_LISTEN_SOCKS];
+};
+
+#define PORT_BITMAP_SIZE	DIV_ROUND_UP(NUM_PORTS, 8)
+
 const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
+void fwd_rule_add(struct fwd_table *fwd, uint8_t proto, uint8_t flags,
+		  const union inany_addr *addr, const char *ifname,
+		  in_port_t first, in_port_t last, in_port_t to);
 
 #define FWD_RULE_STRLEN					    \
 	(IPPROTO_STRLEN - 1				    \
diff --git a/lineread.c b/lineread.c
index b9ceae10..ae495a4f 100644
--- a/lineread.c
+++ b/lineread.c
@@ -20,7 +20,6 @@
 #include <unistd.h>
 
 #include "lineread.h"
-#include "util.h"
 
 /**
  * lineread_init() - Prepare for line by line file reading without allocation
diff --git a/log.h b/log.h
index 6ceb6862..e2657ef7 100644
--- a/log.h
+++ b/log.h
@@ -7,10 +7,42 @@
 #define LOG_H
 
 #include <stdarg.h>
+#include <stdlib.h>
 #include <stdbool.h>
 #include <stddef.h>
 #include <syslog.h>
 
+#ifdef PESTO
+extern int verbosity;
+
+#define die(...)							\
+	do {								\
+		FPRINTF(stderr, __VA_ARGS__);				\
+		FPRINTF(stderr, "\n");					\
+		exit(EXIT_FAILURE);					\
+	} while (0)
+
+#define warn(...)							\
+	do {								\
+		FPRINTF(stderr, __VA_ARGS__);				\
+		FPRINTF(stderr, "\n");					\
+	} while (0)
+
+#define warn_perror(...)						\
+	do {								\
+		FPRINTF(stderr, __VA_ARGS__);				\
+		FPRINTF(stderr, ": %i", errno);				\
+	} while (0)
+
+#define debug(...)							\
+	do {								\
+		if (verbosity > 1) {					\
+			FPRINTF(stderr, __VA_ARGS__);			\
+			FPRINTF(stderr, "\n");				\
+		}							\
+	} while (0)
+#else
+
 /* This would make more sense in util.h, but because we use it in die(), that
  * would cause awkward circular reference problems.
  */
@@ -65,4 +97,5 @@ void __openlog(const char *ident, int option, int facility);
 void logfile_init(const char *name, const char *path, size_t size);
 void __setlogmask(int mask);
 
+#endif /* !PESTO */
 #endif /* LOG_H */
diff --git a/pesto.c b/pesto.c
index 11447575..aa1d584c 100644
--- a/pesto.c
+++ b/pesto.c
@@ -34,25 +34,12 @@
 #include "common.h"
 #include "seccomp_pesto.h"
 #include "serialise.h"
+#include "ports.h"
 #include "fwd_rule.h"
 #include "pesto.h"
+#include "log.h"
 
-static int verbosity = 1;
-
-#define die(...)							\
-	do {								\
-		FPRINTF(stderr, __VA_ARGS__);				\
-		FPRINTF(stderr, "\n");					\
-		exit(EXIT_FAILURE);					\
-	} while (0)
-
-#define debug(...)							\
-	do {								\
-		if (verbosity > 1) {					\
-			FPRINTF(stderr, __VA_ARGS__);			\
-			FPRINTF(stderr, "\n");				\
-		}							\
-	} while (0)
+int verbosity = 1;
 
 /**
  * xmalloc() - Allocate memory, with fatal error on failure
@@ -234,7 +221,7 @@ static void show_state(const struct conf_state *state)
  *
  * Return: 0 on success, won't return on failure
  *
- * #syscalls:pesto connect write close exit_group fstat brk
+ * #syscalls:pesto connect write close exit_group fstat brk getrandom
  * #syscalls:pesto socket s390x:socketcall i686:socketcall
  * #syscalls:pesto recvfrom recvmsg arm:recv ppc64le:recv
  * #syscalls:pesto sendto sendmsg arm:send ppc64le:send
diff --git a/ports.c b/ports.c
index 79abafc9..6e33f3f8 100644
--- a/ports.c
+++ b/ports.c
@@ -26,19 +26,92 @@
 #include <sys/stat.h>
 #include <limits.h>
 #include <unistd.h>
+#include <stdbool.h>
 #include <stdlib.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <netinet/in.h>
 #include <netinet/if_ether.h>
 
+#include "lineread.h"
 #include "common.h"
-#include "util.h"
-#include "ip.h"
-#include "passt.h"
-#include "common.h"
-#include "pesto.h"
 #include "ports.h"
+#include "fwd_rule.h"
+#include "log.h"
+#include "pesto.h"
+
+/* Ephemeral port range: values from RFC 6335 */
+static in_port_t fwd_ephemeral_min = (1 << 15) + (1 << 14);
+static in_port_t fwd_ephemeral_max = NUM_PORTS - 1;
+
+#define PORT_RANGE_SYSCTL	"/proc/sys/net/ipv4/ip_local_port_range"
+
+/** fwd_probe_ephemeral() - Determine what ports this host considers ephemeral
+ *
+ * Work out what ports the host thinks are emphemeral and record it for later
+ * use by fwd_port_is_ephemeral().  If we're unable to probe, assume the range
+ * recommended by RFC 6335.
+ */
+void fwd_probe_ephemeral(void)
+{
+	char *line, *tab, *end;
+	struct lineread lr;
+	long min, max;
+	ssize_t len;
+	int fd;
+
+	fd = open(PORT_RANGE_SYSCTL, O_RDONLY | O_CLOEXEC);
+	if (fd < 0) {
+		warn_perror("Unable to open %s", PORT_RANGE_SYSCTL);
+		return;
+	}
+
+	lineread_init(&lr, fd);
+	len = lineread_get(&lr, &line);
+	close(fd);
+
+	if (len < 0)
+		goto parse_err;
+
+	tab = strchr(line, '\t');
+	if (!tab)
+		goto parse_err;
+	*tab = '\0';
+
+	errno = 0;
+	min = strtol(line, &end, 10);
+	if (*end || errno)
+		goto parse_err;
+
+	errno = 0;
+	max = strtol(tab + 1, &end, 10);
+	if (*end || errno)
+		goto parse_err;
+
+	if (min < 0 || min >= (long)NUM_PORTS ||
+	    max < 0 || max >= (long)NUM_PORTS)
+		goto parse_err;
+
+	fwd_ephemeral_min = min;
+	fwd_ephemeral_max = max;
+
+	return;
+
+parse_err:
+	warn("Unable to parse %s", PORT_RANGE_SYSCTL);
+}
+
+/**
+ * fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap
+ * @map:	Bitmap to update
+ */
+static void fwd_port_map_ephemeral(uint8_t *map)
+{
+	unsigned port;
+
+	for (port = fwd_ephemeral_min; port <= fwd_ephemeral_max; port++)
+		bitmap_set(map, port);
+}
 
 /**
  * next_chunk() - Return the next piece of a string delimited by a character
@@ -103,23 +176,27 @@ static int parse_port_range(const char *s, char **endptr,
 /**
  * conf_ports_range_except() - Set up forwarding for a range of ports minus a
  *                             bitmap of exclusions
- * @c:		Execution context
- * @optname:	Short option name, t, T, u, or U
- * @optarg:	Option argument (port specification)
- * @fwd:	Forwarding table to be updated
- * @addr:	Listening address
- * @ifname:	Listening interface
- * @first:	First port to forward
- * @last:	Last port to forward
- * @exclude:	Bitmap of ports to exclude (may be NULL)
- * @to:		Port to translate @first to when forwarding
- * @flags:	Flags for forwarding entries
+ * @optname:		Short option name, t, T, u, or U
+ * @optarg:		Option argument (port specification)
+ * @fwd:		Forwarding table to be updated
+ * @addr:		Listening address
+ * @ifname:		Listening interface
+ * @first:		First port to forward
+ * @last:		Last port to forward
+ * @exclude:		Bitmap of ports to exclude (may be NULL)
+ * @to:			Port to translate @first to when forwarding
+ * @flags:		Flags for forwarding entries
+ * @can_bindtodevice	SO_BINDTODEVICE is available
+ * @v4_enabled		IPv4 is enabled
+ * @v6_enabled		IPv6 is enabled
  */
-void conf_ports_range_except(const struct ctx *c, char optname,
-			     const char *optarg, struct fwd_table *fwd,
-			     const union inany_addr *addr,
-			     const char *ifname, uint16_t first, uint16_t last,
-			     const uint8_t *exclude, uint16_t to, uint8_t flags)
+void conf_ports_range_except(char optname, const char *optarg,
+			     struct fwd_table *fwd,
+			     const union inany_addr *addr, const char *ifname,
+			     uint16_t first, uint16_t last,
+			     const uint8_t *exclude, uint16_t to, uint8_t flags,
+			     bool can_bindtodevice,
+			     bool v4_enabled, bool v6_enabled)
 {
 	unsigned delta = to - first;
 	unsigned base, i;
@@ -143,7 +220,7 @@ void conf_ports_range_except(const struct ctx *c, char optname,
 				break;
 		}
 
-		if ((optname == 'T' || optname == 'U') && c->no_bindtodevice) {
+		if ((optname == 'T' || optname == 'U') && !can_bindtodevice) {
 			/* FIXME: Once the fwd bitmaps are removed, move this
 			 * workaround to the caller
 			 */
@@ -152,12 +229,12 @@ void conf_ports_range_except(const struct ctx *c, char optname,
 "SO_BINDTODEVICE unavailable, forwarding only 127.0.0.1 and ::1 for '-%c %s'",
 			     optname, optarg);
 
-			if (c->ifi4) {
+			if (v4_enabled) {
 				fwd_rule_add(fwd, proto, flags,
 					     &inany_loopback4, NULL,
 					     base, i - 1, base + delta);
 			}
-			if (c->ifi6) {
+			if (v6_enabled) {
 				fwd_rule_add(fwd, proto, flags,
 					     &inany_loopback6, NULL,
 					     base, i - 1, base + delta);
@@ -172,14 +249,17 @@ void conf_ports_range_except(const struct ctx *c, char optname,
 
 /**
  * conf_ports() - Parse port configuration options, initialise UDP/TCP sockets
- * @c:		Execution context
- * @optname:	Short option name, t, T, u, or U
- * @optarg:	Option argument (port specification)
- * @fwd:	Forwarding table to be updated
- * @mode:	Overall port forwarding mode (updated)
+ * @optname:		Short option name, t, T, u, or U
+ * @optarg:		Option argument (port specification)
+ * @fwd:		Forwarding table to be updated
+ * @mode:		Overall port forwarding mode (updated)
+ * @can_bindtodevice	Whether SO_BINDTODEVICE is available
+ * @v4_enabled		IPv4 is enabled
+ * @v6_enabled		IPv6 is enabled
  */
-void conf_ports(const struct ctx *c, char optname, const char *optarg,
-		struct fwd_table *fwd, enum fwd_mode *mode)
+void conf_ports(char optname, const char *optarg, struct fwd_table *fwd,
+		enum fwd_mode *mode, bool can_bindtodevice,
+		bool v4_enabled, bool v6_enabled)
 {
 	union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
 	char buf[BUFSIZ], *spec, *ifname = NULL, *p;
@@ -194,19 +274,11 @@ void conf_ports(const struct ctx *c, char optname, const char *optarg,
 		return;
 	}
 
-	if ((optname == 't' || optname == 'T') && c->no_tcp)
-		die("TCP port forwarding requested but TCP is disabled");
-	if ((optname == 'u' || optname == 'U') && c->no_udp)
-		die("UDP port forwarding requested but UDP is disabled");
-
 	if (!strcmp(optarg, "auto")) {
 		if (*mode)
 			goto mode_conflict;
 
-		if (c->mode != MODE_PASTA)
-			die("'auto' port forwarding is only allowed for pasta");
-
-		if ((optname == 'T' || optname == 'U') && c->no_bindtodevice) {
+		if ((optname == 'T' || optname == 'U') && can_bindtodevice) {
 			warn(
 "'-%c auto' enabled without unprivileged SO_BINDTODEVICE", optname);
 			warn(
@@ -220,18 +292,15 @@ void conf_ports(const struct ctx *c, char optname, const char *optarg,
 		if (*mode)
 			goto mode_conflict;
 
-		if (c->mode == MODE_PASTA)
-			die("'all' port forwarding is only allowed for passt");
-
 		*mode = FWD_MODE_ALL;
 
 		/* Exclude ephemeral ports */
 		fwd_port_map_ephemeral(exclude);
 
-		conf_ports_range_except(c, optname, optarg, fwd,
-					NULL, NULL,
-					1, NUM_PORTS - 1, exclude,
-					1, FWD_WEAK);
+		conf_ports_range_except(optname, optarg, fwd, NULL, NULL,
+					1, NUM_PORTS - 1, exclude, 1, FWD_WEAK,
+					can_bindtodevice,
+					v4_enabled, v6_enabled);
 		return;
 	}
 
@@ -283,10 +352,10 @@ void conf_ports(const struct ctx *c, char optname, const char *optarg,
 	}
 
 	if (addr) {
-		if (!c->ifi4 && inany_v4(addr)) {
+		if (!v4_enabled && inany_v4(addr)) {
 			die("IPv4 is disabled, can't use -%c %s",
 			    optname, optarg);
-		} else if (!c->ifi6 && !inany_v4(addr)) {
+		} else if (!v6_enabled && !inany_v4(addr)) {
 			die("IPv6 is disabled, can't use -%c %s",
 			    optname, optarg);
 		}
@@ -310,7 +379,7 @@ void conf_ports(const struct ctx *c, char optname, const char *optarg,
 			goto bad;
 	} while ((p = next_chunk(p, ',')));
 
-	if (ifname && c->no_bindtodevice) {
+	if (ifname && !can_bindtodevice) {
 		die(
 "Device binding for '-%c %s' unsupported (requires kernel 5.7+)",
 		    optname, optarg);
@@ -323,10 +392,10 @@ void conf_ports(const struct ctx *c, char optname, const char *optarg,
 		/* Exclude ephemeral ports */
 		fwd_port_map_ephemeral(exclude);
 
-		conf_ports_range_except(c, optname, optarg, fwd,
-					addr, ifname,
-					1, NUM_PORTS - 1, exclude,
-					1, FWD_WEAK);
+		conf_ports_range_except(optname, optarg, fwd, addr, ifname,
+					1, NUM_PORTS - 1, exclude, 1, FWD_WEAK,
+					can_bindtodevice,
+					v4_enabled, v6_enabled);
 		return;
 	}
 
@@ -360,11 +429,11 @@ void conf_ports(const struct ctx *c, char optname, const char *optarg,
 			    optname, optarg);
 		}
 
-		conf_ports_range_except(c, optname, optarg, fwd,
-					addr, ifname,
+		conf_ports_range_except(optname, optarg, fwd, addr, ifname,
 					orig_range.first, orig_range.last,
-					exclude,
-					mapped_range.first, 0);
+					exclude, mapped_range.first, 0,
+					can_bindtodevice,
+					v4_enabled, v6_enabled);
 	} while ((p = next_chunk(p, ',')));
 
 	return;
diff --git a/ports.h b/ports.h
index 3ef50e69..ffe440ac 100644
--- a/ports.h
+++ b/ports.h
@@ -6,6 +6,13 @@
 #ifndef PORTS_H
 #define PORTS_H
 
+#include <stdbool.h>
+
+#include "inany.h"
+
+/* Number of ports for both TCP and UDP */
+#define	NUM_PORTS	(1U << 16)
+
 /**
  * enum fwd_mode - Overall forwarding mode for a direction and protocol
  * @FWD_MODE_UNSET	Initial value, not parsed/configured yet
@@ -22,14 +29,20 @@ enum fwd_mode {
 	FWD_MODE_ALL,
 };
 
-void conf_ports_range_except(const struct ctx *c, char optname,
-			     const char *optarg, struct fwd_table *fwd,
-			     const union inany_addr *addr,
-			     const char *ifname, uint16_t first, uint16_t last,
-			     const uint8_t *exclude, uint16_t to,
-			     uint8_t flags);
-void conf_ports(const struct ctx *c, char optname, const char *optarg,
-		struct fwd_table *fwd, enum fwd_mode *mode);
+struct fwd_table;
+
+void fwd_probe_ephemeral(void);
+bool fwd_port_is_ephemeral(in_port_t port);
+void conf_ports_range_except(char optname, const char *optarg,
+			     struct fwd_table *fwd,
+			     const union inany_addr *addr, const char *ifname,
+			     uint16_t first, uint16_t last,
+			     const uint8_t *exclude, uint16_t to, uint8_t flags,
+			     bool can_bindtodevice,
+			     bool v4_enabled, bool v6_enabled);
+void conf_ports(char optname, const char *optarg, struct fwd_table *fwd,
+		enum fwd_mode *mode, bool can_bindtodevice,
+		bool v4_enabled, bool v6_enabled);
 
 
 #endif /* PORTS_H */
diff --git a/util.c b/util.c
index faa2c6a4..73c9d51d 100644
--- a/util.c
+++ b/util.c
@@ -367,90 +367,6 @@ long timespec_diff_ms(const struct timespec *a, const struct timespec *b)
 	return timespec_diff_us(a, b) / 1000;
 }
 
-/**
- * bitmap_set() - Set single bit in bitmap
- * @map:	Pointer to bitmap
- * @bit:	Bit number to set
- */
-void bitmap_set(uint8_t *map, unsigned bit)
-{
-	unsigned long *word = (unsigned long *)map + BITMAP_WORD(bit);
-
-	*word |= BITMAP_BIT(bit);
-}
-
-/**
- * bitmap_clear() - Clear single bit in bitmap
- * @map:	Pointer to bitmap
- * @bit:	Bit number to clear
- */
-/* cppcheck-suppress unusedFunction */
-void bitmap_clear(uint8_t *map, unsigned bit)
-{
-	unsigned long *word = (unsigned long *)map + BITMAP_WORD(bit);
-
-	*word &= ~BITMAP_BIT(bit);
-}
-
-/**
- * bitmap_isset() - Check for set bit in bitmap
- * @map:	Pointer to bitmap
- * @bit:	Bit number to check
- *
- * Return: true if given bit is set, false if it's not
- */
-bool bitmap_isset(const uint8_t *map, unsigned bit)
-{
-	const unsigned long *word
-		= (const unsigned long *)map + BITMAP_WORD(bit);
-
-	return !!(*word & BITMAP_BIT(bit));
-}
-
-/**
- * bitmap_or() - Logical disjunction (OR) of two bitmaps
- * @dst:	Pointer to result bitmap
- * @size:	Size of bitmaps, in bytes
- * @a:		First operand
- * @b:		Second operand
- */
-/* cppcheck-suppress unusedFunction */
-void bitmap_or(uint8_t *dst, size_t size, const uint8_t *a, const uint8_t *b)
-{
-	unsigned long *dw = (unsigned long *)dst;
-	unsigned long *aw = (unsigned long *)a;
-	unsigned long *bw = (unsigned long *)b;
-	size_t i;
-
-	for (i = 0; i < size / sizeof(long); i++, dw++, aw++, bw++)
-		*dw = *aw | *bw;
-
-	for (i = size / sizeof(long) * sizeof(long); i < size; i++)
-		dst[i] = a[i] | b[i];
-}
-
-/**
- * bitmap_and_not() - Logical conjunction with complement (AND NOT) of bitmap
- * @dst:	Pointer to result bitmap
- * @size:	Size of bitmaps, in bytes
- * @a:		First operand
- * @b:		Second operand
- */
-void bitmap_and_not(uint8_t *dst, size_t size,
-		   const uint8_t *a, const uint8_t *b)
-{
-	unsigned long *dw = (unsigned long *)dst;
-	unsigned long *aw = (unsigned long *)a;
-	unsigned long *bw = (unsigned long *)b;
-	size_t i;
-
-	for (i = 0; i < size / sizeof(long); i++, dw++, aw++, bw++)
-		*dw = *aw & ~*bw;
-
-	for (i = size / sizeof(long) * sizeof(long); i < size; i++)
-		dst[i] = a[i] & ~b[i];
-}
-
 /**
  * ns_enter() - Enter configured user (unless already joined) and network ns
  * @c:		Execution context
diff --git a/util.h b/util.h
index 8495ed9e..8e81a804 100644
--- a/util.h
+++ b/util.h
@@ -28,16 +28,6 @@
 #define IP_MAX_MTU			USHRT_MAX
 #endif
 
-#define DIV_ROUND_UP(n, d)	(((n) + (d) - 1) / (d))
-#define DIV_ROUND_CLOSEST(n, d)	(((n) + (d) / 2) / (d))
-#define ROUND_DOWN(x, y)	((x) & ~((y) - 1))
-#define ROUND_UP(x, y)		(((x) + (y) - 1) & ~((y) - 1))
-
-#define MAX_FROM_BITS(n)	(((1U << (n)) - 1))
-
-#define BITMAP_BIT(n)		(BIT((n) % (sizeof(long) * 8)))
-#define BITMAP_WORD(n)		(n / (sizeof(long) * 8))
-
 #define SWAP(a, b)							\
 	do {								\
 		__typeof__(a) __x = (a); (a) = (b); (b) = __x;		\
@@ -82,8 +72,6 @@ void abort_with_msg(const char *fmt, ...)
 #define V6		1
 #define IP_VERSIONS	2
 
-#define ARRAY_SIZE(a)		((int)(sizeof(a) / sizeof((a)[0])))
-
 #define foreach(item, array)						\
 	for ((item) = (array); (item) - (array) < ARRAY_SIZE(array); (item)++)
 
@@ -164,12 +152,6 @@ int sock_unix(char *sock_path);
 void sock_probe_features(struct ctx *c);
 long timespec_diff_ms(const struct timespec *a, const struct timespec *b);
 int64_t timespec_diff_us(const struct timespec *a, const struct timespec *b);
-void bitmap_set(uint8_t *map, unsigned bit);
-void bitmap_clear(uint8_t *map, unsigned bit);
-bool bitmap_isset(const uint8_t *map, unsigned bit);
-void bitmap_or(uint8_t *dst, size_t size, const uint8_t *a, const uint8_t *b);
-void bitmap_and_not(uint8_t *dst, size_t size,
-		    const uint8_t *a, const uint8_t *b);
 char *line_read(char *buf, size_t len, int fd);
 void ns_enter(const struct ctx *c);
 bool ns_is_init(void);
-- 
2.53.0


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

* [PATCH v3 24/25] pesto, conf: Parse, send and receive new rules
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (22 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 23/25] conf, fwd, ports, util: Move things around for pesto David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  7:37 ` [PATCH v3 25/25] conf, fwd: Allow switching to new rules received from pesto David Gibson
                   ` (2 subsequent siblings)
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

From: Stefano Brivio <sbrivio@redhat.com>

This adds parsing of options using conf_ports() in pesto, builds rules, and
sends them one by one to passt, which now receives them and stores then in
a different table.  We debug print the rules in the "pending" table but
don't yet attempt to activate them.

Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
Message-ID: <20260322141843.4095972-3-sbrivio@redhat.com>
[dwg: Rebased on changes to my earlier patches]
[dwg: Properly preserve the rule flags when they're read in]
[dwg: Removed the non-working logic to activate the new table]
Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 Makefile |  2 +-
 conf.c   | 60 ++++++++++++++++++++++++++++++++++++--
 fwd.c    |  9 +++++-
 passt.h  |  2 ++
 pesto.c  | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 pif.h    |  2 ++
 6 files changed, 157 insertions(+), 6 deletions(-)

diff --git a/Makefile b/Makefile
index b6b6a823..24235702 100644
--- a/Makefile
+++ b/Makefile
@@ -51,7 +51,7 @@ SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
 MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
 
 PESTO_HEADERS = common.h fwd_rule.h inany.h ip.h pesto.h serialise.h ports.h \
-	lineread.h
+	lineread.h pif.h
 PASST_HEADERS = arch.h arp.h checksum.h conf.h dhcp.h dhcpv6.h epoll_ctl.h \
 	flow.h fwd.h flow_table.h icmp.h icmp_flow.h iov.h isolation.h \
 	lineread.h log.h migrate.h ndp.h netlink.h packet.h passt.h pasta.h \
diff --git a/conf.c b/conf.c
index 7f624db6..b4c2074e 100644
--- a/conf.c
+++ b/conf.c
@@ -2046,6 +2046,50 @@ static int conf_send_rules(const struct ctx *c, int fd)
 	return 0;
 }
 
+/**
+ * conf_send_rules() - Send current forwarding rules to dynamic update client (pesto)
+ * @c:		Execution context
+ * @fd:		Socket to the client
+ *
+ * Return: 0 on success, -1 on failure
+ */
+static int conf_recv_rules(const struct ctx *c, int fd)
+{
+	while (1) {
+		struct fwd_table *fwd;
+		struct fwd_rule r;
+		uint32_t count;
+		uint8_t pif;
+		unsigned i;
+
+		if (read_u8(fd, &pif))
+			return -1;
+
+		if (pif == PIF_NONE)
+			break;
+
+		fwd = c->fwd_pending[pif];
+		if (!fwd) {
+			err("Received rules for non-existent table");
+			return -1;
+		}
+
+		/* Clear pending table */
+		memset(fwd, 0, sizeof(*fwd));
+
+		if (read_u32(fd, &count))
+			return -1;
+
+		for (i = 0; i < count; i++) {
+			fwd_rule_read(fd, &r);
+			fwd_rule_add(fwd, r.proto, r.flags, &r.addr, NULL,
+				     r.first, r.last, r.to);
+		}
+	}
+
+	return 0;
+}
+
 /**
  * conf_listen_handler() - Handle events on configuration listening socket
  * @c:		Execution context
@@ -2060,14 +2104,14 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
 	union epoll_ref ref = { .type = EPOLL_TYPE_CONF };
 	struct ucred uc = { 0 };
 	socklen_t len = sizeof(uc);
-	int fd, rc;
+	int fd, rc, i;
 
 	if (events != EPOLLIN) {
 		err("Unexpected event 0x%04x on configuration socket", events);
 		return;
 	}
 
-	fd = accept4(c->fd_control_listen, NULL, NULL, SOCK_NONBLOCK);
+	fd = accept4(c->fd_control_listen, NULL, NULL, 0);
 	if (fd < 0) {
 		warn_perror("accept4() on configuration listening socket");
 		return;
@@ -2108,6 +2152,18 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
 	if (conf_send_rules(c, fd) < 0)
 		goto fail;
 
+	if (conf_recv_rules(c, fd) < 0)
+		goto fail;
+
+	info("Received new forwarding table:");
+	for (i = 0; i < PIF_NUM_TYPES; i++) {
+		if (!c->fwd_pending[i])
+			continue;
+
+		info("Forwarding from %s:", pif_name(i));
+		fwd_rules_print(c->fwd_pending[i]);
+	}
+
 	return;
 
 fail:
diff --git a/fwd.c b/fwd.c
index 4592af25..d54a1f15 100644
--- a/fwd.c
+++ b/fwd.c
@@ -247,6 +247,9 @@ void fwd_neigh_table_init(const struct ctx *c)
 static struct fwd_table fwd_in;
 static struct fwd_table fwd_out;
 
+static struct fwd_table fwd_in_pending;
+static struct fwd_table fwd_out_pending;
+
 /**
  * fwd_rule_init() - Initialise forwarding tables
  * @c:		Execution context
@@ -254,8 +257,12 @@ static struct fwd_table fwd_out;
 void fwd_rule_init(struct ctx *c)
 {
 	c->fwd[PIF_HOST] = &fwd_in;
-	if (c->mode == MODE_PASTA)
+	c->fwd_pending[PIF_HOST] = &fwd_in_pending;
+
+	if (c->mode == MODE_PASTA) {
 		c->fwd[PIF_SPLICE] = &fwd_out;
+		c->fwd_pending[PIF_SPLICE] = &fwd_out_pending;
+	}
 }
 
 /**
diff --git a/passt.h b/passt.h
index b3f049de..1726965d 100644
--- a/passt.h
+++ b/passt.h
@@ -188,6 +188,7 @@ struct ip6_ctx {
  * @pasta_ifi:		Index of namespace interface for pasta
  * @pasta_conf_ns:	Configure namespace after creating it
  * @fwd:		Forwarding tables
+ * @fwd_pending:	Pending forward tables
  * @no_tcp:		Disable TCP operation
  * @tcp:		Context for TCP protocol handler
  * @no_udp:		Disable UDP operation
@@ -270,6 +271,7 @@ struct ctx {
 	int pasta_conf_ns;
 
 	struct fwd_table *fwd[PIF_NUM_TYPES];
+	struct fwd_table *fwd_pending[PIF_NUM_TYPES];
 
 	int no_tcp;
 	struct tcp_ctx tcp;
diff --git a/pesto.c b/pesto.c
index aa1d584c..5ee0352e 100644
--- a/pesto.c
+++ b/pesto.c
@@ -38,6 +38,7 @@
 #include "fwd_rule.h"
 #include "pesto.h"
 #include "log.h"
+#include "pif.h"
 
 int verbosity = 1;
 
@@ -65,6 +66,36 @@ static void usage(const char *name, FILE *f, int status)
 	FPRINTF(f, "Usage: %s [OPTION]... PATH\n", name);
 	FPRINTF(f,
 		"\n"
+		"  -t, --tcp-ports SPEC	TCP inbound port forwarding\n"
+		"    can be specified multiple times\n"
+		"    SPEC can be:\n"
+		"      'none': don't forward any ports\n"
+		"      'auto': forward all ports currently bound in namespace\n"
+		"      'all': forward all unbound, non-ephemeral ports\n"
+		"      a comma-separated list, optionally ranged with '-'\n"
+		"        and optional target ports after ':', with optional\n"
+		"        address specification suffixed by '/' and optional\n"
+		"        interface prefixed by '%%'. Examples:\n"
+		"        -t 22	Forward local port 22 to port 22 in netns\n"
+		"        -t 22:23	Forward local port 22 to port 23\n"
+		"        -t 22,25	Forward ports 22, 25 to ports 22, 25\n"
+		"        -t 22-80	Forward ports 22 to 80\n"
+		"        -t 22-80:32-90	Forward ports 22 to 80 to\n"
+		"			corresponding port numbers plus 10\n"
+		"        -t 192.0.2.1/5	Bind port 5 of 192.0.2.1\n"
+		"        -t 5-25,~10-20	Forward ports 5 to 9, and 21 to 25\n"
+		"        -t ~25		Forward all bound ports except for 25\n"
+		"    IPv6 bound ports are also forwarded for IPv4\n"
+		"  -u, --udp-ports SPEC	UDP inbound port forwarding\n"
+		"    SPEC is as described for TCP above\n"
+		"    IPv6 bound ports are also forwarded for IPv4\n"
+		"    unless specified, with '-t auto', UDP ports with numbers\n"
+		"    corresponding to forwarded TCP port numbers are\n"
+		"    forwarded too\n"
+		"  -T, --tcp-ns SPEC	TCP port forwarding to init namespace\n"
+		"    SPEC is as described above\n"
+		"  -U, --udp-ns SPEC	UDP port forwarding to init namespace\n"
+		"    SPEC is as described above\n"
 		"  -v, --verbose		Be more verbose\n"
 		"  -q, --quiet		Be less verbose\n"
 		"  -h, --help		Display this help message and exit\n"
@@ -221,7 +252,7 @@ static void show_state(const struct conf_state *state)
  *
  * Return: 0 on success, won't return on failure
  *
- * #syscalls:pesto connect write close exit_group fstat brk getrandom
+ * #syscalls:pesto connect write close exit_group fstat brk getrandom openat
  * #syscalls:pesto socket s390x:socketcall i686:socketcall
  * #syscalls:pesto recvfrom recvmsg arm:recv ppc64le:recv
  * #syscalls:pesto sendto sendmsg arm:send ppc64le:send
@@ -233,15 +264,22 @@ int main(int argc, char **argv)
 		{"verbose",	no_argument,		NULL,		'v' },
 		{"help",	no_argument,		NULL,		'h' },
 		{"version",	no_argument,		NULL,		1 },
+		{"tcp-ports",	required_argument,	NULL,		't' },
+		{"udp-ports",	required_argument,	NULL,		'u' },
+		{"tcp-ns",	required_argument,	NULL,		'T' },
+		{"udp-ns",	required_argument,	NULL,		'U' },
 		{ 0 },
 	};
+	struct fwd_table fwd_in = { .count = 0 }, fwd_out = { .count = 0 };
 	struct sockaddr_un a = { AF_UNIX, "" };
-	const char *optstring = "vh";
+	const char *optstring = "vht:u:T:U:";
+	enum fwd_mode fwd_default;
 	struct pesto_hello hello;
 	struct conf_state *state;
 	struct sock_fprog prog;
 	int optname, ret, s;
 	uint32_t s_version;
+	unsigned i;
 
 	prog.len = (unsigned short)sizeof(filter_pesto) /
 				   sizeof(filter_pesto[0]);
@@ -250,6 +288,8 @@ int main(int argc, char **argv)
 	    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
 		die("Failed to apply seccomp filter");
 
+	fwd_probe_ephemeral();
+
 	do {
 		optname = getopt_long(argc, argv, optstring, options, NULL);
 
@@ -266,6 +306,18 @@ int main(int argc, char **argv)
 		case 'v':
 			verbosity++;
 			break;
+		case 't':
+		case 'u':
+			fwd_default = FWD_MODE_UNSET;
+			conf_ports(optname, optarg, &fwd_in, &fwd_default,
+				   true, true, true);
+			break;
+		case 'T':
+		case 'U':
+			fwd_default = FWD_MODE_UNSET;
+			conf_ports(optname, optarg, &fwd_out, &fwd_default,
+				   true, true, true);
+			break;
 		case 1:
 			FPRINTF(stdout, "pesto ");
 			FPRINTF(stdout, VERSION_BLOB);
@@ -291,6 +343,22 @@ int main(int argc, char **argv)
 		    a.sun_path, strerror(errno));
 	}
 
+	debug("Inbound rules from command line:");
+	for (i = 0; i < fwd_in.count; i++) {
+		const struct fwd_rule *rule = &fwd_in.rules[i].rule;
+		char rulestr[FWD_RULE_STRLEN];
+
+		debug("    %s", fwd_rule_ntop(rule, rulestr, sizeof(rulestr)));
+	}
+
+	debug("Outbound rules from command line:");
+	for (i = 0; i < fwd_out.count; i++) {
+		const struct fwd_rule *rule = &fwd_out.rules[i].rule;
+		char rulestr[FWD_RULE_STRLEN];
+
+		debug("    %s", fwd_rule_ntop(rule, rulestr, sizeof(rulestr)));
+	}
+
 	debug("Connected to passt/pasta control socket");
 
 	ret = read_all_buf(s, &hello, sizeof(hello));
@@ -324,5 +392,21 @@ int main(int argc, char **argv)
 
 	show_state(state);
 
+	if (fwd_in.count) {
+		write_u8(s, PIF_HOST);
+		write_u32(s, fwd_in.count);
+		for (i = 0; i < fwd_in.count; i++)
+			fwd_rule_write(s, &fwd_in.rules[i].rule);
+	}
+
+	if (fwd_out.count) {
+		write_u8(s, PIF_SPLICE);
+		write_u32(s, fwd_out.count);
+		for (i = 0; i < fwd_out.count; i++)
+			fwd_rule_write(s, &fwd_out.rules[i].rule);
+	}
+
+	write_u8(s, PIF_NONE);
+
 	exit(0);
 }
diff --git a/pif.h b/pif.h
index 7bb58e5c..c96a0900 100644
--- a/pif.h
+++ b/pif.h
@@ -35,6 +35,7 @@ enum pif_type {
 	PIF_NUM_TYPES,
 };
 
+#ifndef PESTO
 extern const char *pif_type_str[];
 
 static inline const char *pif_type(enum pif_type pt)
@@ -66,5 +67,6 @@ void pif_sockaddr(const struct ctx *c, union sockaddr_inany *sa,
 int pif_listen(const struct ctx *c, enum epoll_type type, uint8_t pif,
 	       const union inany_addr *addr, const char *ifname,
 	       in_port_t port, unsigned rule);
+#endif /* !PESTO */
 
 #endif /* PIF_H */
-- 
2.53.0


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

* [PATCH v3 25/25] conf, fwd: Allow switching to new rules received from pesto
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (23 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 24/25] pesto, conf: Parse, send and receive new rules David Gibson
@ 2026-03-23  7:37 ` David Gibson
  2026-03-23  8:38 ` [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
  2026-03-25  0:56 ` Stefano Brivio
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  7:37 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: David Gibson

We can now receive updates to the forwarding rules from the pesto client
and store them in a "pending" copy of the forwarding tables.  Implement
switching to using the new rules.

The logic is in a new fwd_listen_switch().  For now this closes all
listening sockets related to the old tables, swaps the active and pending
tables, then listens based on the new tables.  In future we look to improve
this so that we don't temporarily stop listening on ports that both the
old and new tables specify.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
---
 conf.c |  2 ++
 fwd.c  | 34 ++++++++++++++++++++++++++++++++++
 fwd.h  |  1 +
 3 files changed, 37 insertions(+)

diff --git a/conf.c b/conf.c
index b4c2074e..b2c99a74 100644
--- a/conf.c
+++ b/conf.c
@@ -2164,6 +2164,8 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
 		fwd_rules_print(c->fwd_pending[i]);
 	}
 
+	fwd_listen_switch(c);
+
 	return;
 
 fail:
diff --git a/fwd.c b/fwd.c
index d54a1f15..2f1479de 100644
--- a/fwd.c
+++ b/fwd.c
@@ -531,6 +531,40 @@ int fwd_listen_init(const struct ctx *c)
 	return 0;
 }
 
+/**
+ * fwd_listen_switch() - Switch from current to pending rules table
+ * @c:		Execution context
+ */
+void fwd_listen_switch(struct ctx *c)
+{
+	struct fwd_table *tmp[PIF_NUM_TYPES];
+	unsigned i;
+
+	/* Stop listening on the old tables */
+	for (i = 0; i < PIF_NUM_TYPES; i++) {
+		struct fwd_table *fwd = c->fwd[i];
+
+		if (!fwd)
+			continue;
+
+		debug("Flushing %u old %s rules", fwd->count, pif_name(i));
+		fwd_listen_close(fwd);
+		memset(fwd, 0, sizeof(*fwd));
+	}
+
+	/* Swap active and pending tables */
+	static_assert(sizeof(tmp) == sizeof(c->fwd) &&
+		      sizeof(tmp) == sizeof(c->fwd_pending),
+		      "Temporary has wrong size");
+	memcpy(&tmp, (void *)c->fwd, sizeof(tmp));
+	memcpy((void *)c->fwd, (void *)c->fwd_pending, sizeof(tmp));
+	memcpy((void *)c->fwd_pending, &tmp, sizeof(tmp));
+
+	/* Start listening on the new tables */
+	if (fwd_listen_init(c) < 0)
+		err("Error switching to new forwarding rules");
+}
+
 /* See enum in kernel's include/net/tcp_states.h */
 #define UDP_LISTEN	0x07
 #define TCP_LISTEN	0x0a
diff --git a/fwd.h b/fwd.h
index a00fe52d..2e068f23 100644
--- a/fwd.h
+++ b/fwd.h
@@ -53,6 +53,7 @@ int fwd_listen_sync(const struct ctx *c, uint8_t pif,
 		    const struct fwd_scan *tcp, const struct fwd_scan *udp);
 void fwd_listen_close(const struct fwd_table *fwd);
 int fwd_listen_init(const struct ctx *c);
+void fwd_listen_switch(struct ctx *c);
 
 bool nat_inbound(const struct ctx *c, const union inany_addr *addr,
 		 union inany_addr *translated);
-- 
2.53.0


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

* Re: [PATCH v3 00/25] RFC: Read-only dynamic update implementation
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (24 preceding siblings ...)
  2026-03-23  7:37 ` [PATCH v3 25/25] conf, fwd: Allow switching to new rules received from pesto David Gibson
@ 2026-03-23  8:38 ` David Gibson
  2026-03-25  0:56 ` Stefano Brivio
  26 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-23  8:38 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev

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

On Mon, Mar 23, 2026 at 06:37:07PM +1100, David Gibson wrote:
> Here's a new draft of dynamic updates.  This now can successfully
> update rules, though I've not tested it very extensively.  Essentially
> this is just barely enough to work, it still could do with rather a
> lot of polish.
> 
> Patches 1..12/22 are preliminary reworks that make moderate sense even
> without pesto - feel free to apply if you're happy with them.

Oops, as well as miscounting here, I forgot to update the overall
subject.  This is no longer read-only.


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

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

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

* Re: [PATCH v3 03/25] serialise: Split functions user for serialisation from util.c
  2026-03-23  7:37 ` [PATCH v3 03/25] serialise: Split functions user for serialisation from util.c David Gibson
@ 2026-03-25  0:54   ` Stefano Brivio
  2026-03-25  1:50     ` David Gibson
  0 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  0:54 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Mon, 23 Mar 2026 18:37:10 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> The read_all_buf() and write_all_buf() functions in util.c are
> primarily used for serialising data structures to a stream during
> migraiton.  We're going to have further use for such serialisation
> when we add dynamic configuration updates, where we'll want to share
> the code with the client program.

Nit, but I'd like to make sure we're not missing anything before
applying this: the "hidden sizeof()" variants seem to be gone now
(thanks), but then we should drop this last sentence, unless I'm
missing something.

Another nit below:

> [...]
>
> +++ b/serialise.h
> @@ -0,0 +1,14 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later
> + * Copyright Red Hat
> + * Author: David Gibson <david@gibson.dropbear.id.au>
> + */
> +
> +#ifndef SERIALISE_H
> +#define SERIALISE_H
> +
> +#include <stddef.h>
> +
> +int read_all_buf(int fd, void *buf, size_t len);
> +int write_all_buf(int fd, const void *buf, size_t len);
> +
> +#endif /* _SERIALISE_H */

This is SERIALISE_H, not _SERIALISE_H.

-- 
Stefano


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

* Re: [PATCH v3 07/25] fwd: Store forwarding tables indexed by (origin) pif
  2026-03-23  7:37 ` [PATCH v3 07/25] fwd: Store forwarding tables indexed by (origin) pif David Gibson
@ 2026-03-25  0:54   ` Stefano Brivio
  2026-03-25  4:04     ` David Gibson
  0 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  0:54 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Mon, 23 Mar 2026 18:37:14 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> Currently we store the inbound (PIF_HOST) and outbound (PIF_SPLICE)
> forwarding tables in separate fields of struct ctx.  In a number of places
> this requires somewhat awkward if or switch constructs to select the
> right table for updates.  Conceptually simplify that by using an index of
> forwarding tables by pif, which as a bonus keeps track generically which
> pifs have implemented forwarding tables so far.
> 
> For now this doesn't simplify a lot textually, because many places that
> need this also have other special cases to apply by pif.  It does simplify
> a few crucial places though, and we expect it will become more useful as
> the flexibility of the forwarding table is improved.
> 
> Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> ---
>  conf.c  | 58 +++++++++++++++++++++++++++++++-------------------
>  flow.c  | 22 +++++++------------
>  fwd.c   | 65 ++++++++++++++++++++++++++++++---------------------------
>  fwd.h   |  4 ++--
>  passt.h |  6 ++----
>  5 files changed, 83 insertions(+), 72 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index b1ebb4a4..6ca61b74 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -1252,11 +1252,17 @@ dns6:
>  		}
>  	}
>  
> -	info("Inbound forwarding:");
> -	fwd_rules_print(&c->fwd_in);
> -	if (c->mode == MODE_PASTA) {
> -		info("Outbound forwarding:");
> -		fwd_rules_print(&c->fwd_out);
> +	for (i = 0; i < PIF_NUM_TYPES; i++) {
> +		const char *dir = "Outbound";
> +
> +		if (!c->fwd[i])
> +			continue;
> +
> +		if (i == PIF_HOST)
> +			dir = "Inbound";
> +
> +		info("%s forwarding rules (%s):", dir, pif_name(i));
> +		fwd_rules_print(c->fwd[i]);
>  	}
>  }
>  
> @@ -2154,18 +2160,24 @@ void conf(struct ctx *c, int argc, char **argv)
>  
>  	/* Forwarding options can be parsed now, after IPv4/IPv6 settings */
>  	fwd_probe_ephemeral();
> +	fwd_rule_init(c);
>  	optind = 0;
>  	do {
>  		name = getopt_long(argc, argv, optstring, options, NULL);
>  
> -		if (name == 't')
> -			conf_ports(c, name, optarg, &c->fwd_in, &tcp_in_mode);
> -		else if (name == 'u')
> -			conf_ports(c, name, optarg, &c->fwd_in, &udp_in_mode);
> -		else if (name == 'T')
> -			conf_ports(c, name, optarg, &c->fwd_out, &tcp_out_mode);
> -		else if (name == 'U')
> -			conf_ports(c, name, optarg, &c->fwd_out, &udp_out_mode);
> +		if (name == 't') {
> +			conf_ports(c, name, optarg, c->fwd[PIF_HOST],
> +				   &tcp_in_mode);
> +		} else if (name == 'u') {
> +			conf_ports(c, name, optarg, c->fwd[PIF_HOST],
> +				   &udp_in_mode);
> +		} else if (name == 'T') {
> +			conf_ports(c, name, optarg, c->fwd[PIF_SPLICE],
> +				   &tcp_out_mode);
> +		} else if (name == 'U') {
> +			conf_ports(c, name, optarg, c->fwd[PIF_SPLICE],
> +				   &udp_out_mode);
> +		}
>  	} while (name != -1);
>  
>  	if (c->mode == MODE_PASTA)
> @@ -2224,20 +2236,24 @@ void conf(struct ctx *c, int argc, char **argv)
>  		udp_out_mode = fwd_default;
>  
>  	if (tcp_in_mode == FWD_MODE_AUTO) {
> -		conf_ports_range_except(c, 't', "auto", &c->fwd_in, NULL, NULL,
> -					1, NUM_PORTS - 1, NULL, 1, FWD_SCAN);
> +		conf_ports_range_except(c, 't', "auto", c->fwd[PIF_HOST],
> +					NULL, NULL, 1, NUM_PORTS - 1, NULL, 1,
> +					FWD_SCAN);
>  	}
>  	if (tcp_out_mode == FWD_MODE_AUTO) {
> -		conf_ports_range_except(c, 'T', "auto", &c->fwd_out, NULL, "lo",
> -					1, NUM_PORTS - 1, NULL, 1, FWD_SCAN);
> +		conf_ports_range_except(c, 'T', "auto", c->fwd[PIF_SPLICE],
> +					NULL, "lo", 1, NUM_PORTS - 1, NULL, 1,
> +					FWD_SCAN);
>  	}
>  	if (udp_in_mode == FWD_MODE_AUTO) {
> -		conf_ports_range_except(c, 'u', "auto", &c->fwd_in, NULL, NULL,
> -					1, NUM_PORTS - 1, NULL, 1, FWD_SCAN);
> +		conf_ports_range_except(c, 'u', "auto", c->fwd[PIF_HOST],
> +					NULL, NULL, 1, NUM_PORTS - 1, NULL, 1,
> +					FWD_SCAN);
>  	}
>  	if (udp_out_mode == FWD_MODE_AUTO) {
> -		conf_ports_range_except(c, 'U', "auto", &c->fwd_out, NULL, "lo",
> -					1, NUM_PORTS - 1, NULL, 1, FWD_SCAN);
> +		conf_ports_range_except(c, 'U', "auto", c->fwd[PIF_SPLICE],
> +					NULL, "lo", 1, NUM_PORTS - 1, NULL, 1,
> +					FWD_SCAN);
>  	}
>  
>  	if (!c->quiet)
> diff --git a/flow.c b/flow.c
> index c84857b2..2972ab87 100644
> --- a/flow.c
> +++ b/flow.c
> @@ -503,10 +503,10 @@ struct flowside *flow_target(const struct ctx *c, union flow *flow,
>  {
>  	char estr[INANY_ADDRSTRLEN], ostr[INANY_ADDRSTRLEN];
>  	struct flow_common *f = &flow->f;
> +	const struct fwd_table *fwd = c->fwd[f->pif[INISIDE]];
>  	const struct flowside *ini = &f->side[INISIDE];
>  	struct flowside *tgt = &f->side[TGTSIDE];
>  	const struct fwd_rule *rule = NULL;
> -	const struct fwd_table *fwd;
>  	uint8_t tgtpif = PIF_NONE;
>  
>  	assert(flow_new_entry == flow && f->state == FLOW_STATE_INI);
> @@ -514,6 +514,11 @@ struct flowside *flow_target(const struct ctx *c, union flow *flow,
>  	assert(f->pif[INISIDE] != PIF_NONE && f->pif[TGTSIDE] == PIF_NONE);
>  	assert(flow->f.state == FLOW_STATE_INI);
>  
> +	if (fwd) {
> +		if (!(rule = fwd_rule_search(fwd, ini, proto, rule_hint)))
> +			goto norule;
> +	}
> +
>  	switch (f->pif[INISIDE]) {
>  	case PIF_TAP:
>  		memcpy(f->tap_omac, MAC_UNDEF, ETH_ALEN);
> @@ -521,20 +526,10 @@ struct flowside *flow_target(const struct ctx *c, union flow *flow,
>  		break;
>  
>  	case PIF_SPLICE:
> -		fwd = &c->fwd_out;
> -
> -		if (!(rule = fwd_rule_search(fwd, ini, proto, rule_hint)))
> -			goto norule;
> -

Coverity Scan doesn't like this:

/home/sbrivio/passt/flow.c:528:3:
  Type: Explicit null dereferenced (FORWARD_NULL)

/home/sbrivio/passt/flow.c:508:2:
  1. assign_zero: Assigning: "rule" = "NULL".
/home/sbrivio/passt/flow.c:511:2:
  2. path: Condition "flow_new_entry == flow", taking true branch.
/home/sbrivio/passt/flow.c:511:2:
  3. path: Condition "f->state == FLOW_STATE_INI", taking true branch.
/home/sbrivio/passt/flow.c:512:2:
  4. path: Condition "f->type == FLOW_TYPE_NONE", taking true branch.
/home/sbrivio/passt/flow.c:513:2:
  5. path: Condition "f->pif[0] != PIF_NONE", taking true branch.
/home/sbrivio/passt/flow.c:513:2:
  6. path: Condition "f->pif[1] == PIF_NONE", taking true branch.
/home/sbrivio/passt/flow.c:514:2:
  7. path: Condition "flow->f.state == FLOW_STATE_INI", taking true branch.
/home/sbrivio/passt/flow.c:516:2:
  8. path: Condition "fwd", taking false branch.
/home/sbrivio/passt/flow.c:521:2:
  9. path: Switch case value "PIF_SPLICE".
/home/sbrivio/passt/flow.c:528:3:
  10. var_deref_model: Passing null pointer "rule" to "fwd_nat_from_splice", which dereferences it.
/home/sbrivio/passt/fwd.c:1095:2:
  10.1. path: Condition "!inany_is_loopback(&ini->eaddr)", taking false branch.
/home/sbrivio/passt/fwd.c:1095:2:
  10.2. path: Condition "!inany_is_loopback(&ini->oaddr)", taking false branch.
/home/sbrivio/passt/fwd.c:1112:2:
  10.3. path: Condition "proto == IPPROTO_UDP", taking true branch.
/home/sbrivio/passt/fwd.c:1116:2:
  10.4. dereference: Dereferencing pointer "rule".

>  		tgtpif = fwd_nat_from_splice(rule, proto, ini, tgt);
>  		break;
>  
>  	case PIF_HOST:
> -		fwd = &c->fwd_in;
> -
> -		if (!(rule = fwd_rule_search(fwd, ini, proto, rule_hint)))
> -			goto norule;
> -

...and not this either:

/home/sbrivio/passt/flow.c:532:3:
  Type: Explicit null dereferenced (FORWARD_NULL)

/home/sbrivio/passt/flow.c:508:2:
  1. assign_zero: Assigning: "rule" = "NULL".
/home/sbrivio/passt/flow.c:511:2:
  2. path: Condition "flow_new_entry == flow", taking true branch.
/home/sbrivio/passt/flow.c:511:2:
  3. path: Condition "f->state == FLOW_STATE_INI", taking true branch.
/home/sbrivio/passt/flow.c:512:2:
  4. path: Condition "f->type == FLOW_TYPE_NONE", taking true branch.
/home/sbrivio/passt/flow.c:513:2:
  5. path: Condition "f->pif[0] != PIF_NONE", taking true branch.
/home/sbrivio/passt/flow.c:513:2:
  6. path: Condition "f->pif[1] == PIF_NONE", taking true branch.
/home/sbrivio/passt/flow.c:514:2:
  7. path: Condition "flow->f.state == FLOW_STATE_INI", taking true branch.
/home/sbrivio/passt/flow.c:516:2:
  8. path: Condition "fwd", taking false branch.
/home/sbrivio/passt/flow.c:521:2:
  9. path: Switch case value "PIF_HOST".
/home/sbrivio/passt/flow.c:532:3:
  10. var_deref_model: Passing null pointer "rule" to "fwd_nat_from_host", which dereferences it.
/home/sbrivio/passt/fwd.c:1173:2:
  10.1. dereference: Dereferencing pointer "rule".

I haven't checked why.

>  		tgtpif = fwd_nat_from_host(c, rule, proto, ini, tgt);
>  		fwd_neigh_mac_get(c, &tgt->oaddr, f->tap_omac);
>  		break;
>
> [...]

-- 
Stefano


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

* Re: [PATCH v3 08/25] fwd: Allow FWD_DUAL_STACK_ANY flag to be passed directly to fwd_rule_add()
  2026-03-23  7:37 ` [PATCH v3 08/25] fwd: Allow FWD_DUAL_STACK_ANY flag to be passed directly to fwd_rule_add() David Gibson
@ 2026-03-25  0:54   ` Stefano Brivio
  2026-03-25  4:07     ` David Gibson
  0 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  0:54 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

Nits:

On Mon, 23 Mar 2026 18:37:15 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> fwd_rule_add() takes a flags parameter, but it only allows the FWD_WEAK
> and FWD_SCAN flags to be specified there.  It doesn't allow the
> FWD_DUAL_STACK_ANY flag to be set, instead expecting a [*] address to be
> indicated by passing NULL as @addr.
> 
> However, for upcoming dynamic rule updates, it's more convenient to be able
> to explicitly pass FWD_DUAL_STACK_ANY along with an address of ::.  Allow
> that mode of calling.
> 
> Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> ---
>  fwd.c | 7 +++++--
>  1 file changed, 5 insertions(+), 2 deletions(-)
> 
> diff --git a/fwd.c b/fwd.c
> index 3395a28e..d73b7ca7 100644
> --- a/fwd.c
> +++ b/fwd.c
> @@ -362,18 +362,21 @@ void fwd_rule_add(struct fwd_table *fwd, uint8_t proto, uint8_t flags,
>  		  in_port_t first, in_port_t last, in_port_t to)
>  {
>  	/* Flags which can be set from the caller */
> -	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN;
> +	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
>  	unsigned num = (unsigned)last - first + 1;
>  	struct fwd_rule *new;
>  	unsigned i, port;
>  
>  	assert(!(flags & ~allowed_flags));
> -

Spurious change, I think? The extra newline here looks good for
readability. Maybe we should group the second assert with this one?

>  	if (fwd->count >= ARRAY_SIZE(fwd->rules))
>  		die("Too many port forwarding ranges");
>  	if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks))
>  		die("Too many listening sockets");
>  
> +	/* Passing a non-wildcard address with DUAL_STACK_ANY  is a bug */

Extra whitespace after DUAL_STACK_ANY.

> +	assert(!(flags & FWD_DUAL_STACK_ANY) || !addr ||
> +	       inany_equals(addr, &inany_any6));
> +
>  	/* Check for any conflicting entries */
>  	for (i = 0; i < fwd->count; i++) {
>  		char newstr[INANY_ADDRSTRLEN], rulestr[INANY_ADDRSTRLEN];

-- 
Stefano


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

* Re: [PATCH v3 13/25] pesto: Introduce stub configuration interface and tool
  2026-03-23  7:37 ` [PATCH v3 13/25] pesto: Introduce stub configuration interface and tool David Gibson
@ 2026-03-25  0:54   ` Stefano Brivio
  0 siblings, 0 replies; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  0:54 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Mon, 23 Mar 2026 18:37:20 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> Add a control socket to passt/pasta for updating configuration at runtime.
> Add a tool (pesto) for communicating with the control socket.
> 
> For now this is a stub implementation that forms the connection and sends
> some version information, but nothing more.
> Example:
>   ./pasta --debug --config-net -c /tmp/pasta.conf -t none
>   ./pesto /tmp/pasta.conf
> 
> Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> [dwg: Based on an early draft from Stefano]
> Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> ---
>  .gitignore   |   2 +
>  Makefile     |  30 +++++++----
>  common.h     |  14 +++++
>  conf.c       | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++-
>  conf.h       |   2 +
>  epoll_type.h |   4 ++
>  log.c        |   1 +
>  passt.1      |   5 ++
>  passt.c      |   9 ++++
>  passt.h      |   6 +++
>  pesto.1      |  46 ++++++++++++++++
>  pesto.c      | 113 ++++++++++++++++++++++++++++++++++++++
>  pesto.h      |  34 ++++++++++++
>  serialise.c  |   3 ++
>  util.h       |   3 --
>  15 files changed, 407 insertions(+), 15 deletions(-)
>  create mode 100644 common.h
>  create mode 100644 pesto.1
>  create mode 100644 pesto.c
>  create mode 100644 pesto.h
> 
> diff --git a/.gitignore b/.gitignore
> index 3c16adc7..3e40d9f7 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -4,9 +4,11 @@
>  /pasta
>  /pasta.avx2
>  /passt-repair
> +/pesto
>  /qrap
>  /pasta.1
>  /seccomp.h
> +/seccomp_pesto.h
>  /seccomp_repair.h
>  /c*.json
>  README.plain.md
> diff --git a/Makefile b/Makefile
> index 5b6891d7..d085c9c1 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -44,17 +44,19 @@ PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
>  	udp.c udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
>  QRAP_SRCS = qrap.c
>  PASST_REPAIR_SRCS = passt-repair.c
> -SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS)
> +PESTO_SRCS = pesto.c serialise.c
> +SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
>  
> -MANPAGES = passt.1 pasta.1 qrap.1 passt-repair.1
> +MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
>  
> +PESTO_HEADERS = common.h pesto.h serialise.h
>  PASST_HEADERS = arch.h arp.h checksum.h conf.h dhcp.h dhcpv6.h epoll_ctl.h \
>  	flow.h fwd.h flow_table.h icmp.h icmp_flow.h inany.h iov.h ip.h \
>  	isolation.h lineread.h log.h migrate.h ndp.h netlink.h packet.h \
> -	passt.h pasta.h pcap.h pif.h repair.h serialise.h siphash.h tap.h tcp.h \
> -	tcp_buf.h tcp_conn.h tcp_internal.h tcp_splice.h tcp_vu.h udp.h \
> -	udp_flow.h udp_internal.h udp_vu.h util.h vhost_user.h virtio.h \
> -	vu_common.h
> +	passt.h pasta.h pcap.h pif.h repair.h siphash.h tap.h tcp.h tcp_buf.h \
> +	tcp_conn.h tcp_internal.h tcp_splice.h tcp_vu.h udp.h udp_flow.h \
> +	udp_internal.h udp_vu.h util.h vhost_user.h virtio.h vu_common.h \
> +	$(PESTO_HEADERS)
>  HEADERS = $(PASST_HEADERS) seccomp.h
>  
>  C := \#include <sys/random.h>\nint main(){int a=getrandom(0, 0, 0);}
> @@ -75,9 +77,9 @@ mandir		?= $(datarootdir)/man
>  man1dir		?= $(mandir)/man1
>  
>  ifeq ($(TARGET_ARCH),x86_64)
> -BIN := passt passt.avx2 pasta pasta.avx2 qrap passt-repair
> +BIN := passt passt.avx2 pasta pasta.avx2 qrap passt-repair pesto
>  else
> -BIN := passt pasta qrap passt-repair
> +BIN := passt pasta qrap passt-repair pesto
>  endif
>  
>  all: $(BIN) $(MANPAGES) docs
> @@ -91,6 +93,9 @@ seccomp.h: seccomp.sh $(PASST_SRCS) $(PASST_HEADERS)
>  seccomp_repair.h: seccomp.sh $(PASST_REPAIR_SRCS)
>  	@ ARCH="$(TARGET_ARCH)" CC="$(CC)" ./seccomp.sh seccomp_repair.h $(PASST_REPAIR_SRCS)
>  
> +seccomp_pesto.h: seccomp.sh $(PESTO_SRCS)
> +	@ ARCH="$(TARGET_ARCH)" CC="$(CC)" ./seccomp.sh seccomp_pesto.h $(PESTO_SRCS)
> +
>  passt: $(PASST_SRCS) $(HEADERS)
>  	$(CC) $(FLAGS) $(CFLAGS) $(CPPFLAGS) $(PASST_SRCS) -o passt $(LDFLAGS)
>  
> @@ -110,6 +115,9 @@ qrap: $(QRAP_SRCS) passt.h
>  passt-repair: $(PASST_REPAIR_SRCS) seccomp_repair.h
>  	$(CC) $(FLAGS) $(CFLAGS) $(CPPFLAGS) $(PASST_REPAIR_SRCS) -o passt-repair $(LDFLAGS)
>  
> +pesto: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
> +	$(CC) $(FLAGS) $(CFLAGS) $(CPPFLAGS) $(PESTO_SRCS) -o pesto $(LDFLAGS)
> +
>  valgrind: EXTRA_SYSCALLS += rt_sigprocmask rt_sigtimedwait rt_sigaction	\
>  			    rt_sigreturn getpid gettid kill clock_gettime \
>  			    mmap|mmap2 munmap open unlink gettimeofday futex \
> @@ -119,7 +127,7 @@ valgrind: all
>  
>  .PHONY: clean
>  clean:
> -	$(RM) $(BIN) *~ *.o seccomp.h seccomp_repair.h pasta.1 \
> +	$(RM) $(BIN) *~ *.o seccomp.h seccomp_repair.h seccomp_pesto.h pasta.1 \
>  		passt.tar passt.tar.gz *.deb *.rpm \
>  		passt.pid README.plain.md
>  
> @@ -173,11 +181,11 @@ docs: README.md
>  		done < README.md;					\
>  	) > README.plain.md
>  
> -clang-tidy: $(PASST_SRCS)
> +clang-tidy: $(PASST_SRCS) $(PESTO_SRCS)
>  	clang-tidy $^ -- $(filter-out -pie,$(FLAGS) $(CFLAGS) $(CPPFLAGS)) \
>  	           -DCLANG_TIDY_58992
>  
> -cppcheck: $(PASST_SRCS) $(HEADERS)
> +cppcheck: $(PASST_SRCS) $(PESTO_SRCS) $(HEADERS)
>  	if cppcheck --check-level=exhaustive /dev/null > /dev/null 2>&1; then \
>  		CPPCHECK_EXHAUSTIVE="--check-level=exhaustive";		\
>  	else								\
> diff --git a/common.h b/common.h
> new file mode 100644
> index 00000000..76a95609
> --- /dev/null
> +++ b/common.h
> @@ -0,0 +1,14 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later
> + * Copyright Red Hat
> + * Author: David Gibson <david@gibson.dropbear.id.au>
> + *
> + * Definitions used by both passt/pasta and other tools
> + */
> +
> +#ifndef COMMON_H
> +#define COMMON_H
> +
> +/* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
> +#define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
> +
> +#endif /* _COMMON_H */
> diff --git a/conf.c b/conf.c
> index d4c2a013..5e8bc665 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -35,6 +35,7 @@
>  #include <netinet/in.h>
>  #include <netinet/if_ether.h>
>  
> +#include "common.h"
>  #include "util.h"
>  #include "ip.h"
>  #include "passt.h"
> @@ -47,6 +48,10 @@
>  #include "isolation.h"
>  #include "log.h"
>  #include "vhost_user.h"
> +#include "epoll_ctl.h"
> +#include "conf.h"
> +#include "pesto.h"
> +#include "serialise.h"
>  
>  #define NETNS_RUN_DIR	"/run/netns"
>  
> @@ -888,6 +893,7 @@ static void usage(const char *name, FILE *f, int status)
>  		"  --runas UID|UID:GID 	Run as given UID, GID, which can be\n"
>  		"    numeric, or login and group names\n"
>  		"    default: drop to user \"nobody\"\n"
> +		"  -c, --conf-path PATH	Configuration socket path\n"
>  		"  -h, --help		Display this help message and exit\n"
>  		"  --version		Show version and exit\n");
>  
> @@ -1425,6 +1431,17 @@ static void conf_open_files(struct ctx *c)
>  		if (c->pidfile_fd < 0)
>  			die_perror("Couldn't open PID file %s", c->pidfile);
>  	}
> +
> +	c->fd_conf = -1;
> +	if (*c->conf_path) {
> +		c->fd_conf_listen = sock_unix(c->conf_path);
> +		if (c->fd_conf_listen < 0) {
> +			die_perror("Couldn't open control socket %s",
> +				   c->conf_path);
> +		}
> +	} else {
> +		c->fd_conf_listen = -1;
> +	}
>  }
>  
>  /**
> @@ -1460,6 +1477,25 @@ fail:
>  	die("Invalid MAC address: %s", str);
>  }
>  
> +/**
> + * conf_sock_listen() - Start listening for connections on configuration socket
> + * @c:		Execution context
> + */
> +static void conf_sock_listen(const struct ctx *c)
> +{
> +	union epoll_ref ref = { .type = EPOLL_TYPE_CONF_LISTEN };
> +
> +	if (c->fd_conf_listen < 0)
> +		return;
> +
> +	if (listen(c->fd_conf_listen, 0))
> +		die_perror("Couldn't listen on configuration socket");
> +
> +	ref.fd = c->fd_conf_listen;
> +	if (epoll_add(c->epollfd, EPOLLIN | EPOLLET, ref))
> +		die_perror("Couldn't add configuration socket to epoll");
> +}
> +
>  /**
>   * conf() - Process command-line arguments and set configuration
>   * @c:		Execution context
> @@ -1542,9 +1578,10 @@ void conf(struct ctx *c, int argc, char **argv)
>  		{"migrate-exit", no_argument,		NULL,		29 },
>  		{"migrate-no-linger", no_argument,	NULL,		30 },
>  		{"stats", required_argument,		NULL,		31 },
> +		{"conf-path",	required_argument,	NULL,		'c' },
>  		{ 0 },
>  	};
> -	const char *optstring = "+dqfel:hs:F:I:p:P:m:a:n:M:g:i:o:D:S:H:461t:u:T:U:";
> +	const char *optstring = "+dqfel:hs:c:F:I:p:P:m:a:n:M:g:i:o:D:S:H:461t:u:T:U:";
>  	const char *logname = (c->mode == MODE_PASTA) ? "pasta" : "passt";
>  	char userns[PATH_MAX] = { 0 }, netns[PATH_MAX] = { 0 };
>  	bool copy_addrs_opt = false, copy_routes_opt = false;
> @@ -1808,6 +1845,13 @@ void conf(struct ctx *c, int argc, char **argv)
>  
>  			c->fd_tap = -1;
>  			break;
> +		case 'c':
> +			ret = snprintf(c->conf_path, sizeof(c->conf_path), "%s",
> +				       optarg);
> +			if (ret <= 0 || ret >= (int)sizeof(c->sock_path))
> +				die("Invalid configuration path: %s", optarg);
> +			c->fd_conf_listen = c->fd_conf = -1;
> +			break;
>  		case 'F':
>  			errno = 0;
>  			fd_tap_opt = strtol(optarg, NULL, 0);
> @@ -2252,4 +2296,108 @@ void conf(struct ctx *c, int argc, char **argv)
>  
>  	if (!c->quiet)
>  		conf_print(c);
> +
> +	conf_sock_listen(c);
> +}
> +
> +/**
> + * conf_listen_handler() - Handle events on configuration listening socket
> + * @c:		Execution context
> + * @events:	epoll events
> + */
> +void conf_listen_handler(struct ctx *c, uint32_t events)
> +{
> +	struct pesto_hello hello = {
> +		.magic = PESTO_SERVER_MAGIC,
> +		.version = htonl(PESTO_PROTOCOL_VERSION),
> +	};
> +	union epoll_ref ref = { .type = EPOLL_TYPE_CONF };
> +	struct ucred uc = { 0 };
> +	socklen_t len = sizeof(uc);
> +	int fd, rc;
> +
> +	if (events != EPOLLIN) {
> +		err("Unexpected event 0x%04x on configuration socket", events);
> +		return;
> +	}
> +
> +	fd = accept4(c->fd_conf_listen, NULL, NULL, SOCK_NONBLOCK);
> +	if (fd < 0) {
> +		warn_perror("accept4() on configuration listening socket");
> +		return;
> +	}
> +
> +	if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &uc, &len) < 0)
> +		warn_perror("Can't get configuration client credentials");
> +
> +	/* Another client is already connected: accept and close right away. */
> +	if (c->fd_conf != -1) {
> +		info("Discarding configuration client, PID %i", uc.pid);
> +		goto fail;
> +	}
> +
> +	ref.fd = fd;
> +	rc = epoll_add(c->epollfd, EPOLLIN | EPOLLET, ref);
> +	if (rc < 0) {
> +		warn_perror("epoll_ctl() on configuration socket");
> +		goto fail;
> +	}
> +
> +	rc = write_all_buf(fd, &hello, sizeof(hello));
> +	if (rc < 0) {
> +		warn_perror("Error writing configuration protocol hello");
> +		goto fail;
> +	}
> +
> +	c->fd_conf = fd;
> +	info("Accepted configuration client, PID %i", uc.pid);
> +	if (!PESTO_PROTOCOL_VERSION) {
> +		warn(
> +"Warning: Using experimental unsupported configuration protocol");
> +	}
> +
> +	return;
> +
> +fail:
> +	close(fd);
> +}
> +
> +/**
> + * conf_handler() - Handle events on configuration socket
> + * @c:		Execution context
> + * @events:	epoll events
> + */
> +void conf_handler(struct ctx *c, uint32_t events)
> +{
> +	if (events & EPOLLIN) {
> +		char discard[BUFSIZ];
> +		ssize_t n;
> +
> +		do {
> +			n = read(c->fd_conf, discard, sizeof(discard));
> +			if (n > 0)
> +				debug("Discarded %zd bytes of config data", n);
> +		} while (n > 0);
> +		if (n == 0) {
> +			debug("Configuration client EOF");
> +			goto close;
> +		}
> +		if (errno != EAGAIN && errno != EWOULDBLOCK) {
> +			err_perror("Error reading config data");
> +			goto close;
> +		}
> +	}
> +
> +	if (events & EPOLLHUP) {
> +		debug("Configuration client hangup");
> +		goto close;
> +	}
> +
> +	return;
> +
> +close:
> +	debug("Closing configuration socket");
> +	epoll_ctl(c->epollfd, EPOLL_CTL_DEL, c->fd_conf, NULL);
> +	close(c->fd_conf);
> +	c->fd_conf = -1;
>  }
> diff --git a/conf.h b/conf.h
> index b45ad746..16f97189 100644
> --- a/conf.h
> +++ b/conf.h
> @@ -8,5 +8,7 @@
>  
>  enum passt_modes conf_mode(int argc, char *argv[]);
>  void conf(struct ctx *c, int argc, char **argv);
> +void conf_listen_handler(struct ctx *c, uint32_t events);
> +void conf_handler(struct ctx *c, uint32_t events);
>  
>  #endif /* CONF_H */
> diff --git a/epoll_type.h b/epoll_type.h
> index a90ffb67..061325aa 100644
> --- a/epoll_type.h
> +++ b/epoll_type.h
> @@ -46,6 +46,10 @@ enum epoll_type {
>  	EPOLL_TYPE_REPAIR,
>  	/* Netlink neighbour subscription socket */
>  	EPOLL_TYPE_NL_NEIGH,
> +	/* Configuration listening socket */
> +	EPOLL_TYPE_CONF_LISTEN,
> +	/* Configuration socket */
> +	EPOLL_TYPE_CONF,
>  
>  	EPOLL_NUM_TYPES,
>  };
> diff --git a/log.c b/log.c
> index 21e3673e..99404e25 100644
> --- a/log.c
> +++ b/log.c
> @@ -26,6 +26,7 @@
>  #include <stdarg.h>
>  #include <sys/socket.h>
>  
> +#include "common.h"
>  #include "linux_dep.h"
>  #include "log.h"
>  #include "util.h"
> diff --git a/passt.1 b/passt.1
> index 13e8df9d..32d70c8d 100644
> --- a/passt.1
> +++ b/passt.1
> @@ -127,6 +127,11 @@ login name and group name can be passed. This requires privileges (either
>  initial effective UID 0 or CAP_SETUID capability) to work.
>  Default is to change to user \fInobody\fR if started as root.
>  
> +.TP
> +.BR \-c ", " \-\-conf-path " " \fIpath " " (EXPERIMENTAL)
> +Path for configuration and control socket used by \fBpesto\fR(1) to
> +dynamically update passt or pasta's configuration.
> +
>  .TP
>  .BR \-h ", " \-\-help
>  Display a help message and exit.
> diff --git a/passt.c b/passt.c
> index f84419c7..d37beef3 100644
> --- a/passt.c
> +++ b/passt.c
> @@ -36,6 +36,7 @@
>  #include <netinet/if_ether.h>
>  #include <libgen.h>
>  
> +#include "common.h"
>  #include "util.h"
>  #include "passt.h"
>  #include "dhcp.h"
> @@ -80,6 +81,8 @@ char *epoll_type_str[] = {
>  	[EPOLL_TYPE_REPAIR_LISTEN]	= "TCP_REPAIR helper listening socket",
>  	[EPOLL_TYPE_REPAIR]		= "TCP_REPAIR helper socket",
>  	[EPOLL_TYPE_NL_NEIGH]		= "netlink neighbour notifier socket",
> +	[EPOLL_TYPE_CONF_LISTEN]	= "configuration listening socket",
> +	[EPOLL_TYPE_CONF]		= "configuration socket",
>  };
>  static_assert(ARRAY_SIZE(epoll_type_str) == EPOLL_NUM_TYPES,
>  	      "epoll_type_str[] doesn't match enum epoll_type");
> @@ -303,6 +306,12 @@ static void passt_worker(void *opaque, int nfds, struct epoll_event *events)
>  		case EPOLL_TYPE_NL_NEIGH:
>  			nl_neigh_notify_handler(c);
>  			break;
> +		case EPOLL_TYPE_CONF_LISTEN:
> +			conf_listen_handler(c, eventmask);
> +			break;
> +		case EPOLL_TYPE_CONF:
> +			conf_handler(c, eventmask);
> +			break;
>  		default:
>  			/* Can't happen */
>  			assert(0);
> diff --git a/passt.h b/passt.h
> index 62b8dcdf..c38bb5ae 100644
> --- a/passt.h
> +++ b/passt.h
> @@ -158,6 +158,7 @@ struct ip6_ctx {
>   * @foreground:		Run in foreground, don't log to stderr by default
>   * @nofile:		Maximum number of open files (ulimit -n)
>   * @sock_path:		Path for UNIX domain socket
> + * @conf_path:		Path for configuration UNIX domain socket
>   * @repair_path:	TCP_REPAIR helper path, can be "none", empty for default
>   * @pcap:		Path for packet capture file
>   * @pidfile:		Path to PID file, empty string if not configured
> @@ -169,6 +170,8 @@ struct ip6_ctx {
>   * @epollfd:		File descriptor for epoll instance
>   * @fd_tap_listen:	File descriptor for listening AF_UNIX socket, if any
>   * @fd_tap:		AF_UNIX socket, tuntap device, or pre-opened socket
> + * @fd_conf_listen:	Listening configuration socket, if any
> + * @fd_conf:		Configuration socket, if any
>   * @fd_repair_listen:	File descriptor for listening TCP_REPAIR socket, if any
>   * @fd_repair:		Connected AF_UNIX socket for TCP_REPAIR helper
>   * @our_tap_mac:	Pasta/passt's MAC on the tap link
> @@ -223,6 +226,7 @@ struct ctx {
>  	int foreground;
>  	int nofile;
>  	char sock_path[UNIX_PATH_MAX];
> +	char conf_path[UNIX_PATH_MAX];
>  	char repair_path[UNIX_PATH_MAX];
>  	char pcap[PATH_MAX];
>  
> @@ -240,6 +244,8 @@ struct ctx {
>  	int epollfd;
>  	int fd_tap_listen;
>  	int fd_tap;
> +	int fd_conf_listen;
> +	int fd_conf;
>  	int fd_repair_listen;
>  	int fd_repair;
>  	unsigned char our_tap_mac[ETH_ALEN];
> diff --git a/pesto.1 b/pesto.1
> new file mode 100644
> index 00000000..338fb8a6
> --- /dev/null
> +++ b/pesto.1
> @@ -0,0 +1,46 @@
> +.\" SPDX-License-Identifier: GPL-2.0-or-later
> +.\" Copyright Red Hat
> +.\" Author: David Gibson <david@gibson.dropbear.id.au>
> +.TH pesto 1
> +
> +.SH NAME
> +.B pesto
> +\- Configure a running \fBpasst\fR(1) or \fBpasta\fR(1) instance.
> +
> +.SH SYNOPSIS
> +.B pesto
> +\fIPATH\fR
> +
> +.SH DESCRIPTION
> +
> +.B pesto
> +is an experimental client to view and update the port forwarding
> +configuration of a running \fBpasst\fR(1) or \fBpasta\fR(1) instance.
> +
> +\fIPATH\fR gives the path to the UNIX domain socket created by
> +\fBpasst\fR or \fBpasta\fR.  It should match the \fB-c\fR command line
> +option given to that instance.
> +
> +.SH AUTHORS
> +
> +Stefano Brivio <sbrivio@redhat.com>,
> +David Gibson <david@gibson.dropbear.id.au>.
> +
> +.SH REPORTING BUGS
> +
> +Please report issues on the bug tracker at https://bugs.passt.top/, or
> +send a message to the passt-user@passt.top mailing list, see
> +https://lists.passt.top/.
> +
> +.SH COPYRIGHT
> +
> +Copyright Red Hat
> +
> +\fBpesto\fR is free software: you can redistribute them and/or modify
> +them under the terms of the GNU General Public License as published by
> +the Free Software Foundation, either version 2 of the License, or (at
> +your option) any later version.
> +
> +.SH SEE ALSO
> +
> +\fBpasst\fR(1), \fBpasta\fR(1), \fBunix\fR(7).
> diff --git a/pesto.c b/pesto.c
> new file mode 100644
> index 00000000..6d066084
> --- /dev/null
> +++ b/pesto.c
> @@ -0,0 +1,113 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +
> +/* PESTO - Programmable Extensible Socket Translation Orchestrator
> + *  front-end for passt(1) and pasta(1) forwarding configuration
> + *
> + * pesto.c - Main program (it's not actually extensible)
> + *
> + * Copyright (c) 2026 Red Hat GmbH
> + * Author: Stefano Brivio <sbrivio@redhat.com>
> + */
> +
> +#include <arpa/inet.h>
> +#include <sys/prctl.h>
> +#include <sys/types.h>
> +#include <sys/socket.h>
> +#include <sys/un.h>
> +#include <errno.h>
> +#include <inttypes.h>
> +#include <stdbool.h>
> +#include <stddef.h>
> +#include <stdint.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <limits.h>
> +#include <unistd.h>
> +
> +#include <linux/audit.h>
> +#include <linux/capability.h>
> +#include <linux/filter.h>
> +#include <linux/seccomp.h>
> +
> +#include "common.h"
> +#include "seccomp_pesto.h"
> +#include "serialise.h"
> +#include "pesto.h"
> +
> +#define die(...)							\
> +	do {								\
> +		FPRINTF(stderr, __VA_ARGS__);				\
> +		FPRINTF(stderr, "\n");					\
> +		exit(EXIT_FAILURE);					\
> +	} while (0)
> +
> +/**
> + * main() - Entry point and whole program with loop
> + * @argc:	Argument count
> + * @argv:	Arguments: socket path, operation, port specifiers
> + *
> + * Return: 0 on success, won't return on failure
> + *
> + * #syscalls:pesto connect write close exit_group fstat brk

In case we end up dropping the malloc() in the next patches, as a
reminder, we should drop brk here as well.

> + * #syscalls:pesto socket s390x:socketcall i686:socketcall
> + * #syscalls:pesto recvfrom recvmsg arm:recv ppc64le:recv
> + * #syscalls:pesto sendto sendmsg arm:send ppc64le:send
> + */
> +int main(int argc, char **argv)
> +{
> +	struct sockaddr_un a = { AF_UNIX, "" };
> +	struct pesto_hello hello;
> +	struct sock_fprog prog;
> +	uint32_t s_version;
> +	int ret, s;
> +
> +	prctl(PR_SET_DUMPABLE, 0);
> +
> +	prog.len = (unsigned short)sizeof(filter_pesto) /
> +				   sizeof(filter_pesto[0]);
> +	prog.filter = filter_pesto;
> +	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) ||
> +	    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
> +		die("Failed to apply seccomp filter");
> +
> +	if (argc < 2)
> +		die("Usage: %s CONTROLPATH", argv[0]);
> +
> +	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
> +		die("Failed to create AF_UNIX socket: %s", strerror(errno));
> +
> +	ret = snprintf(a.sun_path, sizeof(a.sun_path), "%s", argv[1]);
> +	if (ret <= 0 || ret >= (int)sizeof(a.sun_path))
> +		die("Invalid socket path \"%s\"", argv[1]);
> +
> +	ret = connect(s, (struct sockaddr *)&a, sizeof(a));
> +	if (ret < 0) {
> +		die("Failed to connect to %s: %s",
> +		    a.sun_path, strerror(errno));
> +	}
> +
> +	ret = read_all_buf(s, &hello, sizeof(hello));
> +	if (ret < 0)
> +		die("Couldn't read server greeting: %s", strerror(errno));
> +
> +	if (memcmp(hello.magic, PESTO_SERVER_MAGIC, sizeof(hello.magic)))
> +		die("Bad magic number from server");
> +
> +	s_version = ntohl(hello.version);
> +
> +	if (s_version > PESTO_PROTOCOL_VERSION) {
> +		die("Unknown server protocol version %"PRIu32" > %"PRIu32"\n",
> +		    s_version, PESTO_PROTOCOL_VERSION);
> +	}
> +
> +	/* cppcheck-suppress knownConditionTrueFalse */
> +	if (!s_version) {
> +		if (PESTO_PROTOCOL_VERSION)
> +			die("Unsupported experimental server protocol");
> +		FPRINTF(stderr,
> +"Warning: Using experimental protocol version, client and server must match\n");
> +	}
> +
> +	exit(0);
> +}
> diff --git a/pesto.h b/pesto.h
> new file mode 100644
> index 00000000..92d4df3a
> --- /dev/null
> +++ b/pesto.h
> @@ -0,0 +1,34 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later
> + * Copyright Red Hat
> + * Author: David Gibson <david@gibson.dropbear.id.au>
> + *
> + * Definitions and functions used by both client and server of the configuration
> + * update protocol (pesto).
> + */
> +
> +#ifndef PESTO_H
> +#define PESTO_H
> +
> +#include <assert.h>
> +#include <stdint.h>
> +
> +#define PESTO_SERVER_MAGIC	"pesto:s"

Note to self: sneakily change this to basil:s on merge.

> +
> +/* Version 0 is reserved for unreleased / unsupported experimental versions */
> +#define PESTO_PROTOCOL_VERSION	0
> +
> +/**
> + * struct pesto_hello - Server introduction message
> + * @magic:	PESTO_SERVER_MAGIC
> + * @version:	Version number
> + */
> +struct pesto_hello {
> +	char magic[8];
> +	uint32_t version;
> +} __attribute__ ((__packed__));
> +
> +static_assert(sizeof(PESTO_SERVER_MAGIC)
> +	      == sizeof(((struct pesto_hello *)0)->magic),
> +	      "PESTO_SERVER_MAGIC has wrong size");
> +
> +#endif /* PESTO_H */
> diff --git a/serialise.c b/serialise.c
> index d6c3396a..e3ea86e3 100644
> --- a/serialise.c
> +++ b/serialise.c
> @@ -6,6 +6,9 @@
>   * PASTA - Pack A Subtle Tap Abstraction
>   *  for network namespace/tap device mode
>   *
> + * PESTO - Programmable Extensible Socket Translation Orchestrator
> + *  front-end for passt(1) and pasta(1) forwarding configuration
> + *
>   * serialise.c - Serialisation of data structures over bytestreams
>   *
>   * Copyright Red Hat
> diff --git a/util.h b/util.h
> index cb669105..e7993f4d 100644
> --- a/util.h
> +++ b/util.h
> @@ -317,9 +317,6 @@ static inline bool mod_between(unsigned x, unsigned i, unsigned j, unsigned m)
>  	return mod_sub(x, i, m) < mod_sub(j, i, m);
>  }
>  
> -/* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
> -#define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
> -
>  void raw_random(void *buf, size_t buflen);
>  
>  /*

-- 
Stefano


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

* Re: [PATCH v3 14/25] pesto: Add command line option parsing and debug messages
  2026-03-23  7:37 ` [PATCH v3 14/25] pesto: Add command line option parsing and debug messages David Gibson
@ 2026-03-25  0:55   ` Stefano Brivio
  2026-03-25  4:27     ` David Gibson
  0 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  0:55 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Mon, 23 Mar 2026 18:37:21 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> Add basic command line option parsing using getopt_long() to pesto.
> Implement --help, --version, --quiet and --verbose options, along with a

Two questions:

- do we really need --quiet?

- shouldn't we stick to the passt(1) convention where we avoided -v
  altogether to avoid confusion, and we just have --version and -d /
  --debug?

> debug() macro to print debugging messages when given the verbose option.
> 
> Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> ---
>  common.h |  8 ++++++
>  conf.c   | 45 +++++++++++++++++----------------
>  passt.h  | 12 ++++-----
>  pesto.c  | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++------
>  util.h   |  8 ------
>  5 files changed, 108 insertions(+), 42 deletions(-)
> 
> diff --git a/common.h b/common.h
> index 76a95609..927d20a4 100644
> --- a/common.h
> +++ b/common.h
> @@ -8,6 +8,14 @@
>  #ifndef COMMON_H
>  #define COMMON_H
>  
> +#define VERSION_BLOB							       \
> +	VERSION "\n"							       \
> +	"Copyright Red Hat\n"						       \
> +	"GNU General Public License, version 2 or later\n"		       \
> +	"  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>\n"	       \
> +	"This is free software: you are free to change and redistribute it.\n" \
> +	"There is NO WARRANTY, to the extent permitted by law.\n\n"
> +
>  /* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
>  #define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
>  
> diff --git a/conf.c b/conf.c
> index 5e8bc665..7b960fe9 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -1140,6 +1140,9 @@ static void conf_print(const struct ctx *c)
>  	char bufmac[ETH_ADDRSTRLEN], ifn[IFNAMSIZ];
>  	int i;
>  
> +	if (c->fd_control_listen >= 0)
> +		info("Configuration socket: %s", c->control_path);
> +
>  	if (c->ifi4 > 0 || c->ifi6 > 0) {
>  		info("Template interface: %s%s%s%s%s",
>  		     c->ifi4 > 0 ? if_indextoname(c->ifi4, ifn) : "",
> @@ -1432,15 +1435,15 @@ static void conf_open_files(struct ctx *c)
>  			die_perror("Couldn't open PID file %s", c->pidfile);
>  	}
>  
> -	c->fd_conf = -1;
> -	if (*c->conf_path) {
> -		c->fd_conf_listen = sock_unix(c->conf_path);
> -		if (c->fd_conf_listen < 0) {
> +	c->fd_control = -1;
> +	if (*c->control_path) {

For later: maybe I guess we should squash the renaming of this file
descriptor with the earlier patch adding it.

> +		c->fd_control_listen = sock_unix(c->control_path);
> +		if (c->fd_control_listen < 0) {
>  			die_perror("Couldn't open control socket %s",
> -				   c->conf_path);
> +				   c->control_path);
>  		}
>  	} else {
> -		c->fd_conf_listen = -1;
> +		c->fd_control_listen = -1;
>  	}
>  }
>  
> @@ -1485,13 +1488,13 @@ static void conf_sock_listen(const struct ctx *c)
>  {
>  	union epoll_ref ref = { .type = EPOLL_TYPE_CONF_LISTEN };
>  
> -	if (c->fd_conf_listen < 0)
> +	if (c->fd_control_listen < 0)
>  		return;
>  
> -	if (listen(c->fd_conf_listen, 0))
> +	if (listen(c->fd_control_listen, 0))
>  		die_perror("Couldn't listen on configuration socket");
>  
> -	ref.fd = c->fd_conf_listen;
> +	ref.fd = c->fd_control_listen;
>  	if (epoll_add(c->epollfd, EPOLLIN | EPOLLET, ref))
>  		die_perror("Couldn't add configuration socket to epoll");
>  }
> @@ -1846,11 +1849,11 @@ void conf(struct ctx *c, int argc, char **argv)
>  			c->fd_tap = -1;
>  			break;
>  		case 'c':
> -			ret = snprintf(c->conf_path, sizeof(c->conf_path), "%s",
> -				       optarg);
> +			ret = snprintf(c->control_path, sizeof(c->control_path),
> +				       "%s", optarg);
>  			if (ret <= 0 || ret >= (int)sizeof(c->sock_path))
>  				die("Invalid configuration path: %s", optarg);
> -			c->fd_conf_listen = c->fd_conf = -1;
> +			c->fd_control_listen = c->fd_control = -1;
>  			break;
>  		case 'F':
>  			errno = 0;
> @@ -2294,10 +2297,10 @@ void conf(struct ctx *c, int argc, char **argv)
>  					FWD_SCAN);
>  	}
>  
> +	conf_sock_listen(c);
> +
>  	if (!c->quiet)
>  		conf_print(c);
> -
> -	conf_sock_listen(c);
>  }
>  
>  /**
> @@ -2321,7 +2324,7 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
>  		return;
>  	}
>  
> -	fd = accept4(c->fd_conf_listen, NULL, NULL, SOCK_NONBLOCK);
> +	fd = accept4(c->fd_control_listen, NULL, NULL, SOCK_NONBLOCK);
>  	if (fd < 0) {
>  		warn_perror("accept4() on configuration listening socket");
>  		return;
> @@ -2331,7 +2334,7 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
>  		warn_perror("Can't get configuration client credentials");
>  
>  	/* Another client is already connected: accept and close right away. */
> -	if (c->fd_conf != -1) {
> +	if (c->fd_control != -1) {
>  		info("Discarding configuration client, PID %i", uc.pid);
>  		goto fail;
>  	}
> @@ -2349,7 +2352,7 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
>  		goto fail;
>  	}
>  
> -	c->fd_conf = fd;
> +	c->fd_control = fd;
>  	info("Accepted configuration client, PID %i", uc.pid);
>  	if (!PESTO_PROTOCOL_VERSION) {
>  		warn(
> @@ -2374,7 +2377,7 @@ void conf_handler(struct ctx *c, uint32_t events)
>  		ssize_t n;
>  
>  		do {
> -			n = read(c->fd_conf, discard, sizeof(discard));
> +			n = read(c->fd_control, discard, sizeof(discard));
>  			if (n > 0)
>  				debug("Discarded %zd bytes of config data", n);
>  		} while (n > 0);
> @@ -2397,7 +2400,7 @@ void conf_handler(struct ctx *c, uint32_t events)
>  
>  close:
>  	debug("Closing configuration socket");
> -	epoll_ctl(c->epollfd, EPOLL_CTL_DEL, c->fd_conf, NULL);
> -	close(c->fd_conf);
> -	c->fd_conf = -1;
> +	epoll_ctl(c->epollfd, EPOLL_CTL_DEL, c->fd_control, NULL);
> +	close(c->fd_control);
> +	c->fd_control = -1;
>  }
> diff --git a/passt.h b/passt.h
> index c38bb5ae..b3f049de 100644
> --- a/passt.h
> +++ b/passt.h
> @@ -158,7 +158,7 @@ struct ip6_ctx {
>   * @foreground:		Run in foreground, don't log to stderr by default
>   * @nofile:		Maximum number of open files (ulimit -n)
>   * @sock_path:		Path for UNIX domain socket
> - * @conf_path:		Path for configuration UNIX domain socket
> + * @control_path:	Path for control/configuration UNIX domain socket
>   * @repair_path:	TCP_REPAIR helper path, can be "none", empty for default
>   * @pcap:		Path for packet capture file
>   * @pidfile:		Path to PID file, empty string if not configured
> @@ -170,8 +170,8 @@ struct ip6_ctx {
>   * @epollfd:		File descriptor for epoll instance
>   * @fd_tap_listen:	File descriptor for listening AF_UNIX socket, if any
>   * @fd_tap:		AF_UNIX socket, tuntap device, or pre-opened socket
> - * @fd_conf_listen:	Listening configuration socket, if any
> - * @fd_conf:		Configuration socket, if any
> + * @fd_control_listen:	Listening control/configuration socket, if any
> + * @fd_control:		Control/configuration socket, if any
>   * @fd_repair_listen:	File descriptor for listening TCP_REPAIR socket, if any
>   * @fd_repair:		Connected AF_UNIX socket for TCP_REPAIR helper
>   * @our_tap_mac:	Pasta/passt's MAC on the tap link
> @@ -226,7 +226,7 @@ struct ctx {
>  	int foreground;
>  	int nofile;
>  	char sock_path[UNIX_PATH_MAX];
> -	char conf_path[UNIX_PATH_MAX];
> +	char control_path[UNIX_PATH_MAX];
>  	char repair_path[UNIX_PATH_MAX];
>  	char pcap[PATH_MAX];
>  
> @@ -244,8 +244,8 @@ struct ctx {
>  	int epollfd;
>  	int fd_tap_listen;
>  	int fd_tap;
> -	int fd_conf_listen;
> -	int fd_conf;
> +	int fd_control_listen;
> +	int fd_control;
>  	int fd_repair_listen;
>  	int fd_repair;
>  	unsigned char our_tap_mac[ETH_ALEN];
> diff --git a/pesto.c b/pesto.c
> index 6d066084..f8c9e01d 100644
> --- a/pesto.c
> +++ b/pesto.c
> @@ -15,6 +15,7 @@
>  #include <sys/socket.h>
>  #include <sys/un.h>
>  #include <errno.h>
> +#include <getopt.h>
>  #include <inttypes.h>
>  #include <stdbool.h>
>  #include <stddef.h>
> @@ -42,6 +43,32 @@
>  		exit(EXIT_FAILURE);					\
>  	} while (0)
>  
> +#define debug(...)							\
> +	do {								\
> +		if (verbosity > 1) {					\
> +			FPRINTF(stderr, __VA_ARGS__);			\
> +			FPRINTF(stderr, "\n");				\
> +		}							\
> +	} while (0)
> +
> +/**
> + * usage() - Print usage, exit with given status code
> + * @name:	Executable name
> + * @f:		Stream to print usage info to
> + * @status:	Status code for exit(2)
> + */
> +static void usage(const char *name, FILE *f, int status)
> +{
> +	FPRINTF(f, "Usage: %s [OPTION]... PATH\n", name);
> +	FPRINTF(f,
> +		"\n"
> +		"  -v, --verbose		Be more verbose\n"
> +		"  -q, --quiet		Be less verbose\n"
> +		"  -h, --help		Display this help message and exit\n"
> +		"  --version		Show version and exit\n");
> +	exit(status);
> +}
> +
>  /**
>   * main() - Entry point and whole program with loop
>   * @argc:	Argument count
> @@ -56,13 +83,20 @@
>   */
>  int main(int argc, char **argv)
>  {
> +	const struct option options[] = {
> +		{"quiet",	no_argument,		NULL,		'q' },
> +		{"verbose",	no_argument,		NULL,		'v' },
> +		{"help",	no_argument,		NULL,		'h' },
> +		{"version",	no_argument,		NULL,		1 },
> +		{ 0 },
> +	};
>  	struct sockaddr_un a = { AF_UNIX, "" };
> +	const char *optstring = "vh";
>  	struct pesto_hello hello;
>  	struct sock_fprog prog;
> +	int optname, ret, s;
>  	uint32_t s_version;
> -	int ret, s;
> -
> -	prctl(PR_SET_DUMPABLE, 0);

I would, at least by default, keep this exactly in line with the
paranoid stuff I added to passt, for consistency and ease of auditing:
what will happen if we run *this* tool as root? If we choose to drop to
nobody in that case, removing this will be a problem. And we'll
probably not remember to add it back.

> +	int verbosity = 1;

If we skip --quiet, this can be simplified.

>  
>  	prog.len = (unsigned short)sizeof(filter_pesto) /
>  				   sizeof(filter_pesto[0]);
> @@ -71,15 +105,40 @@ int main(int argc, char **argv)
>  	    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
>  		die("Failed to apply seccomp filter");
>  
> -	if (argc < 2)
> -		die("Usage: %s CONTROLPATH", argv[0]);

I was suggesting to use PATH instead of CONTROLPATH in the earlier
patch, but this goes away here and we'll probably want to squash this
anyway so it shouldn't matter.

> +	do {
> +		optname = getopt_long(argc, argv, optstring, options, NULL);
> +
> +		switch (optname) {
> +		case -1:
> +		case 0:
> +			break;
> +		case 'h':
> +			usage(argv[0], stdout, EXIT_SUCCESS);
> +			break;
> +		case 'q':
> +			verbosity--;
> +			break;
> +		case 'v':
> +			verbosity++;
> +			break;
> +		case 1:
> +			FPRINTF(stdout, "pesto ");
> +			FPRINTF(stdout, VERSION_BLOB);
> +			exit(EXIT_SUCCESS);
> +		default:
> +			usage(argv[0], stderr, EXIT_FAILURE);
> +		}
> +	} while (optname != -1);
> +
> +	if (argc - optind != 1)
> +		usage(argv[0], stderr, EXIT_FAILURE);
>  
>  	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
>  		die("Failed to create AF_UNIX socket: %s", strerror(errno));
>  
> -	ret = snprintf(a.sun_path, sizeof(a.sun_path), "%s", argv[1]);
> +	ret = snprintf(a.sun_path, sizeof(a.sun_path), "%s", argv[optind]);
>  	if (ret <= 0 || ret >= (int)sizeof(a.sun_path))
> -		die("Invalid socket path \"%s\"", argv[1]);
> +		die("Invalid socket path \"%s\"", argv[optind]);
>  
>  	ret = connect(s, (struct sockaddr *)&a, sizeof(a));
>  	if (ret < 0) {
> @@ -87,6 +146,8 @@ int main(int argc, char **argv)
>  		    a.sun_path, strerror(errno));
>  	}
>  
> +	debug("Connected to passt/pasta control socket");
> +
>  	ret = read_all_buf(s, &hello, sizeof(hello));
>  	if (ret < 0)
>  		die("Couldn't read server greeting: %s", strerror(errno));
> @@ -96,6 +157,8 @@ int main(int argc, char **argv)
>  
>  	s_version = ntohl(hello.version);
>  
> +	debug("Server protocol version: %"PRIu32, s_version);
> +
>  	if (s_version > PESTO_PROTOCOL_VERSION) {
>  		die("Unknown server protocol version %"PRIu32" > %"PRIu32"\n",
>  		    s_version, PESTO_PROTOCOL_VERSION);
> diff --git a/util.h b/util.h
> index e7993f4d..d7c397f6 100644
> --- a/util.h
> +++ b/util.h
> @@ -21,14 +21,6 @@
>  
>  #include "log.h"
>  
> -#define VERSION_BLOB							       \
> -	VERSION "\n"							       \
> -	"Copyright Red Hat\n"						       \
> -	"GNU General Public License, version 2 or later\n"		       \
> -	"  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>\n"	       \
> -	"This is free software: you are free to change and redistribute it.\n" \
> -	"There is NO WARRANTY, to the extent permitted by law.\n\n"
> -
>  #ifndef SECCOMP_RET_KILL_PROCESS
>  #define SECCOMP_RET_KILL_PROCESS	SECCOMP_RET_KILL
>  #endif

-- 
Stefano


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

* Re: [PATCH v3 15/25] pesto: Expose list of pifs to pesto
  2026-03-23  7:37 ` [PATCH v3 15/25] pesto: Expose list of pifs to pesto David Gibson
@ 2026-03-25  0:56   ` Stefano Brivio
  2026-03-25  4:34     ` David Gibson
  0 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  0:56 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Mon, 23 Mar 2026 18:37:22 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> Extend the dynamic update protocol to expose the pif indices and names
> from a running passt/pasta to the pesto tool.  pesto records that data
> and (for now) prints it out.
> 
> Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> ---
>  conf.c      | 38 +++++++++++++++++++++
>  pesto.c     | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
>  serialise.c | 21 ++++++++++++
>  serialise.h |  3 ++
>  4 files changed, 159 insertions(+), 1 deletion(-)
> 
> diff --git a/conf.c b/conf.c
> index 7b960fe9..b878db94 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -2303,6 +2303,41 @@ void conf(struct ctx *c, int argc, char **argv)
>  		conf_print(c);
>  }
>  
> +/**
> + * conf_send_pifs() - Send list of pifs to dynamic update client (pesto)
> + * @c:		Execution context
> + * @fd:		Socket to the client
> + *
> + * Return: 0 on success, -1 on failure
> + */
> +static int conf_send_pifs(const struct ctx *c, int fd)
> +{
> +	uint32_t num = 0;
> +	unsigned pif;
> +
> +	/* First count the number of pifs with tables */
> +	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> +		if (c->fwd[pif])
> +			num++;
> +	}
> +
> +	if (write_u32(fd, num))
> +		return -1;
> +
> +	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> +		if (!c->fwd[pif])
> +			continue;
> +
> +		if (write_u8(fd, pif))
> +			return -1;
> +
> +		if (write_str(fd, pif_name(pif)) < 0)
> +			return -1;
> +	}
> +
> +	return 0;
> +}
> +
>  /**
>   * conf_listen_handler() - Handle events on configuration listening socket
>   * @c:		Execution context
> @@ -2359,6 +2394,9 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
>  "Warning: Using experimental unsupported configuration protocol");
>  	}
>  
> +	if (conf_send_pifs(c, fd) < 0)
> +		goto fail;
> +
>  	return;
>  
>  fail:
> diff --git a/pesto.c b/pesto.c
> index f8c9e01d..ed4f2aab 100644
> --- a/pesto.c
> +++ b/pesto.c
> @@ -36,6 +36,8 @@
>  #include "serialise.h"
>  #include "pesto.h"
>  
> +static int verbosity = 1;
> +
>  #define die(...)							\
>  	do {								\
>  		FPRINTF(stderr, __VA_ARGS__);				\
> @@ -51,6 +53,19 @@
>  		}							\
>  	} while (0)
>  
> +/**
> + * xmalloc() - Allocate memory, with fatal error on failure
> + * @size:	Number of bytes to allocate
> + */
> +static void *xmalloc(size_t size)
> +{
> +	void *p = malloc(size);

I would really really prefer to skip this unless it's absolutely
necessary, for a number of reasons:

1. this thing is talking to passt which talks to untrusted workloads,
   and there's a risk of arbitrary code execution, which implies the
   possibility of specially crafted messages that might make us
   allocate:

   a. in specific regions and build a heap spraying attack based on that

   b. a lot, and cause a denial of service

   ...and, while this process should be short lived, we can't assume
   that, if somebody achieves arbitrary code execution in passt and
   malicious messages, because we risk looping on rules, or having
   passt send us information very slowly (minutes / hours), etc.

2. besides, we might want to add a "daemon" mode one day, and then it's
   not short lived anymore, which opens the door to leaks and
   double-frees (adding to a. and b. above)

3. we might need to reuse functions from passt and not notice right
   away that they call this xmalloc(), or have to eventually adapt them
   anyway

4. consistency, in messaging ("passt doesn't allocate dynamic memory",
   without caveats) and for auditing reasons

Strings can be 32 bytes, or 1024 if we want to splurge. Unless we need
to pile a lot of them on the stack (but it's not the case in pesto) a
malloc() doesn't really bring any advantage compared to static buffers
here and there. It's not megabytes.

> +
> +	if (!p)
> +		die("Memory allocation failure");
> +	return p;
> +}
> +
>  /**
>   * usage() - Print usage, exit with given status code
>   * @name:	Executable name
> @@ -69,6 +84,83 @@ static void usage(const char *name, FILE *f, int status)
>  	exit(status);
>  }
>  
> +/**
> + * pesto_recv_str() - Receive a string from passt/pasta
> + * @fd:		Control socket
> + *
> + * Return: pointer to malloc()ed string
> + */
> +static const char *pesto_recv_str(int fd)
> +{
> +	uint32_t len;
> +	char *buf;
> +
> +	if (read_u32(fd, &len) < 0)
> +		die("Error reading from control socket");
> +
> +	buf = xmalloc(len);
> +	if (read_all_buf(fd, buf, len) < 0)
> +		die("Error reading from control socket");
> +
> +	return buf;
> +}
> +
> +struct pif_state {
> +	uint8_t pif;
> +	const char *name;
> +};
> +
> +struct conf_state {

More as a to-do list item rather than as a review comment: we need
documentation for those structs.

> +	uint32_t npifs;
> +	struct pif_state pif[];
> +};
> +
> +/**
> + * pesto_read_pifs() - Read pif names and IDs from passt/pasta
> + * @fd:		Control socket
> + */
> +static const struct conf_state *pesto_read_pifs(int fd)
> +{
> +	uint32_t num;
> +	struct conf_state *state;
> +	unsigned i;
> +
> +	if (read_u32(fd, &num) < 0)
> +		die("Error reading from control socket");
> +
> +	debug("Receiving %"PRIu32" interface names", num);
> +
> +	state = xmalloc(sizeof(*state) + num * sizeof(struct pif_state));
> +	state->npifs = num;
> +
> +	for (i = 0; i < num; i++) {
> +		struct pif_state *ps = &state->pif[i];
> +
> +		if (read_u8(fd, &ps->pif) < 0)
> +			die("Error reading from control socket");
> +		ps->name = pesto_recv_str(fd);
> +
> +		debug("%u: %s", ps->pif, ps->name);
> +	}
> +
> +	return state;
> +}
> +
> +/**
> + * show_state() - Show current rule state obtained from passt/pasta
> + * @pifs:	PIF name information
> + */
> +static void show_state(const struct conf_state *state)
> +{
> +	unsigned i;
> +
> +	for (i = 0; i < state->npifs; i++) {
> +		const struct pif_state *ps = &state->pif[i];
> +		printf("Forwarding rules for %s interface\n", ps->name);
> +		printf("\tTBD\n");
> +	}
> +}
> +
>  /**
>   * main() - Entry point and whole program with loop
>   * @argc:	Argument count
> @@ -91,12 +183,12 @@ int main(int argc, char **argv)
>  		{ 0 },
>  	};
>  	struct sockaddr_un a = { AF_UNIX, "" };
> +	const struct conf_state *state;
>  	const char *optstring = "vh";
>  	struct pesto_hello hello;
>  	struct sock_fprog prog;
>  	int optname, ret, s;
>  	uint32_t s_version;
> -	int verbosity = 1;
>  
>  	prog.len = (unsigned short)sizeof(filter_pesto) /
>  				   sizeof(filter_pesto[0]);
> @@ -172,5 +264,9 @@ int main(int argc, char **argv)
>  "Warning: Using experimental protocol version, client and server must match\n");
>  	}
>  
> +	state = pesto_read_pifs(s);
> +
> +	show_state(state);
> +
>  	exit(0);
>  }
> diff --git a/serialise.c b/serialise.c
> index e3ea86e3..94c34d3c 100644
> --- a/serialise.c
> +++ b/serialise.c
> @@ -19,6 +19,7 @@
>  #include <endian.h>
>  #include <errno.h>
>  #include <stdint.h>
> +#include <string.h>
>  #include <unistd.h>
>  
>  #include "serialise.h"
> @@ -121,6 +122,26 @@ int write_all_buf(int fd, const void *buf, size_t len)
>  		return write_all_buf(fd, &beval, sizeof(beval));	\
>  	}
>  
> +#define be8toh(x)	(x)
> +#define htobe8(x)	(x)
> +
> +SERIALISE_UINT(8)
>  SERIALISE_UINT(32)
>  
>  #undef SERIALISE_UNIT
> +
> +/**
> + * write_str() - Write a string to an fd in length/value format
> + * @fd:		Socket to the client
> + * @s:		String to send
> + *
> + * Return: 0 on success, -1 on error
> + */
> +int write_str(int fd, const char *s)
> +{
> +	uint32_t len = strlen(s) + 1; /* Include \0 */
> +
> +	if (write_u32(fd, len) < 0)
> +		return -1;

Even if we don't need to allocate here, I would feel more comfortable
if we passed fixed-size strings only to passt to have a slightly lower
risk of buffer overflows, in general.

> +	return write_all_buf(fd, s, len);
> +}
> diff --git a/serialise.h b/serialise.h
> index 0a4ed086..7ef35786 100644
> --- a/serialise.h
> +++ b/serialise.h
> @@ -16,6 +16,9 @@ int write_all_buf(int fd, const void *buf, size_t len);
>  	int read_u##bits(int fd, uint##bits##_t *val);			\
>  	int write_u##bits(int fd, uint##bits##_t val);
>  
> +SERIALISE_UINT_DECL(8)
>  SERIALISE_UINT_DECL(32)
>  
> +int write_str(int fd, const char *s);
> +
>  #endif /* _SERIALISE_H */

-- 
Stefano


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

* Re: [PATCH v3 20/25] fwd_rule: Move forwarding rule text formatting to common code
  2026-03-23  7:37 ` [PATCH v3 20/25] fwd_rule: Move forwarding rule text formatting to common code David Gibson
@ 2026-03-25  0:56   ` Stefano Brivio
  2026-03-25  4:42     ` David Gibson
  0 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  0:56 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Mon, 23 Mar 2026 18:37:27 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> Move the logic for formatting forwarding rules into strings from
> fwd_rules_print() into fwd_rule.c where it can be shared with pesto.
> We also make the function explicitly construct a string, rather than
> directly printing with info(), for greater flexibility.
> 
> Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> ---
>  Makefile   | 11 ++++----
>  fwd.c      | 40 +++---------------------------
>  fwd_rule.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
>  fwd_rule.h | 11 ++++++++
>  4 files changed, 94 insertions(+), 41 deletions(-)
>  create mode 100644 fwd_rule.c
> 
> diff --git a/Makefile b/Makefile
> index bc325482..44c396e7 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -38,13 +38,14 @@ FLAGS += -DVERSION=\"$(VERSION)\"
>  FLAGS += -DDUAL_STACK_SOCKETS=$(DUAL_STACK_SOCKETS)
>  
>  PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
> -	flow.c fwd.c icmp.c igmp.c inany.c iov.c ip.c isolation.c lineread.c \
> -	log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c pasta.c pcap.c \
> -	pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c tcp_splice.c tcp_vu.c \
> -	udp.c udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
> +	flow.c fwd.c fwd_rule.c icmp.c igmp.c inany.c iov.c ip.c isolation.c \
> +	lineread.c log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c \
> +	pasta.c pcap.c pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c \
> +	tcp_splice.c tcp_vu.c udp.c udp_flow.c udp_vu.c util.c vhost_user.c \
> +	virtio.c vu_common.c
>  QRAP_SRCS = qrap.c
>  PASST_REPAIR_SRCS = passt-repair.c
> -PESTO_SRCS = pesto.c inany.c ip.c serialise.c
> +PESTO_SRCS = pesto.c fwd_rule.c inany.c ip.c serialise.c
>  SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
>  
>  MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
> diff --git a/fwd.c b/fwd.c
> index a32d0a20..20409c62 100644
> --- a/fwd.c
> +++ b/fwd.c
> @@ -304,20 +304,6 @@ parse_err:
>  	warn("Unable to parse %s", PORT_RANGE_SYSCTL);
>  }
>  
> -/**
> - * fwd_rule_addr() - Return match address for a rule
> - * @rule:	Forwarding rule
> - *
> - * Return: matching address for rule, NULL if it matches all addresses
> - */
> -static const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
> -{
> -	if (rule->flags & FWD_DUAL_STACK_ANY)
> -		return NULL;
> -
> -	return &rule->addr;
> -}
> -
>  /**
>   * fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap
>   * @map:	Bitmap to update
> @@ -497,28 +483,10 @@ void fwd_rules_print(const struct fwd_table *fwd)
>  	unsigned i;
>  
>  	for (i = 0; i < fwd->count; i++) {
> -		const struct fwd_rule *rule = &fwd->rules[i].rule;
> -		const char *percent = *rule->ifname ? "%" : "";
> -		const char *weak = "", *scan = "";
> -		char addr[INANY_ADDRSTRLEN];
> -
> -		inany_ntop(fwd_rule_addr(rule), addr, sizeof(addr));
> -		if (rule->flags & FWD_WEAK)
> -			weak = " (best effort)";
> -		if (rule->flags & FWD_SCAN)
> -			scan = " (auto-scan)";
> -
> -		if (rule->first == rule->last) {
> -			info("    %s [%s]%s%s:%hu  =>  %hu %s%s",
> -			     ipproto_name(rule->proto), addr, percent,
> -			     rule->ifname, rule->first, rule->to, weak, scan);
> -		} else {
> -			info("    %s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s",
> -			     ipproto_name(rule->proto), addr, percent,
> -			     rule->ifname, rule->first, rule->last,
> -			     rule->to, rule->last - rule->first + rule->to,
> -			     weak, scan);
> -		}
> +		char rulestr[FWD_RULE_STRLEN];
> +
> +		info("    %s", fwd_rule_ntop(&fwd->rules[i].rule,
> +					     rulestr, sizeof(rulestr)));
>  	}
>  }
>  
> diff --git a/fwd_rule.c b/fwd_rule.c
> new file mode 100644
> index 00000000..dfbdf683
> --- /dev/null
> +++ b/fwd_rule.c
> @@ -0,0 +1,73 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +
> +/* PASST - Plug A Simple Socket Transport
> + *  for qemu/UNIX domain socket mode
> + *
> + * PASTA - Pack A Subtle Tap Abstraction
> + *  for network namespace/tap device mode
> + *
> + * PESTO - Programmable Extensible Socket Translation Orchestrator
> + *  front-end for passt(1) and pasta(1) forwarding configuration
> + *
> + * fwd_rule.c - Helpers for working with forwarding rule specifications
> + *
> + * Copyright Red Hat
> + * Author: David Gibson <david@gibson.dropbear.id.au>
> + */
> +
> +#include <stdio.h>
> +
> +#include "fwd_rule.h"
> +
> +/**
> + * fwd_rule_addr() - Return match address for a rule
> + * @rule:	Forwarding rule
> + *
> + * Return: matching address for rule, NULL if it matches all addresses
> + */
> +const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
> +{
> +	if (rule->flags & FWD_DUAL_STACK_ANY)
> +		return NULL;
> +
> +	return &rule->addr;
> +}
> +
> +/**
> + * fwd_rule_ntop() - Format forwarding rule as a string
> + * @rule:	Rule to format
> + * @dst:	Buffer to store output (should have FWD_RULE_STRLEN bytes)
> + * @size:	Size of @dst
> + */
> +const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size)
> +{
> +	const char *percent = *rule->ifname ? "%" : "";
> +	const char *weak = "", *scan = "";
> +	char addr[INANY_ADDRSTRLEN];
> +	int len;
> +
> +	inany_ntop(fwd_rule_addr(rule), addr, sizeof(addr));
> +	if (rule->flags & FWD_WEAK)
> +		weak = " (best effort)";
> +	if (rule->flags & FWD_SCAN)
> +		scan = " (auto-scan)";
> +
> +	if (rule->first == rule->last) {
> +		len = snprintf(dst, size,
> +			       "%s [%s]%s%s:%hu  =>  %hu %s%s",
> +			       ipproto_name(rule->proto), addr, percent,
> +			       rule->ifname, rule->first, rule->to, weak, scan);
> +	} else {
> +		in_port_t tolast = rule->last - rule->first + rule->to;
> +		len = snprintf(dst, size,
> +			       "%s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s",
> +			       ipproto_name(rule->proto), addr, percent,
> +			       rule->ifname, rule->first, rule->last,
> +			       rule->to, tolast, weak, scan);
> +	}
> +
> +	if (len < 0 || (size_t)len >= size)
> +		return NULL;
> +
> +	return dst;
> +}
> diff --git a/fwd_rule.h b/fwd_rule.h
> index 84ec5cbe..59db0e95 100644
> --- a/fwd_rule.h
> +++ b/fwd_rule.h
> @@ -42,4 +42,15 @@ struct fwd_rule {
>  	uint8_t flags;
>  };
>  
> +const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
> +
> +#define FWD_RULE_STRLEN					    \
> +	(IPPROTO_STRLEN - 1				    \
> +	 + INANY_ADDRSTRLEN - 1				    \
> +	 + IFNAMSIZ - 1					    \
> +	 + sizeof(" (best effort)") - 1			    \
> +	 + sizeof(" (auto-scan)") - 1			    \
> +	 + 15)

I'm not quite sure about the reason for the 15 here, is there some
constant we can use perhaps?

> +const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size);
> +
>  #endif /* FWD_RULE_H */

-- 
Stefano


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

* Re: [PATCH v3 21/25] pesto: Read current ruleset from passt/pasta and display it
  2026-03-23  7:37 ` [PATCH v3 21/25] pesto: Read current ruleset from passt/pasta and display it David Gibson
@ 2026-03-25  0:56   ` Stefano Brivio
  2026-03-25  4:43     ` David Gibson
  0 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  0:56 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Mon, 23 Mar 2026 18:37:28 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> Implement serialisation of our current forwarding rules in conf.c,
> deserialising it to display in the pesto client.
> 
> Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> ---
>  conf.c     | 44 +++++++++++++++++++++++++++++++
>  fwd_rule.c | 40 ++++++++++++++++++++++++++++
>  fwd_rule.h |  3 +++
>  pesto.c    | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++---
>  4 files changed, 160 insertions(+), 4 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index b878db94..7f311914 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -2338,6 +2338,47 @@ static int conf_send_pifs(const struct ctx *c, int fd)
>  	return 0;
>  }
>  
> +/**
> + * conf_send_rules() - Send current forwarding rules to dynamic update client (pesto)

What about:

 * conf_send_rules() - Send forwarding rules to configuration client (pesto)

?

> + * @c:		Execution context
> + * @fd:		Socket to the client
> + *
> + * Return: 0 on success, -1 on failure
> + */
> +static int conf_send_rules(const struct ctx *c, int fd)
> +{
> +	unsigned pif;
> +
> +	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> +		const struct fwd_table *fwd = c->fwd[pif];
> +		unsigned i;
> +
> +		if (!fwd)
> +			continue;
> +
> +		assert(pif);
> +
> +		/* PIF id */
> +		if (write_u8(fd, pif))
> +			return -1;
> +
> +		/* Number of rules */
> +		if (write_u32(fd, fwd->count))
> +			return -1;
> +
> +		for (i = 0; i < fwd->count; i++) {
> +			if (fwd_rule_write(fd, &fwd->rules[i].rule))
> +				return -1;
> +		}
> +	}
> +
> +	/* Write 0 PIF id to finish */
> +	if (write_u8(fd, 0))

We have PIF_NONE, shouldn't we use that to make it obvious that it's an
invalid value?

> +		return -1;
> +
> +	return 0;
> +}
>
> [...]

I stopped reviewing here, the next patches in my series are anyway mine
and after those I guess it doesn't make sense, yet, that I review in
fine detail, as we'll probably need to change a bunch of things.

-- 
Stefano


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

* Re: [PATCH v3 00/25] RFC: Read-only dynamic update implementation
  2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
                   ` (25 preceding siblings ...)
  2026-03-23  8:38 ` [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
@ 2026-03-25  0:56 ` Stefano Brivio
  2026-03-25  1:00   ` Stefano Brivio
  26 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  0:56 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Mon, 23 Mar 2026 18:37:07 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> Here's a new draft of dynamic updates.  This now can successfully
> update rules, though I've not tested it very extensively.  Essentially
> this is just barely enough to work, it still could do with rather a
> lot of polish.
> 
> Patches 1..12/22 are preliminary reworks that make moderate sense even
> without pesto - feel free to apply if you're happy with them.

I'm applying (in a moment, tests are running now):

- 1/25, 2/25,
- not 3/25 (comments pending)
- not 4/25 (depends on 3/25)
- 5/25, 6/25,
- not 7/25 (Coverity Scan warnings),
- not 8/25 (comments pending),
- 9/25, 10/25, 11/25, 12/25.

-- 
Stefano


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

* Re: [PATCH v3 00/25] RFC: Read-only dynamic update implementation
  2026-03-25  0:56 ` Stefano Brivio
@ 2026-03-25  1:00   ` Stefano Brivio
  2026-03-25  4:44     ` David Gibson
  0 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  1:00 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Wed, 25 Mar 2026 01:56:51 +0100
Stefano Brivio <sbrivio@redhat.com> wrote:

> On Mon, 23 Mar 2026 18:37:07 +1100
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > Here's a new draft of dynamic updates.  This now can successfully
> > update rules, though I've not tested it very extensively.  Essentially
> > this is just barely enough to work, it still could do with rather a
> > lot of polish.
> > 
> > Patches 1..12/22 are preliminary reworks that make moderate sense even
> > without pesto - feel free to apply if you're happy with them.  
> 
> I'm applying (in a moment, tests are running now):
> 
> - 1/25, 2/25,
> - not 3/25 (comments pending)
> - not 4/25 (depends on 3/25)
> - 5/25, 6/25,
> - not 7/25 (Coverity Scan warnings),
> - not 8/25 (comments pending),
> - 9/25, 10/25, 11/25, 12/25.

Never mind, most of the "TCP/IPv4: ns to host" tests fail, probably I
missed something while cherry-picking patches from this series. I guess
a re-spin up to 12/25 would be more convenient in this case.

-- 
Stefano


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

* Re: [PATCH v3 03/25] serialise: Split functions user for serialisation from util.c
  2026-03-25  0:54   ` Stefano Brivio
@ 2026-03-25  1:50     ` David Gibson
  0 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-25  1:50 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev

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

On Wed, Mar 25, 2026 at 01:54:07AM +0100, Stefano Brivio wrote:
> On Mon, 23 Mar 2026 18:37:10 +1100
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > The read_all_buf() and write_all_buf() functions in util.c are
> > primarily used for serialising data structures to a stream during
> > migraiton.  We're going to have further use for such serialisation
> > when we add dynamic configuration updates, where we'll want to share
> > the code with the client program.
> 
> Nit, but I'd like to make sure we're not missing anything before
> applying this: the "hidden sizeof()" variants seem to be gone now
> (thanks), but then we should drop this last sentence, unless I'm
> missing something.

Oops, yes.  I removed the _var() ones but forgot to update the message.

> 
> Another nit below:
> 
> > [...]
> >
> > +++ b/serialise.h
> > @@ -0,0 +1,14 @@
> > +/* SPDX-License-Identifier: GPL-2.0-or-later
> > + * Copyright Red Hat
> > + * Author: David Gibson <david@gibson.dropbear.id.au>
> > + */
> > +
> > +#ifndef SERIALISE_H
> > +#define SERIALISE_H
> > +
> > +#include <stddef.h>
> > +
> > +int read_all_buf(int fd, void *buf, size_t len);
> > +int write_all_buf(int fd, const void *buf, size_t len);
> > +
> > +#endif /* _SERIALISE_H */
> 
> This is SERIALISE_H, not _SERIALISE_H.

Fixed.

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

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

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

* Re: [PATCH v3 07/25] fwd: Store forwarding tables indexed by (origin) pif
  2026-03-25  0:54   ` Stefano Brivio
@ 2026-03-25  4:04     ` David Gibson
  0 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-25  4:04 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev

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

On Wed, Mar 25, 2026 at 01:54:24AM +0100, Stefano Brivio wrote:
> On Mon, 23 Mar 2026 18:37:14 +1100
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > Currently we store the inbound (PIF_HOST) and outbound (PIF_SPLICE)
> > forwarding tables in separate fields of struct ctx.  In a number of places
> > this requires somewhat awkward if or switch constructs to select the
> > right table for updates.  Conceptually simplify that by using an index of
> > forwarding tables by pif, which as a bonus keeps track generically which
> > pifs have implemented forwarding tables so far.
> > 
> > For now this doesn't simplify a lot textually, because many places that
> > need this also have other special cases to apply by pif.  It does simplify
> > a few crucial places though, and we expect it will become more useful as
> > the flexibility of the forwarding table is improved.
> > 
> > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>

[snip]
> /home/sbrivio/passt/flow.c:508:2:
>   1. assign_zero: Assigning: "rule" = "NULL".
> /home/sbrivio/passt/flow.c:511:2:
>   2. path: Condition "flow_new_entry == flow", taking true branch.
> /home/sbrivio/passt/flow.c:511:2:
>   3. path: Condition "f->state == FLOW_STATE_INI", taking true branch.
> /home/sbrivio/passt/flow.c:512:2:
>   4. path: Condition "f->type == FLOW_TYPE_NONE", taking true branch.
> /home/sbrivio/passt/flow.c:513:2:
>   5. path: Condition "f->pif[0] != PIF_NONE", taking true branch.
> /home/sbrivio/passt/flow.c:513:2:
>   6. path: Condition "f->pif[1] == PIF_NONE", taking true branch.
> /home/sbrivio/passt/flow.c:514:2:
>   7. path: Condition "flow->f.state == FLOW_STATE_INI", taking true branch.
> /home/sbrivio/passt/flow.c:516:2:
>   8. path: Condition "fwd", taking false branch.
> /home/sbrivio/passt/flow.c:521:2:
>   9. path: Switch case value "PIF_SPLICE".
> /home/sbrivio/passt/flow.c:528:3:
>   10. var_deref_model: Passing null pointer "rule" to "fwd_nat_from_splice", which dereferences it.
> /home/sbrivio/passt/fwd.c:1095:2:
>   10.1. path: Condition "!inany_is_loopback(&ini->eaddr)", taking false branch.
> /home/sbrivio/passt/fwd.c:1095:2:
>   10.2. path: Condition "!inany_is_loopback(&ini->oaddr)", taking false branch.
> /home/sbrivio/passt/fwd.c:1112:2:
>   10.3. path: Condition "proto == IPPROTO_UDP", taking true branch.
> /home/sbrivio/passt/fwd.c:1116:2:
>   10.4. dereference: Dereferencing pointer "rule".
> 
> >  		tgtpif = fwd_nat_from_splice(rule, proto, ini, tgt);
> >  		break;
> >  
> >  	case PIF_HOST:
> > -		fwd = &c->fwd_in;
> > -
> > -		if (!(rule = fwd_rule_search(fwd, ini, proto, rule_hint)))
> > -			goto norule;
> > -
> 
> ...and not this either:
> 
> /home/sbrivio/passt/flow.c:532:3:
>   Type: Explicit null dereferenced (FORWARD_NULL)
> 
> /home/sbrivio/passt/flow.c:508:2:
>   1. assign_zero: Assigning: "rule" = "NULL".
> /home/sbrivio/passt/flow.c:511:2:
>   2. path: Condition "flow_new_entry == flow", taking true branch.
> /home/sbrivio/passt/flow.c:511:2:
>   3. path: Condition "f->state == FLOW_STATE_INI", taking true branch.
> /home/sbrivio/passt/flow.c:512:2:
>   4. path: Condition "f->type == FLOW_TYPE_NONE", taking true branch.
> /home/sbrivio/passt/flow.c:513:2:
>   5. path: Condition "f->pif[0] != PIF_NONE", taking true branch.
> /home/sbrivio/passt/flow.c:513:2:
>   6. path: Condition "f->pif[1] == PIF_NONE", taking true branch.
> /home/sbrivio/passt/flow.c:514:2:
>   7. path: Condition "flow->f.state == FLOW_STATE_INI", taking true branch.
> /home/sbrivio/passt/flow.c:516:2:
>   8. path: Condition "fwd", taking false branch.
> /home/sbrivio/passt/flow.c:521:2:
>   9. path: Switch case value "PIF_HOST".
> /home/sbrivio/passt/flow.c:532:3:
>   10. var_deref_model: Passing null pointer "rule" to "fwd_nat_from_host", which dereferences it.
> /home/sbrivio/passt/fwd.c:1173:2:
>   10.1. dereference: Dereferencing pointer "rule".
> 
> I haven't checked why.
> 
> >  		tgtpif = fwd_nat_from_host(c, rule, proto, ini, tgt);
> >  		fwd_neigh_mac_get(c, &tgt->oaddr, f->tap_omac);
> >  		break;

Oops.  That's because we're relying on the fact that we _do_ have
populated tables for HOST and SPLICE.  That's constructed a very long
way away, so of course coverity can't figure that out.  I've fixed it
with assert()s for now, and those should go away if/when we implement
forwarding tables for TAP.

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

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

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

* Re: [PATCH v3 08/25] fwd: Allow FWD_DUAL_STACK_ANY flag to be passed directly to fwd_rule_add()
  2026-03-25  0:54   ` Stefano Brivio
@ 2026-03-25  4:07     ` David Gibson
  0 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-25  4:07 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev

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

On Wed, Mar 25, 2026 at 01:54:33AM +0100, Stefano Brivio wrote:
> Nits:
> 
> On Mon, 23 Mar 2026 18:37:15 +1100
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > fwd_rule_add() takes a flags parameter, but it only allows the FWD_WEAK
> > and FWD_SCAN flags to be specified there.  It doesn't allow the
> > FWD_DUAL_STACK_ANY flag to be set, instead expecting a [*] address to be
> > indicated by passing NULL as @addr.
> > 
> > However, for upcoming dynamic rule updates, it's more convenient to be able
> > to explicitly pass FWD_DUAL_STACK_ANY along with an address of ::.  Allow
> > that mode of calling.
> > 
> > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > ---
> >  fwd.c | 7 +++++--
> >  1 file changed, 5 insertions(+), 2 deletions(-)
> > 
> > diff --git a/fwd.c b/fwd.c
> > index 3395a28e..d73b7ca7 100644
> > --- a/fwd.c
> > +++ b/fwd.c
> > @@ -362,18 +362,21 @@ void fwd_rule_add(struct fwd_table *fwd, uint8_t proto, uint8_t flags,
> >  		  in_port_t first, in_port_t last, in_port_t to)
> >  {
> >  	/* Flags which can be set from the caller */
> > -	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN;
> > +	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
> >  	unsigned num = (unsigned)last - first + 1;
> >  	struct fwd_rule *new;
> >  	unsigned i, port;
> >  
> >  	assert(!(flags & ~allowed_flags));
> > -
> 
> Spurious change, I think? The extra newline here looks good for
> readability.

Oops, yes.

> Maybe we should group the second assert with this one?

Good idea, done.

> 
> >  	if (fwd->count >= ARRAY_SIZE(fwd->rules))
> >  		die("Too many port forwarding ranges");
> >  	if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks))
> >  		die("Too many listening sockets");
> >  
> > +	/* Passing a non-wildcard address with DUAL_STACK_ANY  is a bug */
> 
> Extra whitespace after DUAL_STACK_ANY.

Fixed.

> 
> > +	assert(!(flags & FWD_DUAL_STACK_ANY) || !addr ||
> > +	       inany_equals(addr, &inany_any6));
> > +
> >  	/* Check for any conflicting entries */
> >  	for (i = 0; i < fwd->count; i++) {
> >  		char newstr[INANY_ADDRSTRLEN], rulestr[INANY_ADDRSTRLEN];
> 
> -- 
> Stefano
> 

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

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

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

* Re: [PATCH v3 14/25] pesto: Add command line option parsing and debug messages
  2026-03-25  0:55   ` Stefano Brivio
@ 2026-03-25  4:27     ` David Gibson
  0 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-25  4:27 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev

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

On Wed, Mar 25, 2026 at 01:55:07AM +0100, Stefano Brivio wrote:
> On Mon, 23 Mar 2026 18:37:21 +1100
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > Add basic command line option parsing using getopt_long() to pesto.
> > Implement --help, --version, --quiet and --verbose options, along with a
> 
> Two questions:
> 
> - do we really need --quiet?
> 
> - shouldn't we stick to the passt(1) convention where we avoided -v
>   altogether to avoid confusion, and we just have --version and -d /
>   --debug?

Fair point.  I've reworked it that way.

> 
> > debug() macro to print debugging messages when given the verbose option.
> > 
> > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > ---
> >  common.h |  8 ++++++
> >  conf.c   | 45 +++++++++++++++++----------------
> >  passt.h  | 12 ++++-----
> >  pesto.c  | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++------
> >  util.h   |  8 ------
> >  5 files changed, 108 insertions(+), 42 deletions(-)
> > 
> > diff --git a/common.h b/common.h
> > index 76a95609..927d20a4 100644
> > --- a/common.h
> > +++ b/common.h
> > @@ -8,6 +8,14 @@
> >  #ifndef COMMON_H
> >  #define COMMON_H
> >  
> > +#define VERSION_BLOB							       \
> > +	VERSION "\n"							       \
> > +	"Copyright Red Hat\n"						       \
> > +	"GNU General Public License, version 2 or later\n"		       \
> > +	"  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>\n"	       \
> > +	"This is free software: you are free to change and redistribute it.\n" \
> > +	"There is NO WARRANTY, to the extent permitted by law.\n\n"
> > +
> >  /* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
> >  #define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
> >  
> > diff --git a/conf.c b/conf.c
> > index 5e8bc665..7b960fe9 100644
> > --- a/conf.c
> > +++ b/conf.c
> > @@ -1140,6 +1140,9 @@ static void conf_print(const struct ctx *c)
> >  	char bufmac[ETH_ADDRSTRLEN], ifn[IFNAMSIZ];
> >  	int i;
> >  
> > +	if (c->fd_control_listen >= 0)
> > +		info("Configuration socket: %s", c->control_path);
> > +
> >  	if (c->ifi4 > 0 || c->ifi6 > 0) {
> >  		info("Template interface: %s%s%s%s%s",
> >  		     c->ifi4 > 0 ? if_indextoname(c->ifi4, ifn) : "",
> > @@ -1432,15 +1435,15 @@ static void conf_open_files(struct ctx *c)
> >  			die_perror("Couldn't open PID file %s", c->pidfile);
> >  	}
> >  
> > -	c->fd_conf = -1;
> > -	if (*c->conf_path) {
> > -		c->fd_conf_listen = sock_unix(c->conf_path);
> > -		if (c->fd_conf_listen < 0) {
> > +	c->fd_control = -1;
> > +	if (*c->control_path) {
> 
> For later: maybe I guess we should squash the renaming of this file
> descriptor with the earlier patch adding it.

Oops, that was my intention, but clearly I updated the wrong patch.

> > +		c->fd_control_listen = sock_unix(c->control_path);
> > +		if (c->fd_control_listen < 0) {
> >  			die_perror("Couldn't open control socket %s",
> > -				   c->conf_path);
> > +				   c->control_path);
> >  		}
> >  	} else {
> > -		c->fd_conf_listen = -1;
> > +		c->fd_control_listen = -1;
> >  	}
> >  }
> >  
> > @@ -1485,13 +1488,13 @@ static void conf_sock_listen(const struct ctx *c)
> >  {
> >  	union epoll_ref ref = { .type = EPOLL_TYPE_CONF_LISTEN };
> >  
> > -	if (c->fd_conf_listen < 0)
> > +	if (c->fd_control_listen < 0)
> >  		return;
> >  
> > -	if (listen(c->fd_conf_listen, 0))
> > +	if (listen(c->fd_control_listen, 0))
> >  		die_perror("Couldn't listen on configuration socket");
> >  
> > -	ref.fd = c->fd_conf_listen;
> > +	ref.fd = c->fd_control_listen;
> >  	if (epoll_add(c->epollfd, EPOLLIN | EPOLLET, ref))
> >  		die_perror("Couldn't add configuration socket to epoll");
> >  }
> > @@ -1846,11 +1849,11 @@ void conf(struct ctx *c, int argc, char **argv)
> >  			c->fd_tap = -1;
> >  			break;
> >  		case 'c':
> > -			ret = snprintf(c->conf_path, sizeof(c->conf_path), "%s",
> > -				       optarg);
> > +			ret = snprintf(c->control_path, sizeof(c->control_path),
> > +				       "%s", optarg);
> >  			if (ret <= 0 || ret >= (int)sizeof(c->sock_path))
> >  				die("Invalid configuration path: %s", optarg);
> > -			c->fd_conf_listen = c->fd_conf = -1;
> > +			c->fd_control_listen = c->fd_control = -1;
> >  			break;
> >  		case 'F':
> >  			errno = 0;
> > @@ -2294,10 +2297,10 @@ void conf(struct ctx *c, int argc, char **argv)
> >  					FWD_SCAN);
> >  	}
> >  
> > +	conf_sock_listen(c);
> > +
> >  	if (!c->quiet)
> >  		conf_print(c);
> > -
> > -	conf_sock_listen(c);
> >  }
> >  
> >  /**
> > @@ -2321,7 +2324,7 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
> >  		return;
> >  	}
> >  
> > -	fd = accept4(c->fd_conf_listen, NULL, NULL, SOCK_NONBLOCK);
> > +	fd = accept4(c->fd_control_listen, NULL, NULL, SOCK_NONBLOCK);
> >  	if (fd < 0) {
> >  		warn_perror("accept4() on configuration listening socket");
> >  		return;
> > @@ -2331,7 +2334,7 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
> >  		warn_perror("Can't get configuration client credentials");
> >  
> >  	/* Another client is already connected: accept and close right away. */
> > -	if (c->fd_conf != -1) {
> > +	if (c->fd_control != -1) {
> >  		info("Discarding configuration client, PID %i", uc.pid);
> >  		goto fail;
> >  	}
> > @@ -2349,7 +2352,7 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
> >  		goto fail;
> >  	}
> >  
> > -	c->fd_conf = fd;
> > +	c->fd_control = fd;
> >  	info("Accepted configuration client, PID %i", uc.pid);
> >  	if (!PESTO_PROTOCOL_VERSION) {
> >  		warn(
> > @@ -2374,7 +2377,7 @@ void conf_handler(struct ctx *c, uint32_t events)
> >  		ssize_t n;
> >  
> >  		do {
> > -			n = read(c->fd_conf, discard, sizeof(discard));
> > +			n = read(c->fd_control, discard, sizeof(discard));
> >  			if (n > 0)
> >  				debug("Discarded %zd bytes of config data", n);
> >  		} while (n > 0);
> > @@ -2397,7 +2400,7 @@ void conf_handler(struct ctx *c, uint32_t events)
> >  
> >  close:
> >  	debug("Closing configuration socket");
> > -	epoll_ctl(c->epollfd, EPOLL_CTL_DEL, c->fd_conf, NULL);
> > -	close(c->fd_conf);
> > -	c->fd_conf = -1;
> > +	epoll_ctl(c->epollfd, EPOLL_CTL_DEL, c->fd_control, NULL);
> > +	close(c->fd_control);
> > +	c->fd_control = -1;
> >  }
> > diff --git a/passt.h b/passt.h
> > index c38bb5ae..b3f049de 100644
> > --- a/passt.h
> > +++ b/passt.h
> > @@ -158,7 +158,7 @@ struct ip6_ctx {
> >   * @foreground:		Run in foreground, don't log to stderr by default
> >   * @nofile:		Maximum number of open files (ulimit -n)
> >   * @sock_path:		Path for UNIX domain socket
> > - * @conf_path:		Path for configuration UNIX domain socket
> > + * @control_path:	Path for control/configuration UNIX domain socket
> >   * @repair_path:	TCP_REPAIR helper path, can be "none", empty for default
> >   * @pcap:		Path for packet capture file
> >   * @pidfile:		Path to PID file, empty string if not configured
> > @@ -170,8 +170,8 @@ struct ip6_ctx {
> >   * @epollfd:		File descriptor for epoll instance
> >   * @fd_tap_listen:	File descriptor for listening AF_UNIX socket, if any
> >   * @fd_tap:		AF_UNIX socket, tuntap device, or pre-opened socket
> > - * @fd_conf_listen:	Listening configuration socket, if any
> > - * @fd_conf:		Configuration socket, if any
> > + * @fd_control_listen:	Listening control/configuration socket, if any
> > + * @fd_control:		Control/configuration socket, if any
> >   * @fd_repair_listen:	File descriptor for listening TCP_REPAIR socket, if any
> >   * @fd_repair:		Connected AF_UNIX socket for TCP_REPAIR helper
> >   * @our_tap_mac:	Pasta/passt's MAC on the tap link
> > @@ -226,7 +226,7 @@ struct ctx {
> >  	int foreground;
> >  	int nofile;
> >  	char sock_path[UNIX_PATH_MAX];
> > -	char conf_path[UNIX_PATH_MAX];
> > +	char control_path[UNIX_PATH_MAX];
> >  	char repair_path[UNIX_PATH_MAX];
> >  	char pcap[PATH_MAX];
> >  
> > @@ -244,8 +244,8 @@ struct ctx {
> >  	int epollfd;
> >  	int fd_tap_listen;
> >  	int fd_tap;
> > -	int fd_conf_listen;
> > -	int fd_conf;
> > +	int fd_control_listen;
> > +	int fd_control;
> >  	int fd_repair_listen;
> >  	int fd_repair;
> >  	unsigned char our_tap_mac[ETH_ALEN];
> > diff --git a/pesto.c b/pesto.c
> > index 6d066084..f8c9e01d 100644
> > --- a/pesto.c
> > +++ b/pesto.c
> > @@ -15,6 +15,7 @@
> >  #include <sys/socket.h>
> >  #include <sys/un.h>
> >  #include <errno.h>
> > +#include <getopt.h>
> >  #include <inttypes.h>
> >  #include <stdbool.h>
> >  #include <stddef.h>
> > @@ -42,6 +43,32 @@
> >  		exit(EXIT_FAILURE);					\
> >  	} while (0)
> >  
> > +#define debug(...)							\
> > +	do {								\
> > +		if (verbosity > 1) {					\
> > +			FPRINTF(stderr, __VA_ARGS__);			\
> > +			FPRINTF(stderr, "\n");				\
> > +		}							\
> > +	} while (0)
> > +
> > +/**
> > + * usage() - Print usage, exit with given status code
> > + * @name:	Executable name
> > + * @f:		Stream to print usage info to
> > + * @status:	Status code for exit(2)
> > + */
> > +static void usage(const char *name, FILE *f, int status)
> > +{
> > +	FPRINTF(f, "Usage: %s [OPTION]... PATH\n", name);
> > +	FPRINTF(f,
> > +		"\n"
> > +		"  -v, --verbose		Be more verbose\n"
> > +		"  -q, --quiet		Be less verbose\n"
> > +		"  -h, --help		Display this help message and exit\n"
> > +		"  --version		Show version and exit\n");
> > +	exit(status);
> > +}
> > +
> >  /**
> >   * main() - Entry point and whole program with loop
> >   * @argc:	Argument count
> > @@ -56,13 +83,20 @@
> >   */
> >  int main(int argc, char **argv)
> >  {
> > +	const struct option options[] = {
> > +		{"quiet",	no_argument,		NULL,		'q' },
> > +		{"verbose",	no_argument,		NULL,		'v' },
> > +		{"help",	no_argument,		NULL,		'h' },
> > +		{"version",	no_argument,		NULL,		1 },
> > +		{ 0 },
> > +	};
> >  	struct sockaddr_un a = { AF_UNIX, "" };
> > +	const char *optstring = "vh";
> >  	struct pesto_hello hello;
> >  	struct sock_fprog prog;
> > +	int optname, ret, s;
> >  	uint32_t s_version;
> > -	int ret, s;
> > -
> > -	prctl(PR_SET_DUMPABLE, 0);
> 
> I would, at least by default, keep this exactly in line with the
> paranoid stuff I added to passt, for consistency and ease of auditing:
> what will happen if we run *this* tool as root? If we choose to drop to
> nobody in that case, removing this will be a problem. And we'll
> probably not remember to add it back.

So, I made that change as a temporary debugging hack which I forgot to
revert.  That said, I don't really see the case for disabling dump in
pesto - if you can trace pesto you could just connect directly to the
control socket.

The possibility of dropping to nobody is the closest to a case I've
seen... but I don't really see a reason we'd want to do that.  It
would be quite awkward for one thing, since nobody probably won't have
permission to the control socket.  Running this as root wouldn't be
the wisest choice, but unlike for passt/pasta I don't think it's scary
enough that we should actively try to stop the user from shooting
themselves in the foot.

That said, it obviously shouldn't be removed in this patch, having
just been added in the previous one.  I've left it in for now.

> > +	int verbosity = 1;
> 
> If we skip --quiet, this can be simplified.

Done.

> >  
> >  	prog.len = (unsigned short)sizeof(filter_pesto) /
> >  				   sizeof(filter_pesto[0]);
> > @@ -71,15 +105,40 @@ int main(int argc, char **argv)
> >  	    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
> >  		die("Failed to apply seccomp filter");
> >  
> > -	if (argc < 2)
> > -		die("Usage: %s CONTROLPATH", argv[0]);
> 
> I was suggesting to use PATH instead of CONTROLPATH in the earlier
> patch, but this goes away here and we'll probably want to squash this
> anyway so it shouldn't matter.

Ah, yes, another update in the wrong patch.  I've fixed it in the
earlier patch anyway, to save you the bother of remembering it goes
away when you next look.

> > +	do {
> > +		optname = getopt_long(argc, argv, optstring, options, NULL);
> > +
> > +		switch (optname) {
> > +		case -1:
> > +		case 0:
> > +			break;
> > +		case 'h':
> > +			usage(argv[0], stdout, EXIT_SUCCESS);
> > +			break;
> > +		case 'q':
> > +			verbosity--;
> > +			break;
> > +		case 'v':
> > +			verbosity++;
> > +			break;
> > +		case 1:
> > +			FPRINTF(stdout, "pesto ");
> > +			FPRINTF(stdout, VERSION_BLOB);
> > +			exit(EXIT_SUCCESS);
> > +		default:
> > +			usage(argv[0], stderr, EXIT_FAILURE);
> > +		}
> > +	} while (optname != -1);
> > +
> > +	if (argc - optind != 1)
> > +		usage(argv[0], stderr, EXIT_FAILURE);
> >  
> >  	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
> >  		die("Failed to create AF_UNIX socket: %s", strerror(errno));
> >  
> > -	ret = snprintf(a.sun_path, sizeof(a.sun_path), "%s", argv[1]);
> > +	ret = snprintf(a.sun_path, sizeof(a.sun_path), "%s", argv[optind]);
> >  	if (ret <= 0 || ret >= (int)sizeof(a.sun_path))
> > -		die("Invalid socket path \"%s\"", argv[1]);
> > +		die("Invalid socket path \"%s\"", argv[optind]);
> >  
> >  	ret = connect(s, (struct sockaddr *)&a, sizeof(a));
> >  	if (ret < 0) {
> > @@ -87,6 +146,8 @@ int main(int argc, char **argv)
> >  		    a.sun_path, strerror(errno));
> >  	}
> >  
> > +	debug("Connected to passt/pasta control socket");
> > +
> >  	ret = read_all_buf(s, &hello, sizeof(hello));
> >  	if (ret < 0)
> >  		die("Couldn't read server greeting: %s", strerror(errno));
> > @@ -96,6 +157,8 @@ int main(int argc, char **argv)
> >  
> >  	s_version = ntohl(hello.version);
> >  
> > +	debug("Server protocol version: %"PRIu32, s_version);
> > +
> >  	if (s_version > PESTO_PROTOCOL_VERSION) {
> >  		die("Unknown server protocol version %"PRIu32" > %"PRIu32"\n",
> >  		    s_version, PESTO_PROTOCOL_VERSION);
> > diff --git a/util.h b/util.h
> > index e7993f4d..d7c397f6 100644
> > --- a/util.h
> > +++ b/util.h
> > @@ -21,14 +21,6 @@
> >  
> >  #include "log.h"
> >  
> > -#define VERSION_BLOB							       \
> > -	VERSION "\n"							       \
> > -	"Copyright Red Hat\n"						       \
> > -	"GNU General Public License, version 2 or later\n"		       \
> > -	"  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>\n"	       \
> > -	"This is free software: you are free to change and redistribute it.\n" \
> > -	"There is NO WARRANTY, to the extent permitted by law.\n\n"
> > -
> >  #ifndef SECCOMP_RET_KILL_PROCESS
> >  #define SECCOMP_RET_KILL_PROCESS	SECCOMP_RET_KILL
> >  #endif
> 
> -- 
> Stefano
> 

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

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

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

* Re: [PATCH v3 15/25] pesto: Expose list of pifs to pesto
  2026-03-25  0:56   ` Stefano Brivio
@ 2026-03-25  4:34     ` David Gibson
  2026-03-25  8:18       ` Stefano Brivio
  0 siblings, 1 reply; 49+ messages in thread
From: David Gibson @ 2026-03-25  4:34 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev

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

On Wed, Mar 25, 2026 at 01:56:22AM +0100, Stefano Brivio wrote:
> On Mon, 23 Mar 2026 18:37:22 +1100
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > Extend the dynamic update protocol to expose the pif indices and names
> > from a running passt/pasta to the pesto tool.  pesto records that data
> > and (for now) prints it out.
> > 
> > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > ---
> >  conf.c      | 38 +++++++++++++++++++++
> >  pesto.c     | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
> >  serialise.c | 21 ++++++++++++
> >  serialise.h |  3 ++
> >  4 files changed, 159 insertions(+), 1 deletion(-)
> > 
> > diff --git a/conf.c b/conf.c
> > index 7b960fe9..b878db94 100644
> > --- a/conf.c
> > +++ b/conf.c
> > @@ -2303,6 +2303,41 @@ void conf(struct ctx *c, int argc, char **argv)
> >  		conf_print(c);
> >  }
> >  
> > +/**
> > + * conf_send_pifs() - Send list of pifs to dynamic update client (pesto)
> > + * @c:		Execution context
> > + * @fd:		Socket to the client
> > + *
> > + * Return: 0 on success, -1 on failure
> > + */
> > +static int conf_send_pifs(const struct ctx *c, int fd)
> > +{
> > +	uint32_t num = 0;
> > +	unsigned pif;
> > +
> > +	/* First count the number of pifs with tables */
> > +	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> > +		if (c->fwd[pif])
> > +			num++;
> > +	}
> > +
> > +	if (write_u32(fd, num))
> > +		return -1;
> > +
> > +	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> > +		if (!c->fwd[pif])
> > +			continue;
> > +
> > +		if (write_u8(fd, pif))
> > +			return -1;
> > +
> > +		if (write_str(fd, pif_name(pif)) < 0)
> > +			return -1;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> >  /**
> >   * conf_listen_handler() - Handle events on configuration listening socket
> >   * @c:		Execution context
> > @@ -2359,6 +2394,9 @@ void conf_listen_handler(struct ctx *c, uint32_t events)
> >  "Warning: Using experimental unsupported configuration protocol");
> >  	}
> >  
> > +	if (conf_send_pifs(c, fd) < 0)
> > +		goto fail;
> > +
> >  	return;
> >  
> >  fail:
> > diff --git a/pesto.c b/pesto.c
> > index f8c9e01d..ed4f2aab 100644
> > --- a/pesto.c
> > +++ b/pesto.c
> > @@ -36,6 +36,8 @@
> >  #include "serialise.h"
> >  #include "pesto.h"
> >  
> > +static int verbosity = 1;
> > +
> >  #define die(...)							\
> >  	do {								\
> >  		FPRINTF(stderr, __VA_ARGS__);				\
> > @@ -51,6 +53,19 @@
> >  		}							\
> >  	} while (0)
> >  
> > +/**
> > + * xmalloc() - Allocate memory, with fatal error on failure
> > + * @size:	Number of bytes to allocate
> > + */
> > +static void *xmalloc(size_t size)
> > +{
> > +	void *p = malloc(size);
> 
> I would really really prefer to skip this unless it's absolutely
> necessary, for a number of reasons:
> 
> 1. this thing is talking to passt which talks to untrusted workloads,
>    and there's a risk of arbitrary code execution, which implies the
>    possibility of specially crafted messages that might make us
>    allocate:
>
>    a. in specific regions and build a heap spraying attack based on that
> 
>    b. a lot, and cause a denial of service

Eh, I suppose.

>    ...and, while this process should be short lived, we can't assume
>    that, if somebody achieves arbitrary code execution in passt and
>    malicious messages, because we risk looping on rules, or having
>    passt send us information very slowly (minutes / hours), etc.

...ah, those are good points.

> 
> 2. besides, we might want to add a "daemon" mode one day, and then it's
>    not short lived anymore, which opens the door to leaks and
>    double-frees (adding to a. and b. above)

I really don't see any reason we'd want that, but I guess that's
irrelevant given that previous paragraph.

> 
> 3. we might need to reuse functions from passt and not notice right
>    away that they call this xmalloc(), or have to eventually adapt them
>    anyway

I mean.. when we test the path in question, we should hit the seccomp
filter, which should make it pretty obvious.

> 4. consistency, in messaging ("passt doesn't allocate dynamic memory",
>    without caveats) and for auditing reasons

Yeah, fair.

> 
> Strings can be 32 bytes, or 1024 if we want to splurge. Unless we need
> to pile a lot of them on the stack (but it's not the case in pesto) a
> malloc() doesn't really bring any advantage compared to static buffers
> here and there. It's not megabytes.

So, you've convinced me in principle, but I'm not putting this at the
top of my priorities.  Using malloc() makes things a bit easier while
we're playing around with the protocol a bunch.  Once we look
reasonably close to a v1 protocol, I'll do the malloc() removal.

> 
> > +
> > +	if (!p)
> > +		die("Memory allocation failure");
> > +	return p;
> > +}
> > +
> >  /**
> >   * usage() - Print usage, exit with given status code
> >   * @name:	Executable name
> > @@ -69,6 +84,83 @@ static void usage(const char *name, FILE *f, int status)
> >  	exit(status);
> >  }
> >  
> > +/**
> > + * pesto_recv_str() - Receive a string from passt/pasta
> > + * @fd:		Control socket
> > + *
> > + * Return: pointer to malloc()ed string
> > + */
> > +static const char *pesto_recv_str(int fd)
> > +{
> > +	uint32_t len;
> > +	char *buf;
> > +
> > +	if (read_u32(fd, &len) < 0)
> > +		die("Error reading from control socket");
> > +
> > +	buf = xmalloc(len);
> > +	if (read_all_buf(fd, buf, len) < 0)
> > +		die("Error reading from control socket");
> > +
> > +	return buf;
> > +}
> > +
> > +struct pif_state {
> > +	uint8_t pif;
> > +	const char *name;
> > +};
> > +
> > +struct conf_state {
> 
> More as a to-do list item rather than as a review comment: we need
> documentation for those structs.

Ah, yes.

> 
> > +	uint32_t npifs;
> > +	struct pif_state pif[];
> > +};
> > +
> > +/**
> > + * pesto_read_pifs() - Read pif names and IDs from passt/pasta
> > + * @fd:		Control socket
> > + */
> > +static const struct conf_state *pesto_read_pifs(int fd)
> > +{
> > +	uint32_t num;
> > +	struct conf_state *state;
> > +	unsigned i;
> > +
> > +	if (read_u32(fd, &num) < 0)
> > +		die("Error reading from control socket");
> > +
> > +	debug("Receiving %"PRIu32" interface names", num);
> > +
> > +	state = xmalloc(sizeof(*state) + num * sizeof(struct pif_state));
> > +	state->npifs = num;
> > +
> > +	for (i = 0; i < num; i++) {
> > +		struct pif_state *ps = &state->pif[i];
> > +
> > +		if (read_u8(fd, &ps->pif) < 0)
> > +			die("Error reading from control socket");
> > +		ps->name = pesto_recv_str(fd);
> > +
> > +		debug("%u: %s", ps->pif, ps->name);
> > +	}
> > +
> > +	return state;
> > +}
> > +
> > +/**
> > + * show_state() - Show current rule state obtained from passt/pasta
> > + * @pifs:	PIF name information
> > + */
> > +static void show_state(const struct conf_state *state)
> > +{
> > +	unsigned i;
> > +
> > +	for (i = 0; i < state->npifs; i++) {
> > +		const struct pif_state *ps = &state->pif[i];
> > +		printf("Forwarding rules for %s interface\n", ps->name);
> > +		printf("\tTBD\n");
> > +	}
> > +}
> > +
> >  /**
> >   * main() - Entry point and whole program with loop
> >   * @argc:	Argument count
> > @@ -91,12 +183,12 @@ int main(int argc, char **argv)
> >  		{ 0 },
> >  	};
> >  	struct sockaddr_un a = { AF_UNIX, "" };
> > +	const struct conf_state *state;
> >  	const char *optstring = "vh";
> >  	struct pesto_hello hello;
> >  	struct sock_fprog prog;
> >  	int optname, ret, s;
> >  	uint32_t s_version;
> > -	int verbosity = 1;
> >  
> >  	prog.len = (unsigned short)sizeof(filter_pesto) /
> >  				   sizeof(filter_pesto[0]);
> > @@ -172,5 +264,9 @@ int main(int argc, char **argv)
> >  "Warning: Using experimental protocol version, client and server must match\n");
> >  	}
> >  
> > +	state = pesto_read_pifs(s);
> > +
> > +	show_state(state);
> > +
> >  	exit(0);
> >  }
> > diff --git a/serialise.c b/serialise.c
> > index e3ea86e3..94c34d3c 100644
> > --- a/serialise.c
> > +++ b/serialise.c
> > @@ -19,6 +19,7 @@
> >  #include <endian.h>
> >  #include <errno.h>
> >  #include <stdint.h>
> > +#include <string.h>
> >  #include <unistd.h>
> >  
> >  #include "serialise.h"
> > @@ -121,6 +122,26 @@ int write_all_buf(int fd, const void *buf, size_t len)
> >  		return write_all_buf(fd, &beval, sizeof(beval));	\
> >  	}
> >  
> > +#define be8toh(x)	(x)
> > +#define htobe8(x)	(x)
> > +
> > +SERIALISE_UINT(8)
> >  SERIALISE_UINT(32)
> >  
> >  #undef SERIALISE_UNIT
> > +
> > +/**
> > + * write_str() - Write a string to an fd in length/value format
> > + * @fd:		Socket to the client
> > + * @s:		String to send
> > + *
> > + * Return: 0 on success, -1 on error
> > + */
> > +int write_str(int fd, const char *s)
> > +{
> > +	uint32_t len = strlen(s) + 1; /* Include \0 */
> > +
> > +	if (write_u32(fd, len) < 0)
> > +		return -1;
> 
> Even if we don't need to allocate here, I would feel more comfortable
> if we passed fixed-size strings only to passt to have a slightly lower
> risk of buffer overflows, in general.

That's fair.  I'd basically already decided to move to fixed length
pif names, just haven't implemented it yet.

> > +	return write_all_buf(fd, s, len);
> > +}
> > diff --git a/serialise.h b/serialise.h
> > index 0a4ed086..7ef35786 100644
> > --- a/serialise.h
> > +++ b/serialise.h
> > @@ -16,6 +16,9 @@ int write_all_buf(int fd, const void *buf, size_t len);
> >  	int read_u##bits(int fd, uint##bits##_t *val);			\
> >  	int write_u##bits(int fd, uint##bits##_t val);
> >  
> > +SERIALISE_UINT_DECL(8)
> >  SERIALISE_UINT_DECL(32)
> >  
> > +int write_str(int fd, const char *s);
> > +
> >  #endif /* _SERIALISE_H */
> 
> -- 
> Stefano
> 

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

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

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

* Re: [PATCH v3 20/25] fwd_rule: Move forwarding rule text formatting to common code
  2026-03-25  0:56   ` Stefano Brivio
@ 2026-03-25  4:42     ` David Gibson
  2026-03-25  8:18       ` Stefano Brivio
  0 siblings, 1 reply; 49+ messages in thread
From: David Gibson @ 2026-03-25  4:42 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev

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

On Wed, Mar 25, 2026 at 01:56:34AM +0100, Stefano Brivio wrote:
> On Mon, 23 Mar 2026 18:37:27 +1100
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > Move the logic for formatting forwarding rules into strings from
> > fwd_rules_print() into fwd_rule.c where it can be shared with pesto.
> > We also make the function explicitly construct a string, rather than
> > directly printing with info(), for greater flexibility.
> > 
> > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > ---
> >  Makefile   | 11 ++++----
> >  fwd.c      | 40 +++---------------------------
> >  fwd_rule.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
> >  fwd_rule.h | 11 ++++++++
> >  4 files changed, 94 insertions(+), 41 deletions(-)
> >  create mode 100644 fwd_rule.c
> > 
> > diff --git a/Makefile b/Makefile
> > index bc325482..44c396e7 100644
> > --- a/Makefile
> > +++ b/Makefile
> > @@ -38,13 +38,14 @@ FLAGS += -DVERSION=\"$(VERSION)\"
> >  FLAGS += -DDUAL_STACK_SOCKETS=$(DUAL_STACK_SOCKETS)
> >  
> >  PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
> > -	flow.c fwd.c icmp.c igmp.c inany.c iov.c ip.c isolation.c lineread.c \
> > -	log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c pasta.c pcap.c \
> > -	pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c tcp_splice.c tcp_vu.c \
> > -	udp.c udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
> > +	flow.c fwd.c fwd_rule.c icmp.c igmp.c inany.c iov.c ip.c isolation.c \
> > +	lineread.c log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c \
> > +	pasta.c pcap.c pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c \
> > +	tcp_splice.c tcp_vu.c udp.c udp_flow.c udp_vu.c util.c vhost_user.c \
> > +	virtio.c vu_common.c
> >  QRAP_SRCS = qrap.c
> >  PASST_REPAIR_SRCS = passt-repair.c
> > -PESTO_SRCS = pesto.c inany.c ip.c serialise.c
> > +PESTO_SRCS = pesto.c fwd_rule.c inany.c ip.c serialise.c
> >  SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
> >  
> >  MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
> > diff --git a/fwd.c b/fwd.c
> > index a32d0a20..20409c62 100644
> > --- a/fwd.c
> > +++ b/fwd.c
> > @@ -304,20 +304,6 @@ parse_err:
> >  	warn("Unable to parse %s", PORT_RANGE_SYSCTL);
> >  }
> >  
> > -/**
> > - * fwd_rule_addr() - Return match address for a rule
> > - * @rule:	Forwarding rule
> > - *
> > - * Return: matching address for rule, NULL if it matches all addresses
> > - */
> > -static const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
> > -{
> > -	if (rule->flags & FWD_DUAL_STACK_ANY)
> > -		return NULL;
> > -
> > -	return &rule->addr;
> > -}
> > -
> >  /**
> >   * fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap
> >   * @map:	Bitmap to update
> > @@ -497,28 +483,10 @@ void fwd_rules_print(const struct fwd_table *fwd)
> >  	unsigned i;
> >  
> >  	for (i = 0; i < fwd->count; i++) {
> > -		const struct fwd_rule *rule = &fwd->rules[i].rule;
> > -		const char *percent = *rule->ifname ? "%" : "";
> > -		const char *weak = "", *scan = "";
> > -		char addr[INANY_ADDRSTRLEN];
> > -
> > -		inany_ntop(fwd_rule_addr(rule), addr, sizeof(addr));
> > -		if (rule->flags & FWD_WEAK)
> > -			weak = " (best effort)";
> > -		if (rule->flags & FWD_SCAN)
> > -			scan = " (auto-scan)";
> > -
> > -		if (rule->first == rule->last) {
> > -			info("    %s [%s]%s%s:%hu  =>  %hu %s%s",
> > -			     ipproto_name(rule->proto), addr, percent,
> > -			     rule->ifname, rule->first, rule->to, weak, scan);
> > -		} else {
> > -			info("    %s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s",
> > -			     ipproto_name(rule->proto), addr, percent,
> > -			     rule->ifname, rule->first, rule->last,
> > -			     rule->to, rule->last - rule->first + rule->to,
> > -			     weak, scan);
> > -		}
> > +		char rulestr[FWD_RULE_STRLEN];
> > +
> > +		info("    %s", fwd_rule_ntop(&fwd->rules[i].rule,
> > +					     rulestr, sizeof(rulestr)));
> >  	}
> >  }
> >  
> > diff --git a/fwd_rule.c b/fwd_rule.c
> > new file mode 100644
> > index 00000000..dfbdf683
> > --- /dev/null
> > +++ b/fwd_rule.c
> > @@ -0,0 +1,73 @@
> > +// SPDX-License-Identifier: GPL-2.0-or-later
> > +
> > +/* PASST - Plug A Simple Socket Transport
> > + *  for qemu/UNIX domain socket mode
> > + *
> > + * PASTA - Pack A Subtle Tap Abstraction
> > + *  for network namespace/tap device mode
> > + *
> > + * PESTO - Programmable Extensible Socket Translation Orchestrator
> > + *  front-end for passt(1) and pasta(1) forwarding configuration
> > + *
> > + * fwd_rule.c - Helpers for working with forwarding rule specifications
> > + *
> > + * Copyright Red Hat
> > + * Author: David Gibson <david@gibson.dropbear.id.au>
> > + */
> > +
> > +#include <stdio.h>
> > +
> > +#include "fwd_rule.h"
> > +
> > +/**
> > + * fwd_rule_addr() - Return match address for a rule
> > + * @rule:	Forwarding rule
> > + *
> > + * Return: matching address for rule, NULL if it matches all addresses
> > + */
> > +const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
> > +{
> > +	if (rule->flags & FWD_DUAL_STACK_ANY)
> > +		return NULL;
> > +
> > +	return &rule->addr;
> > +}
> > +
> > +/**
> > + * fwd_rule_ntop() - Format forwarding rule as a string
> > + * @rule:	Rule to format
> > + * @dst:	Buffer to store output (should have FWD_RULE_STRLEN bytes)
> > + * @size:	Size of @dst
> > + */
> > +const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size)
> > +{
> > +	const char *percent = *rule->ifname ? "%" : "";
> > +	const char *weak = "", *scan = "";
> > +	char addr[INANY_ADDRSTRLEN];
> > +	int len;
> > +
> > +	inany_ntop(fwd_rule_addr(rule), addr, sizeof(addr));
> > +	if (rule->flags & FWD_WEAK)
> > +		weak = " (best effort)";
> > +	if (rule->flags & FWD_SCAN)
> > +		scan = " (auto-scan)";
> > +
> > +	if (rule->first == rule->last) {
> > +		len = snprintf(dst, size,
> > +			       "%s [%s]%s%s:%hu  =>  %hu %s%s",
> > +			       ipproto_name(rule->proto), addr, percent,
> > +			       rule->ifname, rule->first, rule->to, weak, scan);
> > +	} else {
> > +		in_port_t tolast = rule->last - rule->first + rule->to;
> > +		len = snprintf(dst, size,
> > +			       "%s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s",
> > +			       ipproto_name(rule->proto), addr, percent,
> > +			       rule->ifname, rule->first, rule->last,
> > +			       rule->to, tolast, weak, scan);
> > +	}
> > +
> > +	if (len < 0 || (size_t)len >= size)
> > +		return NULL;
> > +
> > +	return dst;
> > +}
> > diff --git a/fwd_rule.h b/fwd_rule.h
> > index 84ec5cbe..59db0e95 100644
> > --- a/fwd_rule.h
> > +++ b/fwd_rule.h
> > @@ -42,4 +42,15 @@ struct fwd_rule {
> >  	uint8_t flags;
> >  };
> >  
> > +const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
> > +
> > +#define FWD_RULE_STRLEN					    \
> > +	(IPPROTO_STRLEN - 1				    \
> > +	 + INANY_ADDRSTRLEN - 1				    \
> > +	 + IFNAMSIZ - 1					    \
> > +	 + sizeof(" (best effort)") - 1			    \
> > +	 + sizeof(" (auto-scan)") - 1			    \
> > +	 + 15)
> 
> I'm not quite sure about the reason for the 15 here, is there some
> constant we can use perhaps?

It's the spacing and punctuation around the other displayed
parameters, roughly the number of non-format characters in
	"%s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s"
(plus one for the \0).  I don't love it either, but I wasn't sure how
to make it less obscure. Is
	sizeof(" []%:-  =>  - ")
an improvement?  I'm not sure.

Oh... that made me realise I'm missing 4 * (UINT16_STRLEN - 1).  I've
fixed that now.

> 
> > +const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size);
> > +
> >  #endif /* FWD_RULE_H */
> 
> -- 
> Stefano
> 

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

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

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

* Re: [PATCH v3 21/25] pesto: Read current ruleset from passt/pasta and display it
  2026-03-25  0:56   ` Stefano Brivio
@ 2026-03-25  4:43     ` David Gibson
  0 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-25  4:43 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev

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

On Wed, Mar 25, 2026 at 01:56:39AM +0100, Stefano Brivio wrote:
> On Mon, 23 Mar 2026 18:37:28 +1100
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > Implement serialisation of our current forwarding rules in conf.c,
> > deserialising it to display in the pesto client.
> > 
> > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > ---
> >  conf.c     | 44 +++++++++++++++++++++++++++++++
> >  fwd_rule.c | 40 ++++++++++++++++++++++++++++
> >  fwd_rule.h |  3 +++
> >  pesto.c    | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++---
> >  4 files changed, 160 insertions(+), 4 deletions(-)
> > 
> > diff --git a/conf.c b/conf.c
> > index b878db94..7f311914 100644
> > --- a/conf.c
> > +++ b/conf.c
> > @@ -2338,6 +2338,47 @@ static int conf_send_pifs(const struct ctx *c, int fd)
> >  	return 0;
> >  }
> >  
> > +/**
> > + * conf_send_rules() - Send current forwarding rules to dynamic update client (pesto)
> 
> What about:
> 
>  * conf_send_rules() - Send forwarding rules to configuration client (pesto)

Done.

> > + * @c:		Execution context
> > + * @fd:		Socket to the client
> > + *
> > + * Return: 0 on success, -1 on failure
> > + */
> > +static int conf_send_rules(const struct ctx *c, int fd)
> > +{
> > +	unsigned pif;
> > +
> > +	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> > +		const struct fwd_table *fwd = c->fwd[pif];
> > +		unsigned i;
> > +
> > +		if (!fwd)
> > +			continue;
> > +
> > +		assert(pif);
> > +
> > +		/* PIF id */
> > +		if (write_u8(fd, pif))
> > +			return -1;
> > +
> > +		/* Number of rules */
> > +		if (write_u32(fd, fwd->count))
> > +			return -1;
> > +
> > +		for (i = 0; i < fwd->count; i++) {
> > +			if (fwd_rule_write(fd, &fwd->rules[i].rule))
> > +				return -1;
> > +		}
> > +	}
> > +
> > +	/* Write 0 PIF id to finish */
> > +	if (write_u8(fd, 0))
> 
> We have PIF_NONE, shouldn't we use that to make it obvious that it's an
> invalid value?

Done.

> 
> > +		return -1;
> > +
> > +	return 0;
> > +}
> >
> > [...]
> 
> I stopped reviewing here, the next patches in my series are anyway mine
> and after those I guess it doesn't make sense, yet, that I review in
> fine detail, as we'll probably need to change a bunch of things.

Understood.

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

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

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

* Re: [PATCH v3 00/25] RFC: Read-only dynamic update implementation
  2026-03-25  1:00   ` Stefano Brivio
@ 2026-03-25  4:44     ` David Gibson
  0 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-25  4:44 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev

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

On Wed, Mar 25, 2026 at 02:00:23AM +0100, Stefano Brivio wrote:
> On Wed, 25 Mar 2026 01:56:51 +0100
> Stefano Brivio <sbrivio@redhat.com> wrote:
> 
> > On Mon, 23 Mar 2026 18:37:07 +1100
> > David Gibson <david@gibson.dropbear.id.au> wrote:
> > 
> > > Here's a new draft of dynamic updates.  This now can successfully
> > > update rules, though I've not tested it very extensively.  Essentially
> > > this is just barely enough to work, it still could do with rather a
> > > lot of polish.
> > > 
> > > Patches 1..12/22 are preliminary reworks that make moderate sense even
> > > without pesto - feel free to apply if you're happy with them.  
> > 
> > I'm applying (in a moment, tests are running now):
> > 
> > - 1/25, 2/25,
> > - not 3/25 (comments pending)
> > - not 4/25 (depends on 3/25)
> > - 5/25, 6/25,
> > - not 7/25 (Coverity Scan warnings),
> > - not 8/25 (comments pending),
> > - 9/25, 10/25, 11/25, 12/25.
> 
> Never mind, most of the "TCP/IPv4: ns to host" tests fail, probably I
> missed something while cherry-picking patches from this series. I guess
> a re-spin up to 12/25 would be more convenient in this case.

Will do.

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

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

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

* Re: [PATCH v3 15/25] pesto: Expose list of pifs to pesto
  2026-03-25  4:34     ` David Gibson
@ 2026-03-25  8:18       ` Stefano Brivio
  2026-03-25  8:31         ` David Gibson
  0 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  8:18 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Wed, 25 Mar 2026 15:34:02 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> On Wed, Mar 25, 2026 at 01:56:22AM +0100, Stefano Brivio wrote:
>
> [...]
>
> So, you've convinced me in principle, but I'm not putting this at the
> top of my priorities.  Using malloc() makes things a bit easier while
> we're playing around with the protocol a bunch.  Once we look
> reasonably close to a v1 protocol, I'll do the malloc() removal.

Sure, it makes sense, it's definitely not needed right now. I guess it
will mostly be a matter of passing a number to the _str() functions and
share constants between client and server.

-- 
Stefano


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

* Re: [PATCH v3 20/25] fwd_rule: Move forwarding rule text formatting to common code
  2026-03-25  4:42     ` David Gibson
@ 2026-03-25  8:18       ` Stefano Brivio
  2026-03-25 23:54         ` David Gibson
  0 siblings, 1 reply; 49+ messages in thread
From: Stefano Brivio @ 2026-03-25  8:18 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev

On Wed, 25 Mar 2026 15:42:00 +1100
David Gibson <david@gibson.dropbear.id.au> wrote:

> On Wed, Mar 25, 2026 at 01:56:34AM +0100, Stefano Brivio wrote:
> > On Mon, 23 Mar 2026 18:37:27 +1100
> > David Gibson <david@gibson.dropbear.id.au> wrote:
> >   
> > > Move the logic for formatting forwarding rules into strings from
> > > fwd_rules_print() into fwd_rule.c where it can be shared with pesto.
> > > We also make the function explicitly construct a string, rather than
> > > directly printing with info(), for greater flexibility.
> > > 
> > > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > > ---
> > >  Makefile   | 11 ++++----
> > >  fwd.c      | 40 +++---------------------------
> > >  fwd_rule.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
> > >  fwd_rule.h | 11 ++++++++
> > >  4 files changed, 94 insertions(+), 41 deletions(-)
> > >  create mode 100644 fwd_rule.c
> > > 
> > > diff --git a/Makefile b/Makefile
> > > index bc325482..44c396e7 100644
> > > --- a/Makefile
> > > +++ b/Makefile
> > > @@ -38,13 +38,14 @@ FLAGS += -DVERSION=\"$(VERSION)\"
> > >  FLAGS += -DDUAL_STACK_SOCKETS=$(DUAL_STACK_SOCKETS)
> > >  
> > >  PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
> > > -	flow.c fwd.c icmp.c igmp.c inany.c iov.c ip.c isolation.c lineread.c \
> > > -	log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c pasta.c pcap.c \
> > > -	pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c tcp_splice.c tcp_vu.c \
> > > -	udp.c udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
> > > +	flow.c fwd.c fwd_rule.c icmp.c igmp.c inany.c iov.c ip.c isolation.c \
> > > +	lineread.c log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c \
> > > +	pasta.c pcap.c pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c \
> > > +	tcp_splice.c tcp_vu.c udp.c udp_flow.c udp_vu.c util.c vhost_user.c \
> > > +	virtio.c vu_common.c
> > >  QRAP_SRCS = qrap.c
> > >  PASST_REPAIR_SRCS = passt-repair.c
> > > -PESTO_SRCS = pesto.c inany.c ip.c serialise.c
> > > +PESTO_SRCS = pesto.c fwd_rule.c inany.c ip.c serialise.c
> > >  SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
> > >  
> > >  MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
> > > diff --git a/fwd.c b/fwd.c
> > > index a32d0a20..20409c62 100644
> > > --- a/fwd.c
> > > +++ b/fwd.c
> > > @@ -304,20 +304,6 @@ parse_err:
> > >  	warn("Unable to parse %s", PORT_RANGE_SYSCTL);
> > >  }
> > >  
> > > -/**
> > > - * fwd_rule_addr() - Return match address for a rule
> > > - * @rule:	Forwarding rule
> > > - *
> > > - * Return: matching address for rule, NULL if it matches all addresses
> > > - */
> > > -static const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
> > > -{
> > > -	if (rule->flags & FWD_DUAL_STACK_ANY)
> > > -		return NULL;
> > > -
> > > -	return &rule->addr;
> > > -}
> > > -
> > >  /**
> > >   * fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap
> > >   * @map:	Bitmap to update
> > > @@ -497,28 +483,10 @@ void fwd_rules_print(const struct fwd_table *fwd)
> > >  	unsigned i;
> > >  
> > >  	for (i = 0; i < fwd->count; i++) {
> > > -		const struct fwd_rule *rule = &fwd->rules[i].rule;
> > > -		const char *percent = *rule->ifname ? "%" : "";
> > > -		const char *weak = "", *scan = "";
> > > -		char addr[INANY_ADDRSTRLEN];
> > > -
> > > -		inany_ntop(fwd_rule_addr(rule), addr, sizeof(addr));
> > > -		if (rule->flags & FWD_WEAK)
> > > -			weak = " (best effort)";
> > > -		if (rule->flags & FWD_SCAN)
> > > -			scan = " (auto-scan)";
> > > -
> > > -		if (rule->first == rule->last) {
> > > -			info("    %s [%s]%s%s:%hu  =>  %hu %s%s",
> > > -			     ipproto_name(rule->proto), addr, percent,
> > > -			     rule->ifname, rule->first, rule->to, weak, scan);
> > > -		} else {
> > > -			info("    %s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s",
> > > -			     ipproto_name(rule->proto), addr, percent,
> > > -			     rule->ifname, rule->first, rule->last,
> > > -			     rule->to, rule->last - rule->first + rule->to,
> > > -			     weak, scan);
> > > -		}
> > > +		char rulestr[FWD_RULE_STRLEN];
> > > +
> > > +		info("    %s", fwd_rule_ntop(&fwd->rules[i].rule,
> > > +					     rulestr, sizeof(rulestr)));
> > >  	}
> > >  }
> > >  
> > > diff --git a/fwd_rule.c b/fwd_rule.c
> > > new file mode 100644
> > > index 00000000..dfbdf683
> > > --- /dev/null
> > > +++ b/fwd_rule.c
> > > @@ -0,0 +1,73 @@
> > > +// SPDX-License-Identifier: GPL-2.0-or-later
> > > +
> > > +/* PASST - Plug A Simple Socket Transport
> > > + *  for qemu/UNIX domain socket mode
> > > + *
> > > + * PASTA - Pack A Subtle Tap Abstraction
> > > + *  for network namespace/tap device mode
> > > + *
> > > + * PESTO - Programmable Extensible Socket Translation Orchestrator
> > > + *  front-end for passt(1) and pasta(1) forwarding configuration
> > > + *
> > > + * fwd_rule.c - Helpers for working with forwarding rule specifications
> > > + *
> > > + * Copyright Red Hat
> > > + * Author: David Gibson <david@gibson.dropbear.id.au>
> > > + */
> > > +
> > > +#include <stdio.h>
> > > +
> > > +#include "fwd_rule.h"
> > > +
> > > +/**
> > > + * fwd_rule_addr() - Return match address for a rule
> > > + * @rule:	Forwarding rule
> > > + *
> > > + * Return: matching address for rule, NULL if it matches all addresses
> > > + */
> > > +const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
> > > +{
> > > +	if (rule->flags & FWD_DUAL_STACK_ANY)
> > > +		return NULL;
> > > +
> > > +	return &rule->addr;
> > > +}
> > > +
> > > +/**
> > > + * fwd_rule_ntop() - Format forwarding rule as a string
> > > + * @rule:	Rule to format
> > > + * @dst:	Buffer to store output (should have FWD_RULE_STRLEN bytes)
> > > + * @size:	Size of @dst
> > > + */
> > > +const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size)
> > > +{
> > > +	const char *percent = *rule->ifname ? "%" : "";
> > > +	const char *weak = "", *scan = "";
> > > +	char addr[INANY_ADDRSTRLEN];
> > > +	int len;
> > > +
> > > +	inany_ntop(fwd_rule_addr(rule), addr, sizeof(addr));
> > > +	if (rule->flags & FWD_WEAK)
> > > +		weak = " (best effort)";
> > > +	if (rule->flags & FWD_SCAN)
> > > +		scan = " (auto-scan)";
> > > +
> > > +	if (rule->first == rule->last) {
> > > +		len = snprintf(dst, size,
> > > +			       "%s [%s]%s%s:%hu  =>  %hu %s%s",
> > > +			       ipproto_name(rule->proto), addr, percent,
> > > +			       rule->ifname, rule->first, rule->to, weak, scan);
> > > +	} else {
> > > +		in_port_t tolast = rule->last - rule->first + rule->to;
> > > +		len = snprintf(dst, size,
> > > +			       "%s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s",
> > > +			       ipproto_name(rule->proto), addr, percent,
> > > +			       rule->ifname, rule->first, rule->last,
> > > +			       rule->to, tolast, weak, scan);
> > > +	}
> > > +
> > > +	if (len < 0 || (size_t)len >= size)
> > > +		return NULL;
> > > +
> > > +	return dst;
> > > +}
> > > diff --git a/fwd_rule.h b/fwd_rule.h
> > > index 84ec5cbe..59db0e95 100644
> > > --- a/fwd_rule.h
> > > +++ b/fwd_rule.h
> > > @@ -42,4 +42,15 @@ struct fwd_rule {
> > >  	uint8_t flags;
> > >  };
> > >  
> > > +const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
> > > +
> > > +#define FWD_RULE_STRLEN					    \
> > > +	(IPPROTO_STRLEN - 1				    \
> > > +	 + INANY_ADDRSTRLEN - 1				    \
> > > +	 + IFNAMSIZ - 1					    \
> > > +	 + sizeof(" (best effort)") - 1			    \
> > > +	 + sizeof(" (auto-scan)") - 1			    \
> > > +	 + 15)  
> > 
> > I'm not quite sure about the reason for the 15 here, is there some
> > constant we can use perhaps?  
> 
> It's the spacing and punctuation around the other displayed
> parameters, roughly the number of non-format characters in
> 	"%s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s"
> (plus one for the \0).  I don't love it either, but I wasn't sure how
> to make it less obscure. Is
> 	sizeof(" []%:-  =>  - ")
> an improvement?  I'm not sure.

I think it definitely is. I can't see a better explanation than that
sizeof(), and it also looks like a good way to double check the number
of additional characters.

If we want to do overdo with robustness, we could perhaps think of
separately defining the format string and using it here for the
calculation as well, which makes future (albeit unlikely) updates of
the format string safer.

But, in that case, dropping characters for each format specifier is
something we would have to do manually anyway... or not do at all and
rely on the size of the format string as it is as an upper bound,
perhaps.

-- 
Stefano


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

* Re: [PATCH v3 15/25] pesto: Expose list of pifs to pesto
  2026-03-25  8:18       ` Stefano Brivio
@ 2026-03-25  8:31         ` David Gibson
  0 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-25  8:31 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev

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

On Wed, Mar 25, 2026 at 09:18:00AM +0100, Stefano Brivio wrote:
> On Wed, 25 Mar 2026 15:34:02 +1100
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > On Wed, Mar 25, 2026 at 01:56:22AM +0100, Stefano Brivio wrote:
> >
> > [...]
> >
> > So, you've convinced me in principle, but I'm not putting this at the
> > top of my priorities.  Using malloc() makes things a bit easier while
> > we're playing around with the protocol a bunch.  Once we look
> > reasonably close to a v1 protocol, I'll do the malloc() removal.
> 
> Sure, it makes sense, it's definitely not needed right now. I guess it
> will mostly be a matter of passing a number to the _str() functions and
> share constants between client and server.

I've actually already eliminated the _str() functions in my latest
draft.  The less convenient case is allocating the arrays of pifs and
rules, I'll tackle that when I get to it.

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

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

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

* Re: [PATCH v3 20/25] fwd_rule: Move forwarding rule text formatting to common code
  2026-03-25  8:18       ` Stefano Brivio
@ 2026-03-25 23:54         ` David Gibson
  0 siblings, 0 replies; 49+ messages in thread
From: David Gibson @ 2026-03-25 23:54 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev

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

On Wed, Mar 25, 2026 at 09:18:07AM +0100, Stefano Brivio wrote:
> On Wed, 25 Mar 2026 15:42:00 +1100
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > On Wed, Mar 25, 2026 at 01:56:34AM +0100, Stefano Brivio wrote:
> > > On Mon, 23 Mar 2026 18:37:27 +1100
> > > David Gibson <david@gibson.dropbear.id.au> wrote:
> > >   
> > > > Move the logic for formatting forwarding rules into strings from
> > > > fwd_rules_print() into fwd_rule.c where it can be shared with pesto.
> > > > We also make the function explicitly construct a string, rather than
> > > > directly printing with info(), for greater flexibility.
> > > > 
> > > > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > > > ---
> > > >  Makefile   | 11 ++++----
> > > >  fwd.c      | 40 +++---------------------------
> > > >  fwd_rule.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
> > > >  fwd_rule.h | 11 ++++++++
> > > >  4 files changed, 94 insertions(+), 41 deletions(-)
> > > >  create mode 100644 fwd_rule.c
> > > > 
> > > > diff --git a/Makefile b/Makefile
> > > > index bc325482..44c396e7 100644
> > > > --- a/Makefile
> > > > +++ b/Makefile
> > > > @@ -38,13 +38,14 @@ FLAGS += -DVERSION=\"$(VERSION)\"
> > > >  FLAGS += -DDUAL_STACK_SOCKETS=$(DUAL_STACK_SOCKETS)
> > > >  
> > > >  PASST_SRCS = arch.c arp.c checksum.c conf.c dhcp.c dhcpv6.c epoll_ctl.c \
> > > > -	flow.c fwd.c icmp.c igmp.c inany.c iov.c ip.c isolation.c lineread.c \
> > > > -	log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c pasta.c pcap.c \
> > > > -	pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c tcp_splice.c tcp_vu.c \
> > > > -	udp.c udp_flow.c udp_vu.c util.c vhost_user.c virtio.c vu_common.c
> > > > +	flow.c fwd.c fwd_rule.c icmp.c igmp.c inany.c iov.c ip.c isolation.c \
> > > > +	lineread.c log.c mld.c ndp.c netlink.c migrate.c packet.c passt.c \
> > > > +	pasta.c pcap.c pif.c repair.c serialise.c tap.c tcp.c tcp_buf.c \
> > > > +	tcp_splice.c tcp_vu.c udp.c udp_flow.c udp_vu.c util.c vhost_user.c \
> > > > +	virtio.c vu_common.c
> > > >  QRAP_SRCS = qrap.c
> > > >  PASST_REPAIR_SRCS = passt-repair.c
> > > > -PESTO_SRCS = pesto.c inany.c ip.c serialise.c
> > > > +PESTO_SRCS = pesto.c fwd_rule.c inany.c ip.c serialise.c
> > > >  SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
> > > >  
> > > >  MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
> > > > diff --git a/fwd.c b/fwd.c
> > > > index a32d0a20..20409c62 100644
> > > > --- a/fwd.c
> > > > +++ b/fwd.c
> > > > @@ -304,20 +304,6 @@ parse_err:
> > > >  	warn("Unable to parse %s", PORT_RANGE_SYSCTL);
> > > >  }
> > > >  
> > > > -/**
> > > > - * fwd_rule_addr() - Return match address for a rule
> > > > - * @rule:	Forwarding rule
> > > > - *
> > > > - * Return: matching address for rule, NULL if it matches all addresses
> > > > - */
> > > > -static const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
> > > > -{
> > > > -	if (rule->flags & FWD_DUAL_STACK_ANY)
> > > > -		return NULL;
> > > > -
> > > > -	return &rule->addr;
> > > > -}
> > > > -
> > > >  /**
> > > >   * fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap
> > > >   * @map:	Bitmap to update
> > > > @@ -497,28 +483,10 @@ void fwd_rules_print(const struct fwd_table *fwd)
> > > >  	unsigned i;
> > > >  
> > > >  	for (i = 0; i < fwd->count; i++) {
> > > > -		const struct fwd_rule *rule = &fwd->rules[i].rule;
> > > > -		const char *percent = *rule->ifname ? "%" : "";
> > > > -		const char *weak = "", *scan = "";
> > > > -		char addr[INANY_ADDRSTRLEN];
> > > > -
> > > > -		inany_ntop(fwd_rule_addr(rule), addr, sizeof(addr));
> > > > -		if (rule->flags & FWD_WEAK)
> > > > -			weak = " (best effort)";
> > > > -		if (rule->flags & FWD_SCAN)
> > > > -			scan = " (auto-scan)";
> > > > -
> > > > -		if (rule->first == rule->last) {
> > > > -			info("    %s [%s]%s%s:%hu  =>  %hu %s%s",
> > > > -			     ipproto_name(rule->proto), addr, percent,
> > > > -			     rule->ifname, rule->first, rule->to, weak, scan);
> > > > -		} else {
> > > > -			info("    %s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s",
> > > > -			     ipproto_name(rule->proto), addr, percent,
> > > > -			     rule->ifname, rule->first, rule->last,
> > > > -			     rule->to, rule->last - rule->first + rule->to,
> > > > -			     weak, scan);
> > > > -		}
> > > > +		char rulestr[FWD_RULE_STRLEN];
> > > > +
> > > > +		info("    %s", fwd_rule_ntop(&fwd->rules[i].rule,
> > > > +					     rulestr, sizeof(rulestr)));
> > > >  	}
> > > >  }
> > > >  
> > > > diff --git a/fwd_rule.c b/fwd_rule.c
> > > > new file mode 100644
> > > > index 00000000..dfbdf683
> > > > --- /dev/null
> > > > +++ b/fwd_rule.c
> > > > @@ -0,0 +1,73 @@
> > > > +// SPDX-License-Identifier: GPL-2.0-or-later
> > > > +
> > > > +/* PASST - Plug A Simple Socket Transport
> > > > + *  for qemu/UNIX domain socket mode
> > > > + *
> > > > + * PASTA - Pack A Subtle Tap Abstraction
> > > > + *  for network namespace/tap device mode
> > > > + *
> > > > + * PESTO - Programmable Extensible Socket Translation Orchestrator
> > > > + *  front-end for passt(1) and pasta(1) forwarding configuration
> > > > + *
> > > > + * fwd_rule.c - Helpers for working with forwarding rule specifications
> > > > + *
> > > > + * Copyright Red Hat
> > > > + * Author: David Gibson <david@gibson.dropbear.id.au>
> > > > + */
> > > > +
> > > > +#include <stdio.h>
> > > > +
> > > > +#include "fwd_rule.h"
> > > > +
> > > > +/**
> > > > + * fwd_rule_addr() - Return match address for a rule
> > > > + * @rule:	Forwarding rule
> > > > + *
> > > > + * Return: matching address for rule, NULL if it matches all addresses
> > > > + */
> > > > +const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
> > > > +{
> > > > +	if (rule->flags & FWD_DUAL_STACK_ANY)
> > > > +		return NULL;
> > > > +
> > > > +	return &rule->addr;
> > > > +}
> > > > +
> > > > +/**
> > > > + * fwd_rule_ntop() - Format forwarding rule as a string
> > > > + * @rule:	Rule to format
> > > > + * @dst:	Buffer to store output (should have FWD_RULE_STRLEN bytes)
> > > > + * @size:	Size of @dst
> > > > + */
> > > > +const char *fwd_rule_ntop(const struct fwd_rule *rule, char *dst, size_t size)
> > > > +{
> > > > +	const char *percent = *rule->ifname ? "%" : "";
> > > > +	const char *weak = "", *scan = "";
> > > > +	char addr[INANY_ADDRSTRLEN];
> > > > +	int len;
> > > > +
> > > > +	inany_ntop(fwd_rule_addr(rule), addr, sizeof(addr));
> > > > +	if (rule->flags & FWD_WEAK)
> > > > +		weak = " (best effort)";
> > > > +	if (rule->flags & FWD_SCAN)
> > > > +		scan = " (auto-scan)";
> > > > +
> > > > +	if (rule->first == rule->last) {
> > > > +		len = snprintf(dst, size,
> > > > +			       "%s [%s]%s%s:%hu  =>  %hu %s%s",
> > > > +			       ipproto_name(rule->proto), addr, percent,
> > > > +			       rule->ifname, rule->first, rule->to, weak, scan);
> > > > +	} else {
> > > > +		in_port_t tolast = rule->last - rule->first + rule->to;
> > > > +		len = snprintf(dst, size,
> > > > +			       "%s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s",
> > > > +			       ipproto_name(rule->proto), addr, percent,
> > > > +			       rule->ifname, rule->first, rule->last,
> > > > +			       rule->to, tolast, weak, scan);
> > > > +	}
> > > > +
> > > > +	if (len < 0 || (size_t)len >= size)
> > > > +		return NULL;
> > > > +
> > > > +	return dst;
> > > > +}
> > > > diff --git a/fwd_rule.h b/fwd_rule.h
> > > > index 84ec5cbe..59db0e95 100644
> > > > --- a/fwd_rule.h
> > > > +++ b/fwd_rule.h
> > > > @@ -42,4 +42,15 @@ struct fwd_rule {
> > > >  	uint8_t flags;
> > > >  };
> > > >  
> > > > +const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
> > > > +
> > > > +#define FWD_RULE_STRLEN					    \
> > > > +	(IPPROTO_STRLEN - 1				    \
> > > > +	 + INANY_ADDRSTRLEN - 1				    \
> > > > +	 + IFNAMSIZ - 1					    \
> > > > +	 + sizeof(" (best effort)") - 1			    \
> > > > +	 + sizeof(" (auto-scan)") - 1			    \
> > > > +	 + 15)  
> > > 
> > > I'm not quite sure about the reason for the 15 here, is there some
> > > constant we can use perhaps?  
> > 
> > It's the spacing and punctuation around the other displayed
> > parameters, roughly the number of non-format characters in
> > 	"%s [%s]%s%s:%hu-%hu  =>  %hu-%hu %s%s"
> > (plus one for the \0).  I don't love it either, but I wasn't sure how
> > to make it less obscure. Is
> > 	sizeof(" []%:-  =>  - ")
> > an improvement?  I'm not sure.
> 
> I think it definitely is. I can't see a better explanation than that
> sizeof(), and it also looks like a good way to double check the number
> of additional characters.

Ok, done.

> If we want to do overdo with robustness, we could perhaps think of
> separately defining the format string and using it here for the
> calculation as well, which makes future (albeit unlikely) updates of
> the format string safer.

Eh, it's a tradeoff with two poor options.  Put the format next to the
printf and we have the current trouble.  Or, put the fomat next to the
length calculation, but then it's more awkward looking at the printf
to check if the parameters are correct.

> But, in that case, dropping characters for each format specifier is
> something we would have to do manually anyway... or not do at all and
> rely on the size of the format string as it is as an upper bound,
> perhaps.

Right.

Or a snprintf wrapper macro + external tool that derives a bound from
the format string and builds a header with it.  Sounds nice, but more
work than I care to do at this time :).

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

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

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

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

Thread overview: 49+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-23  7:37 [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
2026-03-23  7:37 ` [PATCH v3 01/25] conf: runas can be const David Gibson
2026-03-23  7:37 ` [PATCH v3 02/25] vhost_user: Fix assorted minor cppcheck warnings David Gibson
2026-03-23  7:37 ` [PATCH v3 03/25] serialise: Split functions user for serialisation from util.c David Gibson
2026-03-25  0:54   ` Stefano Brivio
2026-03-25  1:50     ` David Gibson
2026-03-23  7:37 ` [PATCH v3 04/25] serialise: Add helpers for serialising unsigned integers David Gibson
2026-03-23  7:37 ` [PATCH v3 05/25] fwd: Move selecting correct scan bitmap into fwd_sync_one() David Gibson
2026-03-23  7:37 ` [PATCH v3 06/25] fwd: Look up rule index in fwd_sync_one() David Gibson
2026-03-23  7:37 ` [PATCH v3 07/25] fwd: Store forwarding tables indexed by (origin) pif David Gibson
2026-03-25  0:54   ` Stefano Brivio
2026-03-25  4:04     ` David Gibson
2026-03-23  7:37 ` [PATCH v3 08/25] fwd: Allow FWD_DUAL_STACK_ANY flag to be passed directly to fwd_rule_add() David Gibson
2026-03-25  0:54   ` Stefano Brivio
2026-03-25  4:07     ` David Gibson
2026-03-23  7:37 ` [PATCH v3 09/25] fwd, conf: Expose ephemeral ports as bitmap rather than function David Gibson
2026-03-23  7:37 ` [PATCH v3 10/25] conf: Don't bother complaining about overlapping excluded ranges David Gibson
2026-03-23  7:37 ` [PATCH v3 11/25] conf: Move check for mapping port 0 to caller David Gibson
2026-03-23  7:37 ` [PATCH v3 12/25] conf: Move check for disabled interfaces earlier David Gibson
2026-03-23  7:37 ` [PATCH v3 13/25] pesto: Introduce stub configuration interface and tool David Gibson
2026-03-25  0:54   ` Stefano Brivio
2026-03-23  7:37 ` [PATCH v3 14/25] pesto: Add command line option parsing and debug messages David Gibson
2026-03-25  0:55   ` Stefano Brivio
2026-03-25  4:27     ` David Gibson
2026-03-23  7:37 ` [PATCH v3 15/25] pesto: Expose list of pifs to pesto David Gibson
2026-03-25  0:56   ` Stefano Brivio
2026-03-25  4:34     ` David Gibson
2026-03-25  8:18       ` Stefano Brivio
2026-03-25  8:31         ` David Gibson
2026-03-23  7:37 ` [PATCH v3 16/25] ip: Prepare ip.[ch] for sharing with pesto tool David Gibson
2026-03-23  7:37 ` [PATCH v3 17/25] inany: Prepare inany.[ch] " David Gibson
2026-03-23  7:37 ` [PATCH v3 18/25] fwd: Split forwading rule specification from its implementation state David Gibson
2026-03-23  7:37 ` [PATCH v3 19/25] ip: Define a bound for the string returned by ipproto_name() David Gibson
2026-03-23  7:37 ` [PATCH v3 20/25] fwd_rule: Move forwarding rule text formatting to common code David Gibson
2026-03-25  0:56   ` Stefano Brivio
2026-03-25  4:42     ` David Gibson
2026-03-25  8:18       ` Stefano Brivio
2026-03-25 23:54         ` David Gibson
2026-03-23  7:37 ` [PATCH v3 21/25] pesto: Read current ruleset from passt/pasta and display it David Gibson
2026-03-25  0:56   ` Stefano Brivio
2026-03-25  4:43     ` David Gibson
2026-03-23  7:37 ` [PATCH v3 22/25] conf: Move port parsing functions to own file, ports.c David Gibson
2026-03-23  7:37 ` [PATCH v3 23/25] conf, fwd, ports, util: Move things around for pesto David Gibson
2026-03-23  7:37 ` [PATCH v3 24/25] pesto, conf: Parse, send and receive new rules David Gibson
2026-03-23  7:37 ` [PATCH v3 25/25] conf, fwd: Allow switching to new rules received from pesto David Gibson
2026-03-23  8:38 ` [PATCH v3 00/25] RFC: Read-only dynamic update implementation David Gibson
2026-03-25  0:56 ` Stefano Brivio
2026-03-25  1:00   ` Stefano Brivio
2026-03-25  4:44     ` David Gibson

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