public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
* [PATCH v8 00/19] Dynamic configuration update implementation
@ 2026-05-05 23:47 Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 01/19] conf, fwd: Stricter rule checking in fwd_rule_add() Stefano Brivio
                   ` (19 more replies)
  0 siblings, 20 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

Changes in v8:
 * Implement --add, --delete, and --clear in 19/19, to add forwarding
   rules instead of replacing tables, delete existing rules, and
   explicitly clear tables
 * Address Laurent's comments for 15/19 and 17/19
 * In 10/19, instead of passing SOCK_NONBLOCK to accept4(), explicitly
   set O_NONBLOCK on the listening socket. Using SOCK_NONBLOCK doesn't
   do what we want, as it results in setting O_NONBLOCK on the new
   socket rather than on the listening one
 * Note: 18/19 is left as it is, I didn't address pending comments
   yet
 * Note: this doesn't include yet changes for AppArmor and SELinux
   policies, as well as changes for the template Fedora spec file.
   I'm still working on them

Changes in v7:
 * Addressed comments from Laurent in 6/18, 8/18, 9/18, 10/18, 11/18,
   12/18, 14/18, 15/18 (details in commit messages of single patches,
   before my Signed-off-by)
 * Note: this doesn't include yet --add and --delete, I'm still
   working on that

Changes in v6:
 * Addressed comments from Jon in 10/18, 11/18, 14/18, and 16/18
 * Dodged all warnings from static checkers (Coverity Scan and
   clang-tidy) with changes in 10/18, 11/18, 16/18, and with a
   new patch, 18/18
 * This does *not* include yet the implementation of --add and
   --delete switches for pesto as I originally intended, I'm
   rather far from being done with those. At the moment I just
   have a "mode selection" implementation for command line
   parsing but merging rules to / removing rules from / clearing
   the current table is something I barely started (and what I
   have at the moment isn't really valuable anyway)

David wrote:

---
Here's the next draft of dynamic configuration updates.  This now can
successfully update rules, though I've not tested it very extensively.

Patches 1..8/18 are preliminary reworks that make sense even without
pesto - feel free to apply if you're happy with them.  I don't think
the rest should be applied yet; we need to at least harden it so passt
can't be blocked indefinitely by a client which sends a partial update
then waits.

Based on my earlier series reworking static checking invocation.

TODO:
 - Don't allow a client which sends a partial configuration then
   blocks also block passt
 - Allow pesto to clear existing configuration, not just add
 - Allow pesto selectively delete existing rules, not just add

Changes in v5:
 * If multiple clients connect at once, they're now blocked until the
   first one finishes, instead of later ones being discarded
Changes in v4:
 * Merged with remainder of forward rule parsing rework series
   * Fix some bugs in rule checking pointed out by Laurent
 * Significantly cleaned up option parsing code
 * Changed from replacing all existing rules to adding new rules
   (clear and remove still TBD)
 * Somewhat simplified protocol (pif names and rules sent in a single
   pass)
 * pesto is now allocation free
 * Fixed commit message and style nits pointed out by Stefano
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 (17):
  conf, fwd: Stricter rule checking in fwd_rule_add()
  fwd_rule: Move ephemeral port probing to fwd_rule.c
  fwd, conf: Move rule parsing code to fwd_rule.[ch]
  fwd_rule: Move conflict checking back within fwd_rule_add()
  fwd: Generalise fwd_rules_info()
  pif: Limit pif names to 128 bytes
  fwd_rule: Fix some format specifiers
  pesto: Introduce stub configuration tool
  pesto, log: Share log.h (but not log.c) with pesto tool
  pesto, conf: Have pesto connect to passt and check versions
  pesto: Expose list of pifs to pesto and display them
  ip: Prepare ip.[ch] for sharing with pesto tool
  inany: Prepare inany.[ch] for sharing with pesto tool
  pesto: Read current ruleset from passt/pasta and optionally display it
  pesto: Parse and add new rules from command line
  pesto, conf: Send updated rules from pesto back to passt/pasta
  conf, fwd: Allow switching to new rules received from pesto

Stefano Brivio (2):
  fwd_rule: Fix static checkers warnings in fwd_rule_add()
  pesto, conf, fwd_rule: Add options and modes to add, delete, clear
    rules

 .gitignore   |   2 +
 Makefile     |  53 ++--
 common.h     | 116 +++++++++
 conf.c       | 696 ++++++++++++++++++++++-----------------------------
 conf.h       |   2 +
 epoll_type.h |   4 +
 flow.c       |   4 +-
 fwd.c        | 169 ++++---------
 fwd.h        |  41 +--
 fwd_rule.c   | 680 +++++++++++++++++++++++++++++++++++++++++++++++--
 fwd_rule.h   |  68 ++++-
 inany.c      |  19 +-
 inany.h      |  17 +-
 ip.c         |  56 +----
 ip.h         |   4 +-
 lineread.c   |   2 +-
 log.h        |  53 +++-
 passt.1      |   5 +
 passt.c      |   8 +
 passt.h      |   8 +
 pesto.1      | 271 ++++++++++++++++++++
 pesto.c      | 520 ++++++++++++++++++++++++++++++++++++++
 pesto.h      |  54 ++++
 pif.c        |   2 +-
 pif.h        |   7 +-
 serialise.c  |   7 +
 serialise.h  |   1 +
 siphash.h    |  13 +
 tap.c        |  52 ++++
 util.h       | 110 +-------
 30 files changed, 2252 insertions(+), 792 deletions(-)
 create mode 100644 common.h
 create mode 100644 pesto.1
 create mode 100644 pesto.c
 create mode 100644 pesto.h

-- 
2.43.0


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

* [PATCH v8 01/19] conf, fwd: Stricter rule checking in fwd_rule_add()
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 02/19] fwd_rule: Move ephemeral port probing to fwd_rule.c Stefano Brivio
                   ` (18 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

Although fwd_rule_add() performs some sanity checks on the rule it is
given, there are invalid rules we don't check for, assuming that its
callers will do that.

That won't be enough when we can get rules inserted by a dynamic update
client without going through the existing parsing code.  So, add stricter
checks to fwd_rule_add(), which is now possible thanks to the capabilities
bits in the struct fwd_table.  Where those duplicate existing checks in the
callers, remove the old copies.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 conf.c | 21 ---------------------
 fwd.c  | 52 +++++++++++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 47 insertions(+), 26 deletions(-)

diff --git a/conf.c b/conf.c
index 6e884e5..b470b0d 100644
--- a/conf.c
+++ b/conf.c
@@ -176,8 +176,6 @@ static void conf_ports_range_except(struct fwd_table *fwd, uint8_t proto,
 			die("Invalid interface name: %s", ifname);
 	}
 
-	assert(first != 0);
-
 	for (base = first; base <= last; base++) {
 		if (exclude && bitmap_isset(exclude, base))
 			continue;
@@ -310,10 +308,6 @@ static void conf_ports_spec(struct fwd_table *fwd, uint8_t proto,
 		if (p != ep) /* Garbage after the ranges */
 			goto bad;
 
-		if (orig_range.first == 0) {
-			die("Can't forward port 0 included in '%s'", spec);
-		}
-
 		conf_ports_range_except(fwd, proto, addr, ifname,
 					orig_range.first, orig_range.last,
 					exclude,
@@ -356,11 +350,6 @@ static void conf_ports(char optname, const char *optarg, struct fwd_table *fwd)
 		return;
 	}
 
-	if (proto == IPPROTO_TCP && !(fwd->caps & FWD_CAP_TCP))
-		die("TCP port forwarding requested but TCP is disabled");
-	if (proto == IPPROTO_UDP && !(fwd->caps & FWD_CAP_UDP))
-		die("UDP port forwarding requested but UDP is disabled");
-
 	strncpy(buf, optarg, sizeof(buf) - 1);
 
 	if ((spec = strchr(buf, '/'))) {
@@ -405,16 +394,6 @@ static void conf_ports(char optname, const char *optarg, struct fwd_table *fwd)
 		addr = NULL;
 	}
 
-	if (addr) {
-		if (!(fwd->caps & FWD_CAP_IPV4) && inany_v4(addr)) {
-			die("IPv4 is disabled, can't use -%c %s",
-			    optname, optarg);
-		} else if (!(fwd->caps & FWD_CAP_IPV6) && !inany_v4(addr)) {
-			die("IPv6 is disabled, can't use -%c %s",
-			    optname, optarg);
-		}
-	}
-
 	if (optname == 'T' || optname == 'U') {
 		assert(!addr && !ifname);
 
diff --git a/fwd.c b/fwd.c
index c7fd1a9..979c149 100644
--- a/fwd.c
+++ b/fwd.c
@@ -367,17 +367,59 @@ int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
 		     new->first, new->last);
 		return -EINVAL;
 	}
+	if (!new->first) {
+		warn("Forwarding rule attempts to map from port 0");
+		return -EINVAL;
+	}
+	if (!new->to ||
+	    (in_port_t)(new->to + new->last - new->first) < new->to) {
+		warn("Forwarding rule attempts to map to port 0");
+		return -EINVAL;
+	}
 	if (new->flags & ~allowed_flags) {
 		warn("Rule has invalid flags 0x%hhx",
 		     new->flags & ~allowed_flags);
 		return -EINVAL;
 	}
-	if (new->flags & FWD_DUAL_STACK_ANY &&
-	    !inany_equals(&new->addr, &inany_any6)) {
-		char astr[INANY_ADDRSTRLEN];
+	if (new->flags & FWD_DUAL_STACK_ANY) {
+		if (!inany_equals(&new->addr, &inany_any6)) {
+			char astr[INANY_ADDRSTRLEN];
 
-		warn("Dual stack rule has non-wildcard address %s",
-		     inany_ntop(&new->addr, astr, sizeof(astr)));
+			warn("Dual stack rule has non-wildcard address %s",
+			     inany_ntop(&new->addr, astr, sizeof(astr)));
+			return -EINVAL;
+		}
+		if (!(fwd->caps & FWD_CAP_IPV4)) {
+			warn("Dual stack forward, but IPv4 not enabled");
+			return -EINVAL;
+		}
+		if (!(fwd->caps & FWD_CAP_IPV6)) {
+			warn("Dual stack forward, but IPv6 not enabled");
+			return -EINVAL;
+		}
+	} else {
+		if (inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV4)) {
+			warn("IPv4 forward, but IPv4 not enabled");
+			return -EINVAL;
+		}
+		if (!inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV6)) {
+			warn("IPv6 forward, but IPv6 not enabled");
+			return -EINVAL;
+		}
+	}
+	if (new->proto == IPPROTO_TCP) {
+		if (!(fwd->caps & FWD_CAP_TCP)) {
+			warn("Can't add TCP forwarding rule, TCP not enabled");
+			return -EINVAL;
+		}
+	} else if (new->proto == IPPROTO_UDP) {
+		if (!(fwd->caps & FWD_CAP_UDP)) {
+			warn("Can't add UDP forwarding rule, UDP not enabled");
+			return -EINVAL;
+		}
+	} else {
+		warn("Unsupported protocol 0x%hhx (%s) for forwarding rule",
+		     new->proto, ipproto_name(new->proto));
 		return -EINVAL;
 	}
 
-- 
2.43.0


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

* [PATCH v8 02/19] fwd_rule: Move ephemeral port probing to fwd_rule.c
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 01/19] conf, fwd: Stricter rule checking in fwd_rule_add() Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 03/19] fwd, conf: Move rule parsing code to fwd_rule.[ch] Stefano Brivio
                   ` (17 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

We want to move parsing of forward rule options to fwd_rule.c so it can
eventually be shared with a configuration client.  As a preliminary step,
move the ephemeral port probing there, which that will need to use.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 fwd.c      | 73 --------------------------------------------------
 fwd.h      |  6 -----
 fwd_rule.c | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 fwd_rule.h |  6 +++++
 4 files changed, 84 insertions(+), 79 deletions(-)

diff --git a/fwd.c b/fwd.c
index 979c149..a6d75b7 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;
diff --git a/fwd.h b/fwd.h
index 3e365d3..e664d1d 100644
--- a/fwd.h
+++ b/fwd.h
@@ -20,12 +20,6 @@
 
 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);
-
 #define FWD_RULE_BITS	8
 #define MAX_FWD_RULES	MAX_FROM_BITS(FWD_RULE_BITS)
 #define FWD_NO_HINT	(-1)
diff --git a/fwd_rule.c b/fwd_rule.c
index 47d8df1..9d48982 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -15,9 +15,87 @@
  * Author: David Gibson <david@gibson.dropbear.id.au>
  */
 
+#include <errno.h>
+#include <fcntl.h>
 #include <stdio.h>
+#include <unistd.h>
 
 #include "fwd_rule.h"
+#include "lineread.h"
+#include "log.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
+ */
+void fwd_port_map_ephemeral(uint8_t *map)
+{
+	unsigned port;
+
+	for (port = fwd_ephemeral_min; port <= fwd_ephemeral_max; port++)
+		bitmap_set(map, port);
+}
 
 /**
  * fwd_rule_addr() - Return match address for a rule
diff --git a/fwd_rule.h b/fwd_rule.h
index edba678..5c7b67a 100644
--- a/fwd_rule.h
+++ b/fwd_rule.h
@@ -17,6 +17,9 @@
 #include "inany.h"
 #include "bitmap.h"
 
+/* Number of ports for both TCP and UDP */
+#define	NUM_PORTS	(1U << 16)
+
 /* Forwarding capability bits */
 #define FWD_CAP_IPV4		BIT(0)
 #define FWD_CAP_IPV6		BIT(1)
@@ -51,6 +54,9 @@ struct fwd_rule {
 	uint8_t flags;
 };
 
+void fwd_probe_ephemeral(void);
+void fwd_port_map_ephemeral(uint8_t *map);
+
 #define FWD_RULE_STRLEN					    \
 	(IPPROTO_STRLEN - 1				    \
 	 + INANY_ADDRSTRLEN - 1				    \
-- 
2.43.0


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

* [PATCH v8 03/19] fwd, conf: Move rule parsing code to fwd_rule.[ch]
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 01/19] conf, fwd: Stricter rule checking in fwd_rule_add() Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 02/19] fwd_rule: Move ephemeral port probing to fwd_rule.c Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 04/19] fwd_rule: Move conflict checking back within fwd_rule_add() Stefano Brivio
                   ` (16 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

The code parsing command line options into forwarding rules has now been
decoupled from most of passt/pasta's internals.  This is good, because
we'll soon want to share it with a configuration update client.

Make the next step by moving this code into fwd_rule.[ch].

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 conf.c     | 376 +------------------------------------------
 fwd.c      |  94 -----------
 fwd.h      |  33 ----
 fwd_rule.c | 464 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 fwd_rule.h |  36 ++++-
 5 files changed, 502 insertions(+), 501 deletions(-)

diff --git a/conf.c b/conf.c
index b470b0d..5aacfe0 100644
--- a/conf.c
+++ b/conf.c
@@ -13,7 +13,6 @@
  */
 
 #include <arpa/inet.h>
-#include <ctype.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <getopt.h>
@@ -66,365 +65,6 @@
 
 const char *pasta_default_ifn = "tap0";
 
-/**
- * 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, const char **endptr,
-			    struct port_range *range)
-{
-	unsigned long first, last;
-	char *ep;
-
-	last = first = strtoul(s, &ep, 10);
-	if (ep == s) /* Parsed nothing */
-		return -EINVAL;
-	if (*ep == '-') { /* we have a last value too */
-		const char *lasts = ep + 1;
-		last = strtoul(lasts, &ep, 10);
-		if (ep == lasts) /* Parsed nothing */
-			return -EINVAL;
-	}
-
-	if ((last < first) || (last >= NUM_PORTS))
-		return -ERANGE;
-
-	range->first = first;
-	range->last = last;
-	*endptr = ep;
-
-	return 0;
-}
-
-/**
- * parse_keyword() - Parse a literal keyword
- * @s:		String to parse
- * @endptr:	Update to the character after the keyword
- * @kw:		Keyword to accept
- *
- * Return: 0, if @s starts with @kw, -EINVAL if it does not
- */
-static int parse_keyword(const char *s, const char **endptr, const char *kw)
-{
-	size_t len = strlen(kw);
-
-	if (strlen(s) < len)
-		return -EINVAL;
-
-	if (memcmp(s, kw, len))
-		return -EINVAL;
-
-	*endptr = s + len;
-	return 0;
-}
-
-/**
- * conf_ports_range_except() - Set up forwarding for a range of ports minus a
- *                             bitmap of exclusions
- * @fwd:	Forwarding table to be updated
- * @proto:	Protocol to forward
- * @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(struct fwd_table *fwd, uint8_t proto,
-				    const union inany_addr *addr,
-				    const char *ifname,
-				    uint16_t first, uint16_t last,
-				    const uint8_t *exclude, uint16_t to,
-				    uint8_t flags)
-{
-	struct fwd_rule rule = {
-		.addr = addr ? *addr : inany_any6,
-		.ifname = { 0 },
-		.proto = proto,
-		.flags = flags,
-	};
-	char rulestr[FWD_RULE_STRLEN];
-	unsigned delta = to - first;
-	unsigned base, i;
-
-	if (!addr)
-		rule.flags |= FWD_DUAL_STACK_ANY;
-	if (ifname) {
-		int ret;
-
-		ret = snprintf(rule.ifname, sizeof(rule.ifname),
-			       "%s", ifname);
-		if (ret <= 0 || (size_t)ret >= sizeof(rule.ifname))
-			die("Invalid interface name: %s", ifname);
-	}
-
-	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;
-		}
-
-		rule.first = base;
-		rule.last = i - 1;
-		rule.to = base + delta;
-
-		fwd_rule_conflict_check(&rule, fwd->rules, fwd->count);
-		if (fwd_rule_add(fwd, &rule) < 0)
-			goto fail;
-
-		base = i - 1;
-	}
-	return;
-
-fail:
-	die("Unable to add rule %s",
-	    fwd_rule_fmt(&rule, rulestr, sizeof(rulestr)));
-}
-
-/*
- * for_each_chunk - Step through delimited chunks of a string
- * @p_:		Pointer to start of each chunk (updated)
- * @ep_:	Pointer to end of each chunk (updated)
- * @s_:		String to step through
- * @sep_:	String of all allowed delimiters
- */
-#define for_each_chunk(p_, ep_, s_, sep_)			\
-	for ((p_) = (s_);					\
-	     (ep_) = (p_) + strcspn((p_), (sep_)), *(p_);	\
-	     (p_) = *(ep_) ? (ep_) + 1 : (ep_))
-
-/**
- * conf_ports_spec() - Parse port range(s) specifier
- * @fwd:	Forwarding table to be updated
- * @proto:	Protocol to forward
- * @addr:	Listening address for forwarding
- * @ifname:	Interface name for listening
- * @spec:	Port range(s) specifier
- */
-static void conf_ports_spec(struct fwd_table *fwd, uint8_t proto,
-			    const union inany_addr *addr, const char *ifname,
-			    const char *spec)
-{
-	uint8_t exclude[PORT_BITMAP_SIZE] = { 0 };
-	bool exclude_only = true;
-	const char *p, *ep;
-	uint8_t flags = 0;
-	unsigned i;
-
-	if (!strcmp(spec, "all")) {
-		/* Treat "all" as equivalent to "": all non-ephemeral ports */
-		spec = "";
-	}
-
-	/* Parse excluded ranges and "auto" in the first pass */
-	for_each_chunk(p, ep, spec, ",") {
-		struct port_range xrange;
-
-		if (isdigit(*p)) {
-			/* Include range, parse later */
-			exclude_only = false;
-			continue;
-		}
-
-		if (parse_keyword(p, &p, "auto") == 0) {
-			if (p != ep) /* Garbage after the keyword */
-				goto bad;
-
-			if (!(fwd->caps & FWD_CAP_SCAN)) {
-				die(
-"'auto' port forwarding is only allowed for pasta");
-			}
-
-			flags |= FWD_SCAN;
-			continue;
-		}
-
-		/* Should be an exclude range */
-		if (*p != '~')
-			goto bad;
-		p++;
-
-		if (parse_port_range(p, &p, &xrange))
-			goto bad;
-		if (p != ep) /* Garbage after the range */
-			goto bad;
-
-		for (i = xrange.first; i <= xrange.last; i++)
-			bitmap_set(exclude, i);
-	}
-
-	if (exclude_only) {
-		/* Exclude ephemeral ports */
-		fwd_port_map_ephemeral(exclude);
-
-		conf_ports_range_except(fwd, proto, addr, ifname,
-					1, NUM_PORTS - 1, exclude,
-					1, flags | FWD_WEAK);
-		return;
-	}
-
-	/* Now process base ranges, skipping exclusions */
-	for_each_chunk(p, ep, spec, ",") {
-		struct port_range orig_range, mapped_range;
-
-		if (!isdigit(*p))
-			/* 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 != ep) /* Garbage after the ranges */
-			goto bad;
-
-		conf_ports_range_except(fwd, proto, addr, ifname,
-					orig_range.first, orig_range.last,
-					exclude,
-					mapped_range.first, flags);
-	}
-
-	return;
-bad:
-	die("Invalid port specifier '%s'", spec);
-}
-
-/**
- * conf_ports() - Parse port configuration options, initialise UDP/TCP sockets
- * @optname:	Short option name, t, T, u, or U
- * @optarg:	Option argument (port specification)
- * @fwd:	Forwarding table to be updated
- */
-static void conf_ports(char optname, const char *optarg, struct fwd_table *fwd)
-{
-	union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
-	char buf[BUFSIZ], *spec, *ifname = NULL;
-	uint8_t proto;
-
-	if (optname == 't' || optname == 'T')
-		proto = IPPROTO_TCP;
-	else if (optname == 'u' || optname == 'U')
-		proto = IPPROTO_UDP;
-	else
-		assert(0);
-
-	if (!strcmp(optarg, "none")) {
-		unsigned i;
-
-		for (i = 0; i < fwd->count; i++) {
-			if (fwd->rules[i].proto == proto) {
-				die("-%c none conflicts with previous options",
-					optname);
-			}
-		}
-		return;
-	}
-
-	strncpy(buf, optarg, sizeof(buf) - 1);
-
-	if ((spec = strchr(buf, '/'))) {
-		*spec = 0;
-		spec++;
-
-		if (optname != 't' && optname != 'u')
-			die("Listening address not allowed for -%c %s",
-			    optname, optarg);
-
-		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) {
-				die("Interface name '%s' is too long (max %u)",
-				    ifname, IFNAMSIZ - 1);
-			}
-		}
-
-		if (ifname == buf + 1) {	/* Interface without address */
-			addr = NULL;
-		} else {
-			char *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))
-				die("Bad forwarding address '%s'", p);
-		}
-	} else {
-		spec = buf;
-
-		addr = NULL;
-	}
-
-	if (optname == 'T' || optname == 'U') {
-		assert(!addr && !ifname);
-
-		if (!(fwd->caps & FWD_CAP_IFNAME)) {
-			warn(
-"SO_BINDTODEVICE unavailable, forwarding only 127.0.0.1 and ::1 for '-%c %s'",
-			     optname, optarg);
-
-			if (fwd->caps & FWD_CAP_IPV4) {
-				conf_ports_spec(fwd, proto,
-						&inany_loopback4, NULL, spec);
-			}
-			if (fwd->caps & FWD_CAP_IPV6) {
-				conf_ports_spec(fwd, proto,
-						&inany_loopback6, NULL, spec);
-			}
-			return;
-		}
-
-		ifname = "lo";
-	}
-
-	if (ifname && !(fwd->caps & FWD_CAP_IFNAME)) {
-		die(
-"Device binding for '-%c %s' unsupported (requires kernel 5.7+)",
-		    optname, optarg);
-	}
-
-	conf_ports_spec(fwd, proto, addr, ifname, spec);
-}
-
 /**
  * add_dns4() - Possibly add the IPv4 address of a DNS resolver to configuration
  * @c:		Execution context
@@ -2160,16 +1800,16 @@ void conf(struct ctx *c, int argc, char **argv)
 
 		if (name == 't') {
 			opt_t = true;
-			conf_ports(name, optarg, c->fwd[PIF_HOST]);
+			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
 		} else if (name == 'u') {
 			opt_u = true;
-			conf_ports(name, optarg, c->fwd[PIF_HOST]);
+			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
 		} else if (name == 'T') {
 			opt_T = true;
-			conf_ports(name, optarg, c->fwd[PIF_SPLICE]);
+			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
 		} else if (name == 'U') {
 			opt_U = true;
-			conf_ports(name, optarg, c->fwd[PIF_SPLICE]);
+			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
 		}
 	} while (name != -1);
 
@@ -2221,13 +1861,13 @@ void conf(struct ctx *c, int argc, char **argv)
 
 	if (c->mode == MODE_PASTA) {
 		if (!opt_t)
-			conf_ports('t', "auto", c->fwd[PIF_HOST]);
+			fwd_rule_parse('t', "auto", c->fwd[PIF_HOST]);
 		if (!opt_T)
-			conf_ports('T', "auto", c->fwd[PIF_SPLICE]);
+			fwd_rule_parse('T', "auto", c->fwd[PIF_SPLICE]);
 		if (!opt_u)
-			conf_ports('u', "auto", c->fwd[PIF_HOST]);
+			fwd_rule_parse('u', "auto", c->fwd[PIF_HOST]);
 		if (!opt_U)
-			conf_ports('U', "auto", c->fwd[PIF_SPLICE]);
+			fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]);
 	}
 
 	if (!c->quiet)
diff --git a/fwd.c b/fwd.c
index a6d75b7..728a783 100644
--- a/fwd.c
+++ b/fwd.c
@@ -275,100 +275,6 @@ void fwd_rule_init(struct ctx *c)
 		c->fwd[PIF_SPLICE] = &fwd_out;
 }
 
-/**
- * fwd_rule_add() - Validate and add a rule to a forwarding table
- * @fwd:	Table to add to
- * @new:	Rule to add
- *
- * Return: 0 on success, negative error code on failure
- */
-int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
-{
-	/* Flags which can be set from the caller */
-	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
-	unsigned num = (unsigned)new->last - new->first + 1;
-	unsigned port;
-
-	if (new->first > new->last) {
-		warn("Rule has invalid port range %u-%u",
-		     new->first, new->last);
-		return -EINVAL;
-	}
-	if (!new->first) {
-		warn("Forwarding rule attempts to map from port 0");
-		return -EINVAL;
-	}
-	if (!new->to ||
-	    (in_port_t)(new->to + new->last - new->first) < new->to) {
-		warn("Forwarding rule attempts to map to port 0");
-		return -EINVAL;
-	}
-	if (new->flags & ~allowed_flags) {
-		warn("Rule has invalid flags 0x%hhx",
-		     new->flags & ~allowed_flags);
-		return -EINVAL;
-	}
-	if (new->flags & FWD_DUAL_STACK_ANY) {
-		if (!inany_equals(&new->addr, &inany_any6)) {
-			char astr[INANY_ADDRSTRLEN];
-
-			warn("Dual stack rule has non-wildcard address %s",
-			     inany_ntop(&new->addr, astr, sizeof(astr)));
-			return -EINVAL;
-		}
-		if (!(fwd->caps & FWD_CAP_IPV4)) {
-			warn("Dual stack forward, but IPv4 not enabled");
-			return -EINVAL;
-		}
-		if (!(fwd->caps & FWD_CAP_IPV6)) {
-			warn("Dual stack forward, but IPv6 not enabled");
-			return -EINVAL;
-		}
-	} else {
-		if (inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV4)) {
-			warn("IPv4 forward, but IPv4 not enabled");
-			return -EINVAL;
-		}
-		if (!inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV6)) {
-			warn("IPv6 forward, but IPv6 not enabled");
-			return -EINVAL;
-		}
-	}
-	if (new->proto == IPPROTO_TCP) {
-		if (!(fwd->caps & FWD_CAP_TCP)) {
-			warn("Can't add TCP forwarding rule, TCP not enabled");
-			return -EINVAL;
-		}
-	} else if (new->proto == IPPROTO_UDP) {
-		if (!(fwd->caps & FWD_CAP_UDP)) {
-			warn("Can't add UDP forwarding rule, UDP not enabled");
-			return -EINVAL;
-		}
-	} else {
-		warn("Unsupported protocol 0x%hhx (%s) for forwarding rule",
-		     new->proto, ipproto_name(new->proto));
-		return -EINVAL;
-	}
-
-	if (fwd->count >= ARRAY_SIZE(fwd->rules)) {
-		warn("Too many rules (maximum %u)", ARRAY_SIZE(fwd->rules));
-		return -ENOSPC;
-	}
-	if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) {
-		warn("Rules require too many listening sockets (maximum %u)",
-		     ARRAY_SIZE(fwd->socks));
-		return -ENOSPC;
-	}
-
-	fwd->rulesocks[fwd->count] = &fwd->socks[fwd->sock_count];
-	for (port = new->first; port <= new->last; port++)
-		fwd->rulesocks[fwd->count][port - new->first] = -1;
-
-	fwd->rules[fwd->count++] = *new;
-	fwd->sock_count += num;
-	return 0;
-}
-
 /**
  * fwd_rule_match() - Does a prospective flow match a given forwarding rule?
  * @rule:	Forwarding rule
diff --git a/fwd.h b/fwd.h
index e664d1d..8f845d0 100644
--- a/fwd.h
+++ b/fwd.h
@@ -20,8 +20,6 @@
 
 struct flowside;
 
-#define FWD_RULE_BITS	8
-#define MAX_FWD_RULES	MAX_FROM_BITS(FWD_RULE_BITS)
 #define FWD_NO_HINT	(-1)
 
 /**
@@ -36,36 +34,6 @@ struct fwd_listen_ref {
 	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 - Forwarding state (per initiating pif)
- * @caps:	Forwarding capabilities for this initiating pif
- * @count:	Number of forwarding rules
- * @rules:	Array of forwarding rules
- * @rulesocks:	Parallel array of @rules (@count valid entries) of pointers to
- *		@socks entries giving the start of the corresponding rule's
- *		sockets within the larger array
- * @sock_count:	Number of entries used in @socks (for all rules combined)
- * @socks:	Listening sockets for forwarding
- */
-struct fwd_table {
-	uint32_t caps;
-	unsigned count;
-	struct fwd_rule rules[MAX_FWD_RULES];
-	int *rulesocks[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
@@ -81,7 +49,6 @@ struct fwd_scan {
 #define FWD_PORT_SCAN_INTERVAL		1000	/* ms */
 
 void fwd_rule_init(struct ctx *c);
-int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new);
 const struct fwd_rule *fwd_rule_search(const struct fwd_table *fwd,
 				       const struct flowside *ini,
 				       uint8_t proto, int hint);
diff --git a/fwd_rule.c b/fwd_rule.c
index 9d48982..cd3dec0 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -15,6 +15,7 @@
  * Author: David Gibson <david@gibson.dropbear.id.au>
  */
 
+#include <ctype.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <stdio.h>
@@ -89,7 +90,7 @@ parse_err:
  * fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap
  * @map:	Bitmap to update
  */
-void fwd_port_map_ephemeral(uint8_t *map)
+static void fwd_port_map_ephemeral(uint8_t *map)
 {
 	unsigned port;
 
@@ -123,6 +124,7 @@ const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
  */
 __attribute__((noinline))
 #endif
+/* cppcheck-suppress staticFunction */
 const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size)
 {
 	const char *percent = *rule->ifname ? "%" : "";
@@ -199,8 +201,8 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
  * @rules:	Existing rules against which to test
  * @count:	Number of rules in @rules
  */
-void fwd_rule_conflict_check(const struct fwd_rule *new,
-			     const struct fwd_rule *rules, size_t count)
+static void fwd_rule_conflict_check(const struct fwd_rule *new,
+				    const struct fwd_rule *rules, size_t count)
 {
 	unsigned i;
 
@@ -215,3 +217,459 @@ void fwd_rule_conflict_check(const struct fwd_rule *new,
 		    fwd_rule_fmt(&rules[i], rulestr, sizeof(rulestr)));
 	}
 }
+
+/**
+ * fwd_rule_add() - Validate and add a rule to a forwarding table
+ * @fwd:	Table to add to
+ * @new:	Rule to add
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+static int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
+{
+	/* Flags which can be set from the caller */
+	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
+	unsigned num = (unsigned)new->last - new->first + 1;
+	unsigned port;
+
+	if (new->first > new->last) {
+		warn("Rule has invalid port range %u-%u",
+		     new->first, new->last);
+		return -EINVAL;
+	}
+	if (!new->first) {
+		warn("Forwarding rule attempts to map from port 0");
+		return -EINVAL;
+	}
+	if (!new->to ||
+	    (in_port_t)(new->to + new->last - new->first) < new->to) {
+		warn("Forwarding rule attempts to map to port 0");
+		return -EINVAL;
+	}
+	if (new->flags & ~allowed_flags) {
+		warn("Rule has invalid flags 0x%hhx",
+		     new->flags & ~allowed_flags);
+		return -EINVAL;
+	}
+	if (new->flags & FWD_DUAL_STACK_ANY) {
+		if (!inany_equals(&new->addr, &inany_any6)) {
+			char astr[INANY_ADDRSTRLEN];
+
+			warn("Dual stack rule has non-wildcard address %s",
+			     inany_ntop(&new->addr, astr, sizeof(astr)));
+			return -EINVAL;
+		}
+		if (!(fwd->caps & FWD_CAP_IPV4)) {
+			warn("Dual stack forward, but IPv4 not enabled");
+			return -EINVAL;
+		}
+		if (!(fwd->caps & FWD_CAP_IPV6)) {
+			warn("Dual stack forward, but IPv6 not enabled");
+			return -EINVAL;
+		}
+	} else {
+		if (inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV4)) {
+			warn("IPv4 forward, but IPv4 not enabled");
+			return -EINVAL;
+		}
+		if (!inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV6)) {
+			warn("IPv6 forward, but IPv6 not enabled");
+			return -EINVAL;
+		}
+	}
+	if (new->proto == IPPROTO_TCP) {
+		if (!(fwd->caps & FWD_CAP_TCP)) {
+			warn("Can't add TCP forwarding rule, TCP not enabled");
+			return -EINVAL;
+		}
+	} else if (new->proto == IPPROTO_UDP) {
+		if (!(fwd->caps & FWD_CAP_UDP)) {
+			warn("Can't add UDP forwarding rule, UDP not enabled");
+			return -EINVAL;
+		}
+	} else {
+		warn("Unsupported protocol 0x%hhx (%s) for forwarding rule",
+		     new->proto, ipproto_name(new->proto));
+		return -EINVAL;
+	}
+
+	if (fwd->count >= ARRAY_SIZE(fwd->rules)) {
+		warn("Too many rules (maximum %u)", ARRAY_SIZE(fwd->rules));
+		return -ENOSPC;
+	}
+	if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) {
+		warn("Rules require too many listening sockets (maximum %u)",
+		     ARRAY_SIZE(fwd->socks));
+		return -ENOSPC;
+	}
+
+	fwd->rulesocks[fwd->count] = &fwd->socks[fwd->sock_count];
+	for (port = new->first; port <= new->last; port++)
+		fwd->rulesocks[fwd->count][port - new->first] = -1;
+
+	fwd->rules[fwd->count++] = *new;
+	fwd->sock_count += num;
+	return 0;
+}
+
+/**
+ * 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, const char **endptr,
+			    struct port_range *range)
+{
+	unsigned long first, last;
+	char *ep;
+
+	last = first = strtoul(s, &ep, 10);
+	if (ep == s) /* Parsed nothing */
+		return -EINVAL;
+	if (*ep == '-') { /* we have a last value too */
+		const char *lasts = ep + 1;
+		last = strtoul(lasts, &ep, 10);
+		if (ep == lasts) /* Parsed nothing */
+			return -EINVAL;
+	}
+
+	if ((last < first) || (last >= NUM_PORTS))
+		return -ERANGE;
+
+	range->first = first;
+	range->last = last;
+	*endptr = ep;
+
+	return 0;
+}
+
+/**
+ * parse_keyword() - Parse a literal keyword
+ * @s:		String to parse
+ * @endptr:	Update to the character after the keyword
+ * @kw:		Keyword to accept
+ *
+ * Return: 0, if @s starts with @kw, -EINVAL if it does not
+ */
+static int parse_keyword(const char *s, const char **endptr, const char *kw)
+{
+	size_t len = strlen(kw);
+
+	if (strlen(s) < len)
+		return -EINVAL;
+
+	if (memcmp(s, kw, len))
+		return -EINVAL;
+
+	*endptr = s + len;
+	return 0;
+}
+
+/**
+ * fwd_rule_range_except() - Set up forwarding for a range of ports minus a
+ *                           bitmap of exclusions
+ * @fwd:	Forwarding table to be updated
+ * @proto:	Protocol to forward
+ * @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 fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
+				  const union inany_addr *addr,
+				  const char *ifname,
+				  uint16_t first, uint16_t last,
+				  const uint8_t *exclude, uint16_t to,
+				  uint8_t flags)
+{
+	struct fwd_rule rule = {
+		.addr = addr ? *addr : inany_any6,
+		.ifname = { 0 },
+		.proto = proto,
+		.flags = flags,
+	};
+	char rulestr[FWD_RULE_STRLEN];
+	unsigned delta = to - first;
+	unsigned base, i;
+
+	if (!addr)
+		rule.flags |= FWD_DUAL_STACK_ANY;
+	if (ifname) {
+		int ret;
+
+		ret = snprintf(rule.ifname, sizeof(rule.ifname),
+			       "%s", ifname);
+		if (ret <= 0 || (size_t)ret >= sizeof(rule.ifname))
+			die("Invalid interface name: %s", ifname);
+	}
+
+	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;
+		}
+
+		rule.first = base;
+		rule.last = i - 1;
+		rule.to = base + delta;
+
+		fwd_rule_conflict_check(&rule, fwd->rules, fwd->count);
+		if (fwd_rule_add(fwd, &rule) < 0)
+			goto fail;
+
+		base = i - 1;
+	}
+	return;
+
+fail:
+	die("Unable to add rule %s",
+	    fwd_rule_fmt(&rule, rulestr, sizeof(rulestr)));
+}
+
+/*
+ * for_each_chunk - Step through delimited chunks of a string
+ * @p_:		Pointer to start of each chunk (updated)
+ * @ep_:	Pointer to end of each chunk (updated)
+ * @s_:		String to step through
+ * @sep_:	String of all allowed delimiters
+ */
+#define for_each_chunk(p_, ep_, s_, sep_)			\
+	for ((p_) = (s_);					\
+	     (ep_) = (p_) + strcspn((p_), (sep_)), *(p_);	\
+	     (p_) = *(ep_) ? (ep_) + 1 : (ep_))
+
+/**
+ * fwd_rule_parse_ports() - Parse port range(s) specifier
+ * @fwd:	Forwarding table to be updated
+ * @proto:	Protocol to forward
+ * @addr:	Listening address for forwarding
+ * @ifname:	Interface name for listening
+ * @spec:	Port range(s) specifier
+ */
+static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
+				 const union inany_addr *addr,
+				 const char *ifname,
+				 const char *spec)
+{
+	uint8_t exclude[PORT_BITMAP_SIZE] = { 0 };
+	bool exclude_only = true;
+	const char *p, *ep;
+	uint8_t flags = 0;
+	unsigned i;
+
+	if (!strcmp(spec, "all")) {
+		/* Treat "all" as equivalent to "": all non-ephemeral ports */
+		spec = "";
+	}
+
+	/* Parse excluded ranges and "auto" in the first pass */
+	for_each_chunk(p, ep, spec, ",") {
+		struct port_range xrange;
+
+		if (isdigit(*p)) {
+			/* Include range, parse later */
+			exclude_only = false;
+			continue;
+		}
+
+		if (parse_keyword(p, &p, "auto") == 0) {
+			if (p != ep) /* Garbage after the keyword */
+				goto bad;
+
+			if (!(fwd->caps & FWD_CAP_SCAN)) {
+				die(
+"'auto' port forwarding is only allowed for pasta");
+			}
+
+			flags |= FWD_SCAN;
+			continue;
+		}
+
+		/* Should be an exclude range */
+		if (*p != '~')
+			goto bad;
+		p++;
+
+		if (parse_port_range(p, &p, &xrange))
+			goto bad;
+		if (p != ep) /* Garbage after the range */
+			goto bad;
+
+		for (i = xrange.first; i <= xrange.last; i++)
+			bitmap_set(exclude, i);
+	}
+
+	if (exclude_only) {
+		/* Exclude ephemeral ports */
+		fwd_port_map_ephemeral(exclude);
+
+		fwd_rule_range_except(fwd, proto, addr, ifname,
+				      1, NUM_PORTS - 1, exclude,
+				      1, flags | FWD_WEAK);
+		return;
+	}
+
+	/* Now process base ranges, skipping exclusions */
+	for_each_chunk(p, ep, spec, ",") {
+		struct port_range orig_range, mapped_range;
+
+		if (!isdigit(*p))
+			/* 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 != ep) /* Garbage after the ranges */
+			goto bad;
+
+		fwd_rule_range_except(fwd, proto, addr, ifname,
+				      orig_range.first, orig_range.last,
+				      exclude,
+				      mapped_range.first, flags);
+	}
+
+	return;
+bad:
+	die("Invalid port specifier '%s'", spec);
+}
+
+/**
+ * fwd_rule_parse() - Parse port configuration option
+ * @optname:	Short option name, t, T, u, or U
+ * @optarg:	Option argument (port specification)
+ * @fwd:	Forwarding table to be updated
+ */
+void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
+{
+	union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
+	char buf[BUFSIZ], *spec, *ifname = NULL;
+	uint8_t proto;
+
+	if (optname == 't' || optname == 'T')
+		proto = IPPROTO_TCP;
+	else if (optname == 'u' || optname == 'U')
+		proto = IPPROTO_UDP;
+	else
+		assert(0);
+
+	if (!strcmp(optarg, "none")) {
+		unsigned i;
+
+		for (i = 0; i < fwd->count; i++) {
+			if (fwd->rules[i].proto == proto) {
+				die("-%c none conflicts with previous options",
+					optname);
+			}
+		}
+		return;
+	}
+
+	strncpy(buf, optarg, sizeof(buf) - 1);
+
+	if ((spec = strchr(buf, '/'))) {
+		*spec = 0;
+		spec++;
+
+		if (optname != 't' && optname != 'u')
+			die("Listening address not allowed for -%c %s",
+			    optname, optarg);
+
+		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) {
+				die("Interface name '%s' is too long (max %u)",
+				    ifname, IFNAMSIZ - 1);
+			}
+		}
+
+		if (ifname == buf + 1) {	/* Interface without address */
+			addr = NULL;
+		} else {
+			char *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))
+				die("Bad forwarding address '%s'", p);
+		}
+	} else {
+		spec = buf;
+
+		addr = NULL;
+	}
+
+	if (optname == 'T' || optname == 'U') {
+		assert(!addr && !ifname);
+
+		if (!(fwd->caps & FWD_CAP_IFNAME)) {
+			warn(
+"SO_BINDTODEVICE unavailable, forwarding only 127.0.0.1 and ::1 for '-%c %s'",
+			     optname, optarg);
+
+			if (fwd->caps & FWD_CAP_IPV4) {
+				fwd_rule_parse_ports(fwd, proto,
+						     &inany_loopback4, NULL,
+						     spec);
+			}
+			if (fwd->caps & FWD_CAP_IPV6) {
+				fwd_rule_parse_ports(fwd, proto,
+						     &inany_loopback6, NULL,
+						     spec);
+			}
+			return;
+		}
+
+		ifname = "lo";
+	}
+
+	if (ifname && !(fwd->caps & FWD_CAP_IFNAME)) {
+		die(
+"Device binding for '-%c %s' unsupported (requires kernel 5.7+)",
+		    optname, optarg);
+	}
+
+	fwd_rule_parse_ports(fwd, proto, addr, ifname, spec);
+}
diff --git a/fwd_rule.h b/fwd_rule.h
index 5c7b67a..f0f4efd 100644
--- a/fwd_rule.h
+++ b/fwd_rule.h
@@ -19,6 +19,7 @@
 
 /* Number of ports for both TCP and UDP */
 #define	NUM_PORTS	(1U << 16)
+#define PORT_BITMAP_SIZE	DIV_ROUND_UP(NUM_PORTS, 8)
 
 /* Forwarding capability bits */
 #define FWD_CAP_IPV4		BIT(0)
@@ -54,8 +55,38 @@ struct fwd_rule {
 	uint8_t flags;
 };
 
+#define FWD_RULE_BITS	8
+#define MAX_FWD_RULES	MAX_FROM_BITS(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 - Forwarding state (per initiating pif)
+ * @caps:	Forwarding capabilities for this initiating pif
+ * @count:	Number of forwarding rules
+ * @rules:	Array of forwarding rules
+ * @rulesocks:	Parallel array of @rules (@count valid entries) of pointers to
+ *		@socks entries giving the start of the corresponding rule's
+ *		sockets within the larger array
+ * @sock_count:	Number of entries used in @socks (for all rules combined)
+ * @socks:	Listening sockets for forwarding
+ */
+struct fwd_table {
+	uint32_t caps;
+	unsigned count;
+	struct fwd_rule rules[MAX_FWD_RULES];
+	int *rulesocks[MAX_FWD_RULES];
+	unsigned sock_count;
+	int socks[MAX_LISTEN_SOCKS];
+};
+
 void fwd_probe_ephemeral(void);
-void fwd_port_map_ephemeral(uint8_t *map);
 
 #define FWD_RULE_STRLEN					    \
 	(IPPROTO_STRLEN - 1				    \
@@ -67,7 +98,6 @@ void fwd_port_map_ephemeral(uint8_t *map);
 const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
 const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size);
 void fwd_rules_info(const struct fwd_rule *rules, size_t count);
-void fwd_rule_conflict_check(const struct fwd_rule *new,
-			     const struct fwd_rule *rules, size_t count);
+void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd);
 
 #endif /* FWD_RULE_H */
-- 
2.43.0


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

* [PATCH v8 04/19] fwd_rule: Move conflict checking back within fwd_rule_add()
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (2 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 03/19] fwd, conf: Move rule parsing code to fwd_rule.[ch] Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 05/19] fwd: Generalise fwd_rules_info() Stefano Brivio
                   ` (15 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

2bffb631d31e ("fwd_rule: Move rule conflict checking from fwd_rule_add()
to caller") moved rule conflict checking out of fwd_rule_add().  This
seemed like a good idea at the time, but turns out to be kind of awkward:
it means we're now checking for conflicts *before* we've checked the rule
for internal consistency (including first <= last), which leaves an awkward
assert() which might fire in unexpected places.

While it's true that it's not really necessary to include this in order to
safely add a rule, the benefits from skipping it are pretty marginal.  So,
for simplicity, fold this check back into fwd_rule_add(), making it
non-fatal.  If we ever have cases with enough rules that the O(n^2) nature
of the check matters, we might need to revisit.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 fwd_rule.c | 38 +++++++++++++-------------------------
 1 file changed, 13 insertions(+), 25 deletions(-)

diff --git a/fwd_rule.c b/fwd_rule.c
index cd3dec0..1413584 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -195,29 +195,6 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
 	return true;
 }
 
-/**
- * fwd_rule_conflict_check() - Die if given rule conflicts with any in list
- * @new:	New rule
- * @rules:	Existing rules against which to test
- * @count:	Number of rules in @rules
- */
-static void fwd_rule_conflict_check(const struct fwd_rule *new,
-				    const struct fwd_rule *rules, size_t count)
-{
-	unsigned i;
-
-	for (i = 0; i < count; i++) {
-		char newstr[FWD_RULE_STRLEN], rulestr[FWD_RULE_STRLEN];
-
-		if (!fwd_rule_conflicts(new, &rules[i]))
-			continue;
-
-		die("Forwarding configuration conflict: %s versus %s",
-		    fwd_rule_fmt(new, newstr, sizeof(newstr)),
-		    fwd_rule_fmt(&rules[i], rulestr, sizeof(rulestr)));
-	}
-}
-
 /**
  * fwd_rule_add() - Validate and add a rule to a forwarding table
  * @fwd:	Table to add to
@@ -230,7 +207,7 @@ static int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
 	/* Flags which can be set from the caller */
 	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
 	unsigned num = (unsigned)new->last - new->first + 1;
-	unsigned port;
+	unsigned port, i;
 
 	if (new->first > new->last) {
 		warn("Rule has invalid port range %u-%u",
@@ -293,6 +270,18 @@ static int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
 		return -EINVAL;
 	}
 
+	for (i = 0; i < fwd->count; i++) {
+		char newstr[FWD_RULE_STRLEN], rulestr[FWD_RULE_STRLEN];
+
+		if (!fwd_rule_conflicts(new, &fwd->rules[i]))
+			continue;
+
+		warn("Forwarding configuration conflict: %s versus %s",
+		     fwd_rule_fmt(new, newstr, sizeof(newstr)),
+		     fwd_rule_fmt(&fwd->rules[i], rulestr, sizeof(rulestr)));
+		return -EEXIST;
+	}
+
 	if (fwd->count >= ARRAY_SIZE(fwd->rules)) {
 		warn("Too many rules (maximum %u)", ARRAY_SIZE(fwd->rules));
 		return -ENOSPC;
@@ -435,7 +424,6 @@ static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
 		rule.last = i - 1;
 		rule.to = base + delta;
 
-		fwd_rule_conflict_check(&rule, fwd->rules, fwd->count);
 		if (fwd_rule_add(fwd, &rule) < 0)
 			goto fail;
 
-- 
2.43.0


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

* [PATCH v8 05/19] fwd: Generalise fwd_rules_info()
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (3 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 04/19] fwd_rule: Move conflict checking back within fwd_rule_add() Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 06/19] pif: Limit pif names to 128 bytes Stefano Brivio
                   ` (14 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

fwd_rules_info() is used to print a full table of forwarding rules for
debugging or the like.  Currently it has one caller, and uses info() to
dump the messages.  However for the upcoming configuration client, we're
going to want to dump the rules in some similar, but not quite identical
ways.  For example, at different severity levels, or to stdout instead of
stderr / system log / logfile.

So, generalise fwd_rules_info() to fwd_rules_dump() which takes a printing
function as a parameter.  Because we want this to work with "functions"
like info, which is actually a macro, we have to convert fwd_rules_dump()
to a macro as well.  We also allow the prefix and suffix for each rule /
line to be provided as a parameter.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 conf.c     |  3 ++-
 fwd_rule.c | 16 ----------------
 fwd_rule.h | 20 +++++++++++++++++++-
 3 files changed, 21 insertions(+), 18 deletions(-)

diff --git a/conf.c b/conf.c
index 5aacfe0..0586107 100644
--- a/conf.c
+++ b/conf.c
@@ -902,7 +902,8 @@ dns6:
 			dir = "Inbound";
 
 		info("%s forwarding rules (%s):", dir, pif_name(i));
-		fwd_rules_info(c->fwd[i]->rules, c->fwd[i]->count);
+		fwd_rules_dump(info, c->fwd[i]->rules, c->fwd[i]->count,
+			       "    ", "");
 	}
 }
 
diff --git a/fwd_rule.c b/fwd_rule.c
index 1413584..777282d 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -124,7 +124,6 @@ const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
  */
 __attribute__((noinline))
 #endif
-/* cppcheck-suppress staticFunction */
 const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size)
 {
 	const char *percent = *rule->ifname ? "%" : "";
@@ -158,21 +157,6 @@ const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size)
 	return dst;
 }
 
-/**
- * fwd_rules_info() - Print forwarding rules for debugging
- * @fwd:	Table to print
- */
-void fwd_rules_info(const struct fwd_rule *rules, size_t count)
-{
-	unsigned i;
-
-	for (i = 0; i < count; i++) {
-		char buf[FWD_RULE_STRLEN];
-
-		info("    %s", fwd_rule_fmt(&rules[i], buf, sizeof(buf)));
-	}
-}
-
 /**
  * fwd_rule_conflicts() - Test if two rules conflict with each other
  * @a, @b:	Rules to test
diff --git a/fwd_rule.h b/fwd_rule.h
index f0f4efd..5855138 100644
--- a/fwd_rule.h
+++ b/fwd_rule.h
@@ -97,7 +97,25 @@ void fwd_probe_ephemeral(void);
 
 const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
 const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size);
-void fwd_rules_info(const struct fwd_rule *rules, size_t count);
 void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd);
 
+/**
+ * fwd_rules_dump() - Dump forwarding rules
+ * @fn:		Printing/logging function to call
+ * @rules:	Array of rules to dump
+ * @count:	Number of rules to dump
+ * @prefix:	String to print at the start of each rule
+ * @suffix:	String to print at the end of each rule
+ */
+#define fwd_rules_dump(fn, rules, count, prefix, suffix)		\
+	do {								\
+		unsigned i_;						\
+		for (i_ = 0; i_ < (count); i_++) {			\
+			char buf_[FWD_RULE_STRLEN];			\
+			fn("%s%s%s", prefix,				\
+			   fwd_rule_fmt(&(rules)[i_], buf_, sizeof(buf_)), \
+			   suffix);					\
+		}							\
+	} while (0)
+
 #endif /* FWD_RULE_H */
-- 
2.43.0


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

* [PATCH v8 06/19] pif: Limit pif names to 128 bytes
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (4 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 05/19] fwd: Generalise fwd_rules_info() Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 07/19] fwd_rule: Fix some format specifiers Stefano Brivio
                   ` (13 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

All current pif names are quite short, and we expect them to remain short
when/if we allow arbitrary pifs.  However, because of the structure of
the current code we don't enforce any limit on the length.

This will become more important with dynamic configuration updates, so
start enforcing a length limit.  Specifically we allow pif names to be up
to 128 bytes (PIF_NAME_SIZE), including the terminating \0.  This is
more or less arbitrary, but seems like it should be comfortably enough for
all the cases we have in mind.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
[sbrivio: Fixed typo in comment, reported by Laurent]
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 pif.c | 2 +-
 pif.h | 5 ++++-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/pif.c b/pif.c
index 1e80724..d5e3161 100644
--- a/pif.c
+++ b/pif.c
@@ -17,7 +17,7 @@
 #include "inany.h"
 #include "epoll_ctl.h"
 
-const char *pif_type_str[] = {
+const char pif_type_str[][PIF_NAME_SIZE] = {
 	[PIF_NONE]		= "<none>",
 	[PIF_HOST]		= "HOST",
 	[PIF_TAP]		= "TAP",
diff --git a/pif.h b/pif.h
index 7bb58e5..553c742 100644
--- a/pif.h
+++ b/pif.h
@@ -35,7 +35,9 @@ enum pif_type {
 	PIF_NUM_TYPES,
 };
 
-extern const char *pif_type_str[];
+/* Maximum size of a pif name, including \0 */
+#define	PIF_NAME_SIZE	(128)
+extern const char pif_type_str[][PIF_NAME_SIZE];
 
 static inline const char *pif_type(enum pif_type pt)
 {
@@ -43,6 +45,7 @@ static inline const char *pif_type(enum pif_type pt)
 		return pif_type_str[pt];
 	else
 		return "?";
+	static_assert(sizeof("?") <= PIF_NAME_SIZE);
 }
 
 static inline const char *pif_name(uint8_t pif)
-- 
2.43.0


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

* [PATCH v8 07/19] fwd_rule: Fix some format specifiers
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (5 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 06/19] pif: Limit pif names to 128 bytes Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 08/19] pesto: Introduce stub configuration tool Stefano Brivio
                   ` (12 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

The new warnings in fwd_rule_add() have some not quite technically correct
format specifiers.  For some weird reason these don't trip warnings on
passt itself, but do when we re-use this code in the upcoming configuration
client.  Fix them.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 fwd_rule.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/fwd_rule.c b/fwd_rule.c
index 777282d..7fd20dd 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -208,7 +208,7 @@ static int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
 		return -EINVAL;
 	}
 	if (new->flags & ~allowed_flags) {
-		warn("Rule has invalid flags 0x%hhx",
+		warn("Rule has invalid flags 0x%x",
 		     new->flags & ~allowed_flags);
 		return -EINVAL;
 	}
@@ -267,11 +267,11 @@ static int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
 	}
 
 	if (fwd->count >= ARRAY_SIZE(fwd->rules)) {
-		warn("Too many rules (maximum %u)", ARRAY_SIZE(fwd->rules));
+		warn("Too many rules (maximum %d)", ARRAY_SIZE(fwd->rules));
 		return -ENOSPC;
 	}
 	if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) {
-		warn("Rules require too many listening sockets (maximum %u)",
+		warn("Rules require too many listening sockets (maximum %d)",
 		     ARRAY_SIZE(fwd->socks));
 		return -ENOSPC;
 	}
-- 
2.43.0


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

* [PATCH v8 08/19] pesto: Introduce stub configuration tool
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (6 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 07/19] fwd_rule: Fix some format specifiers Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 09/19] pesto, log: Share log.h (but not log.c) with pesto tool Stefano Brivio
                   ` (11 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

Build a new "pesto" binary, which will become the tool to update a running
passt/pasta's configuration.  For now, we just build a stub binary which
sets up a basic environment, parses trivial command line options but does
nothing else.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
[sbrivio: Dropped leading _ from comment to include guard endif,
 reported by Laurent]
[sbrivio: Formatting changes in pesto.1: use 80 columns instead of
 wrapping at about 75. Add description for -d, -h, --version.]
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
---
 .gitignore |   2 +
 Makefile   |  42 +++++++++++------
 common.h   |  24 ++++++++++
 pesto.1    |  59 ++++++++++++++++++++++++
 pesto.c    | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 pesto.h    |  12 +++++
 util.h     |  12 +----
 7 files changed, 257 insertions(+), 26 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 3c16adc..3e40d9f 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 7875d23..030681b 100644
--- a/Makefile
+++ b/Makefile
@@ -47,19 +47,21 @@ PASST_SRCS = arch.c arp.c bitmap.c checksum.c conf.c dhcp.c dhcpv6.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)
-
-MANPAGES = passt.1 pasta.1 qrap.1 passt-repair.1
-
-PASST_HEADERS = arch.h arp.h bitmap.h checksum.h conf.h dhcp.h dhcpv6.h \
-	epoll_ctl.h flow.h fwd.h fwd_rule.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
+PESTO_SRCS = pesto.c
+SRCS = $(PASST_SRCS) $(QRAP_SRCS) $(PASST_REPAIR_SRCS) $(PESTO_SRCS)
+
+MANPAGES = passt.1 pasta.1 pesto.1 qrap.1 passt-repair.1
+
+PASST_HEADERS = arch.h arp.h bitmap.h checksum.h common.h conf.h dhcp.h \
+	dhcpv6.h epoll_ctl.h flow.h fwd.h fwd_rule.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 pesto.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
 QRAP_HEADERS = arp.h ip.h passt.h util.h
 PASST_REPAIR_HEADERS = linux_dep.h
+PESTO_HEADERS = common.h pesto.h
 
 C := \#include <sys/random.h>\nint main(){int a=getrandom(0, 0, 0);}
 ifeq ($(shell printf "$(C)" | $(CC) -S -xc - -o - >/dev/null 2>&1; echo $$?),0)
@@ -78,7 +80,7 @@ docdir		?= $(datarootdir)/doc/passt
 mandir		?= $(datarootdir)/man
 man1dir		?= $(mandir)/man1
 
-BASEBIN = passt qrap passt-repair
+BASEBIN = passt qrap passt-repair pesto
 ifeq ($(TARGET_ARCH),x86_64)
 BASEBIN += passt.avx2
 endif
@@ -100,6 +102,9 @@ seccomp.h: seccomp.sh $(PASST_SRCS) $(PASST_HEADERS)
 seccomp_repair.h: seccomp.sh $(PASST_REPAIR_SRCS) $(PASST_REPAIR_HEADERS)
 	@ 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)
+
 $(BASEBIN): %:
 	$(CC) $(BASE_CPPFLAGS) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $(filter %.c,$^) -o $@
 
@@ -116,6 +121,8 @@ qrap: $(QRAP_SRCS) $(QRAP_HEADERS)
 
 passt-repair: $(PASST_REPAIR_SRCS) $(PASST_REPAIR_HEADERS) seccomp_repair.h
 
+pesto: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
+
 valgrind: EXTRA_SYSCALLS += rt_sigprocmask rt_sigtimedwait rt_sigaction	\
 			    rt_sigreturn getpid gettid kill clock_gettime \
 			    mmap|mmap2 munmap open unlink gettimeofday futex \
@@ -126,7 +133,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
 
@@ -183,7 +190,8 @@ docs: README.md
 CLANG_TIDY = clang-tidy
 CLANG_TIDY_FLAGS = -DCLANG_TIDY_58992
 
-clang-tidy: passt.clang-tidy passt-repair.clang-tidy qrap.clang-tidy
+clang-tidy: passt.clang-tidy passt-repair.clang-tidy pesto.clang-tidy \
+	qrap.clang-tidy
 
 .PHONY: %.clang-tidy
 %.clang-tidy:
@@ -191,6 +199,7 @@ clang-tidy: passt.clang-tidy passt-repair.clang-tidy qrap.clang-tidy
 
 passt.clang-tidy: $(PASST_SRCS) $(PASST_HEADERS) seccomp.h
 passt-repair.clang-tidy: $(PASST_REPAIR_SRCS) $(PASST_REPAIR_HEADERS) seccomp_repair.h
+pesto.clang-tidy: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
 qrap.clang-tidy: $(QRAP_SRCS) $(QRAP_HEADERS)
 
 CPPCHECK = cppcheck
@@ -206,7 +215,7 @@ CPPCHECK_FLAGS = --std=c11 --error-exitcode=1 --enable=all --force	\
 	--suppress=unusedStructMember					\
 	 -D CPPCHECK_6936
 
-cppcheck: passt.cppcheck passt-repair.cppcheck qrap.cppcheck
+cppcheck: passt.cppcheck passt-repair.cppcheck pesto.cppcheck qrap.cppcheck
 
 .PHONY: %.cppcheck
 %.cppcheck:
@@ -215,6 +224,9 @@ cppcheck: passt.cppcheck passt-repair.cppcheck qrap.cppcheck
 passt.cppcheck: $(PASST_SRCS) $(PASST_HEADERS) seccomp.h
 passt-repair.cppcheck: $(PASST_REPAIR_SRCS) $(PASST_REPAIR_HEADERS) seccomp_repair.h
 
+pesto.cppcheck: CPPCHECK_FLAGS += --suppress=unmatchedSuppression
+pesto.cppcheck: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
+
 qrap.cppcheck: BASE_CPPFLAGS += -DARCH=\"$(TARGET_ARCH)\"
 qrap.cppcheck: CPPCHECK_FLAGS += --suppress=unusedFunction
 qrap.cppcheck: $(QRAP_SRCS) $(QRAP_HEADERS)
diff --git a/common.h b/common.h
new file mode 100644
index 0000000..f3506b4
--- /dev/null
+++ b/common.h
@@ -0,0 +1,24 @@
+/* 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
+
+#include <string.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__)
+
+#endif /* COMMON_H */
diff --git a/pesto.1 b/pesto.1
new file mode 100644
index 0000000..b06433d
--- /dev/null
+++ b/pesto.1
@@ -0,0 +1,59 @@
+.\" 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
+[\fIOPTION\fR]... \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 OPTIONS
+
+.TP
+.BR \-d ", " \-\-debug
+Be verbose.
+
+.TP
+.BR \-h ", " \-\-help
+Display a help message and exit.
+
+.TP
+.BR \-\-version
+Show version and exit.
+
+.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 it and/or modify
+it 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 0000000..9f2fa5d
--- /dev/null
+++ b/pesto.c
@@ -0,0 +1,132 @@
+// 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 <getopt.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 "pesto.h"
+
+static bool debug_flag = false;
+
+static char stdout_buf[BUFSIZ];
+
+#define die(...)							\
+	do {								\
+		FPRINTF(stderr, __VA_ARGS__);				\
+		FPRINTF(stderr, "\n");					\
+		exit(EXIT_FAILURE);					\
+	} 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)
+ *
+ * #syscalls:pesto exit_group fstat write
+ */
+static void usage(const char *name, FILE *f, int status)
+{
+	FPRINTF(f, "Usage: %s [OPTION]... PATH\n", name);
+	FPRINTF(f,
+		"\n"
+		"  -d, --debug		Print debugging messages\n"
+		"  -h, --help		Display this help message and exit\n"
+		"  --version		Show version and exit\n");
+	exit(status);
+}
+
+/**
+ * main() - Dynamic reconfiguration client main program
+ * @argc:	Argument count
+ * @argv:	Arguments: socket path, operation, port specifiers
+ *
+ * Return: 0 on success, won't return on failure
+ *
+ * #syscalls:pesto exit_group fstat read write
+ */
+int main(int argc, char **argv)
+{
+	const struct option options[] = {
+		{"debug",	no_argument,		NULL,		'd' },
+		{"help",	no_argument,		NULL,		'h' },
+		{"version",	no_argument,		NULL,		1 },
+		{ 0 },
+	};
+	const char *optstring = "dh";
+	struct sock_fprog prog;
+	int optname;
+
+	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");
+
+	/* Explicitly set stdout buffer, otherwise printf() might allocate,
+	 * breaking our seccomp profile.
+	 */
+	if (setvbuf(stdout, stdout_buf, _IOFBF, sizeof(stdout_buf)))
+		die("Failed to set stdout buffer");
+
+	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 'd':
+			debug_flag = true;
+			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);
+
+	printf("debug_flag=%d, path=\"%s\"\n", debug_flag, argv[optind]);
+
+	die("pesto is not implemented yet");
+}
diff --git a/pesto.h b/pesto.h
new file mode 100644
index 0000000..e9b329f
--- /dev/null
+++ b/pesto.h
@@ -0,0 +1,12 @@
+/* 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
+
+#endif /* PESTO_H */
diff --git a/util.h b/util.h
index 92aeabc..770ff93 100644
--- a/util.h
+++ b/util.h
@@ -19,16 +19,9 @@
 #include <sys/syscall.h>
 #include <net/ethernet.h>
 
+#include "common.h"
 #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
@@ -307,9 +300,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.43.0


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

* [PATCH v8 09/19] pesto, log: Share log.h (but not log.c) with pesto tool
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (7 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 08/19] pesto: Introduce stub configuration tool Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions Stefano Brivio
                   ` (10 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

In pesto we're going to want several levels of error/warning messages, much
like passt itself.  Particularly as we start to share mode code between
passt and pesto, we want to use a similar interface to emit those.  However
we don't want to use the same implementation - logging to a file or syslog
doesn't make sense for the command line tool.

To accomplish this loosely share log.h, but not log.c between pesto and
passt.  In fact, an #ifdef means even most of log.h isn't actually shared,
but we do provide similar warn(), die() etc. macros.

This includes the *_perror() variants, which need strerror().  However,
we want to avoid allocations for pesto as we do for passt, and strerror()
allocates in some libc versions.  Therefore, also move our workaround for
this to be shared with pesto.

Reviewed-by: Laurent Vivier <lvivier@redhat.com>
[dwg: Based on changes part of a larger patch by Stefano]
Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
[sbrivio: Dropped debug_perror_() as it's not used anyway, Laurent was
 asking about its name]
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 Makefile |  6 +++++-
 common.h | 32 ++++++++++++++++++++++++++++++++
 log.h    | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 pesto.c  | 14 ++++----------
 util.h   | 32 --------------------------------
 5 files changed, 93 insertions(+), 44 deletions(-)

diff --git a/Makefile b/Makefile
index 030681b..f6cec8a 100644
--- a/Makefile
+++ b/Makefile
@@ -61,7 +61,7 @@ PASST_HEADERS = arch.h arp.h bitmap.h checksum.h common.h conf.h dhcp.h \
 	vhost_user.h virtio.h vu_common.h
 QRAP_HEADERS = arp.h ip.h passt.h util.h
 PASST_REPAIR_HEADERS = linux_dep.h
-PESTO_HEADERS = common.h pesto.h
+PESTO_HEADERS = common.h pesto.h log.h
 
 C := \#include <sys/random.h>\nint main(){int a=getrandom(0, 0, 0);}
 ifeq ($(shell printf "$(C)" | $(CC) -S -xc - -o - >/dev/null 2>&1; echo $$?),0)
@@ -121,6 +121,7 @@ qrap: $(QRAP_SRCS) $(QRAP_HEADERS)
 
 passt-repair: $(PASST_REPAIR_SRCS) $(PASST_REPAIR_HEADERS) seccomp_repair.h
 
+pesto: BASE_CPPFLAGS += -DPESTO
 pesto: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
 
 valgrind: EXTRA_SYSCALLS += rt_sigprocmask rt_sigtimedwait rt_sigaction	\
@@ -221,9 +222,12 @@ cppcheck: passt.cppcheck passt-repair.cppcheck pesto.cppcheck qrap.cppcheck
 %.cppcheck:
 	$(CPPCHECK) $(CPPCHECK_FLAGS) $(BASE_CPPFLAGS) $^
 
+passt.cppcheck: BASE_CPPFLAGS += -UPESTO
 passt.cppcheck: $(PASST_SRCS) $(PASST_HEADERS) seccomp.h
+
 passt-repair.cppcheck: $(PASST_REPAIR_SRCS) $(PASST_REPAIR_HEADERS) seccomp_repair.h
 
+pesto.cppcheck: BASE_CPPFLAGS += -DPESTO
 pesto.cppcheck: CPPCHECK_FLAGS += --suppress=unmatchedSuppression
 pesto.cppcheck: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
 
diff --git a/common.h b/common.h
index f3506b4..4251781 100644
--- a/common.h
+++ b/common.h
@@ -21,4 +21,36 @@
 /* FPRINTF() intentionally silences cert-err33-c clang-tidy warnings */
 #define FPRINTF(f, ...)	(void)fprintf(f, __VA_ARGS__)
 
+/*
+ * Starting from glibc 2.40.9000 and commit 25a5eb4010df ("string: strerror,
+ * strsignal cannot use buffer after dlmopen (bug 32026)"), strerror() needs
+ * getrandom(2) and brk(2) as it allocates memory for the locale-translated
+ * error description, but our seccomp profiles forbid both.
+ *
+ * Use the strerror_() wrapper instead, calling into strerrordesc_np() to get
+ * a static untranslated string. It's a GNU implementation, but also defined by
+ * bionic.
+ *
+ * If strerrordesc_np() is not defined (e.g. musl), call strerror(). C libraries
+ * not defining strerrordesc_np() are expected to provide strerror()
+ * implementations that are simple enough for us to call.
+ */
+__attribute__ ((weak)) const char *strerrordesc_np(int errnum);
+
+/**
+ * strerror_() - strerror() wrapper calling strerrordesc_np() if available
+ * @errnum:	Error code
+ *
+ * Return: error description string
+ */
+static inline const char *strerror_(int errnum)
+{
+	if (strerrordesc_np)
+		return strerrordesc_np(errnum);
+
+	return strerror(errnum);
+}
+
+#define strerror(x) @ "Don't call strerror() directly, use strerror_() instead"
+
 #endif /* COMMON_H */
diff --git a/log.h b/log.h
index dbab006..c6befe3 100644
--- a/log.h
+++ b/log.h
@@ -6,8 +6,57 @@
 #ifndef LOG_H
 #define LOG_H
 
-#include <stdarg.h>
 #include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#ifdef PESTO
+
+#include <errno.h>
+
+#include "common.h"
+
+extern bool debug_flag;
+
+#define msg(...)							\
+	do {								\
+		FPRINTF(stderr, __VA_ARGS__);				\
+		FPRINTF(stderr, "\n");					\
+	} while (0)
+
+#define msg_perror(...)							\
+	do {								\
+		int errno_ = errno;					\
+		FPRINTF(stderr, __VA_ARGS__);				\
+		FPRINTF(stderr, ": %s\n", strerror_(errno_));		\
+	} while (0)
+
+#define die(...)							\
+	do {								\
+		msg(__VA_ARGS__);					\
+		exit(EXIT_FAILURE);					\
+	} while (0)
+
+#define die_perror(...)							\
+	do {								\
+		msg_perror(__VA_ARGS__);				\
+		exit(EXIT_FAILURE);					\
+	} while (0)
+
+#define warn(...)		msg(__VA_ARGS__)
+#define warn_perror(...)	msg_perror(__VA_ARGS__)
+#define info(...)		msg(__VA_ARGS__)
+#define info_perror(...)	msg_perror(__VA_ARGS__)
+
+#define debug(...)							\
+	do {								\
+		if (debug_flag)						\
+			msg(__VA_ARGS__);				\
+	} while (0)
+
+#else /* !PESTO */
+
+#include <stdarg.h>
 #include <stddef.h>
 #include <syslog.h>
 
@@ -109,4 +158,6 @@ 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 9f2fa5d..f0916e8 100644
--- a/pesto.c
+++ b/pesto.c
@@ -34,18 +34,12 @@
 #include "common.h"
 #include "seccomp_pesto.h"
 #include "pesto.h"
+#include "log.h"
 
-static bool debug_flag = false;
+bool debug_flag = false;
 
 static char stdout_buf[BUFSIZ];
 
-#define die(...)							\
-	do {								\
-		FPRINTF(stderr, __VA_ARGS__);				\
-		FPRINTF(stderr, "\n");					\
-		exit(EXIT_FAILURE);					\
-	} while (0)
-
 /**
  * usage() - Print usage, exit with given status code
  * @name:	Executable name
@@ -99,7 +93,7 @@ int main(int argc, char **argv)
 	 * breaking our seccomp profile.
 	 */
 	if (setvbuf(stdout, stdout_buf, _IOFBF, sizeof(stdout_buf)))
-		die("Failed to set stdout buffer");
+		die_perror("Failed to set stdout buffer");
 
 	do {
 		optname = getopt_long(argc, argv, optstring, options, NULL);
@@ -126,7 +120,7 @@ int main(int argc, char **argv)
 	if (argc - optind != 1)
 		usage(argv[0], stderr, EXIT_FAILURE);
 
-	printf("debug_flag=%d, path=\"%s\"\n", debug_flag, argv[optind]);
+	debug("debug_flag=%d, path=\"%s\"", debug_flag, argv[optind]);
 
 	die("pesto is not implemented yet");
 }
diff --git a/util.h b/util.h
index 770ff93..e90be47 100644
--- a/util.h
+++ b/util.h
@@ -302,38 +302,6 @@ static inline bool mod_between(unsigned x, unsigned i, unsigned j, unsigned m)
 
 void raw_random(void *buf, size_t buflen);
 
-/*
- * Starting from glibc 2.40.9000 and commit 25a5eb4010df ("string: strerror,
- * strsignal cannot use buffer after dlmopen (bug 32026)"), strerror() needs
- * getrandom(2) and brk(2) as it allocates memory for the locale-translated
- * error description, but our seccomp profiles forbid both.
- *
- * Use the strerror_() wrapper instead, calling into strerrordesc_np() to get
- * a static untranslated string. It's a GNU implementation, but also defined by
- * bionic.
- *
- * If strerrordesc_np() is not defined (e.g. musl), call strerror(). C libraries
- * not defining strerrordesc_np() are expected to provide strerror()
- * implementations that are simple enough for us to call.
- */
-__attribute__ ((weak)) const char *strerrordesc_np(int errnum);
-
-/**
- * strerror_() - strerror() wrapper calling strerrordesc_np() if available
- * @errnum:	Error code
- *
- * Return: error description string
- */
-static inline const char *strerror_(int errnum)
-{
-	if (strerrordesc_np)
-		return strerrordesc_np(errnum);
-
-	return strerror(errnum);
-}
-
-#define strerror(x) @ "Don't call strerror() directly, use strerror_() instead"
-
 /*
  * Workarounds for https://github.com/llvm/llvm-project/issues/58992
  *
-- 
2.43.0


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

* [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (8 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 09/19] pesto, log: Share log.h (but not log.c) with pesto tool Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-06  5:38   ` David Gibson
  2026-05-05 23:47 ` [PATCH v8 11/19] pesto: Expose list of pifs to pesto and display them Stefano Brivio
                   ` (9 subsequent siblings)
  19 siblings, 1 reply; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

Start implementing pesto in earnest.  Create a control/configuration
socket in passt.  Have pesto connect to it and retrieve a server greeting
Perform some basic version checking.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
[sbrivio: Avoid potential recursive calling between conf_accept() and
 conf_close(), reported by clang-tidy]
[sbrivio: In conf(), check we're not exceeding sizeof(c->control_path)
 instead of sizeof(c->socket_path), and, in pesto's main(), print
 argv[optind] instead of argv[1] to indicate an invalid socket path,
 both reported by Jon Maloy]
[sbrivio: In pesto's main(), drop unnecessary newline from error
 message, reported by Laurent]
[sbrivio: Don't use SOCK_NONBLOCK on accept4(), as that only applies
 to the *new* file descriptor, which we don't want -- set O_NONBLOCK
 on the listening file descriptor using fcntl()]
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 Makefile     |   8 ++-
 conf.c       | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 conf.h       |   2 +
 epoll_type.h |   4 ++
 passt.1      |   5 ++
 passt.c      |   8 +++
 passt.h      |   6 ++
 pesto.c      |  47 ++++++++++++-
 pesto.h      |  22 ++++++
 serialise.c  |   3 +
 10 files changed, 283 insertions(+), 6 deletions(-)

diff --git a/Makefile b/Makefile
index f6cec8a..1718ddb 100644
--- a/Makefile
+++ b/Makefile
@@ -47,7 +47,7 @@ PASST_SRCS = arch.c arp.c bitmap.c checksum.c conf.c dhcp.c dhcpv6.c \
 	vhost_user.c virtio.c vu_common.c
 QRAP_SRCS = qrap.c
 PASST_REPAIR_SRCS = passt-repair.c
-PESTO_SRCS = pesto.c
+PESTO_SRCS = pesto.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
@@ -61,7 +61,7 @@ PASST_HEADERS = arch.h arp.h bitmap.h checksum.h common.h conf.h dhcp.h \
 	vhost_user.h virtio.h vu_common.h
 QRAP_HEADERS = arp.h ip.h passt.h util.h
 PASST_REPAIR_HEADERS = linux_dep.h
-PESTO_HEADERS = common.h pesto.h log.h
+PESTO_HEADERS = common.h pesto.h log.h serialise.h
 
 C := \#include <sys/random.h>\nint main(){int a=getrandom(0, 0, 0);}
 ifeq ($(shell printf "$(C)" | $(CC) -S -xc - -o - >/dev/null 2>&1; echo $$?),0)
@@ -228,7 +228,9 @@ passt.cppcheck: $(PASST_SRCS) $(PASST_HEADERS) seccomp.h
 passt-repair.cppcheck: $(PASST_REPAIR_SRCS) $(PASST_REPAIR_HEADERS) seccomp_repair.h
 
 pesto.cppcheck: BASE_CPPFLAGS += -DPESTO
-pesto.cppcheck: CPPCHECK_FLAGS += --suppress=unmatchedSuppression
+pesto.cppcheck: CPPCHECK_FLAGS += \
+	--suppress=unusedFunction:serialise.c \
+	--suppress=staticFunction:serialise.c
 pesto.cppcheck: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
 
 qrap.cppcheck: BASE_CPPFLAGS += -DARCH=\"$(TARGET_ARCH)\"
diff --git a/conf.c b/conf.c
index 0586107..5ec0072 100644
--- a/conf.c
+++ b/conf.c
@@ -48,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"
 
@@ -541,6 +545,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");
 
@@ -779,6 +784,9 @@ static void conf_print(const struct ctx *c)
 	char buf[INANY_ADDRSTRLEN];
 	int i;
 
+	if (c->fd_control_listen >= 0)
+		info("Configuration socket: %s", c->control_path);
+
 	if (c->ifi4 > 0 || c->ifi6 > 0) {
 		char ifn[IFNAMSIZ];
 
@@ -1072,6 +1080,19 @@ 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_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->control_path);
+		}
+		if (fcntl(c->fd_control_listen, F_SETFL, O_NONBLOCK))
+			die_perror("Couldn't set O_NONBLOCK on control socket");
+	} else {
+		c->fd_control_listen = -1;
+	}
 }
 
 /**
@@ -1107,6 +1128,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_control_listen < 0)
+		return;
+
+	if (listen(c->fd_control_listen, 0))
+		die_perror("Couldn't listen on configuration socket");
+
+	ref.fd = c->fd_control_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
@@ -1189,9 +1229,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";
 	bool opt_t = false, opt_T = false, opt_u = false, opt_U = false;
 	char userns[PATH_MAX] = { 0 }, netns[PATH_MAX] = { 0 };
@@ -1449,6 +1490,13 @@ void conf(struct ctx *c, int argc, char **argv)
 
 			c->fd_tap = -1;
 			break;
+		case 'c':
+			ret = snprintf(c->control_path, sizeof(c->control_path),
+				       "%s", optarg);
+			if (ret <= 0 || ret >= (int)sizeof(c->control_path))
+				die("Invalid configuration path: %s", optarg);
+			c->fd_control_listen = c->fd_control = -1;
+			break;
 		case 'F':
 			errno = 0;
 			fd_tap_opt = strtol(optarg, NULL, 0);
@@ -1871,6 +1919,140 @@ void conf(struct ctx *c, int argc, char **argv)
 			fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]);
 	}
 
+	conf_sock_listen(c);
+
 	if (!c->quiet)
 		conf_print(c);
 }
+
+static void conf_accept(struct ctx *c);
+
+/**
+ * conf_close() - Close configuration / control socket and clean up
+ * @c:		Execution context
+ */
+static void conf_close(struct ctx *c)
+{
+	debug("Closing configuration socket");
+	epoll_ctl(c->epollfd, EPOLL_CTL_DEL, c->fd_control, NULL);
+	close(c->fd_control);
+	c->fd_control = -1;
+}
+
+/**
+ * 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)
+{
+	if (events != EPOLLIN) {
+		err("Unexpected event 0x%04x on configuration socket", events);
+		return;
+	}
+
+	if (c->fd_control >= 0) {
+		/* Ignore the new connection for now, blocking it until the
+		 * current one finishes.
+		 */
+		return;
+	}
+
+	conf_accept(c);
+}
+
+/**
+ * conf_accept() - Accept a new control connection
+ * @c:		Execution context
+ */
+static void conf_accept(struct ctx *c)
+{
+	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;
+
+retry:
+	err("%s: %i", __func__, __LINE__);
+	fd = accept4(c->fd_control_listen, NULL, NULL, SOCK_CLOEXEC);
+	if (fd < 0) {
+		err("%s: %i", __func__, __LINE__);
+		if (errno != EAGAIN)
+			warn_perror("accept4() on configuration listening socket");
+		return;
+	}
+
+	err("%s: %i", __func__, __LINE__);
+
+	if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &uc, &len) < 0)
+		warn_perror("Can't get configuration client credentials");
+
+	c->fd_control = 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;
+	}
+
+	info("Accepted configuration client, PID %i", uc.pid);
+	if (!PESTO_PROTOCOL_VERSION) {
+		warn(
+"Warning: Using experimental unsupported configuration protocol");
+	}
+
+	return;
+
+fail:
+	conf_close(c);
+	goto retry;
+}
+
+/**
+ * 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_control, 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:
+	conf_close(c);
+
+	/* Check if any other clients are waiting to connect */
+	conf_accept(c);
+}
diff --git a/conf.h b/conf.h
index b45ad74..16f9718 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 a90ffb6..061325a 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/passt.1 b/passt.1
index 6303aeb..908fd4a 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 f84419c..bc42ea3 100644
--- a/passt.c
+++ b/passt.c
@@ -80,6 +80,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 +305,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 62b8dcd..b3f049d 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
+ * @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
@@ -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_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
@@ -223,6 +226,7 @@ struct ctx {
 	int foreground;
 	int nofile;
 	char sock_path[UNIX_PATH_MAX];
+	char control_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_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 f0916e8..ab476c5 100644
--- a/pesto.c
+++ b/pesto.c
@@ -33,6 +33,7 @@
 
 #include "common.h"
 #include "seccomp_pesto.h"
+#include "serialise.h"
 #include "pesto.h"
 #include "log.h"
 
@@ -66,6 +67,8 @@ static void usage(const char *name, FILE *f, int status)
  *
  * Return: 0 on success, won't return on failure
  *
+ * #syscalls:pesto socket s390x:socketcall i686:socketcall
+ * #syscalls:pesto connect shutdown close
  * #syscalls:pesto exit_group fstat read write
  */
 int main(int argc, char **argv)
@@ -76,9 +79,12 @@ int main(int argc, char **argv)
 		{"version",	no_argument,		NULL,		1 },
 		{ 0 },
 	};
+	struct sockaddr_un a = { AF_UNIX, "" };
 	const char *optstring = "dh";
+	struct pesto_hello hello;
 	struct sock_fprog prog;
-	int optname;
+	int optname, ret, s;
+	uint32_t s_version;
 
 	prctl(PR_SET_DUMPABLE, 0);
 
@@ -122,5 +128,42 @@ int main(int argc, char **argv)
 
 	debug("debug_flag=%d, path=\"%s\"", debug_flag, argv[optind]);
 
-	die("pesto is not implemented yet");
+	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
+		die_perror("Failed to create AF_UNIX socket");
+
+	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[optind]);
+
+	ret = connect(s, (struct sockaddr *)&a, sizeof(a));
+	if (ret < 0) {
+		die_perror("Failed to connect to %s", a.sun_path);
+	}
+
+	ret = read_all_buf(s, &hello, sizeof(hello));
+	if (ret < 0)
+		die_perror("Couldn't read server greeting");
+
+	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,
+		    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");
+	}
+
+	if (shutdown(s, SHUT_RDWR) < 0 || close(s) < 0)
+		die_perror("Error shutting down control socket");
+
+	exit(0);
 }
diff --git a/pesto.h b/pesto.h
index e9b329f..92d4df3 100644
--- a/pesto.h
+++ b/pesto.h
@@ -9,4 +9,26 @@
 #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 944e741..346df99 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
-- 
2.43.0


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

* [PATCH v8 11/19] pesto: Expose list of pifs to pesto and display them
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (9 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 12/19] ip: Prepare ip.[ch] for sharing with pesto tool Stefano Brivio
                   ` (8 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

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 prints it out.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
[sbrivio: In read_pif_conf(), force a redundant termination of the
 interface name, the existing check isn't obvious enough for static
 checkers]
[sbrivio: Drop @resv_ left-over in description of struct
 pesto_pif_info, reported by Jon Maloy]
[sbrivio: Fix minor nits reported by Laurent]
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 Makefile    |   1 +
 common.h    |   2 +
 conf.c      |  41 ++++++++++++++++
 pesto.c     | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 pesto.h     |  18 ++++++-
 pif.h       |   5 +-
 serialise.c |   4 ++
 serialise.h |   1 +
 util.h      |   2 -
 9 files changed, 201 insertions(+), 7 deletions(-)

diff --git a/Makefile b/Makefile
index 1718ddb..6da76b4 100644
--- a/Makefile
+++ b/Makefile
@@ -223,6 +223,7 @@ cppcheck: passt.cppcheck passt-repair.cppcheck pesto.cppcheck qrap.cppcheck
 	$(CPPCHECK) $(CPPCHECK_FLAGS) $(BASE_CPPFLAGS) $^
 
 passt.cppcheck: BASE_CPPFLAGS += -UPESTO
+passt.cppcheck: CPPCHECK_FLAGS += --suppress=unusedFunction:serialise.c
 passt.cppcheck: $(PASST_SRCS) $(PASST_HEADERS) seccomp.h
 
 passt-repair.cppcheck: $(PASST_REPAIR_SRCS) $(PASST_REPAIR_HEADERS) seccomp_repair.h
diff --git a/common.h b/common.h
index 4251781..68573b4 100644
--- a/common.h
+++ b/common.h
@@ -53,4 +53,6 @@ static inline const char *strerror_(int errnum)
 
 #define strerror(x) @ "Don't call strerror() directly, use strerror_() instead"
 
+#define ARRAY_SIZE(a)		((int)(sizeof(a) / sizeof((a)[0])))
+
 #endif /* COMMON_H */
diff --git a/conf.c b/conf.c
index 5ec0072..39b1880 100644
--- a/conf.c
+++ b/conf.c
@@ -1927,6 +1927,43 @@ void conf(struct ctx *c, int argc, char **argv)
 
 static void conf_accept(struct ctx *c);
 
+/**
+ * conf_send_rules() - Send current forwarding rules to config client (pesto)
+ * @c:		Execution context
+ * @fd:		Socket to the client
+ *
+ * Return: 0 on success, -1 on failure
+ *
+ * FIXME: So far only sends pif ids and names
+ */
+static int conf_send_rules(const struct ctx *c, int fd)
+{
+	unsigned pif;
+
+	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
+		struct pesto_pif_info info;
+		int rc;
+
+		if (!c->fwd[pif])
+			continue;
+
+		assert(pif != PIF_NONE);
+
+		rc = snprintf(info.name, sizeof(info.name), "%s", pif_name(pif));
+		assert(rc >= 0 && (size_t)rc < sizeof(info.name));
+
+		if (write_u8(fd, pif) < 0)
+			return -1;
+		if (write_all_buf(fd, &info, sizeof(info)) < 0)
+			return -1;
+	}
+
+	if (write_u8(fd, PIF_NONE) < 0)
+		return -1;
+
+	return 0;
+}
+
 /**
  * conf_close() - Close configuration / control socket and clean up
  * @c:		Execution context
@@ -1970,6 +2007,7 @@ static void conf_accept(struct ctx *c)
 	struct pesto_hello hello = {
 		.magic = PESTO_SERVER_MAGIC,
 		.version = htonl(PESTO_PROTOCOL_VERSION),
+		.pif_name_size = htonl(PIF_NAME_SIZE),
 	};
 	union epoll_ref ref = { .type = EPOLL_TYPE_CONF };
 	struct ucred uc = { 0 };
@@ -2010,6 +2048,9 @@ retry:
 "Warning: Using experimental unsupported configuration protocol");
 	}
 
+	if (conf_send_rules(c, fd) < 0)
+		goto fail;
+
 	return;
 
 fail:
diff --git a/pesto.c b/pesto.c
index ab476c5..b33492a 100644
--- a/pesto.c
+++ b/pesto.c
@@ -60,6 +60,127 @@ static void usage(const char *name, FILE *f, int status)
 	exit(status);
 }
 
+/* Maximum number of pifs with rule tables */
+#define MAX_PIFS	3
+
+struct pif_configuration {
+	uint8_t pif;
+	char name[PIF_NAME_SIZE];
+};
+
+struct configuration {
+	uint32_t npifs;
+	struct pif_configuration pif[MAX_PIFS];
+};
+
+/**
+ * pif_conf_by_num() - Find a pif's configuration by pif id
+ * @conf:	Configuration description
+ * @pif:	pif id
+ *
+ * Return: pointer to the pif_configuration for @pif, or NULL if not found
+ */
+static struct pif_configuration *pif_conf_by_num(struct configuration *conf,
+						 uint8_t pif)
+{
+	unsigned i;
+
+	for (i = 0; i < conf->npifs; i++) {
+		if (conf->pif[i].pif == pif)
+			return &conf->pif[i];
+	}
+
+	return NULL;
+}
+
+/**
+ * pif_conf_by_name() - Find a pif's configuration by name
+ * @conf:	Configuration description
+ * @name:	Interface name
+ *
+ * Return: pif_configuration for pif named @name, or NULL if not found
+ */
+static struct pif_configuration *pif_conf_by_name(struct configuration *conf,
+						  const char *name)
+{
+	unsigned i;
+
+	for (i = 0; i < conf->npifs; i++) {
+		if (strcmp(conf->pif[i].name, name) == 0)
+			return &conf->pif[i];
+	}
+
+	return NULL;
+}
+
+/**
+ * pesto_read_rules() - Read rulestate from passt/pasta
+ * @fd:		Control socket
+ * @conf:	Configuration description to update
+ */
+static bool read_pif_conf(int fd, struct configuration *conf)
+{
+	struct pif_configuration *pc;
+	struct pesto_pif_info info;
+	uint8_t pif;
+
+	if (read_u8(fd, &pif) < 0)
+		die("Error reading from control socket");
+
+	if (pif == PIF_NONE)
+		return false;
+
+	debug("Receiving config for PIF %"PRIu8, pif);
+
+	if (conf->npifs >= ARRAY_SIZE(conf->pif)) {
+		die("passt has more pifs than pesto can manage (max %d)",
+		    ARRAY_SIZE(conf->pif));
+	}
+
+	pc = &conf->pif[conf->npifs];
+	pc->pif = pif;
+
+	if (read_all_buf(fd, &info, sizeof(info)) < 0)
+		die("Error reading from control socket");
+
+	if (info.name[sizeof(info.name)-1])
+		die("Interface name was not NULL terminated");
+	/* Redundant, to make static checkers happy */
+	info.name[sizeof(info.name) - 1] = '\0';
+
+	static_assert(sizeof(info.name) == sizeof(pc->name),
+		      "Mismatching pif name lengths");
+	memcpy(pc->name, info.name, sizeof(pc->name));
+
+	debug("PIF %"PRIu8": %s", pc->pif, pc->name);
+
+	/* O(n^2), but n is bounded by MAX_PIFS */
+	if (pif_conf_by_num(conf, pc->pif))
+		die("Received duplicate interface identifier");
+
+	/* O(n^2), but n is bounded by MAX_PIFS */
+	if (pif_conf_by_name(conf, pc->name))
+		die("Received duplicate interface name");
+
+	conf->npifs++;
+	return true;
+}
+
+/**
+ * show_conf() - Show current configuration obtained from passt/pasta
+ * @conf:	Configuration description
+ */
+static void show_conf(const struct configuration *conf)
+{
+	unsigned i;
+
+	for (i = 0; i < conf->npifs; i++) {
+		const struct pif_configuration *pc = &conf->pif[i];
+		printf("  %s\n", pc->name);
+		printf("    TBD\n");
+	}
+}
+
 /**
  * main() - Dynamic reconfiguration client main program
  * @argc:	Argument count
@@ -80,6 +201,7 @@ int main(int argc, char **argv)
 		{ 0 },
 	};
 	struct sockaddr_un a = { AF_UNIX, "" };
+	struct configuration conf = { 0 };
 	const char *optstring = "dh";
 	struct pesto_hello hello;
 	struct sock_fprog prog;
@@ -162,6 +284,18 @@ int main(int argc, char **argv)
 "Warning: Using experimental protocol version, client and server must match\n");
 	}
 
+	if (ntohl(hello.pif_name_size) != PIF_NAME_SIZE) {
+		die("Server has unexpected pif name size (%"
+		    PRIu32" not %"PRIu32 ")",
+		    ntohl(hello.pif_name_size), PIF_NAME_SIZE);
+	}
+
+	while (read_pif_conf(s, &conf))
+		;
+
+	printf("passt/pasta configuration (%s)\n", a.sun_path);
+	show_conf(&conf);
+
 	if (shutdown(s, SHUT_RDWR) < 0 || close(s) < 0)
 		die_perror("Error shutting down control socket");
 
diff --git a/pesto.h b/pesto.h
index 92d4df3..b0064a0 100644
--- a/pesto.h
+++ b/pesto.h
@@ -17,18 +17,32 @@
 /* Version 0 is reserved for unreleased / unsupported experimental versions */
 #define PESTO_PROTOCOL_VERSION	0
 
+/* Maximum size of a pif name, including \0 */
+#define	PIF_NAME_SIZE	(128)
+#define PIF_NONE	0
+
 /**
  * struct pesto_hello - Server introduction message
- * @magic:	PESTO_SERVER_MAGIC
- * @version:	Version number
+ * @magic:		PESTO_SERVER_MAGIC
+ * @version:		Version number
+ * @pif_name_size:	Server's value for PIF_NAME_SIZE
  */
 struct pesto_hello {
 	char magic[8];
 	uint32_t version;
+	uint32_t pif_name_size;
 } __attribute__ ((__packed__));
 
 static_assert(sizeof(PESTO_SERVER_MAGIC)
 	      == sizeof(((struct pesto_hello *)0)->magic),
 	      "PESTO_SERVER_MAGIC has wrong size");
 
+/**
+ * struct pesto_pif_info - Message with basic metadata about a pif
+ * @name:	Name (\0 terminated)
+ */
+struct pesto_pif_info {
+	char name[PIF_NAME_SIZE];
+} __attribute__ ((__packed__));
+
 #endif /* PESTO_H */
diff --git a/pif.h b/pif.h
index 553c742..48d4919 100644
--- a/pif.h
+++ b/pif.h
@@ -11,6 +11,7 @@
 
 #include <netinet/in.h>
 
+#include "pesto.h"
 #include "epoll_type.h"
 
 union inany_addr;
@@ -24,7 +25,7 @@ union sockaddr_inany;
  */
 enum pif_type {
 	/* Invalid or not present pif */
-	PIF_NONE = 0,
+	PIF_NONE_ = PIF_NONE,
 	/* Host socket interface */
 	PIF_HOST,
 	/* Qemu socket or namespace tuntap interface */
@@ -35,8 +36,6 @@ enum pif_type {
 	PIF_NUM_TYPES,
 };
 
-/* Maximum size of a pif name, including \0 */
-#define	PIF_NAME_SIZE	(128)
 extern const char pif_type_str[][PIF_NAME_SIZE];
 
 static inline const char *pif_type(enum pif_type pt)
diff --git a/serialise.c b/serialise.c
index 346df99..e083112 100644
--- a/serialise.c
+++ b/serialise.c
@@ -121,6 +121,10 @@ 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_UINT
diff --git a/serialise.h b/serialise.h
index a88f3de..4714f4c 100644
--- a/serialise.h
+++ b/serialise.h
@@ -16,6 +16,7 @@ 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)
 
 #endif /* SERIALISE_H */
diff --git a/util.h b/util.h
index e90be47..c788382 100644
--- a/util.h
+++ b/util.h
@@ -87,8 +87,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)++)
 
-- 
2.43.0


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

* [PATCH v8 12/19] ip: Prepare ip.[ch] for sharing with pesto tool
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (10 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 11/19] pesto: Expose list of pifs to pesto and display them Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 13/19] inany: Prepare inany.[ch] " Stefano Brivio
                   ` (7 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

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>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
[sbrivio: Dropped duplicate definition of __bswap_constant_32() while
 at it, reported by Laurent]
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 common.h | 42 ++++++++++++++++++++++++++++++++++++++++++
 inany.h  |  2 ++
 ip.c     | 56 +++-----------------------------------------------------
 ip.h     |  4 +---
 tap.c    | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 util.h   | 48 ------------------------------------------------
 6 files changed, 100 insertions(+), 104 deletions(-)

diff --git a/common.h b/common.h
index 68573b4..ae6908d 100644
--- a/common.h
+++ b/common.h
@@ -55,4 +55,46 @@ static inline const char *strerror_(int errnum)
 
 #define ARRAY_SIZE(a)		((int)(sizeof(a) / sizeof((a)[0])))
 
+#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_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/inany.h b/inany.h
index 30e2416..1f7741d 100644
--- a/inany.h
+++ b/inany.h
@@ -10,8 +10,10 @@
 #define INANY_H
 
 #include <assert.h>
+#include <stdbool.h>
 #include <string.h>
 
+#include "util.h"
 #include "ip.h"
 #include "siphash.h"
 
diff --git a/ip.c b/ip.c
index 25fa407..f2506bb 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
@@ -16,61 +19,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 fb4119a..aab9b86 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,8 +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);
-
 #define IPPROTO_STRLEN		(sizeof("<unknown protocol>"))
 const char *ipproto_name(uint8_t proto);
 
diff --git a/tap.c b/tap.c
index 7d06189..cfd7b64 100644
--- a/tap.c
+++ b/tap.c
@@ -874,6 +874,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 c788382..dc14c78 100644
--- a/util.h
+++ b/util.h
@@ -101,54 +101,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.43.0


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

* [PATCH v8 13/19] inany: Prepare inany.[ch] for sharing with pesto tool
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (11 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 12/19] ip: Prepare ip.[ch] for sharing with pesto tool Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 14/19] pesto: Read current ruleset from passt/pasta and optionally display it Stefano Brivio
                   ` (6 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

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>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 common.h   | 20 ++++++++++++++++++--
 flow.c     |  4 ++--
 fwd.c      |  2 +-
 fwd.h      |  1 +
 fwd_rule.h |  1 +
 inany.c    | 19 ++++++++++++++-----
 inany.h    | 17 ++---------------
 pif.h      |  1 +
 siphash.h  | 13 +++++++++++++
 util.h     | 16 ----------------
 10 files changed, 53 insertions(+), 41 deletions(-)

diff --git a/common.h b/common.h
index ae6908d..f2518f6 100644
--- a/common.h
+++ b/common.h
@@ -18,9 +18,27 @@
 	"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 MIN
+#define MIN(x, y)		(((x) < (y)) ? (x) : (y))
+#endif
+#ifndef MAX
+#define MAX(x, y)		(((x) > (y)) ? (x) : (y))
+#endif
+
+#define MAX_FROM_BITS(n)	(((1U << (n)) - 1))
+
 /* 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])))
+
+#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 UINT16_STRLEN		(sizeof("65535"))
+
 /*
  * Starting from glibc 2.40.9000 and commit 25a5eb4010df ("string: strerror,
  * strsignal cannot use buffer after dlmopen (bug 32026)"), strerror() needs
@@ -53,8 +71,6 @@ static inline const char *strerror_(int errnum)
 
 #define strerror(x) @ "Don't call strerror() directly, use strerror_() instead"
 
-#define ARRAY_SIZE(a)		((int)(sizeof(a) / sizeof((a)[0])))
-
 #ifndef __bswap_constant_16
 #define __bswap_constant_16(x)						\
 	((uint16_t) ((((x) >> 8) & 0xff) | (((x) & 0xff) << 8)))
diff --git a/flow.c b/flow.c
index 56a6c6d..91f2b81 100644
--- a/flow.c
+++ b/flow.c
@@ -680,8 +680,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 728a783..8849cfc 100644
--- a/fwd.c
+++ b/fwd.c
@@ -80,7 +80,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/fwd.h b/fwd.h
index 8f845d0..ac24782 100644
--- a/fwd.h
+++ b/fwd.h
@@ -19,6 +19,7 @@
 #include "fwd_rule.h"
 
 struct flowside;
+struct ctx;
 
 #define FWD_NO_HINT	(-1)
 
diff --git a/fwd_rule.h b/fwd_rule.h
index 5855138..f51f1b4 100644
--- a/fwd_rule.h
+++ b/fwd_rule.h
@@ -13,6 +13,7 @@
 #include <net/if.h>
 #include <netinet/in.h>
 
+#include "common.h"
 #include "ip.h"
 #include "inany.h"
 #include "bitmap.h"
diff --git a/inany.c b/inany.c
index 2a586ed..23faf3f 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>
@@ -13,9 +23,8 @@
 #include <arpa/inet.h>
 #include <errno.h>
 
-#include "util.h"
+#include "common.h"
 #include "ip.h"
-#include "siphash.h"
 #include "inany.h"
 #include "fwd.h"
 
diff --git a/inany.h b/inany.h
index 1f7741d..73385b9 100644
--- a/inany.h
+++ b/inany.h
@@ -11,13 +11,11 @@
 
 #include <assert.h>
 #include <stdbool.h>
+#include <stddef.h>
 #include <string.h>
 
-#include "util.h"
+#include "common.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
@@ -301,17 +299,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/pif.h b/pif.h
index 48d4919..62223d1 100644
--- a/pif.h
+++ b/pif.h
@@ -16,6 +16,7 @@
 
 union inany_addr;
 union sockaddr_inany;
+struct ctx;
 
 /**
  * enum pif_type - Type of passt/pasta interface ("pif")
diff --git a/siphash.h b/siphash.h
index bbddcac..313b894 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 dc14c78..70aadeb 100644
--- a/util.h
+++ b/util.h
@@ -29,20 +29,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))
-#define ROUND_UP(x, y)		(((x) + (y) - 1) & ~((y) - 1))
-
-#define MAX_FROM_BITS(n)	(((1U << (n)) - 1))
-
 #define SWAP(a, b)							\
 	do {								\
 		__typeof__(a) __x = (a); (a) = (b); (b) = __x;		\
@@ -202,8 +188,6 @@ static inline const char *af_name(sa_family_t af)
 	}
 }
 
-#define UINT16_STRLEN		(sizeof("65535"))
-
 /* inet address (- '\0') + port (u16) (- '\0') + ':' + '\0' */
 #define SOCKADDR_INET_STRLEN					\
 	(INET_ADDRSTRLEN-1 + UINT16_STRLEN-1 + sizeof(":"))
-- 
2.43.0


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

* [PATCH v8 14/19] pesto: Read current ruleset from passt/pasta and optionally display it
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (12 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 13/19] inany: Prepare inany.[ch] " Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 15/19] pesto: Parse and add new rules from command line Stefano Brivio
                   ` (5 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

Implement serialisation of our current forwarding rules in conf.c,
deserialising it to display in the pesto client.  Doing this requires
adding ip.c, inany.c, bitmap.c, lineread.c and fwd_rule.c to the pesto
build.  With previous preparations that now requires only a trivial change
to lineread.c.

Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
[sbrivio: Use ntohs() for rule->to instead of htons() in
 fwd_rule_read(), reported by Jon Maloy]
[sbrivio: Add upper bound check on pc->fwd.count for count of rules
 received by pesto, reported missing by Laurent, plus nits also
 reported by Laurent]
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 Makefile   | 18 +++++++++++++-----
 conf.c     | 12 +++++++++++-
 fwd_rule.c | 40 ++++++++++++++++++++++++++++++++++++++++
 fwd_rule.h |  4 ++++
 lineread.c |  2 +-
 pesto.c    | 38 ++++++++++++++++++++++++++++++++++++--
 pesto.h    |  6 ++++++
 7 files changed, 111 insertions(+), 9 deletions(-)

diff --git a/Makefile b/Makefile
index 6da76b4..9e99dd1 100644
--- a/Makefile
+++ b/Makefile
@@ -47,7 +47,7 @@ PASST_SRCS = arch.c arp.c bitmap.c checksum.c conf.c dhcp.c dhcpv6.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 bitmap.c fwd_rule.c inany.c ip.c lineread.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
@@ -61,7 +61,8 @@ PASST_HEADERS = arch.h arp.h bitmap.h checksum.h common.h conf.h dhcp.h \
 	vhost_user.h virtio.h vu_common.h
 QRAP_HEADERS = arp.h ip.h passt.h util.h
 PASST_REPAIR_HEADERS = linux_dep.h
-PESTO_HEADERS = common.h pesto.h log.h serialise.h
+PESTO_HEADERS = common.h pesto.h bitmap.h fwd_rule.h inany.h ip.h lineread.h \
+	log.h serialise.h
 
 C := \#include <sys/random.h>\nint main(){int a=getrandom(0, 0, 0);}
 ifeq ($(shell printf "$(C)" | $(CC) -S -xc - -o - >/dev/null 2>&1; echo $$?),0)
@@ -223,15 +224,22 @@ cppcheck: passt.cppcheck passt-repair.cppcheck pesto.cppcheck qrap.cppcheck
 	$(CPPCHECK) $(CPPCHECK_FLAGS) $(BASE_CPPFLAGS) $^
 
 passt.cppcheck: BASE_CPPFLAGS += -UPESTO
-passt.cppcheck: CPPCHECK_FLAGS += --suppress=unusedFunction:serialise.c
+passt.cppcheck: CPPCHECK_FLAGS += \
+	--suppress=unusedFunction:fwd_rule.c \
+	--suppress=unusedFunction:serialise.c
 passt.cppcheck: $(PASST_SRCS) $(PASST_HEADERS) seccomp.h
 
 passt-repair.cppcheck: $(PASST_REPAIR_SRCS) $(PASST_REPAIR_HEADERS) seccomp_repair.h
 
 pesto.cppcheck: BASE_CPPFLAGS += -DPESTO
 pesto.cppcheck: CPPCHECK_FLAGS += \
-	--suppress=unusedFunction:serialise.c \
-	--suppress=staticFunction:serialise.c
+	--suppress=unusedFunction:bitmap.c \
+	--suppress=unusedFunction:inany.h \
+	--suppress=unusedFunction:inany.c \
+	--suppress=unusedFunction:ip.h \
+	--suppress=unusedFunction:fwd_rule.c \
+	--suppress=staticFunction:fwd_rule.c \
+	--suppress=unusedFunction:serialise.c
 pesto.cppcheck: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
 
 qrap.cppcheck: BASE_CPPFLAGS += -DARCH=\"$(TARGET_ARCH)\"
diff --git a/conf.c b/conf.c
index 39b1880..844be60 100644
--- a/conf.c
+++ b/conf.c
@@ -1941,21 +1941,30 @@ static int conf_send_rules(const struct ctx *c, int fd)
 	unsigned pif;
 
 	for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
+		struct fwd_table *fwd = c->fwd[pif];
 		struct pesto_pif_info info;
+		unsigned i;
 		int rc;
 
-		if (!c->fwd[pif])
+		if (!fwd)
 			continue;
 
 		assert(pif != PIF_NONE);
 
 		rc = snprintf(info.name, sizeof(info.name), "%s", pif_name(pif));
 		assert(rc >= 0 && (size_t)rc < sizeof(info.name));
+		info.caps = htonl(fwd->caps);
+		info.count = htonl(fwd->count);
 
 		if (write_u8(fd, pif) < 0)
 			return -1;
 		if (write_all_buf(fd, &info, sizeof(info)) < 0)
 			return -1;
+
+		for (i = 0; i < fwd->count; i++) {
+			if (fwd_rule_write(fd, &fwd->rules[i]))
+				return -1;
+		}
 	}
 
 	if (write_u8(fd, PIF_NONE) < 0)
@@ -2008,6 +2017,7 @@ static void conf_accept(struct ctx *c)
 		.magic = PESTO_SERVER_MAGIC,
 		.version = htonl(PESTO_PROTOCOL_VERSION),
 		.pif_name_size = htonl(PIF_NAME_SIZE),
+		.ifnamsiz = htonl(IFNAMSIZ),
 	};
 	union epoll_ref ref = { .type = EPOLL_TYPE_CONF };
 	struct ucred uc = { 0 };
diff --git a/fwd_rule.c b/fwd_rule.c
index 7fd20dd..c2824d5 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -24,6 +24,7 @@
 #include "fwd_rule.h"
 #include "lineread.h"
 #include "log.h"
+#include "serialise.h"
 
 /* Ephemeral port range: values from RFC 6335 */
 static in_port_t fwd_ephemeral_min = (1 << 15) + (1 << 14);
@@ -645,3 +646,42 @@ void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
 
 	fwd_rule_parse_ports(fwd, proto, addr, ifname, spec);
 }
+
+/**
+ * fwd_rule_read() - Read serialised rule from an fd
+ * @fd:		fd to deserialise from
+ * @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 = ntohs(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 f51f1b4..330d49e 100644
--- a/fwd_rule.h
+++ b/fwd_rule.h
@@ -29,6 +29,8 @@
 #define FWD_CAP_UDP		BIT(3)
 #define FWD_CAP_SCAN		BIT(4)
 #define FWD_CAP_IFNAME		BIT(5)
+#define FWD_CAP_ALL		(FWD_CAP_IPV4 | FWD_CAP_IPV6 | FWD_CAP_TCP | \
+				 FWD_CAP_UDP | FWD_CAP_SCAN | FWD_CAP_IFNAME)
 
 /**
  * struct fwd_rule - Forwarding rule governing a range of ports
@@ -99,6 +101,8 @@ void fwd_probe_ephemeral(void);
 const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
 const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size);
 void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd);
+int fwd_rule_read(int fd, struct fwd_rule *rule);
+int fwd_rule_write(int fd, const struct fwd_rule *rule);
 
 /**
  * fwd_rules_dump() - Dump forwarding rules
diff --git a/lineread.c b/lineread.c
index b9ceae1..a4269a6 100644
--- a/lineread.c
+++ b/lineread.c
@@ -19,8 +19,8 @@
 #include <stdbool.h>
 #include <unistd.h>
 
+#include "common.h"
 #include "lineread.h"
-#include "util.h"
 
 /**
  * lineread_init() - Prepare for line by line file reading without allocation
diff --git a/pesto.c b/pesto.c
index b33492a..92a8cb2 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"
 #include "log.h"
 
@@ -66,6 +67,7 @@ static void usage(const char *name, FILE *f, int status)
 struct pif_configuration {
 	uint8_t pif;
 	char name[PIF_NAME_SIZE];
+	struct fwd_table fwd;
 };
 
 struct configuration {
@@ -123,6 +125,7 @@ static bool read_pif_conf(int fd, struct configuration *conf)
 	struct pif_configuration *pc;
 	struct pesto_pif_info info;
 	uint8_t pif;
+	unsigned i;
 
 	if (read_u8(fd, &pif) < 0)
 		die("Error reading from control socket");
@@ -151,8 +154,20 @@ static bool read_pif_conf(int fd, struct configuration *conf)
 	static_assert(sizeof(info.name) == sizeof(pc->name),
 		      "Mismatching pif name lengths");
 	memcpy(pc->name, info.name, sizeof(pc->name));
+	pc->fwd.caps = ntohl(info.caps);
+
+	pc->fwd.count = ntohl(info.count);
+	if (pc->fwd.count > MAX_FWD_RULES)
+		die("Too many forwarding rules");
 
-	debug("PIF %"PRIu8": %s", pc->pif, pc->name);
+	debug("PIF %"PRIu8": %s, %"PRIu32" rules, capabilities 0x%"PRIx32
+	      ":%s%s%s%s%s%s", pc->pif, pc->name, pc->fwd.count, pc->fwd.caps,
+	      pc->fwd.caps & FWD_CAP_IPV4 ? " IPv4" : "",
+	      pc->fwd.caps & FWD_CAP_IPV6 ? " IPv6" : "",
+	      pc->fwd.caps & FWD_CAP_TCP ? " TCP" : "",
+	      pc->fwd.caps & FWD_CAP_UDP ? " UDP" : "",
+	      pc->fwd.caps & FWD_CAP_SCAN ? " scan" : "",
+	      pc->fwd.caps & FWD_CAP_IFNAME ? " ifname" : "");
 
 	/* O(n^2), but n is bounded by MAX_PIFS */
 	if (pif_conf_by_num(conf, pc->pif))
@@ -162,6 +177,18 @@ static bool read_pif_conf(int fd, struct configuration *conf)
 	if (pif_conf_by_name(conf, pc->name))
 		die("Received duplicate interface name");
 
+	/* NOTE: We read the fwd rules directly into fwd.rules, rather than
+	 * using fwd_rule_add().  This means we can read and display rules even
+	 * if something has gone wrong (in pesto or passt) and we get rules that
+	 * fwd_rule_add() would reject.  It does have the side effect that we
+	 * never assign socket space for the fwd rules, but we don't need that
+	 * within pesto.
+	 */
+	for (i = 0; i < pc->fwd.count; i++) {
+		if (fwd_rule_read(fd, &pc->fwd.rules[i]) < 0)
+			die("Error reading from control socket");
+	}
+
 	conf->npifs++;
 	return true;
 }
@@ -177,7 +204,8 @@ static void show_conf(const struct configuration *conf)
 	for (i = 0; i < conf->npifs; i++) {
 		const struct pif_configuration *pc = &conf->pif[i];
 		printf("  %s\n", pc->name);
-		printf("    TBD\n");
+		fwd_rules_dump(printf, pc->fwd.rules, pc->fwd.count,
+			       "    ", "\n");
 	}
 }
 
@@ -290,6 +318,12 @@ int main(int argc, char **argv)
 		    ntohl(hello.pif_name_size), PIF_NAME_SIZE);
 	}
 
+	if (ntohl(hello.ifnamsiz) != IFNAMSIZ) {
+		die("Server has unexpected IFNAMSIZ (%"
+		    PRIu32" not %"PRIu32 ")",
+		    ntohl(hello.ifnamsiz), IFNAMSIZ);
+	}
+
 	while (read_pif_conf(s, &conf))
 		;
 
diff --git a/pesto.h b/pesto.h
index b0064a0..981c084 100644
--- a/pesto.h
+++ b/pesto.h
@@ -26,11 +26,13 @@
  * @magic:		PESTO_SERVER_MAGIC
  * @version:		Version number
  * @pif_name_size:	Server's value for PIF_NAME_SIZE
+ * @ifnamsiz:		Server's value for IFNAMSIZ
  */
 struct pesto_hello {
 	char magic[8];
 	uint32_t version;
 	uint32_t pif_name_size;
+	uint32_t ifnamsiz;
 } __attribute__ ((__packed__));
 
 static_assert(sizeof(PESTO_SERVER_MAGIC)
@@ -40,9 +42,13 @@ static_assert(sizeof(PESTO_SERVER_MAGIC)
 /**
  * struct pesto_pif_info - Message with basic metadata about a pif
  * @name:	Name (\0 terminated)
+ * @caps:	Forwarding capabilities for this pif
+ * @count:	Number of forwarding rules for this pif
  */
 struct pesto_pif_info {
 	char name[PIF_NAME_SIZE];
+	uint32_t caps;
+	uint32_t count;
 } __attribute__ ((__packed__));
 
 #endif /* PESTO_H */
-- 
2.43.0


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

* [PATCH v8 15/19] pesto: Parse and add new rules from command line
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (13 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 14/19] pesto: Read current ruleset from passt/pasta and optionally display it Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-06  7:13   ` Laurent Vivier
  2026-05-05 23:47 ` [PATCH v8 16/19] pesto, conf: Send updated rules from pesto back to passt/pasta Stefano Brivio
                   ` (4 subsequent siblings)
  19 siblings, 1 reply; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

This adds parsing of options using fwd_rule_parse(), validates them and
adds them to the existing rules. It doesn't yet send those rules back to
passt or pasta.

Message-ID: <20260322141843.4095972-3-sbrivio@redhat.com>
[dwg: Based on an early draft by Stefano]
Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
[sbrivio: Recycled usage messages for -T and -U from conf.c as
 suggested by Laurent, dropped unrelated whitespace change]
[sbrivio: Add description of -t, -u, -T, -U to pesto.1]
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
---
 Makefile   |   1 +
 fwd_rule.c |   2 +-
 fwd_rule.h |   1 +
 pesto.1    | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 pesto.c    | 111 ++++++++++++++++++++++++++++++++++++++++++++--
 5 files changed, 237 insertions(+), 5 deletions(-)

diff --git a/Makefile b/Makefile
index 9e99dd1..c746b55 100644
--- a/Makefile
+++ b/Makefile
@@ -226,6 +226,7 @@ cppcheck: passt.cppcheck passt-repair.cppcheck pesto.cppcheck qrap.cppcheck
 passt.cppcheck: BASE_CPPFLAGS += -UPESTO
 passt.cppcheck: CPPCHECK_FLAGS += \
 	--suppress=unusedFunction:fwd_rule.c \
+	--suppress=staticFunction:fwd_rule.c \
 	--suppress=unusedFunction:serialise.c
 passt.cppcheck: $(PASST_SRCS) $(PASST_HEADERS) seccomp.h
 
diff --git a/fwd_rule.c b/fwd_rule.c
index c2824d5..b55e4df 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -187,7 +187,7 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
  *
  * Return: 0 on success, negative error code on failure
  */
-static int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
+int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
 {
 	/* Flags which can be set from the caller */
 	const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
diff --git a/fwd_rule.h b/fwd_rule.h
index 330d49e..f43b37d 100644
--- a/fwd_rule.h
+++ b/fwd_rule.h
@@ -103,6 +103,7 @@ const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size);
 void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd);
 int fwd_rule_read(int fd, struct fwd_rule *rule);
 int fwd_rule_write(int fd, const struct fwd_rule *rule);
+int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new);
 
 /**
  * fwd_rules_dump() - Dump forwarding rules
diff --git a/pesto.1 b/pesto.1
index b06433d..1ea1d12 100644
--- a/pesto.1
+++ b/pesto.1
@@ -31,6 +31,133 @@ Be verbose.
 .BR \-h ", " \-\-help
 Display a help message and exit.
 
+.TP
+.BR \-t ", " \-\-tcp-ports " " \fIspec
+Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of:
+.RS
+
+.TP
+.BR none
+Don't forward any ports
+
+.TP
+[\fIaddress\fR[\fB%\fR\fIinterface\fR]\fB/\fR]\fIports\fR ...
+Specific ports to forward.  Optionally, a specific listening address
+and interface name (since Linux 5.7) can be specified.  \fIports\fR
+may be either:
+.RS
+.TP
+\fBall\fR
+Forward all unbound, non-ephemeral ports, as permitted by current capabilities.
+No failures are reported for unavailable ports, unless no ports could be
+forwarded at all.
+.RE
+
+.RS
+or a comma-separated list of entries which may be any of:
+.TP
+\fIfirst\fR[\fB-\fR\fIlast\fR][\fB:\fR\fItofirst\fR[\fB-\fR\fItolast\fR]]
+Include range. Forward port numbers between \fIfirst\fR and \fIlast\fR
+(inclusive) to ports between \fItofirst\fR and \fItolast\fR.  If
+\fItofirst\fR and \fItolast\fR are omitted, assume the same as
+\fIfirst\fR and \fIlast\fR.  If \fIlast\fR is omitted, assume the same
+as \fIfirst\fR.
+
+.TP
+\fB~\fR\fIfirst\fR[\fB-\fR\fIlast\fR]
+Exclude range.  Don't forward port numbers between \fIfirst\fR and
+\fIlast\fR.  This takes precedences over include ranges.
+
+.TP
+.BR auto
+\fBpasta\fR only.  Only forward ports in the specified set if the
+target ports are bound in the namespace. The list of ports is
+periodically derived (every second) from listening sockets reported by
+\fI/proc/net/tcp\fR and \fI/proc/net/tcp6\fR, see \fBproc\fR(5).
+.RE
+
+Specifying excluded ranges only implies that all other non-ephemeral
+ports are forwarded. Specifying no ranges at all implies forwarding
+all non-ephemeral ports permitted by current capabilities.  In this
+case, no failures are reported for unavailable ports, unless no ports
+could be forwarded at all.
+
+Examples:
+.RS
+.TP
+-t all
+Forward all unbound, non-ephemeral ports as permitted by current
+capabilities to the corresponding port on the guest or namespace
+.TP
+-t ::1/all
+For the local address ::1, forward all unbound, non-ephemeral ports as
+permitted by current capabilities
+.TP
+-t 22
+Forward local port 22 to port 22 on the guest or namespace
+.TP
+-t 22:23
+Forward local port 22 to port 23 on the guest or namespace
+.TP
+-t 22,25
+Forward local ports 22 and 25 to ports 22 and 25 on the guest or namespace
+.TP
+-t 22-80
+Forward local ports between 22 and 80 to corresponding ports on the guest or
+namespace
+.TP
+-t 22-80:32-90
+Forward local ports between 22 and 80 to ports between 32 and 90 on the guest or
+namespace
+.TP
+-t 192.0.2.1/22
+Forward local port 22, bound to 192.0.2.1, to port 22 on the guest or namespace
+.TP
+-t 192.0.2.1%eth0/22
+Forward local port 22, bound to 192.0.2.1 and interface eth0, to port 22
+.TP
+-t %eth0/22
+Forward local port 22, bound to any address on interface eth0, to port 22
+.TP
+-t 2000-5000,~3000-3010
+Forward local ports between 2000 and 5000, except for those between 3000 and
+3010
+.TP
+-t 192.0.2.1/20-30,~25
+For the local address 192.0.2.1, forward ports between 20 and 24 and between 26
+and 30
+.TP
+-t ~20000-20010
+Forward all ports to the guest, except for the range from 20000 to 20010
+.TP
+-t auto
+Automatically forward any ports which are bound in the namespace
+.TP
+-t ::1/auto
+Automatically forward any ports which are bound in the namespace,
+listening only on local port ::1
+.TP
+-t 8000-8010,auto
+Forward ports in the range 8000-8010 if and only if they are bound in
+the namespace
+.RE
+.RE
+
+.TP
+.BR \-u ", " \-\-udp-ports " " \fIspec
+Configure UDP port forwarding to guest. \fIspec\fR is as described for TCP
+above.
+
+.TP
+.BR \-T ", " \-\-tcp-ns " " \fIspec
+Configure TCP port forwarding from target namespace to init namespace.
+\fIspec\fR is as described above.
+
+.TP
+.BR \-U ", " \-\-udp-ns " " \fIspec
+Configure UDP port forwarding from target namespace to init namespace.
+\fIspec\fR is as described above.
+
 .TP
 .BR \-\-version
 Show version and exit.
diff --git a/pesto.c b/pesto.c
index 92a8cb2..16b3a5a 100644
--- a/pesto.c
+++ b/pesto.c
@@ -55,6 +55,43 @@ 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"
+		"      [ADDR[%%IFACE]/]PORTS: forward specific ports\n"
+		"        PORTS is either 'all' (forward all unbound, non-ephemeral\n"
+		"        ports), or a comma-separated list of ports, optionally\n"
+		"        ranged with '-' and optional target ports after ':'.\n"
+		"        Ranges can be reduced by excluding ports or ranges\n"
+		"        prefixed by '~'.\n"
+		"        The 'auto' keyword may be given to only forward\n"
+		"        ports which are bound in the target namespace\n"
+		"        Examples:\n"
+		"        -t all         Forward all ports\n"
+		"        -t 127.0.0.1/all Forward all ports from local address\n"
+		"                         127.0.0.1\n"
+		"        -t 22		Forward local port 22 to 22\n"
+		"        -t 22:23	Forward local port 22 to 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 ports except for 25\n"
+		"        -t auto	Forward all ports bound in namespace\n"
+		"        -t 192.0.2.2/auto Forward ports from 192.0.2.2 if\n"
+		"                          they are bound in the namespace\n"
+		"        -t 8000-8010,auto Forward ports 8000-8010 if they\n"
+		"                          are bound in the namespace\n"
+		"  -u, --udp-ports SPEC	UDP inbound port forwarding\n"
+		"    SPEC is as described for TCP above\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"
+		"  -s, --show		Show configuration before and after\n"
 		"  -d, --debug		Print debugging messages\n"
 		"  -h, --help		Display this help message and exit\n"
 		"  --version		Show version and exit\n");
@@ -207,6 +244,8 @@ static void show_conf(const struct configuration *conf)
 		fwd_rules_dump(printf, pc->fwd.rules, pc->fwd.count,
 			       "    ", "\n");
 	}
+	/* Flush stdout, so this doesn't get misordered with later debug()s */
+	(void)fflush(stdout);
 }
 
 /**
@@ -218,7 +257,7 @@ static void show_conf(const struct configuration *conf)
  *
  * #syscalls:pesto socket s390x:socketcall i686:socketcall
  * #syscalls:pesto connect shutdown close
- * #syscalls:pesto exit_group fstat read write
+ * #syscalls:pesto exit_group fstat read write openat
  */
 int main(int argc, char **argv)
 {
@@ -226,11 +265,18 @@ int main(int argc, char **argv)
 		{"debug",	no_argument,		NULL,		'd' },
 		{"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' },
+		{"show",	no_argument,		NULL,		's' },
 		{ 0 },
 	};
+	struct pif_configuration *inbound, *outbound;
 	struct sockaddr_un a = { AF_UNIX, "" };
+	const char *optstring = "dht:u:T:U:s";
 	struct configuration conf = { 0 };
-	const char *optstring = "dh";
+	bool update = false, show = false;
 	struct pesto_hello hello;
 	struct sock_fprog prog;
 	int optname, ret, s;
@@ -251,6 +297,8 @@ int main(int argc, char **argv)
 	if (setvbuf(stdout, stdout_buf, _IOFBF, sizeof(stdout_buf)))
 		die_perror("Failed to set stdout buffer");
 
+	fwd_probe_ephemeral();
+
 	do {
 		optname = getopt_long(argc, argv, optstring, options, NULL);
 
@@ -258,6 +306,16 @@ int main(int argc, char **argv)
 		case -1:
 		case 0:
 			break;
+		case 't':
+		case 'u':
+		case 'T':
+		case 'U':
+			/* Parse these options after we've read state from passt/pasta */
+			update = true;
+			break;
+		case 's':
+			show = true;
+			break;
 		case 'h':
 			usage(argv[0], stdout, EXIT_SUCCESS);
 			break;
@@ -290,6 +348,8 @@ int main(int argc, char **argv)
 		die_perror("Failed to connect to %s", a.sun_path);
 	}
 
+	debug("Connected to passt/pasta control socket");
+
 	ret = read_all_buf(s, &hello, sizeof(hello));
 	if (ret < 0)
 		die_perror("Couldn't read server greeting");
@@ -327,9 +387,52 @@ int main(int argc, char **argv)
 	while (read_pif_conf(s, &conf))
 		;
 
-	printf("passt/pasta configuration (%s)\n", a.sun_path);
-	show_conf(&conf);
+	if (!update) {
+		printf("passt/pasta configuration (%s)\n", a.sun_path);
+		show_conf(&conf);
+		goto noupdate;
+	}
+
+	if (show) {
+		printf("Previous configuration (%s)\n", a.sun_path);
+		show_conf(&conf);
+	}
+
+	inbound = pif_conf_by_name(&conf, "HOST");
+	outbound = pif_conf_by_name(&conf, "SPLICE");
+
+	optind = 0;
+	do {
+		optname = getopt_long(argc, argv, optstring, options, NULL);
+
+		switch (optname) {
+		case 't':
+		case 'u':
+			if (!inbound) {
+				die("Can't use -%c, no inbound interface",
+				    optname);
+			}
+			fwd_rule_parse(optname, optarg, &inbound->fwd);
+			break;
+		case 'T':
+		case 'U':
+			if (!outbound) {
+				die("Can't use -%c, no outbound interface",
+				    optname);
+			}
+			fwd_rule_parse(optname, optarg, &outbound->fwd);
+			break;
+		default:
+			continue;
+		}
+	} while (optname != -1);
+
+	if (show) {
+		printf("Updated configuration (%s)\n", a.sun_path);
+		show_conf(&conf);
+	}
 
+noupdate:
 	if (shutdown(s, SHUT_RDWR) < 0 || close(s) < 0)
 		die_perror("Error shutting down control socket");
 
-- 
2.43.0


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

* [PATCH v8 16/19] pesto, conf: Send updated rules from pesto back to passt/pasta
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (14 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 15/19] pesto: Parse and add new rules from command line Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-05 23:47 ` [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto Stefano Brivio
                   ` (3 subsequent siblings)
  19 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

Extend pesto to send the updated rule configuration back to passt/pasta.
Extend passt/pasta to read the new configuration and store the new rules in
a "pending" table.   We don't yet attempt to activate them.

Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
[dwg: Based on an early draft from Stefano]
[sbrivio: Add redundant check on interface names being terminated in
 conf_recv_rules(), to make static checkers happy]
[sbrivio: Make conf_recv_rules() return -1 if fwd_rule_read() fails,
 as suggested by Jon Maloy]
Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
Reviewed-by: Laurent Vivier <lvivier@redhat.com>
---
 Makefile |  5 ---
 conf.c   | 94 ++++++++++++++++++++++++++++++++++++++++++++++++--------
 fwd.c    | 10 +++++-
 passt.h  |  2 ++
 pesto.c  | 35 +++++++++++++++++++++
 5 files changed, 127 insertions(+), 19 deletions(-)

diff --git a/Makefile b/Makefile
index c746b55..ae755a0 100644
--- a/Makefile
+++ b/Makefile
@@ -224,10 +224,6 @@ cppcheck: passt.cppcheck passt-repair.cppcheck pesto.cppcheck qrap.cppcheck
 	$(CPPCHECK) $(CPPCHECK_FLAGS) $(BASE_CPPFLAGS) $^
 
 passt.cppcheck: BASE_CPPFLAGS += -UPESTO
-passt.cppcheck: CPPCHECK_FLAGS += \
-	--suppress=unusedFunction:fwd_rule.c \
-	--suppress=staticFunction:fwd_rule.c \
-	--suppress=unusedFunction:serialise.c
 passt.cppcheck: $(PASST_SRCS) $(PASST_HEADERS) seccomp.h
 
 passt-repair.cppcheck: $(PASST_REPAIR_SRCS) $(PASST_REPAIR_HEADERS) seccomp_repair.h
@@ -238,7 +234,6 @@ pesto.cppcheck: CPPCHECK_FLAGS += \
 	--suppress=unusedFunction:inany.h \
 	--suppress=unusedFunction:inany.c \
 	--suppress=unusedFunction:ip.h \
-	--suppress=unusedFunction:fwd_rule.c \
 	--suppress=staticFunction:fwd_rule.c \
 	--suppress=unusedFunction:serialise.c
 pesto.cppcheck: $(PESTO_SRCS) $(PESTO_HEADERS) seccomp_pesto.h
diff --git a/conf.c b/conf.c
index 844be60..76344da 100644
--- a/conf.c
+++ b/conf.c
@@ -1973,6 +1973,62 @@ static int conf_send_rules(const struct ctx *c, int fd)
 	return 0;
 }
 
+/**
+ * conf_recv_rules() - Receive forwarding rules from configuration client
+ * @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;
+
+		if (pif >= ARRAY_SIZE(c->fwd_pending) ||
+		    !(fwd = c->fwd_pending[pif])) {
+			err("Received rules for non-existent table");
+			return -1;
+		}
+
+		if (read_u32(fd, &count))
+			return -1;
+
+		if (count > MAX_FWD_RULES) {
+			err("Received %"PRIu32" rules (maximum %u)",
+			    count, MAX_FWD_RULES);
+			return -1;
+		}
+
+		for (i = 0; i < count; i++) {
+			if (fwd_rule_read(fd, &r))
+				return -1;
+
+			if (r.ifname[sizeof(r.ifname) - 1]) {
+				err("Interface name was not NULL terminated");
+				return -1;
+			}
+			/* Redundant, to make static checkers happy */
+			r.ifname[sizeof(r.ifname) - 1] = '\0';
+
+			if (fwd_rule_add(fwd, &r) < 0)
+				return -1;
+		}
+	}
+
+	return 0;
+}
+
 /**
  * conf_close() - Close configuration / control socket and clean up
  * @c:		Execution context
@@ -2076,21 +2132,33 @@ fail:
 void conf_handler(struct ctx *c, uint32_t events)
 {
 	if (events & EPOLLIN) {
-		char discard[BUFSIZ];
-		ssize_t n;
-
-		do {
-			n = read(c->fd_control, 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;
+		unsigned pif;
+
+		/* Clear pending tables */
+		for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
+			struct fwd_table *fwd = c->fwd_pending[pif];
+
+			if (!fwd)
+				continue;
+			fwd->count = 0;
+			fwd->sock_count = 0;
 		}
-		if (errno != EAGAIN && errno != EWOULDBLOCK) {
-			err_perror("Error reading config data");
+
+		/* FIXME: this could block indefinitely if the client doesn't
+		 * write as much as it should
+		 */
+		if (conf_recv_rules(c, c->fd_control) < 0)
 			goto close;
+
+		for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
+			struct fwd_table *fwd = c->fwd_pending[pif];
+
+			if (!fwd)
+				continue;
+
+			info("New forwarding rules for %s:", pif_name(pif));
+			fwd_rules_dump(info, fwd->rules, fwd->count,
+				       "    ", "");
 		}
 	}
 
diff --git a/fwd.c b/fwd.c
index 8849cfc..d93d2e5 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
@@ -269,10 +272,15 @@ void fwd_rule_init(struct ctx *c)
 		caps |= FWD_CAP_IFNAME;
 
 	fwd_in.caps = fwd_out.caps = caps;
+	fwd_in_pending.caps = fwd_out_pending.caps = caps;
 
 	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 b3f049d..1726965 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 16b3a5a..73fdc39 100644
--- a/pesto.c
+++ b/pesto.c
@@ -230,6 +230,39 @@ static bool read_pif_conf(int fd, struct configuration *conf)
 	return true;
 }
 
+/**
+ * send_conf() - Send updated configuration to passt/pasta
+ * @fd:		Control socket
+ * @conf:	Updated configuration
+ */
+static void send_conf(int fd, const struct configuration *conf)
+{
+	unsigned i;
+
+	for (i = 0; i < conf->npifs; i++) {
+		const struct pif_configuration *pc = &conf->pif[i];
+		unsigned j;
+
+		if (write_u8(fd, pc->pif) < 0)
+			goto fail;
+
+		if (write_u32(fd, pc->fwd.count) < 0)
+			goto fail;
+
+		for (j = 0; j < pc->fwd.count; j++) {
+			if (fwd_rule_write(fd, &pc->fwd.rules[j]) < 0)
+				goto fail;
+		}
+	}
+
+	if (write_u8(fd, PIF_NONE) < 0)
+		goto fail;
+	return;
+
+fail:
+	die_perror("Error writing to control socket");
+}
+
 /**
  * show_conf() - Show current configuration obtained from passt/pasta
  * @conf:	Configuration description
@@ -432,6 +465,8 @@ int main(int argc, char **argv)
 		show_conf(&conf);
 	}
 
+	send_conf(s, &conf);
+
 noupdate:
 	if (shutdown(s, SHUT_RDWR) < 0 || close(s) < 0)
 		die_perror("Error shutting down control socket");
-- 
2.43.0


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

* [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (15 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 16/19] pesto, conf: Send updated rules from pesto back to passt/pasta Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-06  7:15   ` Laurent Vivier
  2026-05-06  8:12   ` Laurent Vivier
  2026-05-05 23:47 ` [PATCH v8 18/19] fwd_rule: Fix static checkers warnings in fwd_rule_add() Stefano Brivio
                   ` (2 subsequent siblings)
  19 siblings, 2 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

From: David Gibson <david@gibson.dropbear.id.au>

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>
[sbrivio: In fwd_listen_switch(), use the destination size as argument
 to memcpy(), instead of sizeof(tmp), as suggested by Laurent]
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 conf.c |  5 ++---
 fwd.c  | 34 ++++++++++++++++++++++++++++++++++
 fwd.h  |  1 +
 3 files changed, 37 insertions(+), 3 deletions(-)

diff --git a/conf.c b/conf.c
index 76344da..3f48793 100644
--- a/conf.c
+++ b/conf.c
@@ -2160,15 +2160,14 @@ void conf_handler(struct ctx *c, uint32_t events)
 			fwd_rules_dump(info, fwd->rules, fwd->count,
 				       "    ", "");
 		}
+
+		fwd_listen_switch(c);
 	}
 
 	if (events & EPOLLHUP) {
 		debug("Configuration client hangup");
-		goto close;
 	}
 
-	return;
-
 close:
 	conf_close(c);
 
diff --git a/fwd.c b/fwd.c
index d93d2e5..0697435 100644
--- a/fwd.c
+++ b/fwd.c
@@ -534,6 +534,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);
+		fwd->count = fwd->sock_count = 0;
+	}
+
+	/* 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(c->fwd));
+	memcpy((void *)c->fwd_pending, &tmp, sizeof(c->fwd_pending));
+
+	/* 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 ac24782..b60697d 100644
--- a/fwd.h
+++ b/fwd.h
@@ -61,6 +61,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.43.0


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

* [PATCH v8 18/19] fwd_rule: Fix static checkers warnings in fwd_rule_add()
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (16 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-06  7:18   ` Laurent Vivier
  2026-05-05 23:47 ` [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules Stefano Brivio
  2026-05-06  6:53 ` [PATCH v8 00/19] Dynamic configuration update implementation David Gibson
  19 siblings, 1 reply; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

The new checks are actually sufficient but not enough for Coverity
Scan. Now that fwd->sock_count and new->last are affected or supplied
by clients, we need explicit (albeit redundant) checks on them.

Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 fwd_rule.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/fwd_rule.c b/fwd_rule.c
index b55e4df..03e8e80 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -271,13 +271,22 @@ int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
 		warn("Too many rules (maximum %d)", ARRAY_SIZE(fwd->rules));
 		return -ENOSPC;
 	}
+
 	if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) {
 		warn("Rules require too many listening sockets (maximum %d)",
 		     ARRAY_SIZE(fwd->socks));
 		return -ENOSPC;
 	}
+	/* Redundant, to make static checkers happy */
+	if (fwd->sock_count > ARRAY_SIZE(fwd->socks))
+		return -ENOSPC;
 
 	fwd->rulesocks[fwd->count] = &fwd->socks[fwd->sock_count];
+
+	/* Redundant ('num' checked above), but not for static checkers */
+	if (new->last > ARRAY_SIZE(fwd->socks) + new->first)
+		return -ENOSPC;
+
 	for (port = new->first; port <= new->last; port++)
 		fwd->rulesocks[fwd->count][port - new->first] = -1;
 
-- 
2.43.0


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

* [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (17 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 18/19] fwd_rule: Fix static checkers warnings in fwd_rule_add() Stefano Brivio
@ 2026-05-05 23:47 ` Stefano Brivio
  2026-05-06  6:45   ` David Gibson
  2026-05-06  6:53 ` [PATCH v8 00/19] Dynamic configuration update implementation David Gibson
  19 siblings, 1 reply; 44+ messages in thread
From: Stefano Brivio @ 2026-05-05 23:47 UTC (permalink / raw)
  To: passt-dev; +Cc: Jon Maloy, David Gibson, Laurent Vivier

Instead of just being able to replace the existing forwarding table,
implement --add and --delete options to maintain the table and add
or delete specific ports.

The option --clear PIF forces the clearing of a table, instead.

These options can be combined arbitrarily and are handled as
sequential commands, as now described in pesto(1).

If no option is given before forwarding specifiers for a matching
table, the command line is interpreted as a replacement of the
existing rules.

To this end:

- there's no protocol change, as pesto is anyway sending updated
  copies of the table

- the forwarding table functions now include a new fwd_rule_del(),
  which deletes existing rule only if a matching one is found

- a trivial fwd_rule_clear() is factored out from the existing
  conf_handler() implementation, so that it can be directly used
  in pesto

The entry points for parsing of port specifiers now take an additional
'del' parameter which is passed down all the way before reaching the
fwd_rule_add() implementation. If a rule should be deleted, at that
point, fwd_rule_del() is called instead.

Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
---
 conf.c     | 26 ++++++----------
 fwd_rule.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++-------
 fwd_rule.h |  4 ++-
 pesto.1    | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++
 pesto.c    | 53 ++++++++++++++++++++++++++++---
 5 files changed, 227 insertions(+), 32 deletions(-)

diff --git a/conf.c b/conf.c
index 3f48793..909c34c 100644
--- a/conf.c
+++ b/conf.c
@@ -1849,16 +1849,16 @@ void conf(struct ctx *c, int argc, char **argv)
 
 		if (name == 't') {
 			opt_t = true;
-			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
+			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
 		} else if (name == 'u') {
 			opt_u = true;
-			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
+			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
 		} else if (name == 'T') {
 			opt_T = true;
-			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
+			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
 		} else if (name == 'U') {
 			opt_U = true;
-			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
+			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
 		}
 	} while (name != -1);
 
@@ -1910,13 +1910,13 @@ void conf(struct ctx *c, int argc, char **argv)
 
 	if (c->mode == MODE_PASTA) {
 		if (!opt_t)
-			fwd_rule_parse('t', "auto", c->fwd[PIF_HOST]);
+			fwd_rule_parse('t', false, "auto", c->fwd[PIF_HOST]);
 		if (!opt_T)
-			fwd_rule_parse('T', "auto", c->fwd[PIF_SPLICE]);
+			fwd_rule_parse('T', false, "auto", c->fwd[PIF_SPLICE]);
 		if (!opt_u)
-			fwd_rule_parse('u', "auto", c->fwd[PIF_HOST]);
+			fwd_rule_parse('u', false, "auto", c->fwd[PIF_HOST]);
 		if (!opt_U)
-			fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]);
+			fwd_rule_parse('U', false, "auto", c->fwd[PIF_SPLICE]);
 	}
 
 	conf_sock_listen(c);
@@ -2135,14 +2135,8 @@ void conf_handler(struct ctx *c, uint32_t events)
 		unsigned pif;
 
 		/* Clear pending tables */
-		for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
-			struct fwd_table *fwd = c->fwd_pending[pif];
-
-			if (!fwd)
-				continue;
-			fwd->count = 0;
-			fwd->sock_count = 0;
-		}
+		for (pif = 0; pif < PIF_NUM_TYPES; pif++)
+			fwd_rule_clear(c->fwd_pending[pif]);
 
 		/* FIXME: this could block indefinitely if the client doesn't
 		 * write as much as it should
diff --git a/fwd_rule.c b/fwd_rule.c
index 03e8e80..eb9a601 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -180,6 +180,66 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
 	return true;
 }
 
+/**
+ * fwd_rule_clear() - Clear a forwarding table
+ * @fwd:	Table to clear (might be NULL)
+ */
+void fwd_rule_clear(struct fwd_table *fwd)
+{
+	if (!fwd)
+		return;
+
+	fwd->count = 0;
+	fwd->sock_count = 0;
+}
+
+/**
+ * fwd_rule_del() - Partially validate and delete a rule from a forwarding table
+ * @fwd:	Table to delete from
+ * @rule:	Rule to delete (must match an existing rule)
+ *
+ * Return: 0 on success, negative error code on failure (-ENOENT if not found)
+ *
+ * NOTE: This function can't be used for a forwarding table with valid sockets
+ * stored in fwd->rulesocks.
+ */
+static int fwd_rule_del(struct fwd_table *fwd, const struct fwd_rule *rule)
+{
+	unsigned num, i;
+
+	for (i = 0; i < fwd->count; i++) {
+		if (fwd_rule_conflicts(rule, &fwd->rules[i]))
+			break;
+	}
+
+	if (i == fwd->count) {
+		char newstr[FWD_RULE_STRLEN];
+
+		warn("Couldn't find forwarding rule to delete: %s",
+		     fwd_rule_fmt(rule, newstr, sizeof(newstr)));
+		return -ENOENT;
+	}
+
+	/* Don't use anything else from 'rule' as passed, it's not validated */
+	rule = &fwd->rules[i];
+	num = (unsigned)rule->last - rule->first + 1;
+
+	fwd->count--;
+
+	memmove((void *)(fwd->rulesocks + i), (void *)(fwd->rulesocks + i + 1),
+		(fwd->count - i) * sizeof(*fwd->rulesocks));
+
+	/* TODO: move sockets stored starting from fwd->rulesocks[i + i], should
+	 * we ever need to delete rules from a table with open sockets.
+	 */
+	fwd->sock_count -= num;
+
+	memmove(fwd->rules + i, fwd->rules + i + 1,
+		(fwd->count - i) * sizeof(*fwd->rules));
+
+	return 0;
+}
+
 /**
  * fwd_rule_add() - Validate and add a rule to a forwarding table
  * @fwd:	Table to add to
@@ -368,6 +428,7 @@ static int parse_keyword(const char *s, const char **endptr, const char *kw)
  * fwd_rule_range_except() - Set up forwarding for a range of ports minus a
  *                           bitmap of exclusions
  * @fwd:	Forwarding table to be updated
+ * @del:	Delete resulting rules from forwarding table, instead of adding
  * @proto:	Protocol to forward
  * @addr:	Listening address
  * @ifname:	Listening interface
@@ -377,8 +438,8 @@ static int parse_keyword(const char *s, const char **endptr, const char *kw)
  * @to:		Port to translate @first to when forwarding
  * @flags:	Flags for forwarding entries
  */
-static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
-				  const union inany_addr *addr,
+static void fwd_rule_range_except(struct fwd_table *fwd, bool del,
+				  uint8_t proto, const union inany_addr *addr,
 				  const char *ifname,
 				  uint16_t first, uint16_t last,
 				  const uint8_t *exclude, uint16_t to,
@@ -418,8 +479,13 @@ static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
 		rule.last = i - 1;
 		rule.to = base + delta;
 
-		if (fwd_rule_add(fwd, &rule) < 0)
-			goto fail;
+		if (del) {
+			if (fwd_rule_del(fwd, &rule) < 0)
+				goto fail;
+		} else {
+			if (fwd_rule_add(fwd, &rule) < 0)
+				goto fail;
+		}
 
 		base = i - 1;
 	}
@@ -445,12 +511,13 @@ fail:
 /**
  * fwd_rule_parse_ports() - Parse port range(s) specifier
  * @fwd:	Forwarding table to be updated
+ * @del:	Delete resulting rules from forwarding table, instead of adding
  * @proto:	Protocol to forward
  * @addr:	Listening address for forwarding
  * @ifname:	Interface name for listening
  * @spec:	Port range(s) specifier
  */
-static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
+static void fwd_rule_parse_ports(struct fwd_table *fwd, bool del, uint8_t proto,
 				 const union inany_addr *addr,
 				 const char *ifname,
 				 const char *spec)
@@ -507,7 +574,7 @@ static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
 		/* Exclude ephemeral ports */
 		fwd_port_map_ephemeral(exclude);
 
-		fwd_rule_range_except(fwd, proto, addr, ifname,
+		fwd_rule_range_except(fwd, del, proto, addr, ifname,
 				      1, NUM_PORTS - 1, exclude,
 				      1, flags | FWD_WEAK);
 		return;
@@ -537,7 +604,7 @@ static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
 		if (p != ep) /* Garbage after the ranges */
 			goto bad;
 
-		fwd_rule_range_except(fwd, proto, addr, ifname,
+		fwd_rule_range_except(fwd, del, proto, addr, ifname,
 				      orig_range.first, orig_range.last,
 				      exclude,
 				      mapped_range.first, flags);
@@ -551,10 +618,12 @@ bad:
 /**
  * fwd_rule_parse() - Parse port configuration option
  * @optname:	Short option name, t, T, u, or U
+ * @del:	Delete resulting rules from forwarding table, instead of adding
  * @optarg:	Option argument (port specification)
  * @fwd:	Forwarding table to be updated
  */
-void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
+void fwd_rule_parse(char optname, bool del, const char *optarg,
+		    struct fwd_table *fwd)
 {
 	union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
 	char buf[BUFSIZ], *spec, *ifname = NULL;
@@ -632,12 +701,12 @@ void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
 			     optname, optarg);
 
 			if (fwd->caps & FWD_CAP_IPV4) {
-				fwd_rule_parse_ports(fwd, proto,
+				fwd_rule_parse_ports(fwd, del, proto,
 						     &inany_loopback4, NULL,
 						     spec);
 			}
 			if (fwd->caps & FWD_CAP_IPV6) {
-				fwd_rule_parse_ports(fwd, proto,
+				fwd_rule_parse_ports(fwd, del, proto,
 						     &inany_loopback6, NULL,
 						     spec);
 			}
@@ -653,7 +722,7 @@ void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
 		    optname, optarg);
 	}
 
-	fwd_rule_parse_ports(fwd, proto, addr, ifname, spec);
+	fwd_rule_parse_ports(fwd, del, proto, addr, ifname, spec);
 }
 
 /**
diff --git a/fwd_rule.h b/fwd_rule.h
index f43b37d..ae9a3cb 100644
--- a/fwd_rule.h
+++ b/fwd_rule.h
@@ -100,9 +100,11 @@ void fwd_probe_ephemeral(void);
 
 const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
 const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size);
-void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd);
+void fwd_rule_parse(char optname, bool del, const char *optarg,
+		    struct fwd_table *fwd);
 int fwd_rule_read(int fd, struct fwd_rule *rule);
 int fwd_rule_write(int fd, const struct fwd_rule *rule);
+void fwd_rule_clear(struct fwd_table *fwd);
 int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new);
 
 /**
diff --git a/pesto.1 b/pesto.1
index 1ea1d12..cd0f3dc 100644
--- a/pesto.1
+++ b/pesto.1
@@ -31,6 +31,42 @@ Be verbose.
 .BR \-h ", " \-\-help
 Display a help message and exit.
 
+.TP
+.BR \-A ", " \-\-add
+Add the port forwarding specifiers following this option to the current
+forwarding table, rather than replacing it.
+
+This option can be given multiple times, as it might follow previous deletions
+(see \fB--delete\fR below), and implies that all the specifiers following it,
+before a further \fB--delete\fR option occurs, will be handled as additions.
+
+See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
+more details.
+
+.TP
+.BR \-D ", " \-\-delete
+Delete the port forwarding specifiers following this option from the current
+forwarding table, rather than adding them it.
+
+This option can be given multiple times, as it might follow previous additions
+(see \fB--add\fR above), and implies that all the specifiers following it,
+before a further \fB--add\fR option occurs, will be handled as deletions.
+
+See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
+more details.
+
+.TP
+.BR \-C ", " \-\-clear " " \fIpif
+Clear the forwarding table associated to a given \fIpif\fR, that is, a
+conceptual type of interface in \fBpasst\fR(1) or \fBpasta\fR(1) representing a
+specific data path and direction.
+
+The available \fIpif\fR names can be obtained by querying the current forwarding
+configuration, which can be done by calling \fBpesto\fR(1) without options.
+
+See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
+more details.
+
 .TP
 .BR \-t ", " \-\-tcp-ports " " \fIspec
 Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of:
@@ -162,6 +198,55 @@ Configure UDP port forwarding from target namespace to init namespace.
 .BR \-\-version
 Show version and exit.
 
+.SH NOTES
+
+.SS Adding, deleting, clearing rules
+
+The options \fB--add\fR, \fB--delete\fR, and \fB--clear\fR are handled as
+sequential commands to manipulate the current forwarding tables. If none of them
+is given, forwarding specifiers for a given table are intended as replacement of
+the corresponding table. That is:
+
+.nf
+	pesto -t 1024 -U 1025
+.fi
+
+will \fBreplace\fR the current TCP inbound port forwarding table with a single
+rule, forwarding port 1024, and will similarly replace the UDP outbound
+forwarding table with a single forwarding rule for port 1025. This usage is a
+short-hand form for:
+
+.nf
+	pesto -C HOST -t 1024 -C SPLICE -U 1025
+.fi
+
+The options \fB--add\fR and \fB--delete\fR are used to \fBadd new specific
+rules or delete existing ones\fR, instead of replacing tables. For example:
+
+.nf
+	pesto -A -t 2000 -D -t 3000 -U 5000
+.fi
+
+will add a forwarding rule for inbound TCP port 2000, and delete inbound TCP
+port 3000 as well as outbound UDP port 5000 from the existing set of rules.
+
+All these options are interpreted as sequential commands and can be arbitrarily
+combined. For example:
+
+.nf
+	pesto -A -t 2000 -C HOST -A -T 3000 -t 2001 -D -u 5000
+.fi
+
+will, in order:
+
+.RS
+- add inbound TCP port 2000
+- clear inbound ports, reverting the addition above
+- add outbound TCP port 3000
+- add inbound TCP port 2001
+- delete inbound UDP port 5000
+.RE
+
 .SH AUTHORS
 
 Stefano Brivio <sbrivio@redhat.com>,
diff --git a/pesto.c b/pesto.c
index 73fdc39..143d4c6 100644
--- a/pesto.c
+++ b/pesto.c
@@ -55,6 +55,9 @@ static void usage(const char *name, FILE *f, int status)
 	FPRINTF(f, "Usage: %s [OPTION]... PATH\n", name);
 	FPRINTF(f,
 		"\n"
+		"  -A, --add		Add following specifiers to forwards\n"
+		"  -D, --delete		Delete following specifiers instead\n"
+		"  -C, --clear PIF	Clear forwarding table for given PIF\n"
 		"  -t, --tcp-ports SPEC	TCP inbound port forwarding\n"
 		"    can be specified multiple times\n"
 		"    SPEC can be:\n"
@@ -298,6 +301,9 @@ int main(int argc, char **argv)
 		{"debug",	no_argument,		NULL,		'd' },
 		{"help",	no_argument,		NULL,		'h' },
 		{"version",	no_argument,		NULL,		1 },
+		{"add",		no_argument,		NULL,		'A' },
+		{"delete",	no_argument,		NULL,		'D' },
+		{"clear",	required_argument,	NULL,		'C' },
 		{"tcp-ports",	required_argument,	NULL,		't' },
 		{"udp-ports",	required_argument,	NULL,		'u' },
 		{"tcp-ns",	required_argument,	NULL,		'T' },
@@ -305,9 +311,11 @@ int main(int argc, char **argv)
 		{"show",	no_argument,		NULL,		's' },
 		{ 0 },
 	};
+	enum { MODE_CLEAR, MODE_ADD, MODE_DEL } mode = MODE_CLEAR;
+	bool inbound_cleared = false, outbound_cleared = false;
 	struct pif_configuration *inbound, *outbound;
+	const char *optstring = "dhADC:t:u:T:U:s";
 	struct sockaddr_un a = { AF_UNIX, "" };
-	const char *optstring = "dht:u:T:U:s";
 	struct configuration conf = { 0 };
 	bool update = false, show = false;
 	struct pesto_hello hello;
@@ -339,11 +347,16 @@ int main(int argc, char **argv)
 		case -1:
 		case 0:
 			break;
+		case 'C':
 		case 't':
 		case 'u':
 		case 'T':
 		case 'U':
-			/* Parse these options after we've read state from passt/pasta */
+		case 'A':
+		case 'D':
+			/* Parse these options after we've read state from
+			 * passt/pasta
+			 */
 			update = true;
 			break;
 		case 's':
@@ -439,13 +452,38 @@ int main(int argc, char **argv)
 		optname = getopt_long(argc, argv, optstring, options, NULL);
 
 		switch (optname) {
+		case 'A':
+			mode = MODE_ADD;
+			break;
+		case 'D':
+			mode = MODE_DEL;
+			break;
+		case 'C':
+			if (!strcmp(optarg, "HOST")) {
+				fwd_rule_clear(&inbound->fwd);
+				inbound_cleared = true;
+			} else if (!strcmp(optarg, "SPLICE")) {
+				fwd_rule_clear(&outbound->fwd);
+				outbound_cleared = true;
+			} else {
+				die("Unsupported pif name %s", optarg);
+			}
+
+			break;
 		case 't':
 		case 'u':
 			if (!inbound) {
 				die("Can't use -%c, no inbound interface",
 				    optname);
 			}
-			fwd_rule_parse(optname, optarg, &inbound->fwd);
+
+			if (mode == MODE_CLEAR && !inbound_cleared) {
+				fwd_rule_clear(&inbound->fwd);
+				inbound_cleared = true;
+			}
+
+			fwd_rule_parse(optname, mode == MODE_DEL, optarg,
+				       &inbound->fwd);
 			break;
 		case 'T':
 		case 'U':
@@ -453,7 +491,14 @@ int main(int argc, char **argv)
 				die("Can't use -%c, no outbound interface",
 				    optname);
 			}
-			fwd_rule_parse(optname, optarg, &outbound->fwd);
+
+			if (mode == MODE_CLEAR && !outbound_cleared) {
+				fwd_rule_clear(&outbound->fwd);
+				outbound_cleared = true;
+			}
+
+			fwd_rule_parse(optname, mode == MODE_DEL, optarg,
+				       &inbound->fwd);
 			break;
 		default:
 			continue;
-- 
2.43.0


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

* Re: [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions
  2026-05-05 23:47 ` [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions Stefano Brivio
@ 2026-05-06  5:38   ` David Gibson
  2026-05-06  7:06     ` Laurent Vivier
  2026-05-06  7:55     ` Stefano Brivio
  0 siblings, 2 replies; 44+ messages in thread
From: David Gibson @ 2026-05-06  5:38 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev, Jon Maloy, Laurent Vivier

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

On Wed, May 06, 2026 at 01:47:10AM +0200, Stefano Brivio wrote:
> From: David Gibson <david@gibson.dropbear.id.au>
> 
> Start implementing pesto in earnest.  Create a control/configuration
> socket in passt.  Have pesto connect to it and retrieve a server greeting
> Perform some basic version checking.
> 
> Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> [sbrivio: Avoid potential recursive calling between conf_accept() and
>  conf_close(), reported by clang-tidy]

Huh.  For some reason that warning didn't trip for me.  Although it's
technically true they can mutually recurse, I believe they're both
tail calls, so it shouldn't eat the stack.

> [sbrivio: In conf(), check we're not exceeding sizeof(c->control_path)
>  instead of sizeof(c->socket_path), and, in pesto's main(), print
>  argv[optind] instead of argv[1] to indicate an invalid socket path,
>  both reported by Jon Maloy]
> [sbrivio: In pesto's main(), drop unnecessary newline from error
>  message, reported by Laurent]
> [sbrivio: Don't use SOCK_NONBLOCK on accept4(), as that only applies
>  to the *new* file descriptor, which we don't want -- set O_NONBLOCK
>  on the listening file descriptor using fcntl()]

Making the new (accepted) socket non-blocking was the intended
behaviour here.  We also want non-blocking for the listening socket,
but that was already done in feab892c7 ("tap, repair: Use
SOCK_NONBLOCK and SOCK_CLOEXEC on Unix sockets").

WIth the current design, I guess we don't want non-blocking on the
accepted socket, although I don't think it actually matters very much.
We will want non-blocking it when we change this to read out the
updated rules incrementally, rather than all at once.

[snip]
> @@ -1072,6 +1080,19 @@ 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_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->control_path);
> +		}
> +		if (fcntl(c->fd_control_listen, F_SETFL, O_NONBLOCK))
> +			die_perror("Couldn't set O_NONBLOCK on control socket");

So, this is unneccessary because of feab892c7.

-- 
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] 44+ messages in thread

* Re: [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules
  2026-05-05 23:47 ` [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules Stefano Brivio
@ 2026-05-06  6:45   ` David Gibson
  2026-05-06  8:22     ` Stefano Brivio
  0 siblings, 1 reply; 44+ messages in thread
From: David Gibson @ 2026-05-06  6:45 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev, Jon Maloy, Laurent Vivier

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

On Wed, May 06, 2026 at 01:47:19AM +0200, Stefano Brivio wrote:
> Instead of just being able to replace the existing forwarding table,

As of my last version, we already added, rather than replacing.

> implement --add and --delete options to maintain the table and add
> or delete specific ports.
> 
> The option --clear PIF forces the clearing of a table, instead.
> 
> These options can be combined arbitrarily and are handled as
> sequential commands, as now described in pesto(1).
> 
> If no option is given before forwarding specifiers for a matching
> table, the command line is interpreted as a replacement of the
> existing rules.
> 
> To this end:
> 
> - there's no protocol change, as pesto is anyway sending updated
>   copies of the table
> 
> - the forwarding table functions now include a new fwd_rule_del(),
>   which deletes existing rule only if a matching one is found
> 
> - a trivial fwd_rule_clear() is factored out from the existing
>   conf_handler() implementation, so that it can be directly used
>   in pesto
> 
> The entry points for parsing of port specifiers now take an additional
> 'del' parameter which is passed down all the way before reaching the
> fwd_rule_add() implementation. If a rule should be deleted, at that
> point, fwd_rule_del() is called instead.
> 
> Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> ---
>  conf.c     | 26 ++++++----------
>  fwd_rule.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++-------
>  fwd_rule.h |  4 ++-
>  pesto.1    | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++
>  pesto.c    | 53 ++++++++++++++++++++++++++++---
>  5 files changed, 227 insertions(+), 32 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index 3f48793..909c34c 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -1849,16 +1849,16 @@ void conf(struct ctx *c, int argc, char **argv)
>  
>  		if (name == 't') {
>  			opt_t = true;
> -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
>  		} else if (name == 'u') {
>  			opt_u = true;
> -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
>  		} else if (name == 'T') {
>  			opt_T = true;
> -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
>  		} else if (name == 'U') {
>  			opt_U = true;
> -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
>  		}
>  	} while (name != -1);
>  
> @@ -1910,13 +1910,13 @@ void conf(struct ctx *c, int argc, char **argv)
>  
>  	if (c->mode == MODE_PASTA) {
>  		if (!opt_t)
> -			fwd_rule_parse('t', "auto", c->fwd[PIF_HOST]);
> +			fwd_rule_parse('t', false, "auto", c->fwd[PIF_HOST]);
>  		if (!opt_T)
> -			fwd_rule_parse('T', "auto", c->fwd[PIF_SPLICE]);
> +			fwd_rule_parse('T', false, "auto", c->fwd[PIF_SPLICE]);
>  		if (!opt_u)
> -			fwd_rule_parse('u', "auto", c->fwd[PIF_HOST]);
> +			fwd_rule_parse('u', false, "auto", c->fwd[PIF_HOST]);
>  		if (!opt_U)
> -			fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]);
> +			fwd_rule_parse('U', false, "auto", c->fwd[PIF_SPLICE]);
>  	}
>  
>  	conf_sock_listen(c);
> @@ -2135,14 +2135,8 @@ void conf_handler(struct ctx *c, uint32_t events)
>  		unsigned pif;
>  
>  		/* Clear pending tables */
> -		for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> -			struct fwd_table *fwd = c->fwd_pending[pif];
> -
> -			if (!fwd)
> -				continue;
> -			fwd->count = 0;
> -			fwd->sock_count = 0;
> -		}
> +		for (pif = 0; pif < PIF_NUM_TYPES; pif++)
> +			fwd_rule_clear(c->fwd_pending[pif]);
>  
>  		/* FIXME: this could block indefinitely if the client doesn't
>  		 * write as much as it should
> diff --git a/fwd_rule.c b/fwd_rule.c
> index 03e8e80..eb9a601 100644
> --- a/fwd_rule.c
> +++ b/fwd_rule.c
> @@ -180,6 +180,66 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
>  	return true;
>  }
>  
> +/**
> + * fwd_rule_clear() - Clear a forwarding table
> + * @fwd:	Table to clear (might be NULL)
> + */
> +void fwd_rule_clear(struct fwd_table *fwd)
> +{
> +	if (!fwd)
> +		return;
> +

Not essential, but I wonder if it would be wise to verify that there
are no currently open sockets associated with any of the rules.

> +	fwd->count = 0;
> +	fwd->sock_count = 0;
> +}
> +
> +/**
> + * fwd_rule_del() - Partially validate and delete a rule from a forwarding table
> + * @fwd:	Table to delete from
> + * @rule:	Rule to delete (must match an existing rule)
> + *
> + * Return: 0 on success, negative error code on failure (-ENOENT if not found)
> + *
> + * NOTE: This function can't be used for a forwarding table with valid sockets
> + * stored in fwd->rulesocks.

The exact meaning of this isn't very clear to me.  Does "valid" mean
"open" or something else?

I think what you're getting at is that every entry in fwd->socks[]
must be -1.  Or at least every entry with index in [0,sock_count)

> + */
> +static int fwd_rule_del(struct fwd_table *fwd, const struct fwd_rule *rule)
> +{
> +	unsigned num, i;
> +
> +	for (i = 0; i < fwd->count; i++) {
> +		if (fwd_rule_conflicts(rule, &fwd->rules[i]))
> +			break;
> +	}

So, this deletes any conflicting rule, not only exact matches.  That's
not very clear from the description of @rule.

> +
> +	if (i == fwd->count) {
> +		char newstr[FWD_RULE_STRLEN];
> +
> +		warn("Couldn't find forwarding rule to delete: %s",
> +		     fwd_rule_fmt(rule, newstr, sizeof(newstr)));
> +		return -ENOENT;
> +	}
> +
> +	/* Don't use anything else from 'rule' as passed, it's not validated */
> +	rule = &fwd->rules[i];
> +	num = (unsigned)rule->last - rule->first + 1;
> +
> +	fwd->count--;
> +
> +	memmove((void *)(fwd->rulesocks + i), (void *)(fwd->rulesocks + i + 1),

I don't think the explicit (void *) casts are necessary - they should
be implicit from memmove()s signature.

> +		(fwd->count - i) * sizeof(*fwd->rulesocks));

Is memmove() guaranteed to be safe if given a zero length?  That will
occur if deleting the last entry.

> +	/* TODO: move sockets stored starting from fwd->rulesocks[i + i], should
> +	 * we ever need to delete rules from a table with open sockets.
> +	 */
> +	fwd->sock_count -= num;
> +
> +	memmove(fwd->rules + i, fwd->rules + i + 1,
> +		(fwd->count - i) * sizeof(*fwd->rules)

Again, is this safe if i == fwd->count?

> +
> +	return 0;
> +}
> +
>  /**
>   * fwd_rule_add() - Validate and add a rule to a forwarding table
>   * @fwd:	Table to add to
> @@ -368,6 +428,7 @@ static int parse_keyword(const char *s, const char **endptr, const char *kw)
>   * fwd_rule_range_except() - Set up forwarding for a range of ports minus a
>   *                           bitmap of exclusions
>   * @fwd:	Forwarding table to be updated
> + * @del:	Delete resulting rules from forwarding table, instead of adding

Clunky, but it gets the job done.

>   * @proto:	Protocol to forward
>   * @addr:	Listening address
>   * @ifname:	Listening interface
> @@ -377,8 +438,8 @@ static int parse_keyword(const char *s, const char **endptr, const char *kw)
>   * @to:		Port to translate @first to when forwarding
>   * @flags:	Flags for forwarding entries
>   */
> -static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
> -				  const union inany_addr *addr,
> +static void fwd_rule_range_except(struct fwd_table *fwd, bool del,
> +				  uint8_t proto, const union inany_addr *addr,
>  				  const char *ifname,
>  				  uint16_t first, uint16_t last,
>  				  const uint8_t *exclude, uint16_t to,
> @@ -418,8 +479,13 @@ static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
>  		rule.last = i - 1;
>  		rule.to = base + delta;
>  
> -		if (fwd_rule_add(fwd, &rule) < 0)
> -			goto fail;
> +		if (del) {
> +			if (fwd_rule_del(fwd, &rule) < 0)
> +				goto fail;
> +		} else {
> +			if (fwd_rule_add(fwd, &rule) < 0)
> +				goto fail;
> +		}
>  
>  		base = i - 1;
>  	}
> @@ -445,12 +511,13 @@ fail:
>  /**
>   * fwd_rule_parse_ports() - Parse port range(s) specifier
>   * @fwd:	Forwarding table to be updated
> + * @del:	Delete resulting rules from forwarding table, instead of adding
>   * @proto:	Protocol to forward
>   * @addr:	Listening address for forwarding
>   * @ifname:	Interface name for listening
>   * @spec:	Port range(s) specifier
>   */
> -static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
> +static void fwd_rule_parse_ports(struct fwd_table *fwd, bool del, uint8_t proto,
>  				 const union inany_addr *addr,
>  				 const char *ifname,
>  				 const char *spec)
> @@ -507,7 +574,7 @@ static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
>  		/* Exclude ephemeral ports */
>  		fwd_port_map_ephemeral(exclude);
>  
> -		fwd_rule_range_except(fwd, proto, addr, ifname,
> +		fwd_rule_range_except(fwd, del, proto, addr, ifname,
>  				      1, NUM_PORTS - 1, exclude,
>  				      1, flags | FWD_WEAK);
>  		return;
> @@ -537,7 +604,7 @@ static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
>  		if (p != ep) /* Garbage after the ranges */
>  			goto bad;
>  
> -		fwd_rule_range_except(fwd, proto, addr, ifname,
> +		fwd_rule_range_except(fwd, del, proto, addr, ifname,
>  				      orig_range.first, orig_range.last,
>  				      exclude,
>  				      mapped_range.first, flags);
> @@ -551,10 +618,12 @@ bad:
>  /**
>   * fwd_rule_parse() - Parse port configuration option
>   * @optname:	Short option name, t, T, u, or U
> + * @del:	Delete resulting rules from forwarding table, instead of adding
>   * @optarg:	Option argument (port specification)
>   * @fwd:	Forwarding table to be updated
>   */
> -void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
> +void fwd_rule_parse(char optname, bool del, const char *optarg,
> +		    struct fwd_table *fwd)
>  {
>  	union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
>  	char buf[BUFSIZ], *spec, *ifname = NULL;
> @@ -632,12 +701,12 @@ void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
>  			     optname, optarg);
>  
>  			if (fwd->caps & FWD_CAP_IPV4) {
> -				fwd_rule_parse_ports(fwd, proto,
> +				fwd_rule_parse_ports(fwd, del, proto,
>  						     &inany_loopback4, NULL,
>  						     spec);
>  			}
>  			if (fwd->caps & FWD_CAP_IPV6) {
> -				fwd_rule_parse_ports(fwd, proto,
> +				fwd_rule_parse_ports(fwd, del, proto,
>  						     &inany_loopback6, NULL,
>  						     spec);
>  			}
> @@ -653,7 +722,7 @@ void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
>  		    optname, optarg);
>  	}
>  
> -	fwd_rule_parse_ports(fwd, proto, addr, ifname, spec);
> +	fwd_rule_parse_ports(fwd, del, proto, addr, ifname, spec);
>  }
>  
>  /**
> diff --git a/fwd_rule.h b/fwd_rule.h
> index f43b37d..ae9a3cb 100644
> --- a/fwd_rule.h
> +++ b/fwd_rule.h
> @@ -100,9 +100,11 @@ void fwd_probe_ephemeral(void);
>  
>  const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
>  const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size);
> -void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd);
> +void fwd_rule_parse(char optname, bool del, const char *optarg,
> +		    struct fwd_table *fwd);
>  int fwd_rule_read(int fd, struct fwd_rule *rule);
>  int fwd_rule_write(int fd, const struct fwd_rule *rule);
> +void fwd_rule_clear(struct fwd_table *fwd);
>  int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new);
>  
>  /**
> diff --git a/pesto.1 b/pesto.1
> index 1ea1d12..cd0f3dc 100644
> --- a/pesto.1
> +++ b/pesto.1
> @@ -31,6 +31,42 @@ Be verbose.
>  .BR \-h ", " \-\-help
>  Display a help message and exit.
>  
> +.TP
> +.BR \-A ", " \-\-add
> +Add the port forwarding specifiers following this option to the current
> +forwarding table, rather than replacing it.
> +
> +This option can be given multiple times, as it might follow previous deletions
> +(see \fB--delete\fR below), and implies that all the specifiers following it,
> +before a further \fB--delete\fR option occurs, will be handled as additions.
> +
> +See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
> +more details.
> +
> +.TP
> +.BR \-D ", " \-\-delete
> +Delete the port forwarding specifiers following this option from the current
> +forwarding table, rather than adding them it.
> +
> +This option can be given multiple times, as it might follow previous additions
> +(see \fB--add\fR above), and implies that all the specifiers following it,
> +before a further \fB--add\fR option occurs, will be handled as deletions.
> +
> +See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
> +more details.
> +
> +.TP
> +.BR \-C ", " \-\-clear " " \fIpif
> +Clear the forwarding table associated to a given \fIpif\fR, that is, a
> +conceptual type of interface in \fBpasst\fR(1) or \fBpasta\fR(1) representing a
> +specific data path and direction.
> +
> +The available \fIpif\fR names can be obtained by querying the current forwarding
> +configuration, which can be done by calling \fBpesto\fR(1) without options.
> +
> +See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
> +more details.
> +
>  .TP
>  .BR \-t ", " \-\-tcp-ports " " \fIspec
>  Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of:
> @@ -162,6 +198,55 @@ Configure UDP port forwarding from target namespace to init namespace.
>  .BR \-\-version
>  Show version and exit.
>  
> +.SH NOTES
> +
> +.SS Adding, deleting, clearing rules
> +
> +The options \fB--add\fR, \fB--delete\fR, and \fB--clear\fR are handled as
> +sequential commands to manipulate the current forwarding tables. If none of them
> +is given, forwarding specifiers for a given table are intended as replacement of
> +the corresponding table. That is:

I thought we wanted to default to add, rather than replace.  That
seems both a little simpler to implement and agruably more likely to
be what peopke want.

> +.nf
> +	pesto -t 1024 -U 1025
> +.fi
> +
> +will \fBreplace\fR the current TCP inbound port forwarding table with a single
> +rule, forwarding port 1024, and will similarly replace the UDP outbound
> +forwarding table with a single forwarding rule for port 1025. This usage is a
> +short-hand form for:
> +
> +.nf
> +	pesto -C HOST -t 1024 -C SPLICE -U 1025
> +.fi
> +
> +The options \fB--add\fR and \fB--delete\fR are used to \fBadd new specific
> +rules or delete existing ones\fR, instead of replacing tables. For example:
> +
> +.nf
> +	pesto -A -t 2000 -D -t 3000 -U 5000
> +.fi
> +
> +will add a forwarding rule for inbound TCP port 2000, and delete inbound TCP
> +port 3000 as well as outbound UDP port 5000 from the existing set of rules.
> +
> +All these options are interpreted as sequential commands and can be arbitrarily
> +combined. For example:
> +
> +.nf
> +	pesto -A -t 2000 -C HOST -A -T 3000 -t 2001 -D -u 5000
> +.fi
> +
> +will, in order:
> +
> +.RS
> +- add inbound TCP port 2000
> +- clear inbound ports, reverting the addition above
> +- add outbound TCP port 3000
> +- add inbound TCP port 2001
> +- delete inbound UDP port 5000
> +.RE
> +
>  .SH AUTHORS
>  
>  Stefano Brivio <sbrivio@redhat.com>,
> diff --git a/pesto.c b/pesto.c
> index 73fdc39..143d4c6 100644
> --- a/pesto.c
> +++ b/pesto.c
> @@ -55,6 +55,9 @@ static void usage(const char *name, FILE *f, int status)
>  	FPRINTF(f, "Usage: %s [OPTION]... PATH\n", name);
>  	FPRINTF(f,
>  		"\n"
> +		"  -A, --add		Add following specifiers to forwards\n"
> +		"  -D, --delete		Delete following specifiers instead\n"
> +		"  -C, --clear PIF	Clear forwarding table for given PIF\n"
>  		"  -t, --tcp-ports SPEC	TCP inbound port forwarding\n"
>  		"    can be specified multiple times\n"
>  		"    SPEC can be:\n"
> @@ -298,6 +301,9 @@ int main(int argc, char **argv)
>  		{"debug",	no_argument,		NULL,		'd' },
>  		{"help",	no_argument,		NULL,		'h' },
>  		{"version",	no_argument,		NULL,		1 },
> +		{"add",		no_argument,		NULL,		'A' },
> +		{"delete",	no_argument,		NULL,		'D' },
> +		{"clear",	required_argument,	NULL,		'C' },
>  		{"tcp-ports",	required_argument,	NULL,		't' },
>  		{"udp-ports",	required_argument,	NULL,		'u' },
>  		{"tcp-ns",	required_argument,	NULL,		'T' },
> @@ -305,9 +311,11 @@ int main(int argc, char **argv)
>  		{"show",	no_argument,		NULL,		's' },
>  		{ 0 },
>  	};
> +	enum { MODE_CLEAR, MODE_ADD, MODE_DEL } mode = MODE_CLEAR;

MODE_CLEAR doesn't make sense to me.  Unlike add or del, clear is a
once-off operation, it's not clear to me how it would affect the
interpretation of -[tTuU].


> +	bool inbound_cleared = false, outbound_cleared = false;
>  	struct pif_configuration *inbound, *outbound;
> +	const char *optstring = "dhADC:t:u:T:U:s";
>  	struct sockaddr_un a = { AF_UNIX, "" };
> -	const char *optstring = "dht:u:T:U:s";
>  	struct configuration conf = { 0 };
>  	bool update = false, show = false;
>  	struct pesto_hello hello;
> @@ -339,11 +347,16 @@ int main(int argc, char **argv)
>  		case -1:
>  		case 0:
>  			break;
> +		case 'C':
>  		case 't':
>  		case 'u':
>  		case 'T':
>  		case 'U':
> -			/* Parse these options after we've read state from passt/pasta */
> +		case 'A':
> +		case 'D':
> +			/* Parse these options after we've read state from
> +			 * passt/pasta
> +			 */
>  			update = true;
>  			break;
>  		case 's':
> @@ -439,13 +452,38 @@ int main(int argc, char **argv)
>  		optname = getopt_long(argc, argv, optstring, options, NULL);
>  
>  		switch (optname) {
> +		case 'A':
> +			mode = MODE_ADD;
> +			break;
> +		case 'D':
> +			mode = MODE_DEL;
> +			break;
> +		case 'C':
> +			if (!strcmp(optarg, "HOST")) {
> +				fwd_rule_clear(&inbound->fwd);
> +				inbound_cleared = true;
> +			} else if (!strcmp(optarg, "SPLICE")) {
> +				fwd_rule_clear(&outbound->fwd);

outbound will be NULL if talking to passt, so this could SEGV.

> +				outbound_cleared = true;
> +			} else {
> +				die("Unsupported pif name %s", optarg);
> +			}

For the time being pesto is limited to a single "inbound" and single
"outbound" table, simply because we haven't devised syntax for
anything else.  However, we don't actually need that for --clear.
Since it already takes a pif name we can use pif_conf_by_name() to
clear an arbitrary named pif's rules.

> +
> +			break;
>  		case 't':
>  		case 'u':
>  			if (!inbound) {
>  				die("Can't use -%c, no inbound interface",
>  				    optname);
>  			}
> -			fwd_rule_parse(optname, optarg, &inbound->fwd);
> +
> +			if (mode == MODE_CLEAR && !inbound_cleared) {
> +				fwd_rule_clear(&inbound->fwd);
> +				inbound_cleared = true;
> +			}
> +
> +			fwd_rule_parse(optname, mode == MODE_DEL, optarg,
> +				       &inbound->fwd);
>  			break;
>  		case 'T':
>  		case 'U':
> @@ -453,7 +491,14 @@ int main(int argc, char **argv)
>  				die("Can't use -%c, no outbound interface",
>  				    optname);
>  			}
> -			fwd_rule_parse(optname, optarg, &outbound->fwd);
> +
> +			if (mode == MODE_CLEAR && !outbound_cleared) {
> +				fwd_rule_clear(&outbound->fwd);
> +				outbound_cleared = true;
> +			}
> +
> +			fwd_rule_parse(optname, mode == MODE_DEL, optarg,
> +				       &inbound->fwd);
>  			break;
>  		default:
>  			continue;
> -- 
> 2.43.0
> 

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

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

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

* Re: [PATCH v8 00/19] Dynamic configuration update implementation
  2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
                   ` (18 preceding siblings ...)
  2026-05-05 23:47 ` [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules Stefano Brivio
@ 2026-05-06  6:53 ` David Gibson
  19 siblings, 0 replies; 44+ messages in thread
From: David Gibson @ 2026-05-06  6:53 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev, Jon Maloy, Laurent Vivier

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

On Wed, May 06, 2026 at 01:47:00AM +0200, Stefano Brivio wrote:
> Changes in v8:
>  * Implement --add, --delete, and --clear in 19/19, to add forwarding
>    rules instead of replacing tables, delete existing rules, and
>    explicitly clear tables
>  * Address Laurent's comments for 15/19 and 17/19
>  * In 10/19, instead of passing SOCK_NONBLOCK to accept4(), explicitly
>    set O_NONBLOCK on the listening socket. Using SOCK_NONBLOCK doesn't
>    do what we want, as it results in setting O_NONBLOCK on the new
>    socket rather than on the listening one
>  * Note: 18/19 is left as it is, I didn't address pending comments
>    yet
>  * Note: this doesn't include yet changes for AppArmor and SELinux
>    policies, as well as changes for the template Fedora spec file.
>    I'm still working on them

I haven't re-reviewed the whole series, but these changes all seem
good, with the exception of 19/19 and a few concerns on 10/19 which
I've sent separate mails about.

> 
> Changes in v7:
>  * Addressed comments from Laurent in 6/18, 8/18, 9/18, 10/18, 11/18,
>    12/18, 14/18, 15/18 (details in commit messages of single patches,
>    before my Signed-off-by)
>  * Note: this doesn't include yet --add and --delete, I'm still
>    working on that
> 
> Changes in v6:
>  * Addressed comments from Jon in 10/18, 11/18, 14/18, and 16/18
>  * Dodged all warnings from static checkers (Coverity Scan and
>    clang-tidy) with changes in 10/18, 11/18, 16/18, and with a
>    new patch, 18/18
>  * This does *not* include yet the implementation of --add and
>    --delete switches for pesto as I originally intended, I'm
>    rather far from being done with those. At the moment I just
>    have a "mode selection" implementation for command line
>    parsing but merging rules to / removing rules from / clearing
>    the current table is something I barely started (and what I
>    have at the moment isn't really valuable anyway)
> 
> David wrote:
> 
> ---
> Here's the next draft of dynamic configuration updates.  This now can
> successfully update rules, though I've not tested it very extensively.
> 
> Patches 1..8/18 are preliminary reworks that make sense even without
> pesto - feel free to apply if you're happy with them.  I don't think
> the rest should be applied yet; we need to at least harden it so passt
> can't be blocked indefinitely by a client which sends a partial update
> then waits.
> 
> Based on my earlier series reworking static checking invocation.
> 
> TODO:
>  - Don't allow a client which sends a partial configuration then
>    blocks also block passt
>  - Allow pesto to clear existing configuration, not just add
>  - Allow pesto selectively delete existing rules, not just add
> 
> Changes in v5:
>  * If multiple clients connect at once, they're now blocked until the
>    first one finishes, instead of later ones being discarded
> Changes in v4:
>  * Merged with remainder of forward rule parsing rework series
>    * Fix some bugs in rule checking pointed out by Laurent
>  * Significantly cleaned up option parsing code
>  * Changed from replacing all existing rules to adding new rules
>    (clear and remove still TBD)
>  * Somewhat simplified protocol (pif names and rules sent in a single
>    pass)
>  * pesto is now allocation free
>  * Fixed commit message and style nits pointed out by Stefano
> 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 (17):
>   conf, fwd: Stricter rule checking in fwd_rule_add()
>   fwd_rule: Move ephemeral port probing to fwd_rule.c
>   fwd, conf: Move rule parsing code to fwd_rule.[ch]
>   fwd_rule: Move conflict checking back within fwd_rule_add()
>   fwd: Generalise fwd_rules_info()
>   pif: Limit pif names to 128 bytes
>   fwd_rule: Fix some format specifiers
>   pesto: Introduce stub configuration tool
>   pesto, log: Share log.h (but not log.c) with pesto tool
>   pesto, conf: Have pesto connect to passt and check versions
>   pesto: Expose list of pifs to pesto and display them
>   ip: Prepare ip.[ch] for sharing with pesto tool
>   inany: Prepare inany.[ch] for sharing with pesto tool
>   pesto: Read current ruleset from passt/pasta and optionally display it
>   pesto: Parse and add new rules from command line
>   pesto, conf: Send updated rules from pesto back to passt/pasta
>   conf, fwd: Allow switching to new rules received from pesto
> 
> Stefano Brivio (2):
>   fwd_rule: Fix static checkers warnings in fwd_rule_add()
>   pesto, conf, fwd_rule: Add options and modes to add, delete, clear
>     rules
> 
>  .gitignore   |   2 +
>  Makefile     |  53 ++--
>  common.h     | 116 +++++++++
>  conf.c       | 696 ++++++++++++++++++++++-----------------------------
>  conf.h       |   2 +
>  epoll_type.h |   4 +
>  flow.c       |   4 +-
>  fwd.c        | 169 ++++---------
>  fwd.h        |  41 +--
>  fwd_rule.c   | 680 +++++++++++++++++++++++++++++++++++++++++++++++--
>  fwd_rule.h   |  68 ++++-
>  inany.c      |  19 +-
>  inany.h      |  17 +-
>  ip.c         |  56 +----
>  ip.h         |   4 +-
>  lineread.c   |   2 +-
>  log.h        |  53 +++-
>  passt.1      |   5 +
>  passt.c      |   8 +
>  passt.h      |   8 +
>  pesto.1      | 271 ++++++++++++++++++++
>  pesto.c      | 520 ++++++++++++++++++++++++++++++++++++++
>  pesto.h      |  54 ++++
>  pif.c        |   2 +-
>  pif.h        |   7 +-
>  serialise.c  |   7 +
>  serialise.h  |   1 +
>  siphash.h    |  13 +
>  tap.c        |  52 ++++
>  util.h       | 110 +-------
>  30 files changed, 2252 insertions(+), 792 deletions(-)
>  create mode 100644 common.h
>  create mode 100644 pesto.1
>  create mode 100644 pesto.c
>  create mode 100644 pesto.h
> 
> -- 
> 2.43.0
> 

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

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

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

* Re: [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions
  2026-05-06  5:38   ` David Gibson
@ 2026-05-06  7:06     ` Laurent Vivier
  2026-05-06  7:41       ` David Gibson
  2026-05-06  7:55     ` Stefano Brivio
  1 sibling, 1 reply; 44+ messages in thread
From: Laurent Vivier @ 2026-05-06  7:06 UTC (permalink / raw)
  To: David Gibson, Stefano Brivio; +Cc: passt-dev, Jon Maloy

On 5/6/26 07:38, David Gibson wrote:
> On Wed, May 06, 2026 at 01:47:10AM +0200, Stefano Brivio wrote:
>> From: David Gibson <david@gibson.dropbear.id.au>
>>
>> Start implementing pesto in earnest.  Create a control/configuration
>> socket in passt.  Have pesto connect to it and retrieve a server greeting
>> Perform some basic version checking.
>>
>> Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
>> [sbrivio: Avoid potential recursive calling between conf_accept() and
>>   conf_close(), reported by clang-tidy]
> 
> Huh.  For some reason that warning didn't trip for me.  Although it's
> technically true they can mutually recurse, I believe they're both
> tail calls, so it shouldn't eat the stack.
> 
>> [sbrivio: In conf(), check we're not exceeding sizeof(c->control_path)
>>   instead of sizeof(c->socket_path), and, in pesto's main(), print
>>   argv[optind] instead of argv[1] to indicate an invalid socket path,
>>   both reported by Jon Maloy]
>> [sbrivio: In pesto's main(), drop unnecessary newline from error
>>   message, reported by Laurent]
>> [sbrivio: Don't use SOCK_NONBLOCK on accept4(), as that only applies
>>   to the *new* file descriptor, which we don't want -- set O_NONBLOCK
>>   on the listening file descriptor using fcntl()]
> 
> Making the new (accepted) socket non-blocking was the intended
> behaviour here.  We also want non-blocking for the listening socket,
> but that was already done in feab892c7 ("tap, repair: Use
> SOCK_NONBLOCK and SOCK_CLOEXEC on Unix sockets").

This patch has been dropped since v6 of the series.

Thanks,
Laurent


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

* Re: [PATCH v8 15/19] pesto: Parse and add new rules from command line
  2026-05-05 23:47 ` [PATCH v8 15/19] pesto: Parse and add new rules from command line Stefano Brivio
@ 2026-05-06  7:13   ` Laurent Vivier
  2026-05-06  9:15     ` Stefano Brivio
  0 siblings, 1 reply; 44+ messages in thread
From: Laurent Vivier @ 2026-05-06  7:13 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: Jon Maloy, David Gibson

On 5/6/26 01:47, Stefano Brivio wrote:
> From: David Gibson <david@gibson.dropbear.id.au>
> 
> This adds parsing of options using fwd_rule_parse(), validates them and
> adds them to the existing rules. It doesn't yet send those rules back to
> passt or pasta.
> 
> Message-ID: <20260322141843.4095972-3-sbrivio@redhat.com>
> [dwg: Based on an early draft by Stefano]
> Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> [sbrivio: Recycled usage messages for -T and -U from conf.c as
>   suggested by Laurent, dropped unrelated whitespace change]
> [sbrivio: Add description of -t, -u, -T, -U to pesto.1]

And -s, --show ?

> Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> Reviewed-by: Laurent Vivier <lvivier@redhat.com>
> ---
>   Makefile   |   1 +
>   fwd_rule.c |   2 +-
>   fwd_rule.h |   1 +
>   pesto.1    | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++
>   pesto.c    | 111 ++++++++++++++++++++++++++++++++++++++++++++--
>   5 files changed, 237 insertions(+), 5 deletions(-)


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

* Re: [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto
  2026-05-05 23:47 ` [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto Stefano Brivio
@ 2026-05-06  7:15   ` Laurent Vivier
  2026-05-06  8:12   ` Laurent Vivier
  1 sibling, 0 replies; 44+ messages in thread
From: Laurent Vivier @ 2026-05-06  7:15 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: Jon Maloy, David Gibson

On 5/6/26 01:47, Stefano Brivio wrote:
> From: David Gibson <david@gibson.dropbear.id.au>
> 
> 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>
> [sbrivio: In fwd_listen_switch(), use the destination size as argument
>   to memcpy(), instead of sizeof(tmp), as suggested by Laurent]
> Signed-off-by: Stefano Brivio <sbrivio@redhat.com>

Reviewed-by: Laurent Vivier <lvivier@redhat.com>

> ---
>   conf.c |  5 ++---
>   fwd.c  | 34 ++++++++++++++++++++++++++++++++++
>   fwd.h  |  1 +
>   3 files changed, 37 insertions(+), 3 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index 76344da..3f48793 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -2160,15 +2160,14 @@ void conf_handler(struct ctx *c, uint32_t events)
>   			fwd_rules_dump(info, fwd->rules, fwd->count,
>   				       "    ", "");
>   		}
> +
> +		fwd_listen_switch(c);
>   	}
>   
>   	if (events & EPOLLHUP) {
>   		debug("Configuration client hangup");
> -		goto close;
>   	}
>   
> -	return;
> -
>   close:
>   	conf_close(c);
>   
> diff --git a/fwd.c b/fwd.c
> index d93d2e5..0697435 100644
> --- a/fwd.c
> +++ b/fwd.c
> @@ -534,6 +534,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);
> +		fwd->count = fwd->sock_count = 0;
> +	}
> +
> +	/* 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(c->fwd));
> +	memcpy((void *)c->fwd_pending, &tmp, sizeof(c->fwd_pending));
> +
> +	/* 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 ac24782..b60697d 100644
> --- a/fwd.h
> +++ b/fwd.h
> @@ -61,6 +61,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);


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

* Re: [PATCH v8 18/19] fwd_rule: Fix static checkers warnings in fwd_rule_add()
  2026-05-05 23:47 ` [PATCH v8 18/19] fwd_rule: Fix static checkers warnings in fwd_rule_add() Stefano Brivio
@ 2026-05-06  7:18   ` Laurent Vivier
  0 siblings, 0 replies; 44+ messages in thread
From: Laurent Vivier @ 2026-05-06  7:18 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: Jon Maloy, David Gibson

On 5/6/26 01:47, Stefano Brivio wrote:
> The new checks are actually sufficient but not enough for Coverity
> Scan. Now that fwd->sock_count and new->last are affected or supplied
> by clients, we need explicit (albeit redundant) checks on them.
> 
> Signed-off-by: Stefano Brivio <sbrivio@redhat.com>

Reviewed-by: Laurent Vivier <lvivier@redhat.com>

> ---
>   fwd_rule.c | 9 +++++++++
>   1 file changed, 9 insertions(+)
> 
> diff --git a/fwd_rule.c b/fwd_rule.c
> index b55e4df..03e8e80 100644
> --- a/fwd_rule.c
> +++ b/fwd_rule.c
> @@ -271,13 +271,22 @@ int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
>   		warn("Too many rules (maximum %d)", ARRAY_SIZE(fwd->rules));
>   		return -ENOSPC;
>   	}
> +
>   	if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) {
>   		warn("Rules require too many listening sockets (maximum %d)",
>   		     ARRAY_SIZE(fwd->socks));
>   		return -ENOSPC;
>   	}
> +	/* Redundant, to make static checkers happy */
> +	if (fwd->sock_count > ARRAY_SIZE(fwd->socks))
> +		return -ENOSPC;
>   
>   	fwd->rulesocks[fwd->count] = &fwd->socks[fwd->sock_count];
> +
> +	/* Redundant ('num' checked above), but not for static checkers */
> +	if (new->last > ARRAY_SIZE(fwd->socks) + new->first)
> +		return -ENOSPC;
> +
>   	for (port = new->first; port <= new->last; port++)
>   		fwd->rulesocks[fwd->count][port - new->first] = -1;
>   


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

* Re: [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions
  2026-05-06  7:06     ` Laurent Vivier
@ 2026-05-06  7:41       ` David Gibson
  0 siblings, 0 replies; 44+ messages in thread
From: David Gibson @ 2026-05-06  7:41 UTC (permalink / raw)
  To: Laurent Vivier; +Cc: Stefano Brivio, passt-dev, Jon Maloy

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

On Wed, May 06, 2026 at 09:06:05AM +0200, Laurent Vivier wrote:
> On 5/6/26 07:38, David Gibson wrote:
> > On Wed, May 06, 2026 at 01:47:10AM +0200, Stefano Brivio wrote:
> > > From: David Gibson <david@gibson.dropbear.id.au>
> > > 
> > > Start implementing pesto in earnest.  Create a control/configuration
> > > socket in passt.  Have pesto connect to it and retrieve a server greeting
> > > Perform some basic version checking.
> > > 
> > > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > > [sbrivio: Avoid potential recursive calling between conf_accept() and
> > >   conf_close(), reported by clang-tidy]
> > 
> > Huh.  For some reason that warning didn't trip for me.  Although it's
> > technically true they can mutually recurse, I believe they're both
> > tail calls, so it shouldn't eat the stack.
> > 
> > > [sbrivio: In conf(), check we're not exceeding sizeof(c->control_path)
> > >   instead of sizeof(c->socket_path), and, in pesto's main(), print
> > >   argv[optind] instead of argv[1] to indicate an invalid socket path,
> > >   both reported by Jon Maloy]
> > > [sbrivio: In pesto's main(), drop unnecessary newline from error
> > >   message, reported by Laurent]
> > > [sbrivio: Don't use SOCK_NONBLOCK on accept4(), as that only applies
> > >   to the *new* file descriptor, which we don't want -- set O_NONBLOCK
> > >   on the listening file descriptor using fcntl()]
> > 
> > Making the new (accepted) socket non-blocking was the intended
> > behaviour here.  We also want non-blocking for the listening socket,
> > but that was already done in feab892c7 ("tap, repair: Use
> > SOCK_NONBLOCK and SOCK_CLOEXEC on Unix sockets").
> 
> This patch has been dropped since v6 of the series.

Oh, right.  Sorry, I forgot it had been part of this series; thought
it was already merged from an earlier series.

What was the reason for dropping 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] 44+ messages in thread

* Re: [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions
  2026-05-06  5:38   ` David Gibson
  2026-05-06  7:06     ` Laurent Vivier
@ 2026-05-06  7:55     ` Stefano Brivio
  2026-05-06  8:21       ` David Gibson
  1 sibling, 1 reply; 44+ messages in thread
From: Stefano Brivio @ 2026-05-06  7:55 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev, Jon Maloy, Laurent Vivier

On Wed, 6 May 2026 15:38:30 +1000
David Gibson <david@gibson.dropbear.id.au> wrote:

> On Wed, May 06, 2026 at 01:47:10AM +0200, Stefano Brivio wrote:
> > From: David Gibson <david@gibson.dropbear.id.au>
> > 
> > Start implementing pesto in earnest.  Create a control/configuration
> > socket in passt.  Have pesto connect to it and retrieve a server greeting
> > Perform some basic version checking.
> > 
> > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > [sbrivio: Avoid potential recursive calling between conf_accept() and
> >  conf_close(), reported by clang-tidy]  
> 
> Huh.  For some reason that warning didn't trip for me.  Although it's
> technically true they can mutually recurse, I believe they're both
> tail calls, so it shouldn't eat the stack.
> 
> > [sbrivio: In conf(), check we're not exceeding sizeof(c->control_path)
> >  instead of sizeof(c->socket_path), and, in pesto's main(), print
> >  argv[optind] instead of argv[1] to indicate an invalid socket path,
> >  both reported by Jon Maloy]
> > [sbrivio: In pesto's main(), drop unnecessary newline from error
> >  message, reported by Laurent]
> > [sbrivio: Don't use SOCK_NONBLOCK on accept4(), as that only applies
> >  to the *new* file descriptor, which we don't want -- set O_NONBLOCK
> >  on the listening file descriptor using fcntl()]  
> 
> Making the new (accepted) socket non-blocking was the intended
> behaviour here.  We also want non-blocking for the listening socket,
> but that was already done in feab892c7 ("tap, repair: Use
> SOCK_NONBLOCK and SOCK_CLOEXEC on Unix sockets").

Oops, now that Laurent mentioned it, I realised I dropped it
accidentally while / after debugging things on v6, and:

> WIth the current design, I guess we don't want non-blocking on the
> accepted socket, although I don't think it actually matters very much.

...this is the issue I was trying to fix: if the accepted socket is
non-blocking, messages are cut short sometimes, and in general things
don't work.

I don't remember if this was while testing things on Fedora or Debian,
it only happened in one of the two environments.

So, while it was accidental (I really didn't leave any note for a cover
letter, so I'm almost certain there was no other reason for me to drop
it), I think it's actually a good idea to drop it for the following
reasons:

- O_NONBLOCK on the accepted socket breaks things

- the rest looks correct to me but fairly out of scope, and I have very
  limited time for testing things in detail right now, so I'd rather
  keep that patch for later

Without that patch, and my follow-up change to this patch, we're just
adding two lines for this specific behaviour, instead of 18.

> We will want non-blocking it when we change this to read out the
> updated rules incrementally, rather than all at once.

Right, so maybe we can keep that patch for that moment. Or even before,
again, I think it's all correct and desirable, just out of scope (this
isn't the reason why I dropped it though, I would have written a note).

-- 
Stefano


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

* Re: [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto
  2026-05-05 23:47 ` [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto Stefano Brivio
  2026-05-06  7:15   ` Laurent Vivier
@ 2026-05-06  8:12   ` Laurent Vivier
  2026-05-06  8:23     ` David Gibson
  2026-05-06  8:39     ` Stefano Brivio
  1 sibling, 2 replies; 44+ messages in thread
From: Laurent Vivier @ 2026-05-06  8:12 UTC (permalink / raw)
  To: Stefano Brivio, passt-dev; +Cc: Jon Maloy, David Gibson

On 5/6/26 01:47, Stefano Brivio wrote:
> From: David Gibson <david@gibson.dropbear.id.au>
> 
> 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>
> [sbrivio: In fwd_listen_switch(), use the destination size as argument
>   to memcpy(), instead of sizeof(tmp), as suggested by Laurent]
> Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> ---
>   conf.c |  5 ++---
>   fwd.c  | 34 ++++++++++++++++++++++++++++++++++
>   fwd.h  |  1 +
>   3 files changed, 37 insertions(+), 3 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index 76344da..3f48793 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -2160,15 +2160,14 @@ void conf_handler(struct ctx *c, uint32_t events)
>   			fwd_rules_dump(info, fwd->rules, fwd->count,
>   				       "    ", "");
>   		}
> +
> +		fwd_listen_switch(c);
>   	}
>   
>   	if (events & EPOLLHUP) {
>   		debug("Configuration client hangup");
> -		goto close;
>   	}
>   
> -	return;
> -
>   close:
>   	conf_close(c);
>   
> diff --git a/fwd.c b/fwd.c
> index d93d2e5..0697435 100644
> --- a/fwd.c
> +++ b/fwd.c
> @@ -534,6 +534,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);
> +		fwd->count = fwd->sock_count = 0;
> +	}
> +
> +	/* Swap active and pending tables */
> +	static_assert(sizeof(tmp) == sizeof(c->fwd) &&
> +		      sizeof(tmp) == sizeof(c->fwd_pending),
> +		      "Temporary has wrong size");

At this point:

c->fwd[PIF_HOST] = &fwd_in;
c->fwd[PIF_SPLICE] = &fwd_out;

c->fwd_pending[PIF_HOST] = &fwd_in_pending;
c->fwd_pending[PIF_SPLICE] = &fwd_out_pending;

> +	memcpy(&tmp, (void *)c->fwd, sizeof(tmp));
> +	memcpy((void *)c->fwd, (void *)c->fwd_pending, sizeof(c->fwd));
> +	memcpy((void *)c->fwd_pending, &tmp, sizeof(c->fwd_pending));

At this point:

c->fwd[PIF_HOST] = &fwd_in_pending;
c->fwd[PIF_SPLICE] = &fwd_out_pending;

c->fwd_pending[PIF_HOST] = &fwd_in;
c->fwd_pending[PIF_SPLICE] = &fwd_out;

Perhaps it should be noted somewhere to avoid confusion in the future?
Or to copy the content of the rules rather than the pointer to the rules?

Thanks,
Laurent


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

* Re: [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions
  2026-05-06  7:55     ` Stefano Brivio
@ 2026-05-06  8:21       ` David Gibson
  2026-05-06  8:30         ` Stefano Brivio
  0 siblings, 1 reply; 44+ messages in thread
From: David Gibson @ 2026-05-06  8:21 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev, Jon Maloy, Laurent Vivier

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

On Wed, May 06, 2026 at 09:55:32AM +0200, Stefano Brivio wrote:
> On Wed, 6 May 2026 15:38:30 +1000
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > On Wed, May 06, 2026 at 01:47:10AM +0200, Stefano Brivio wrote:
> > > From: David Gibson <david@gibson.dropbear.id.au>
> > > 
> > > Start implementing pesto in earnest.  Create a control/configuration
> > > socket in passt.  Have pesto connect to it and retrieve a server greeting
> > > Perform some basic version checking.
> > > 
> > > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > > [sbrivio: Avoid potential recursive calling between conf_accept() and
> > >  conf_close(), reported by clang-tidy]  
> > 
> > Huh.  For some reason that warning didn't trip for me.  Although it's
> > technically true they can mutually recurse, I believe they're both
> > tail calls, so it shouldn't eat the stack.
> > 
> > > [sbrivio: In conf(), check we're not exceeding sizeof(c->control_path)
> > >  instead of sizeof(c->socket_path), and, in pesto's main(), print
> > >  argv[optind] instead of argv[1] to indicate an invalid socket path,
> > >  both reported by Jon Maloy]
> > > [sbrivio: In pesto's main(), drop unnecessary newline from error
> > >  message, reported by Laurent]
> > > [sbrivio: Don't use SOCK_NONBLOCK on accept4(), as that only applies
> > >  to the *new* file descriptor, which we don't want -- set O_NONBLOCK
> > >  on the listening file descriptor using fcntl()]  
> > 
> > Making the new (accepted) socket non-blocking was the intended
> > behaviour here.  We also want non-blocking for the listening socket,
> > but that was already done in feab892c7 ("tap, repair: Use
> > SOCK_NONBLOCK and SOCK_CLOEXEC on Unix sockets").
> 
> Oops, now that Laurent mentioned it, I realised I dropped it
> accidentally while / after debugging things on v6, and:

Ah, right.

> > WIth the current design, I guess we don't want non-blocking on the
> > accepted socket, although I don't think it actually matters very much.
> 
> ...this is the issue I was trying to fix: if the accepted socket is
> non-blocking, messages are cut short sometimes, and in general things
> don't work.

Hrm.  I was pretty sure setting it blocking just meant you'd always
get *some* data instead of EAGAIN.  I don't believe it prevents either
short reads or short writes.

Both sides should already be using {read,write}_all_buf() to handle
short read/writes, so I'm really not sure where it's going wrong if
the accepted socket is non-blocking (other than maybe spinning on
EAGAIN more than we'd like).

> I don't remember if this was while testing things on Fedora or Debian,
> it only happened in one of the two environments.
> 
> So, while it was accidental (I really didn't leave any note for a cover
> letter, so I'm almost certain there was no other reason for me to drop
> it), I think it's actually a good idea to drop it for the following
> reasons:
> 
> - O_NONBLOCK on the accepted socket breaks things

The earlier patch doesn't affect O_NONBLOCK on the accepted socket,
only on the listening socket (it's not inherited).

> - the rest looks correct to me but fairly out of scope, and I have very
>   limited time for testing things in detail right now, so I'd rather
>   keep that patch for later

It's in scope because O_NONBLOCK on the listening socket is essential
to implementing the "concurrent client blocks" instead of "concurrent
client is rejected" behaviour.  Without O_NONBLOCK on the listening
socket, we can't safely call conf_accept() anywhere other than in
response to the epoll event - because looking for additional
connections after we close one could block.

> Without that patch, and my follow-up change to this patch, we're just
> adding two lines for this specific behaviour, instead of 18.
> 
> > We will want non-blocking it when we change this to read out the
> > updated rules incrementally, rather than all at once.
> 
> Right, so maybe we can keep that patch for that moment. Or even
> before,

"that patch" meaning the sock_unix() one?  Again, that affects the
listening socket behaviour.  What we need for incrementally reading
the rules is about the accepted socket behaviour.

> again, I think it's all correct and desirable, just out of scope (this
> isn't the reason why I dropped it though, I would have written a note).
> 
> -- 
> 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] 44+ messages in thread

* Re: [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules
  2026-05-06  6:45   ` David Gibson
@ 2026-05-06  8:22     ` Stefano Brivio
  2026-05-06  8:48       ` David Gibson
  0 siblings, 1 reply; 44+ messages in thread
From: Stefano Brivio @ 2026-05-06  8:22 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev, Jon Maloy, Laurent Vivier

On Wed, 6 May 2026 16:45:27 +1000
David Gibson <david@gibson.dropbear.id.au> wrote:

> On Wed, May 06, 2026 at 01:47:19AM +0200, Stefano Brivio wrote:
> > Instead of just being able to replace the existing forwarding table,  
> 
> As of my last version, we already added, rather than replacing.

Right, I noticed that, but this isn't the default behaviour we
discussed, so I assumed it was accidental, and planned to go back and
check the reason why.

Given that it wasn't accidental, I'll simply adjust this part of the
commit message.

> > implement --add and --delete options to maintain the table and add
> > or delete specific ports.
> > 
> > The option --clear PIF forces the clearing of a table, instead.
> > 
> > These options can be combined arbitrarily and are handled as
> > sequential commands, as now described in pesto(1).
> > 
> > If no option is given before forwarding specifiers for a matching
> > table, the command line is interpreted as a replacement of the
> > existing rules.
> > 
> > To this end:
> > 
> > - there's no protocol change, as pesto is anyway sending updated
> >   copies of the table
> > 
> > - the forwarding table functions now include a new fwd_rule_del(),
> >   which deletes existing rule only if a matching one is found
> > 
> > - a trivial fwd_rule_clear() is factored out from the existing
> >   conf_handler() implementation, so that it can be directly used
> >   in pesto
> > 
> > The entry points for parsing of port specifiers now take an additional
> > 'del' parameter which is passed down all the way before reaching the
> > fwd_rule_add() implementation. If a rule should be deleted, at that
> > point, fwd_rule_del() is called instead.
> > 
> > Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> > ---
> >  conf.c     | 26 ++++++----------
> >  fwd_rule.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++-------
> >  fwd_rule.h |  4 ++-
> >  pesto.1    | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++
> >  pesto.c    | 53 ++++++++++++++++++++++++++++---
> >  5 files changed, 227 insertions(+), 32 deletions(-)
> > 
> > diff --git a/conf.c b/conf.c
> > index 3f48793..909c34c 100644
> > --- a/conf.c
> > +++ b/conf.c
> > @@ -1849,16 +1849,16 @@ void conf(struct ctx *c, int argc, char **argv)
> >  
> >  		if (name == 't') {
> >  			opt_t = true;
> > -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
> >  		} else if (name == 'u') {
> >  			opt_u = true;
> > -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
> >  		} else if (name == 'T') {
> >  			opt_T = true;
> > -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
> >  		} else if (name == 'U') {
> >  			opt_U = true;
> > -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
> >  		}
> >  	} while (name != -1);
> >  
> > @@ -1910,13 +1910,13 @@ void conf(struct ctx *c, int argc, char **argv)
> >  
> >  	if (c->mode == MODE_PASTA) {
> >  		if (!opt_t)
> > -			fwd_rule_parse('t', "auto", c->fwd[PIF_HOST]);
> > +			fwd_rule_parse('t', false, "auto", c->fwd[PIF_HOST]);
> >  		if (!opt_T)
> > -			fwd_rule_parse('T', "auto", c->fwd[PIF_SPLICE]);
> > +			fwd_rule_parse('T', false, "auto", c->fwd[PIF_SPLICE]);
> >  		if (!opt_u)
> > -			fwd_rule_parse('u', "auto", c->fwd[PIF_HOST]);
> > +			fwd_rule_parse('u', false, "auto", c->fwd[PIF_HOST]);
> >  		if (!opt_U)
> > -			fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]);
> > +			fwd_rule_parse('U', false, "auto", c->fwd[PIF_SPLICE]);
> >  	}
> >  
> >  	conf_sock_listen(c);
> > @@ -2135,14 +2135,8 @@ void conf_handler(struct ctx *c, uint32_t events)
> >  		unsigned pif;
> >  
> >  		/* Clear pending tables */
> > -		for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> > -			struct fwd_table *fwd = c->fwd_pending[pif];
> > -
> > -			if (!fwd)
> > -				continue;
> > -			fwd->count = 0;
> > -			fwd->sock_count = 0;
> > -		}
> > +		for (pif = 0; pif < PIF_NUM_TYPES; pif++)
> > +			fwd_rule_clear(c->fwd_pending[pif]);
> >  
> >  		/* FIXME: this could block indefinitely if the client doesn't
> >  		 * write as much as it should
> > diff --git a/fwd_rule.c b/fwd_rule.c
> > index 03e8e80..eb9a601 100644
> > --- a/fwd_rule.c
> > +++ b/fwd_rule.c
> > @@ -180,6 +180,66 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
> >  	return true;
> >  }
> >  
> > +/**
> > + * fwd_rule_clear() - Clear a forwarding table
> > + * @fwd:	Table to clear (might be NULL)
> > + */
> > +void fwd_rule_clear(struct fwd_table *fwd)
> > +{
> > +	if (!fwd)
> > +		return;
> > +  
> 
> Not essential, but I wonder if it would be wise to verify that there
> are no currently open sockets associated with any of the rules.

With a loop, I suppose. I can add it as a TODO comment because I guess
it would be good to handle that case (open sockets left) for
fwd_rule_del() as well, and a part of the implementation can probably
be common.

> > +	fwd->count = 0;
> > +	fwd->sock_count = 0;
> > +}
> > +
> > +/**
> > + * fwd_rule_del() - Partially validate and delete a rule from a forwarding table
> > + * @fwd:	Table to delete from
> > + * @rule:	Rule to delete (must match an existing rule)
> > + *
> > + * Return: 0 on success, negative error code on failure (-ENOENT if not found)
> > + *
> > + * NOTE: This function can't be used for a forwarding table with valid sockets
> > + * stored in fwd->rulesocks.  
> 
> The exact meaning of this isn't very clear to me.  Does "valid" mean
> "open" or something else?

It means valid at some point, not necessarily open right now. I'll
change it to "open" for clarity.

> I think what you're getting at is that every entry in fwd->socks[]
> must be -1.  Or at least every entry with index in [0,sock_count)

Yes.

> > + */
> > +static int fwd_rule_del(struct fwd_table *fwd, const struct fwd_rule *rule)
> > +{
> > +	unsigned num, i;
> > +
> > +	for (i = 0; i < fwd->count; i++) {
> > +		if (fwd_rule_conflicts(rule, &fwd->rules[i]))
> > +			break;
> > +	}  
> 
> So, this deletes any conflicting rule, not only exact matches.  That's
> not very clear from the description of @rule.

It deletes the first one (but given that fwd_rule_conflicts() is called
on insertion, there should be a single one).

It's good enough for our purposes right now, even though we might want
to make that more sophisticated in the future. I'll change the
description of @rule.

> > +
> > +	if (i == fwd->count) {
> > +		char newstr[FWD_RULE_STRLEN];
> > +
> > +		warn("Couldn't find forwarding rule to delete: %s",
> > +		     fwd_rule_fmt(rule, newstr, sizeof(newstr)));
> > +		return -ENOENT;
> > +	}
> > +
> > +	/* Don't use anything else from 'rule' as passed, it's not validated */
> > +	rule = &fwd->rules[i];
> > +	num = (unsigned)rule->last - rule->first + 1;
> > +
> > +	fwd->count--;
> > +
> > +	memmove((void *)(fwd->rulesocks + i), (void *)(fwd->rulesocks + i + 1),  
> 
> I don't think the explicit (void *) casts are necessary - they should
> be implicit from memmove()s signature.

They aren't from a C point of view, but clang-tidy reports a
bugprone-multi-level-implicit-pointer-conversion warning if I don't do
that, because it's not obvious that we want the same kind of cast as the
implicit one.

> > +		(fwd->count - i) * sizeof(*fwd->rulesocks));  
> 
> Is memmove() guaranteed to be safe if given a zero length?  That will
> occur if deleting the last entry.

In practice, this is quite commonly done in the Linux kernel and
elsewhere and I never hit issues, against different C libraries.

I'm actually not sure what issue I should get, C11 says that it "copies
n characters from the object pointed to by s2 into the object pointed to
by s1", and if "n" is 0, nothing is done.

There's no specific mention of that in C11, but I don't think there
needs to be one, unlike with realloc() with a zero size and possibly
others problematic cases.

So, yes, as far as I know it's "safe", but with no explicit guarantee
(and I don't think any is needed).

> > +	/* TODO: move sockets stored starting from fwd->rulesocks[i + i], should
> > +	 * we ever need to delete rules from a table with open sockets.
> > +	 */
> > +	fwd->sock_count -= num;
> > +
> > +	memmove(fwd->rules + i, fwd->rules + i + 1,
> > +		(fwd->count - i) * sizeof(*fwd->rules)  
> 
> Again, is this safe if i == fwd->count?

Same as above.

> > +
> > +	return 0;
> > +}
> > +
> >  /**
> >   * fwd_rule_add() - Validate and add a rule to a forwarding table
> >   * @fwd:	Table to add to
> > @@ -368,6 +428,7 @@ static int parse_keyword(const char *s, const char **endptr, const char *kw)
> >   * fwd_rule_range_except() - Set up forwarding for a range of ports minus a
> >   *                           bitmap of exclusions
> >   * @fwd:	Forwarding table to be updated
> > + * @del:	Delete resulting rules from forwarding table, instead of adding  
> 
> Clunky, but it gets the job done.
> 
> >   * @proto:	Protocol to forward
> >   * @addr:	Listening address
> >   * @ifname:	Listening interface
> > @@ -377,8 +438,8 @@ static int parse_keyword(const char *s, const char **endptr, const char *kw)
> >   * @to:		Port to translate @first to when forwarding
> >   * @flags:	Flags for forwarding entries
> >   */
> > -static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
> > -				  const union inany_addr *addr,
> > +static void fwd_rule_range_except(struct fwd_table *fwd, bool del,
> > +				  uint8_t proto, const union inany_addr *addr,
> >  				  const char *ifname,
> >  				  uint16_t first, uint16_t last,
> >  				  const uint8_t *exclude, uint16_t to,
> > @@ -418,8 +479,13 @@ static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
> >  		rule.last = i - 1;
> >  		rule.to = base + delta;
> >  
> > -		if (fwd_rule_add(fwd, &rule) < 0)
> > -			goto fail;
> > +		if (del) {
> > +			if (fwd_rule_del(fwd, &rule) < 0)
> > +				goto fail;
> > +		} else {
> > +			if (fwd_rule_add(fwd, &rule) < 0)
> > +				goto fail;
> > +		}
> >  
> >  		base = i - 1;
> >  	}
> > @@ -445,12 +511,13 @@ fail:
> >  /**
> >   * fwd_rule_parse_ports() - Parse port range(s) specifier
> >   * @fwd:	Forwarding table to be updated
> > + * @del:	Delete resulting rules from forwarding table, instead of adding
> >   * @proto:	Protocol to forward
> >   * @addr:	Listening address for forwarding
> >   * @ifname:	Interface name for listening
> >   * @spec:	Port range(s) specifier
> >   */
> > -static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
> > +static void fwd_rule_parse_ports(struct fwd_table *fwd, bool del, uint8_t proto,
> >  				 const union inany_addr *addr,
> >  				 const char *ifname,
> >  				 const char *spec)
> > @@ -507,7 +574,7 @@ static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
> >  		/* Exclude ephemeral ports */
> >  		fwd_port_map_ephemeral(exclude);
> >  
> > -		fwd_rule_range_except(fwd, proto, addr, ifname,
> > +		fwd_rule_range_except(fwd, del, proto, addr, ifname,
> >  				      1, NUM_PORTS - 1, exclude,
> >  				      1, flags | FWD_WEAK);
> >  		return;
> > @@ -537,7 +604,7 @@ static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
> >  		if (p != ep) /* Garbage after the ranges */
> >  			goto bad;
> >  
> > -		fwd_rule_range_except(fwd, proto, addr, ifname,
> > +		fwd_rule_range_except(fwd, del, proto, addr, ifname,
> >  				      orig_range.first, orig_range.last,
> >  				      exclude,
> >  				      mapped_range.first, flags);
> > @@ -551,10 +618,12 @@ bad:
> >  /**
> >   * fwd_rule_parse() - Parse port configuration option
> >   * @optname:	Short option name, t, T, u, or U
> > + * @del:	Delete resulting rules from forwarding table, instead of adding
> >   * @optarg:	Option argument (port specification)
> >   * @fwd:	Forwarding table to be updated
> >   */
> > -void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
> > +void fwd_rule_parse(char optname, bool del, const char *optarg,
> > +		    struct fwd_table *fwd)
> >  {
> >  	union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
> >  	char buf[BUFSIZ], *spec, *ifname = NULL;
> > @@ -632,12 +701,12 @@ void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
> >  			     optname, optarg);
> >  
> >  			if (fwd->caps & FWD_CAP_IPV4) {
> > -				fwd_rule_parse_ports(fwd, proto,
> > +				fwd_rule_parse_ports(fwd, del, proto,
> >  						     &inany_loopback4, NULL,
> >  						     spec);
> >  			}
> >  			if (fwd->caps & FWD_CAP_IPV6) {
> > -				fwd_rule_parse_ports(fwd, proto,
> > +				fwd_rule_parse_ports(fwd, del, proto,
> >  						     &inany_loopback6, NULL,
> >  						     spec);
> >  			}
> > @@ -653,7 +722,7 @@ void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
> >  		    optname, optarg);
> >  	}
> >  
> > -	fwd_rule_parse_ports(fwd, proto, addr, ifname, spec);
> > +	fwd_rule_parse_ports(fwd, del, proto, addr, ifname, spec);
> >  }
> >  
> >  /**
> > diff --git a/fwd_rule.h b/fwd_rule.h
> > index f43b37d..ae9a3cb 100644
> > --- a/fwd_rule.h
> > +++ b/fwd_rule.h
> > @@ -100,9 +100,11 @@ void fwd_probe_ephemeral(void);
> >  
> >  const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
> >  const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size);
> > -void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd);
> > +void fwd_rule_parse(char optname, bool del, const char *optarg,
> > +		    struct fwd_table *fwd);
> >  int fwd_rule_read(int fd, struct fwd_rule *rule);
> >  int fwd_rule_write(int fd, const struct fwd_rule *rule);
> > +void fwd_rule_clear(struct fwd_table *fwd);
> >  int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new);
> >  
> >  /**
> > diff --git a/pesto.1 b/pesto.1
> > index 1ea1d12..cd0f3dc 100644
> > --- a/pesto.1
> > +++ b/pesto.1
> > @@ -31,6 +31,42 @@ Be verbose.
> >  .BR \-h ", " \-\-help
> >  Display a help message and exit.
> >  
> > +.TP
> > +.BR \-A ", " \-\-add
> > +Add the port forwarding specifiers following this option to the current
> > +forwarding table, rather than replacing it.
> > +
> > +This option can be given multiple times, as it might follow previous deletions
> > +(see \fB--delete\fR below), and implies that all the specifiers following it,
> > +before a further \fB--delete\fR option occurs, will be handled as additions.
> > +
> > +See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
> > +more details.
> > +
> > +.TP
> > +.BR \-D ", " \-\-delete
> > +Delete the port forwarding specifiers following this option from the current
> > +forwarding table, rather than adding them it.
> > +
> > +This option can be given multiple times, as it might follow previous additions
> > +(see \fB--add\fR above), and implies that all the specifiers following it,
> > +before a further \fB--add\fR option occurs, will be handled as deletions.
> > +
> > +See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
> > +more details.
> > +
> > +.TP
> > +.BR \-C ", " \-\-clear " " \fIpif
> > +Clear the forwarding table associated to a given \fIpif\fR, that is, a
> > +conceptual type of interface in \fBpasst\fR(1) or \fBpasta\fR(1) representing a
> > +specific data path and direction.
> > +
> > +The available \fIpif\fR names can be obtained by querying the current forwarding
> > +configuration, which can be done by calling \fBpesto\fR(1) without options.
> > +
> > +See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
> > +more details.
> > +
> >  .TP
> >  .BR \-t ", " \-\-tcp-ports " " \fIspec
> >  Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of:
> > @@ -162,6 +198,55 @@ Configure UDP port forwarding from target namespace to init namespace.
> >  .BR \-\-version
> >  Show version and exit.
> >  
> > +.SH NOTES
> > +
> > +.SS Adding, deleting, clearing rules
> > +
> > +The options \fB--add\fR, \fB--delete\fR, and \fB--clear\fR are handled as
> > +sequential commands to manipulate the current forwarding tables. If none of them
> > +is given, forwarding specifiers for a given table are intended as replacement of
> > +the corresponding table. That is:  
> 
> I thought we wanted to default to add, rather than replace.  That
> seems both a little simpler to implement and agruably more likely to
> be what peopke want.

We previously discussed we would default to replace for brevity,
because otherwise users would need to explicitly clear tables, but also
because it's consistent with:

- the idea that we replace the current table

- the existing command line syntax that just "programs" a new table.

That is, the cost of typing:

  -A -t 22

is marginally higher compared to:

  -t 22

but:

  -C HOST -t 22

in case a replacement is desired looks substantially more to me.

It's really not that important at the moment as Podman will anyway
explicitly pass options. Of course it's not a great idea to change this
later for non-automated users, but in any case I think the reason I
explained above (and we discussed in the past) are still valid.

> > +.nf
> > +	pesto -t 1024 -U 1025
> > +.fi
> > +
> > +will \fBreplace\fR the current TCP inbound port forwarding table with a single
> > +rule, forwarding port 1024, and will similarly replace the UDP outbound
> > +forwarding table with a single forwarding rule for port 1025. This usage is a
> > +short-hand form for:
> > +
> > +.nf
> > +	pesto -C HOST -t 1024 -C SPLICE -U 1025
> > +.fi
> > +
> > +The options \fB--add\fR and \fB--delete\fR are used to \fBadd new specific
> > +rules or delete existing ones\fR, instead of replacing tables. For example:
> > +
> > +.nf
> > +	pesto -A -t 2000 -D -t 3000 -U 5000
> > +.fi
> > +
> > +will add a forwarding rule for inbound TCP port 2000, and delete inbound TCP
> > +port 3000 as well as outbound UDP port 5000 from the existing set of rules.
> > +
> > +All these options are interpreted as sequential commands and can be arbitrarily
> > +combined. For example:
> > +
> > +.nf
> > +	pesto -A -t 2000 -C HOST -A -T 3000 -t 2001 -D -u 5000
> > +.fi
> > +
> > +will, in order:
> > +
> > +.RS
> > +- add inbound TCP port 2000
> > +- clear inbound ports, reverting the addition above
> > +- add outbound TCP port 3000
> > +- add inbound TCP port 2001
> > +- delete inbound UDP port 5000
> > +.RE
> > +
> >  .SH AUTHORS
> >  
> >  Stefano Brivio <sbrivio@redhat.com>,
> > diff --git a/pesto.c b/pesto.c
> > index 73fdc39..143d4c6 100644
> > --- a/pesto.c
> > +++ b/pesto.c
> > @@ -55,6 +55,9 @@ static void usage(const char *name, FILE *f, int status)
> >  	FPRINTF(f, "Usage: %s [OPTION]... PATH\n", name);
> >  	FPRINTF(f,
> >  		"\n"
> > +		"  -A, --add		Add following specifiers to forwards\n"
> > +		"  -D, --delete		Delete following specifiers instead\n"
> > +		"  -C, --clear PIF	Clear forwarding table for given PIF\n"
> >  		"  -t, --tcp-ports SPEC	TCP inbound port forwarding\n"
> >  		"    can be specified multiple times\n"
> >  		"    SPEC can be:\n"
> > @@ -298,6 +301,9 @@ int main(int argc, char **argv)
> >  		{"debug",	no_argument,		NULL,		'd' },
> >  		{"help",	no_argument,		NULL,		'h' },
> >  		{"version",	no_argument,		NULL,		1 },
> > +		{"add",		no_argument,		NULL,		'A' },
> > +		{"delete",	no_argument,		NULL,		'D' },
> > +		{"clear",	required_argument,	NULL,		'C' },
> >  		{"tcp-ports",	required_argument,	NULL,		't' },
> >  		{"udp-ports",	required_argument,	NULL,		'u' },
> >  		{"tcp-ns",	required_argument,	NULL,		'T' },
> > @@ -305,9 +311,11 @@ int main(int argc, char **argv)
> >  		{"show",	no_argument,		NULL,		's' },
> >  		{ 0 },
> >  	};
> > +	enum { MODE_CLEAR, MODE_ADD, MODE_DEL } mode = MODE_CLEAR;  
> 
> MODE_CLEAR doesn't make sense to me.  Unlike add or del, clear is a
> once-off operation, it's not clear to me how it would affect the
> interpretation of -[tTuU].

It needs to be a mode, and the default one, to make sure we default to
replacing tables for a given direction of forwarding.

It's not exactly one-off as it's a starting mode until -A or -D are
given. The interpretation is as described in the man page.

> > +	bool inbound_cleared = false, outbound_cleared = false;
> >  	struct pif_configuration *inbound, *outbound;
> > +	const char *optstring = "dhADC:t:u:T:U:s";
> >  	struct sockaddr_un a = { AF_UNIX, "" };
> > -	const char *optstring = "dht:u:T:U:s";
> >  	struct configuration conf = { 0 };
> >  	bool update = false, show = false;
> >  	struct pesto_hello hello;
> > @@ -339,11 +347,16 @@ int main(int argc, char **argv)
> >  		case -1:
> >  		case 0:
> >  			break;
> > +		case 'C':
> >  		case 't':
> >  		case 'u':
> >  		case 'T':
> >  		case 'U':
> > -			/* Parse these options after we've read state from passt/pasta */
> > +		case 'A':
> > +		case 'D':
> > +			/* Parse these options after we've read state from
> > +			 * passt/pasta
> > +			 */
> >  			update = true;
> >  			break;
> >  		case 's':
> > @@ -439,13 +452,38 @@ int main(int argc, char **argv)
> >  		optname = getopt_long(argc, argv, optstring, options, NULL);
> >  
> >  		switch (optname) {
> > +		case 'A':
> > +			mode = MODE_ADD;
> > +			break;
> > +		case 'D':
> > +			mode = MODE_DEL;
> > +			break;
> > +		case 'C':
> > +			if (!strcmp(optarg, "HOST")) {
> > +				fwd_rule_clear(&inbound->fwd);
> > +				inbound_cleared = true;
> > +			} else if (!strcmp(optarg, "SPLICE")) {
> > +				fwd_rule_clear(&outbound->fwd);  
> 
> outbound will be NULL if talking to passt, so this could SEGV.

Ah, right, nice catch, I'll fix that in v9.

> > +				outbound_cleared = true;
> > +			} else {
> > +				die("Unsupported pif name %s", optarg);
> > +			}  
> 
> For the time being pesto is limited to a single "inbound" and single
> "outbound" table, simply because we haven't devised syntax for
> anything else.  However, we don't actually need that for --clear.
> Since it already takes a pif name we can use pif_conf_by_name() to
> clear an arbitrary named pif's rules.

I'll change that in v9, assuming it works (I haven't tried yet).

> > +
> > +			break;
> >  		case 't':
> >  		case 'u':
> >  			if (!inbound) {
> >  				die("Can't use -%c, no inbound interface",
> >  				    optname);
> >  			}
> > -			fwd_rule_parse(optname, optarg, &inbound->fwd);
> > +
> > +			if (mode == MODE_CLEAR && !inbound_cleared) {
> > +				fwd_rule_clear(&inbound->fwd);
> > +				inbound_cleared = true;
> > +			}
> > +
> > +			fwd_rule_parse(optname, mode == MODE_DEL, optarg,
> > +				       &inbound->fwd);
> >  			break;
> >  		case 'T':
> >  		case 'U':
> > @@ -453,7 +491,14 @@ int main(int argc, char **argv)
> >  				die("Can't use -%c, no outbound interface",
> >  				    optname);
> >  			}
> > -			fwd_rule_parse(optname, optarg, &outbound->fwd);
> > +
> > +			if (mode == MODE_CLEAR && !outbound_cleared) {
> > +				fwd_rule_clear(&outbound->fwd);
> > +				outbound_cleared = true;
> > +			}
> > +
> > +			fwd_rule_parse(optname, mode == MODE_DEL, optarg,
> > +				       &inbound->fwd);
> >  			break;
> >  		default:
> >  			continue;
> > -- 
> > 2.43.0
> >   
> 

-- 
Stefano


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

* Re: [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto
  2026-05-06  8:12   ` Laurent Vivier
@ 2026-05-06  8:23     ` David Gibson
  2026-05-06  8:39     ` Stefano Brivio
  1 sibling, 0 replies; 44+ messages in thread
From: David Gibson @ 2026-05-06  8:23 UTC (permalink / raw)
  To: Laurent Vivier; +Cc: Stefano Brivio, passt-dev, Jon Maloy

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

On Wed, May 06, 2026 at 10:12:21AM +0200, Laurent Vivier wrote:
> On 5/6/26 01:47, Stefano Brivio wrote:
> > From: David Gibson <david@gibson.dropbear.id.au>
> > 
> > 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>
> > [sbrivio: In fwd_listen_switch(), use the destination size as argument
> >   to memcpy(), instead of sizeof(tmp), as suggested by Laurent]
> > Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> > ---
> >   conf.c |  5 ++---
> >   fwd.c  | 34 ++++++++++++++++++++++++++++++++++
> >   fwd.h  |  1 +
> >   3 files changed, 37 insertions(+), 3 deletions(-)
> > 
> > diff --git a/conf.c b/conf.c
> > index 76344da..3f48793 100644
> > --- a/conf.c
> > +++ b/conf.c
> > @@ -2160,15 +2160,14 @@ void conf_handler(struct ctx *c, uint32_t events)
> >   			fwd_rules_dump(info, fwd->rules, fwd->count,
> >   				       "    ", "");
> >   		}
> > +
> > +		fwd_listen_switch(c);
> >   	}
> >   	if (events & EPOLLHUP) {
> >   		debug("Configuration client hangup");
> > -		goto close;
> >   	}
> > -	return;
> > -
> >   close:
> >   	conf_close(c);
> > diff --git a/fwd.c b/fwd.c
> > index d93d2e5..0697435 100644
> > --- a/fwd.c
> > +++ b/fwd.c
> > @@ -534,6 +534,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);
> > +		fwd->count = fwd->sock_count = 0;
> > +	}
> > +
> > +	/* Swap active and pending tables */
> > +	static_assert(sizeof(tmp) == sizeof(c->fwd) &&
> > +		      sizeof(tmp) == sizeof(c->fwd_pending),
> > +		      "Temporary has wrong size");
> 
> At this point:
> 
> c->fwd[PIF_HOST] = &fwd_in;
> c->fwd[PIF_SPLICE] = &fwd_out;
> 
> c->fwd_pending[PIF_HOST] = &fwd_in_pending;
> c->fwd_pending[PIF_SPLICE] = &fwd_out_pending;
> 
> > +	memcpy(&tmp, (void *)c->fwd, sizeof(tmp));
> > +	memcpy((void *)c->fwd, (void *)c->fwd_pending, sizeof(c->fwd));
> > +	memcpy((void *)c->fwd_pending, &tmp, sizeof(c->fwd_pending));
> 
> At this point:
> 
> c->fwd[PIF_HOST] = &fwd_in_pending;
> c->fwd[PIF_SPLICE] = &fwd_out_pending;
> 
> c->fwd_pending[PIF_HOST] = &fwd_in;
> c->fwd_pending[PIF_SPLICE] = &fwd_out;
> 
> Perhaps it should be noted somewhere to avoid confusion in the future?
> Or to copy the content of the rules rather than the pointer to the rules?

Ah, good point.  The problem is the names of the globals - it's not
really one "real" and one "pending" table, just two tables that switch
between those roles.

-- 
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] 44+ messages in thread

* Re: [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions
  2026-05-06  8:21       ` David Gibson
@ 2026-05-06  8:30         ` Stefano Brivio
  0 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-06  8:30 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev, Jon Maloy, Laurent Vivier

On Wed, 6 May 2026 18:21:32 +1000
David Gibson <david@gibson.dropbear.id.au> wrote:

> On Wed, May 06, 2026 at 09:55:32AM +0200, Stefano Brivio wrote:
> > On Wed, 6 May 2026 15:38:30 +1000
> > David Gibson <david@gibson.dropbear.id.au> wrote:
> >   
> > > On Wed, May 06, 2026 at 01:47:10AM +0200, Stefano Brivio wrote:  
> > > > From: David Gibson <david@gibson.dropbear.id.au>
> > > > 
> > > > Start implementing pesto in earnest.  Create a control/configuration
> > > > socket in passt.  Have pesto connect to it and retrieve a server greeting
> > > > Perform some basic version checking.
> > > > 
> > > > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > > > [sbrivio: Avoid potential recursive calling between conf_accept() and
> > > >  conf_close(), reported by clang-tidy]    
> > > 
> > > Huh.  For some reason that warning didn't trip for me.  Although it's
> > > technically true they can mutually recurse, I believe they're both
> > > tail calls, so it shouldn't eat the stack.
> > >   
> > > > [sbrivio: In conf(), check we're not exceeding sizeof(c->control_path)
> > > >  instead of sizeof(c->socket_path), and, in pesto's main(), print
> > > >  argv[optind] instead of argv[1] to indicate an invalid socket path,
> > > >  both reported by Jon Maloy]
> > > > [sbrivio: In pesto's main(), drop unnecessary newline from error
> > > >  message, reported by Laurent]
> > > > [sbrivio: Don't use SOCK_NONBLOCK on accept4(), as that only applies
> > > >  to the *new* file descriptor, which we don't want -- set O_NONBLOCK
> > > >  on the listening file descriptor using fcntl()]    
> > > 
> > > Making the new (accepted) socket non-blocking was the intended
> > > behaviour here.  We also want non-blocking for the listening socket,
> > > but that was already done in feab892c7 ("tap, repair: Use
> > > SOCK_NONBLOCK and SOCK_CLOEXEC on Unix sockets").  
> > 
> > Oops, now that Laurent mentioned it, I realised I dropped it
> > accidentally while / after debugging things on v6, and:  
> 
> Ah, right.
> 
> > > WIth the current design, I guess we don't want non-blocking on the
> > > accepted socket, although I don't think it actually matters very much.  
> > 
> > ...this is the issue I was trying to fix: if the accepted socket is
> > non-blocking, messages are cut short sometimes, and in general things
> > don't work.  
> 
> Hrm.  I was pretty sure setting it blocking just meant you'd always
> get *some* data instead of EAGAIN.  I don't believe it prevents either
> short reads or short writes.

Maybe, but something caused actual problems for me, otherwise I
wouldn't have played with it at all. The current behaviour with this
patch is something I tested quite heavily by now.

> Both sides should already be using {read,write}_all_buf() to handle
> short read/writes, so I'm really not sure where it's going wrong if
> the accepted socket is non-blocking (other than maybe spinning on
> EAGAIN more than we'd like).
> 
> > I don't remember if this was while testing things on Fedora or Debian,
> > it only happened in one of the two environments.
> > 
> > So, while it was accidental (I really didn't leave any note for a cover
> > letter, so I'm almost certain there was no other reason for me to drop
> > it), I think it's actually a good idea to drop it for the following
> > reasons:
> > 
> > - O_NONBLOCK on the accepted socket breaks things  
> 
> The earlier patch doesn't affect O_NONBLOCK on the accepted socket,
> only on the listening socket (it's not inherited).
> 
> > - the rest looks correct to me but fairly out of scope, and I have very
> >   limited time for testing things in detail right now, so I'd rather
> >   keep that patch for later  
> 
> It's in scope because O_NONBLOCK on the listening socket is essential

This can be implemented in two lines like the current version does. The
rest is out of scope.

> to implementing the "concurrent client blocks" instead of "concurrent
> client is rejected" behaviour.  Without O_NONBLOCK on the listening
> socket, we can't safely call conf_accept() anywhere other than in
> response to the epoll event - because looking for additional
> connections after we close one could block.
> 
> > Without that patch, and my follow-up change to this patch, we're just
> > adding two lines for this specific behaviour, instead of 18.
> >   
> > > We will want non-blocking it when we change this to read out the
> > > updated rules incrementally, rather than all at once.  
> > 
> > Right, so maybe we can keep that patch for that moment. Or even
> > before,  
> 
> "that patch" meaning the sock_unix() one?  Again, that affects the
> listening socket behaviour.  What we need for incrementally reading
> the rules is about the accepted socket behaviour.

Yes. That doesn't just affect the listening socket, it affects a bunch
of things, and they can all be checked and revisited, more conveniently,
together, at a later point.

-- 
Stefano


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

* Re: [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto
  2026-05-06  8:12   ` Laurent Vivier
  2026-05-06  8:23     ` David Gibson
@ 2026-05-06  8:39     ` Stefano Brivio
  2026-05-06  8:49       ` Stefano Brivio
  1 sibling, 1 reply; 44+ messages in thread
From: Stefano Brivio @ 2026-05-06  8:39 UTC (permalink / raw)
  To: Laurent Vivier; +Cc: passt-dev, Jon Maloy, David Gibson

On Wed, 6 May 2026 10:12:21 +0200
Laurent Vivier <lvivier@redhat.com> wrote:

> On 5/6/26 01:47, Stefano Brivio wrote:
> > From: David Gibson <david@gibson.dropbear.id.au>
> > 
> > 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>
> > [sbrivio: In fwd_listen_switch(), use the destination size as argument
> >   to memcpy(), instead of sizeof(tmp), as suggested by Laurent]
> > Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> > ---
> >   conf.c |  5 ++---
> >   fwd.c  | 34 ++++++++++++++++++++++++++++++++++
> >   fwd.h  |  1 +
> >   3 files changed, 37 insertions(+), 3 deletions(-)
> > 
> > diff --git a/conf.c b/conf.c
> > index 76344da..3f48793 100644
> > --- a/conf.c
> > +++ b/conf.c
> > @@ -2160,15 +2160,14 @@ void conf_handler(struct ctx *c, uint32_t events)
> >   			fwd_rules_dump(info, fwd->rules, fwd->count,
> >   				       "    ", "");
> >   		}
> > +
> > +		fwd_listen_switch(c);
> >   	}
> >   
> >   	if (events & EPOLLHUP) {
> >   		debug("Configuration client hangup");
> > -		goto close;
> >   	}
> >   
> > -	return;
> > -
> >   close:
> >   	conf_close(c);
> >   
> > diff --git a/fwd.c b/fwd.c
> > index d93d2e5..0697435 100644
> > --- a/fwd.c
> > +++ b/fwd.c
> > @@ -534,6 +534,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);
> > +		fwd->count = fwd->sock_count = 0;
> > +	}
> > +
> > +	/* Swap active and pending tables */
> > +	static_assert(sizeof(tmp) == sizeof(c->fwd) &&
> > +		      sizeof(tmp) == sizeof(c->fwd_pending),
> > +		      "Temporary has wrong size");  
> 
> At this point:
> 
> c->fwd[PIF_HOST] = &fwd_in;
> c->fwd[PIF_SPLICE] = &fwd_out;
> 
> c->fwd_pending[PIF_HOST] = &fwd_in_pending;
> c->fwd_pending[PIF_SPLICE] = &fwd_out_pending;
> 
> > +	memcpy(&tmp, (void *)c->fwd, sizeof(tmp));
> > +	memcpy((void *)c->fwd, (void *)c->fwd_pending, sizeof(c->fwd));
> > +	memcpy((void *)c->fwd_pending, &tmp, sizeof(c->fwd_pending));  
> 
> At this point:
> 
> c->fwd[PIF_HOST] = &fwd_in_pending;
> c->fwd[PIF_SPLICE] = &fwd_out_pending;
> 
> c->fwd_pending[PIF_HOST] = &fwd_in;
> c->fwd_pending[PIF_SPLICE] = &fwd_out;

Yeah, makes sense, I can change that in v9.

> Perhaps it should be noted somewhere to avoid confusion in the future?

What do you think should be noted exactly, and where? Can you show a
practical example of the change you're proposing?

> Or to copy the content of the rules rather than the pointer to the rules?
> 
> Thanks,
> Laurent

-- 
Stefano


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

* Re: [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules
  2026-05-06  8:22     ` Stefano Brivio
@ 2026-05-06  8:48       ` David Gibson
  2026-05-06  8:56         ` Stefano Brivio
  0 siblings, 1 reply; 44+ messages in thread
From: David Gibson @ 2026-05-06  8:48 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev, Jon Maloy, Laurent Vivier

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

On Wed, May 06, 2026 at 10:22:20AM +0200, Stefano Brivio wrote:
> On Wed, 6 May 2026 16:45:27 +1000
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > On Wed, May 06, 2026 at 01:47:19AM +0200, Stefano Brivio wrote:
> > > Instead of just being able to replace the existing forwarding table,  
> > 
> > As of my last version, we already added, rather than replacing.
> 
> Right, I noticed that, but this isn't the default behaviour we
> discussed, so I assumed it was accidental, and planned to go back and
> check the reason why.
> 
> Given that it wasn't accidental, I'll simply adjust this part of the
> commit message.
> 
> > > implement --add and --delete options to maintain the table and add
> > > or delete specific ports.
> > > 
> > > The option --clear PIF forces the clearing of a table, instead.
> > > 
> > > These options can be combined arbitrarily and are handled as
> > > sequential commands, as now described in pesto(1).
> > > 
> > > If no option is given before forwarding specifiers for a matching
> > > table, the command line is interpreted as a replacement of the
> > > existing rules.
> > > 
> > > To this end:
> > > 
> > > - there's no protocol change, as pesto is anyway sending updated
> > >   copies of the table
> > > 
> > > - the forwarding table functions now include a new fwd_rule_del(),
> > >   which deletes existing rule only if a matching one is found
> > > 
> > > - a trivial fwd_rule_clear() is factored out from the existing
> > >   conf_handler() implementation, so that it can be directly used
> > >   in pesto
> > > 
> > > The entry points for parsing of port specifiers now take an additional
> > > 'del' parameter which is passed down all the way before reaching the
> > > fwd_rule_add() implementation. If a rule should be deleted, at that
> > > point, fwd_rule_del() is called instead.
> > > 
> > > Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> > > ---
> > >  conf.c     | 26 ++++++----------
> > >  fwd_rule.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++-------
> > >  fwd_rule.h |  4 ++-
> > >  pesto.1    | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++
> > >  pesto.c    | 53 ++++++++++++++++++++++++++++---
> > >  5 files changed, 227 insertions(+), 32 deletions(-)
> > > 
> > > diff --git a/conf.c b/conf.c
> > > index 3f48793..909c34c 100644
> > > --- a/conf.c
> > > +++ b/conf.c
> > > @@ -1849,16 +1849,16 @@ void conf(struct ctx *c, int argc, char **argv)
> > >  
> > >  		if (name == 't') {
> > >  			opt_t = true;
> > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
> > >  		} else if (name == 'u') {
> > >  			opt_u = true;
> > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
> > >  		} else if (name == 'T') {
> > >  			opt_T = true;
> > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
> > >  		} else if (name == 'U') {
> > >  			opt_U = true;
> > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
> > >  		}
> > >  	} while (name != -1);
> > >  
> > > @@ -1910,13 +1910,13 @@ void conf(struct ctx *c, int argc, char **argv)
> > >  
> > >  	if (c->mode == MODE_PASTA) {
> > >  		if (!opt_t)
> > > -			fwd_rule_parse('t', "auto", c->fwd[PIF_HOST]);
> > > +			fwd_rule_parse('t', false, "auto", c->fwd[PIF_HOST]);
> > >  		if (!opt_T)
> > > -			fwd_rule_parse('T', "auto", c->fwd[PIF_SPLICE]);
> > > +			fwd_rule_parse('T', false, "auto", c->fwd[PIF_SPLICE]);
> > >  		if (!opt_u)
> > > -			fwd_rule_parse('u', "auto", c->fwd[PIF_HOST]);
> > > +			fwd_rule_parse('u', false, "auto", c->fwd[PIF_HOST]);
> > >  		if (!opt_U)
> > > -			fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]);
> > > +			fwd_rule_parse('U', false, "auto", c->fwd[PIF_SPLICE]);
> > >  	}
> > >  
> > >  	conf_sock_listen(c);
> > > @@ -2135,14 +2135,8 @@ void conf_handler(struct ctx *c, uint32_t events)
> > >  		unsigned pif;
> > >  
> > >  		/* Clear pending tables */
> > > -		for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> > > -			struct fwd_table *fwd = c->fwd_pending[pif];
> > > -
> > > -			if (!fwd)
> > > -				continue;
> > > -			fwd->count = 0;
> > > -			fwd->sock_count = 0;
> > > -		}
> > > +		for (pif = 0; pif < PIF_NUM_TYPES; pif++)
> > > +			fwd_rule_clear(c->fwd_pending[pif]);
> > >  
> > >  		/* FIXME: this could block indefinitely if the client doesn't
> > >  		 * write as much as it should
> > > diff --git a/fwd_rule.c b/fwd_rule.c
> > > index 03e8e80..eb9a601 100644
> > > --- a/fwd_rule.c
> > > +++ b/fwd_rule.c
> > > @@ -180,6 +180,66 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
> > >  	return true;
> > >  }
> > >  
> > > +/**
> > > + * fwd_rule_clear() - Clear a forwarding table
> > > + * @fwd:	Table to clear (might be NULL)
> > > + */
> > > +void fwd_rule_clear(struct fwd_table *fwd)
> > > +{
> > > +	if (!fwd)
> > > +		return;
> > > +  
> > 
> > Not essential, but I wonder if it would be wise to verify that there
> > are no currently open sockets associated with any of the rules.
> 
> With a loop, I suppose. I can add it as a TODO comment because I guess
> it would be good to handle that case (open sockets left) for
> fwd_rule_del() as well, and a part of the implementation can probably
> be common.
> 
> > > +	fwd->count = 0;
> > > +	fwd->sock_count = 0;
> > > +}
> > > +
> > > +/**
> > > + * fwd_rule_del() - Partially validate and delete a rule from a forwarding table
> > > + * @fwd:	Table to delete from
> > > + * @rule:	Rule to delete (must match an existing rule)
> > > + *
> > > + * Return: 0 on success, negative error code on failure (-ENOENT if not found)
> > > + *
> > > + * NOTE: This function can't be used for a forwarding table with valid sockets
> > > + * stored in fwd->rulesocks.  
> > 
> > The exact meaning of this isn't very clear to me.  Does "valid" mean
> > "open" or something else?
> 
> It means valid at some point, not necessarily open right now. I'll
> change it to "open" for clarity.

I'm not sure what "valid at some point" means, either.

> > I think what you're getting at is that every entry in fwd->socks[]
> > must be -1.  Or at least every entry with index in [0,sock_count)
> 
> Yes.
> 
> > > + */
> > > +static int fwd_rule_del(struct fwd_table *fwd, const struct fwd_rule *rule)
> > > +{
> > > +	unsigned num, i;
> > > +
> > > +	for (i = 0; i < fwd->count; i++) {
> > > +		if (fwd_rule_conflicts(rule, &fwd->rules[i]))
> > > +			break;
> > > +	}  
> > 
> > So, this deletes any conflicting rule, not only exact matches.  That's
> > not very clear from the description of @rule.
> 
> It deletes the first one

Oh, good point.  Which actually elevates this to a bug, not just a
debate about the best semantics, because...

> (but given that fwd_rule_conflicts() is called
> on insertion, there should be a single one).

... that's not correct.  "conflicts" is not transitive, so (for
example) in the cases below:
	-t 1000-2000 -t 4000-5000 --delete -t 500-5500
	-t 127.0.0.1/100 -t 127.0.0.2/100 --delete -t 100
The deleted rule conflicts with both the added rules, but they don't
conflict with each other.

I don't think "delete all conflicting rules" is a great either, since
it means that:
	-t 1000-1999 -t 2000-2999 --delete -t 1500-2500
maps nothing at all, which seems pretty surprising.

> It's good enough for our purposes right now, even though we might want
> to make that more sophisticated in the future. I'll change the
> description of @rule.

I really think the current behaviour is too confusing.  Only deleting
exact matches (and giving an error if there's a conflict that's not an
exact match) I think *is* good enough for now, so that's what I'd
suggest.

> > > +
> > > +	if (i == fwd->count) {
> > > +		char newstr[FWD_RULE_STRLEN];
> > > +
> > > +		warn("Couldn't find forwarding rule to delete: %s",
> > > +		     fwd_rule_fmt(rule, newstr, sizeof(newstr)));
> > > +		return -ENOENT;
> > > +	}
> > > +
> > > +	/* Don't use anything else from 'rule' as passed, it's not validated */
> > > +	rule = &fwd->rules[i];
> > > +	num = (unsigned)rule->last - rule->first + 1;
> > > +
> > > +	fwd->count--;
> > > +
> > > +	memmove((void *)(fwd->rulesocks + i), (void *)(fwd->rulesocks + i + 1),  
> > 
> > I don't think the explicit (void *) casts are necessary - they should
> > be implicit from memmove()s signature.
> 
> They aren't from a C point of view, but clang-tidy reports a
> bugprone-multi-level-implicit-pointer-conversion warning if I don't do
> that, because it's not obvious that we want the same kind of cast as the
> implicit one.

Ah, ok.

> > > +		(fwd->count - i) * sizeof(*fwd->rulesocks));  
> > 
> > Is memmove() guaranteed to be safe if given a zero length?  That will
> > occur if deleting the last entry.
> 
> In practice, this is quite commonly done in the Linux kernel and
> elsewhere and I never hit issues, against different C libraries.

Ok.

> I'm actually not sure what issue I should get, C11 says that it "copies
> n characters from the object pointed to by s2 into the object pointed to
> by s1", and if "n" is 0, nothing is done.
> 
> There's no specific mention of that in C11, but I don't think there
> needs to be one, unlike with realloc() with a zero size and possibly
> others problematic cases.
> 
> So, yes, as far as I know it's "safe", but with no explicit guarantee
> (and I don't think any is needed).
> 
> > > +	/* TODO: move sockets stored starting from fwd->rulesocks[i + i], should
> > > +	 * we ever need to delete rules from a table with open sockets.
> > > +	 */
> > > +	fwd->sock_count -= num;
> > > +
> > > +	memmove(fwd->rules + i, fwd->rules + i + 1,
> > > +		(fwd->count - i) * sizeof(*fwd->rules)  
> > 
> > Again, is this safe if i == fwd->count?
> 
> Same as above.
> 
> > > +
> > > +	return 0;
> > > +}
> > > +
> > >  /**
> > >   * fwd_rule_add() - Validate and add a rule to a forwarding table
> > >   * @fwd:	Table to add to
> > > @@ -368,6 +428,7 @@ static int parse_keyword(const char *s, const char **endptr, const char *kw)
> > >   * fwd_rule_range_except() - Set up forwarding for a range of ports minus a
> > >   *                           bitmap of exclusions
> > >   * @fwd:	Forwarding table to be updated
> > > + * @del:	Delete resulting rules from forwarding table, instead of adding  
> > 
> > Clunky, but it gets the job done.
> > 
> > >   * @proto:	Protocol to forward
> > >   * @addr:	Listening address
> > >   * @ifname:	Listening interface
> > > @@ -377,8 +438,8 @@ static int parse_keyword(const char *s, const char **endptr, const char *kw)
> > >   * @to:		Port to translate @first to when forwarding
> > >   * @flags:	Flags for forwarding entries
> > >   */
> > > -static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
> > > -				  const union inany_addr *addr,
> > > +static void fwd_rule_range_except(struct fwd_table *fwd, bool del,
> > > +				  uint8_t proto, const union inany_addr *addr,
> > >  				  const char *ifname,
> > >  				  uint16_t first, uint16_t last,
> > >  				  const uint8_t *exclude, uint16_t to,
> > > @@ -418,8 +479,13 @@ static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
> > >  		rule.last = i - 1;
> > >  		rule.to = base + delta;
> > >  
> > > -		if (fwd_rule_add(fwd, &rule) < 0)
> > > -			goto fail;
> > > +		if (del) {
> > > +			if (fwd_rule_del(fwd, &rule) < 0)
> > > +				goto fail;
> > > +		} else {
> > > +			if (fwd_rule_add(fwd, &rule) < 0)
> > > +				goto fail;
> > > +		}
> > >  
> > >  		base = i - 1;
> > >  	}
> > > @@ -445,12 +511,13 @@ fail:
> > >  /**
> > >   * fwd_rule_parse_ports() - Parse port range(s) specifier
> > >   * @fwd:	Forwarding table to be updated
> > > + * @del:	Delete resulting rules from forwarding table, instead of adding
> > >   * @proto:	Protocol to forward
> > >   * @addr:	Listening address for forwarding
> > >   * @ifname:	Interface name for listening
> > >   * @spec:	Port range(s) specifier
> > >   */
> > > -static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
> > > +static void fwd_rule_parse_ports(struct fwd_table *fwd, bool del, uint8_t proto,
> > >  				 const union inany_addr *addr,
> > >  				 const char *ifname,
> > >  				 const char *spec)
> > > @@ -507,7 +574,7 @@ static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
> > >  		/* Exclude ephemeral ports */
> > >  		fwd_port_map_ephemeral(exclude);
> > >  
> > > -		fwd_rule_range_except(fwd, proto, addr, ifname,
> > > +		fwd_rule_range_except(fwd, del, proto, addr, ifname,
> > >  				      1, NUM_PORTS - 1, exclude,
> > >  				      1, flags | FWD_WEAK);
> > >  		return;
> > > @@ -537,7 +604,7 @@ static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
> > >  		if (p != ep) /* Garbage after the ranges */
> > >  			goto bad;
> > >  
> > > -		fwd_rule_range_except(fwd, proto, addr, ifname,
> > > +		fwd_rule_range_except(fwd, del, proto, addr, ifname,
> > >  				      orig_range.first, orig_range.last,
> > >  				      exclude,
> > >  				      mapped_range.first, flags);
> > > @@ -551,10 +618,12 @@ bad:
> > >  /**
> > >   * fwd_rule_parse() - Parse port configuration option
> > >   * @optname:	Short option name, t, T, u, or U
> > > + * @del:	Delete resulting rules from forwarding table, instead of adding
> > >   * @optarg:	Option argument (port specification)
> > >   * @fwd:	Forwarding table to be updated
> > >   */
> > > -void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
> > > +void fwd_rule_parse(char optname, bool del, const char *optarg,
> > > +		    struct fwd_table *fwd)
> > >  {
> > >  	union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
> > >  	char buf[BUFSIZ], *spec, *ifname = NULL;
> > > @@ -632,12 +701,12 @@ void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
> > >  			     optname, optarg);
> > >  
> > >  			if (fwd->caps & FWD_CAP_IPV4) {
> > > -				fwd_rule_parse_ports(fwd, proto,
> > > +				fwd_rule_parse_ports(fwd, del, proto,
> > >  						     &inany_loopback4, NULL,
> > >  						     spec);
> > >  			}
> > >  			if (fwd->caps & FWD_CAP_IPV6) {
> > > -				fwd_rule_parse_ports(fwd, proto,
> > > +				fwd_rule_parse_ports(fwd, del, proto,
> > >  						     &inany_loopback6, NULL,
> > >  						     spec);
> > >  			}
> > > @@ -653,7 +722,7 @@ void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
> > >  		    optname, optarg);
> > >  	}
> > >  
> > > -	fwd_rule_parse_ports(fwd, proto, addr, ifname, spec);
> > > +	fwd_rule_parse_ports(fwd, del, proto, addr, ifname, spec);
> > >  }
> > >  
> > >  /**
> > > diff --git a/fwd_rule.h b/fwd_rule.h
> > > index f43b37d..ae9a3cb 100644
> > > --- a/fwd_rule.h
> > > +++ b/fwd_rule.h
> > > @@ -100,9 +100,11 @@ void fwd_probe_ephemeral(void);
> > >  
> > >  const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule);
> > >  const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size);
> > > -void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd);
> > > +void fwd_rule_parse(char optname, bool del, const char *optarg,
> > > +		    struct fwd_table *fwd);
> > >  int fwd_rule_read(int fd, struct fwd_rule *rule);
> > >  int fwd_rule_write(int fd, const struct fwd_rule *rule);
> > > +void fwd_rule_clear(struct fwd_table *fwd);
> > >  int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new);
> > >  
> > >  /**
> > > diff --git a/pesto.1 b/pesto.1
> > > index 1ea1d12..cd0f3dc 100644
> > > --- a/pesto.1
> > > +++ b/pesto.1
> > > @@ -31,6 +31,42 @@ Be verbose.
> > >  .BR \-h ", " \-\-help
> > >  Display a help message and exit.
> > >  
> > > +.TP
> > > +.BR \-A ", " \-\-add
> > > +Add the port forwarding specifiers following this option to the current
> > > +forwarding table, rather than replacing it.
> > > +
> > > +This option can be given multiple times, as it might follow previous deletions
> > > +(see \fB--delete\fR below), and implies that all the specifiers following it,
> > > +before a further \fB--delete\fR option occurs, will be handled as additions.
> > > +
> > > +See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
> > > +more details.
> > > +
> > > +.TP
> > > +.BR \-D ", " \-\-delete
> > > +Delete the port forwarding specifiers following this option from the current
> > > +forwarding table, rather than adding them it.
> > > +
> > > +This option can be given multiple times, as it might follow previous additions
> > > +(see \fB--add\fR above), and implies that all the specifiers following it,
> > > +before a further \fB--add\fR option occurs, will be handled as deletions.
> > > +
> > > +See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
> > > +more details.
> > > +
> > > +.TP
> > > +.BR \-C ", " \-\-clear " " \fIpif
> > > +Clear the forwarding table associated to a given \fIpif\fR, that is, a
> > > +conceptual type of interface in \fBpasst\fR(1) or \fBpasta\fR(1) representing a
> > > +specific data path and direction.
> > > +
> > > +The available \fIpif\fR names can be obtained by querying the current forwarding
> > > +configuration, which can be done by calling \fBpesto\fR(1) without options.
> > > +
> > > +See the section \fBAdding, deleting, clearing rules\fR in the \fBNOTES\fR for
> > > +more details.
> > > +
> > >  .TP
> > >  .BR \-t ", " \-\-tcp-ports " " \fIspec
> > >  Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of:
> > > @@ -162,6 +198,55 @@ Configure UDP port forwarding from target namespace to init namespace.
> > >  .BR \-\-version
> > >  Show version and exit.
> > >  
> > > +.SH NOTES
> > > +
> > > +.SS Adding, deleting, clearing rules
> > > +
> > > +The options \fB--add\fR, \fB--delete\fR, and \fB--clear\fR are handled as
> > > +sequential commands to manipulate the current forwarding tables. If none of them
> > > +is given, forwarding specifiers for a given table are intended as replacement of
> > > +the corresponding table. That is:  
> > 
> > I thought we wanted to default to add, rather than replace.  That
> > seems both a little simpler to implement and agruably more likely to
> > be what peopke want.
> 
> We previously discussed we would default to replace for brevity,

Oh, ok.  Apparently I'd forgotten.

> because otherwise users would need to explicitly clear tables, but also
> because it's consistent with:
> 
> - the idea that we replace the current table
> 
> - the existing command line syntax that just "programs" a new table.
> 
> That is, the cost of typing:
> 
>   -A -t 22
> 
> is marginally higher compared to:
> 
>   -t 22
> 
> but:
> 
>   -C HOST -t 22
> 
> in case a replacement is desired looks substantially more to me.

Ok, fair enough.

> It's really not that important at the moment as Podman will anyway
> explicitly pass options. Of course it's not a great idea to change this
> later for non-automated users, but in any case I think the reason I
> explained above (and we discussed in the past) are still valid.
> 
> > > +.nf
> > > +	pesto -t 1024 -U 1025
> > > +.fi
> > > +
> > > +will \fBreplace\fR the current TCP inbound port forwarding table with a single
> > > +rule, forwarding port 1024, and will similarly replace the UDP outbound
> > > +forwarding table with a single forwarding rule for port 1025. This usage is a
> > > +short-hand form for:
> > > +
> > > +.nf
> > > +	pesto -C HOST -t 1024 -C SPLICE -U 1025
> > > +.fi
> > > +
> > > +The options \fB--add\fR and \fB--delete\fR are used to \fBadd new specific
> > > +rules or delete existing ones\fR, instead of replacing tables. For example:
> > > +
> > > +.nf
> > > +	pesto -A -t 2000 -D -t 3000 -U 5000
> > > +.fi
> > > +
> > > +will add a forwarding rule for inbound TCP port 2000, and delete inbound TCP
> > > +port 3000 as well as outbound UDP port 5000 from the existing set of rules.
> > > +
> > > +All these options are interpreted as sequential commands and can be arbitrarily
> > > +combined. For example:
> > > +
> > > +.nf
> > > +	pesto -A -t 2000 -C HOST -A -T 3000 -t 2001 -D -u 5000
> > > +.fi
> > > +
> > > +will, in order:
> > > +
> > > +.RS
> > > +- add inbound TCP port 2000
> > > +- clear inbound ports, reverting the addition above
> > > +- add outbound TCP port 3000
> > > +- add inbound TCP port 2001
> > > +- delete inbound UDP port 5000
> > > +.RE
> > > +
> > >  .SH AUTHORS
> > >  
> > >  Stefano Brivio <sbrivio@redhat.com>,
> > > diff --git a/pesto.c b/pesto.c
> > > index 73fdc39..143d4c6 100644
> > > --- a/pesto.c
> > > +++ b/pesto.c
> > > @@ -55,6 +55,9 @@ static void usage(const char *name, FILE *f, int status)
> > >  	FPRINTF(f, "Usage: %s [OPTION]... PATH\n", name);
> > >  	FPRINTF(f,
> > >  		"\n"
> > > +		"  -A, --add		Add following specifiers to forwards\n"
> > > +		"  -D, --delete		Delete following specifiers instead\n"
> > > +		"  -C, --clear PIF	Clear forwarding table for given PIF\n"
> > >  		"  -t, --tcp-ports SPEC	TCP inbound port forwarding\n"
> > >  		"    can be specified multiple times\n"
> > >  		"    SPEC can be:\n"
> > > @@ -298,6 +301,9 @@ int main(int argc, char **argv)
> > >  		{"debug",	no_argument,		NULL,		'd' },
> > >  		{"help",	no_argument,		NULL,		'h' },
> > >  		{"version",	no_argument,		NULL,		1 },
> > > +		{"add",		no_argument,		NULL,		'A' },
> > > +		{"delete",	no_argument,		NULL,		'D' },
> > > +		{"clear",	required_argument,	NULL,		'C' },
> > >  		{"tcp-ports",	required_argument,	NULL,		't' },
> > >  		{"udp-ports",	required_argument,	NULL,		'u' },
> > >  		{"tcp-ns",	required_argument,	NULL,		'T' },
> > > @@ -305,9 +311,11 @@ int main(int argc, char **argv)
> > >  		{"show",	no_argument,		NULL,		's' },
> > >  		{ 0 },
> > >  	};
> > > +	enum { MODE_CLEAR, MODE_ADD, MODE_DEL } mode = MODE_CLEAR;  
> > 
> > MODE_CLEAR doesn't make sense to me.  Unlike add or del, clear is a
> > once-off operation, it's not clear to me how it would affect the
> > interpretation of -[tTuU].
> 
> It needs to be a mode, and the default one, to make sure we default to
> replacing tables for a given direction of forwarding.
> 
> It's not exactly one-off as it's a starting mode until -A or -D are
> given. The interpretation is as described in the man page.

Yeah, ok, it's necessary to support the replace-by-default semantics.

> > > +	bool inbound_cleared = false, outbound_cleared = false;
> > >  	struct pif_configuration *inbound, *outbound;
> > > +	const char *optstring = "dhADC:t:u:T:U:s";
> > >  	struct sockaddr_un a = { AF_UNIX, "" };
> > > -	const char *optstring = "dht:u:T:U:s";
> > >  	struct configuration conf = { 0 };
> > >  	bool update = false, show = false;
> > >  	struct pesto_hello hello;
> > > @@ -339,11 +347,16 @@ int main(int argc, char **argv)
> > >  		case -1:
> > >  		case 0:
> > >  			break;
> > > +		case 'C':
> > >  		case 't':
> > >  		case 'u':
> > >  		case 'T':
> > >  		case 'U':
> > > -			/* Parse these options after we've read state from passt/pasta */
> > > +		case 'A':
> > > +		case 'D':
> > > +			/* Parse these options after we've read state from
> > > +			 * passt/pasta
> > > +			 */
> > >  			update = true;
> > >  			break;
> > >  		case 's':
> > > @@ -439,13 +452,38 @@ int main(int argc, char **argv)
> > >  		optname = getopt_long(argc, argv, optstring, options, NULL);
> > >  
> > >  		switch (optname) {
> > > +		case 'A':
> > > +			mode = MODE_ADD;
> > > +			break;
> > > +		case 'D':
> > > +			mode = MODE_DEL;
> > > +			break;
> > > +		case 'C':
> > > +			if (!strcmp(optarg, "HOST")) {
> > > +				fwd_rule_clear(&inbound->fwd);
> > > +				inbound_cleared = true;
> > > +			} else if (!strcmp(optarg, "SPLICE")) {
> > > +				fwd_rule_clear(&outbound->fwd);  
> > 
> > outbound will be NULL if talking to passt, so this could SEGV.
> 
> Ah, right, nice catch, I'll fix that in v9.

In principle 'inbound' could also be NULL, although not with any
current passt version.  Generalising as mentioned below should
generalise the fix as well though.

> > > +				outbound_cleared = true;
> > > +			} else {
> > > +				die("Unsupported pif name %s", optarg);
> > > +			}  
> > 
> > For the time being pesto is limited to a single "inbound" and single
> > "outbound" table, simply because we haven't devised syntax for
> > anything else.  However, we don't actually need that for --clear.
> > Since it already takes a pif name we can use pif_conf_by_name() to
> > clear an arbitrary named pif's rules.
> 
> I'll change that in v9, assuming it works (I haven't tried yet).
> 
> > > +
> > > +			break;
> > >  		case 't':
> > >  		case 'u':
> > >  			if (!inbound) {
> > >  				die("Can't use -%c, no inbound interface",
> > >  				    optname);
> > >  			}
> > > -			fwd_rule_parse(optname, optarg, &inbound->fwd);
> > > +
> > > +			if (mode == MODE_CLEAR && !inbound_cleared) {
> > > +				fwd_rule_clear(&inbound->fwd);
> > > +				inbound_cleared = true;
> > > +			}
> > > +
> > > +			fwd_rule_parse(optname, mode == MODE_DEL, optarg,
> > > +				       &inbound->fwd);
> > >  			break;
> > >  		case 'T':
> > >  		case 'U':
> > > @@ -453,7 +491,14 @@ int main(int argc, char **argv)
> > >  				die("Can't use -%c, no outbound interface",
> > >  				    optname);
> > >  			}
> > > -			fwd_rule_parse(optname, optarg, &outbound->fwd);
> > > +
> > > +			if (mode == MODE_CLEAR && !outbound_cleared) {
> > > +				fwd_rule_clear(&outbound->fwd);
> > > +				outbound_cleared = true;
> > > +			}
> > > +
> > > +			fwd_rule_parse(optname, mode == MODE_DEL, optarg,
> > > +				       &inbound->fwd);
> > >  			break;
> > >  		default:
> > >  			continue;
> > > -- 
> > > 2.43.0
> > >   
> > 
> 
> -- 
> 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] 44+ messages in thread

* Re: [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto
  2026-05-06  8:39     ` Stefano Brivio
@ 2026-05-06  8:49       ` Stefano Brivio
  2026-05-06  8:52         ` David Gibson
  0 siblings, 1 reply; 44+ messages in thread
From: Stefano Brivio @ 2026-05-06  8:49 UTC (permalink / raw)
  To: Laurent Vivier; +Cc: passt-dev, Jon Maloy, David Gibson

On Wed, 6 May 2026 10:39:30 +0200
Stefano Brivio <sbrivio@redhat.com> wrote:

> On Wed, 6 May 2026 10:12:21 +0200
> Laurent Vivier <lvivier@redhat.com> wrote:
> 
> > On 5/6/26 01:47, Stefano Brivio wrote:  
> > > From: David Gibson <david@gibson.dropbear.id.au>
> > > 
> > > 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>
> > > [sbrivio: In fwd_listen_switch(), use the destination size as argument
> > >   to memcpy(), instead of sizeof(tmp), as suggested by Laurent]
> > > Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> > > ---
> > >   conf.c |  5 ++---
> > >   fwd.c  | 34 ++++++++++++++++++++++++++++++++++
> > >   fwd.h  |  1 +
> > >   3 files changed, 37 insertions(+), 3 deletions(-)
> > > 
> > > diff --git a/conf.c b/conf.c
> > > index 76344da..3f48793 100644
> > > --- a/conf.c
> > > +++ b/conf.c
> > > @@ -2160,15 +2160,14 @@ void conf_handler(struct ctx *c, uint32_t events)
> > >   			fwd_rules_dump(info, fwd->rules, fwd->count,
> > >   				       "    ", "");
> > >   		}
> > > +
> > > +		fwd_listen_switch(c);
> > >   	}
> > >   
> > >   	if (events & EPOLLHUP) {
> > >   		debug("Configuration client hangup");
> > > -		goto close;
> > >   	}
> > >   
> > > -	return;
> > > -
> > >   close:
> > >   	conf_close(c);
> > >   
> > > diff --git a/fwd.c b/fwd.c
> > > index d93d2e5..0697435 100644
> > > --- a/fwd.c
> > > +++ b/fwd.c
> > > @@ -534,6 +534,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);
> > > +		fwd->count = fwd->sock_count = 0;
> > > +	}
> > > +
> > > +	/* Swap active and pending tables */
> > > +	static_assert(sizeof(tmp) == sizeof(c->fwd) &&
> > > +		      sizeof(tmp) == sizeof(c->fwd_pending),
> > > +		      "Temporary has wrong size");    
> > 
> > At this point:
> > 
> > c->fwd[PIF_HOST] = &fwd_in;
> > c->fwd[PIF_SPLICE] = &fwd_out;
> > 
> > c->fwd_pending[PIF_HOST] = &fwd_in_pending;
> > c->fwd_pending[PIF_SPLICE] = &fwd_out_pending;
> >   
> > > +	memcpy(&tmp, (void *)c->fwd, sizeof(tmp));
> > > +	memcpy((void *)c->fwd, (void *)c->fwd_pending, sizeof(c->fwd));
> > > +	memcpy((void *)c->fwd_pending, &tmp, sizeof(c->fwd_pending));    
> > 
> > At this point:
> > 
> > c->fwd[PIF_HOST] = &fwd_in_pending;
> > c->fwd[PIF_SPLICE] = &fwd_out_pending;
> > 
> > c->fwd_pending[PIF_HOST] = &fwd_in;
> > c->fwd_pending[PIF_SPLICE] = &fwd_out;  
> 
> Yeah, makes sense, I can change that in v9.
> 
> > Perhaps it should be noted somewhere to avoid confusion in the future?  
> 
> What do you think should be noted exactly, and where? Can you show a
> practical example of the change you're proposing?

...I'm leaving like it is in v9 to make sure I'm not misinterpreting
you, and also because the current (v8) version is obviously correct and
I also tested it fairly heavily by now.

I'd suggest optimising this (and commenting as needed) in a separate
patch later.

-- 
Stefano


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

* Re: [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto
  2026-05-06  8:49       ` Stefano Brivio
@ 2026-05-06  8:52         ` David Gibson
  2026-05-06  9:11           ` Laurent Vivier
  0 siblings, 1 reply; 44+ messages in thread
From: David Gibson @ 2026-05-06  8:52 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: Laurent Vivier, passt-dev, Jon Maloy

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

On Wed, May 06, 2026 at 10:49:55AM +0200, Stefano Brivio wrote:
> On Wed, 6 May 2026 10:39:30 +0200
> Stefano Brivio <sbrivio@redhat.com> wrote:
> 
> > On Wed, 6 May 2026 10:12:21 +0200
> > Laurent Vivier <lvivier@redhat.com> wrote:
> > 
> > > On 5/6/26 01:47, Stefano Brivio wrote:  
> > > > From: David Gibson <david@gibson.dropbear.id.au>
> > > > 
> > > > 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>
> > > > [sbrivio: In fwd_listen_switch(), use the destination size as argument
> > > >   to memcpy(), instead of sizeof(tmp), as suggested by Laurent]
> > > > Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> > > > ---
> > > >   conf.c |  5 ++---
> > > >   fwd.c  | 34 ++++++++++++++++++++++++++++++++++
> > > >   fwd.h  |  1 +
> > > >   3 files changed, 37 insertions(+), 3 deletions(-)
> > > > 
> > > > diff --git a/conf.c b/conf.c
> > > > index 76344da..3f48793 100644
> > > > --- a/conf.c
> > > > +++ b/conf.c
> > > > @@ -2160,15 +2160,14 @@ void conf_handler(struct ctx *c, uint32_t events)
> > > >   			fwd_rules_dump(info, fwd->rules, fwd->count,
> > > >   				       "    ", "");
> > > >   		}
> > > > +
> > > > +		fwd_listen_switch(c);
> > > >   	}
> > > >   
> > > >   	if (events & EPOLLHUP) {
> > > >   		debug("Configuration client hangup");
> > > > -		goto close;
> > > >   	}
> > > >   
> > > > -	return;
> > > > -
> > > >   close:
> > > >   	conf_close(c);
> > > >   
> > > > diff --git a/fwd.c b/fwd.c
> > > > index d93d2e5..0697435 100644
> > > > --- a/fwd.c
> > > > +++ b/fwd.c
> > > > @@ -534,6 +534,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);
> > > > +		fwd->count = fwd->sock_count = 0;
> > > > +	}
> > > > +
> > > > +	/* Swap active and pending tables */
> > > > +	static_assert(sizeof(tmp) == sizeof(c->fwd) &&
> > > > +		      sizeof(tmp) == sizeof(c->fwd_pending),
> > > > +		      "Temporary has wrong size");    
> > > 
> > > At this point:
> > > 
> > > c->fwd[PIF_HOST] = &fwd_in;
> > > c->fwd[PIF_SPLICE] = &fwd_out;
> > > 
> > > c->fwd_pending[PIF_HOST] = &fwd_in_pending;
> > > c->fwd_pending[PIF_SPLICE] = &fwd_out_pending;
> > >   
> > > > +	memcpy(&tmp, (void *)c->fwd, sizeof(tmp));
> > > > +	memcpy((void *)c->fwd, (void *)c->fwd_pending, sizeof(c->fwd));
> > > > +	memcpy((void *)c->fwd_pending, &tmp, sizeof(c->fwd_pending));    
> > > 
> > > At this point:
> > > 
> > > c->fwd[PIF_HOST] = &fwd_in_pending;
> > > c->fwd[PIF_SPLICE] = &fwd_out_pending;
> > > 
> > > c->fwd_pending[PIF_HOST] = &fwd_in;
> > > c->fwd_pending[PIF_SPLICE] = &fwd_out;  
> > 
> > Yeah, makes sense, I can change that in v9.
> > 
> > > Perhaps it should be noted somewhere to avoid confusion in the future?  
> > 
> > What do you think should be noted exactly, and where? Can you show a
> > practical example of the change you're proposing?
> 
> ...I'm leaving like it is in v9 to make sure I'm not misinterpreting
> you, and also because the current (v8) version is obviously correct and
> I also tested it fairly heavily by now.
> 
> I'd suggest optimising this (and commenting as needed) in a separate
> patch later.

As noted in another branch of this thread, I think all it really needs
is renaming the globals that c->fwd and c->fwd_pending point to.  They
should just be fwd_in_[12] (or an array of 2 tables), instead of
implying a semantic difference between the plain and "pending" copies.

-- 
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] 44+ messages in thread

* Re: [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules
  2026-05-06  8:48       ` David Gibson
@ 2026-05-06  8:56         ` Stefano Brivio
  2026-05-06  9:22           ` David Gibson
  2026-05-06 12:52           ` Stefano Brivio
  0 siblings, 2 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-06  8:56 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev, Jon Maloy, Laurent Vivier

On Wed, 6 May 2026 18:48:10 +1000
David Gibson <david@gibson.dropbear.id.au> wrote:

> On Wed, May 06, 2026 at 10:22:20AM +0200, Stefano Brivio wrote:
> > On Wed, 6 May 2026 16:45:27 +1000
> > David Gibson <david@gibson.dropbear.id.au> wrote:
> >   
> > > On Wed, May 06, 2026 at 01:47:19AM +0200, Stefano Brivio wrote:  
> > > > Instead of just being able to replace the existing forwarding table,    
> > > 
> > > As of my last version, we already added, rather than replacing.  
> > 
> > Right, I noticed that, but this isn't the default behaviour we
> > discussed, so I assumed it was accidental, and planned to go back and
> > check the reason why.
> > 
> > Given that it wasn't accidental, I'll simply adjust this part of the
> > commit message.
> >   
> > > > implement --add and --delete options to maintain the table and add
> > > > or delete specific ports.
> > > > 
> > > > The option --clear PIF forces the clearing of a table, instead.
> > > > 
> > > > These options can be combined arbitrarily and are handled as
> > > > sequential commands, as now described in pesto(1).
> > > > 
> > > > If no option is given before forwarding specifiers for a matching
> > > > table, the command line is interpreted as a replacement of the
> > > > existing rules.
> > > > 
> > > > To this end:
> > > > 
> > > > - there's no protocol change, as pesto is anyway sending updated
> > > >   copies of the table
> > > > 
> > > > - the forwarding table functions now include a new fwd_rule_del(),
> > > >   which deletes existing rule only if a matching one is found
> > > > 
> > > > - a trivial fwd_rule_clear() is factored out from the existing
> > > >   conf_handler() implementation, so that it can be directly used
> > > >   in pesto
> > > > 
> > > > The entry points for parsing of port specifiers now take an additional
> > > > 'del' parameter which is passed down all the way before reaching the
> > > > fwd_rule_add() implementation. If a rule should be deleted, at that
> > > > point, fwd_rule_del() is called instead.
> > > > 
> > > > Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> > > > ---
> > > >  conf.c     | 26 ++++++----------
> > > >  fwd_rule.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++-------
> > > >  fwd_rule.h |  4 ++-
> > > >  pesto.1    | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++
> > > >  pesto.c    | 53 ++++++++++++++++++++++++++++---
> > > >  5 files changed, 227 insertions(+), 32 deletions(-)
> > > > 
> > > > diff --git a/conf.c b/conf.c
> > > > index 3f48793..909c34c 100644
> > > > --- a/conf.c
> > > > +++ b/conf.c
> > > > @@ -1849,16 +1849,16 @@ void conf(struct ctx *c, int argc, char **argv)
> > > >  
> > > >  		if (name == 't') {
> > > >  			opt_t = true;
> > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
> > > >  		} else if (name == 'u') {
> > > >  			opt_u = true;
> > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
> > > >  		} else if (name == 'T') {
> > > >  			opt_T = true;
> > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
> > > >  		} else if (name == 'U') {
> > > >  			opt_U = true;
> > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
> > > >  		}
> > > >  	} while (name != -1);
> > > >  
> > > > @@ -1910,13 +1910,13 @@ void conf(struct ctx *c, int argc, char **argv)
> > > >  
> > > >  	if (c->mode == MODE_PASTA) {
> > > >  		if (!opt_t)
> > > > -			fwd_rule_parse('t', "auto", c->fwd[PIF_HOST]);
> > > > +			fwd_rule_parse('t', false, "auto", c->fwd[PIF_HOST]);
> > > >  		if (!opt_T)
> > > > -			fwd_rule_parse('T', "auto", c->fwd[PIF_SPLICE]);
> > > > +			fwd_rule_parse('T', false, "auto", c->fwd[PIF_SPLICE]);
> > > >  		if (!opt_u)
> > > > -			fwd_rule_parse('u', "auto", c->fwd[PIF_HOST]);
> > > > +			fwd_rule_parse('u', false, "auto", c->fwd[PIF_HOST]);
> > > >  		if (!opt_U)
> > > > -			fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]);
> > > > +			fwd_rule_parse('U', false, "auto", c->fwd[PIF_SPLICE]);
> > > >  	}
> > > >  
> > > >  	conf_sock_listen(c);
> > > > @@ -2135,14 +2135,8 @@ void conf_handler(struct ctx *c, uint32_t events)
> > > >  		unsigned pif;
> > > >  
> > > >  		/* Clear pending tables */
> > > > -		for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> > > > -			struct fwd_table *fwd = c->fwd_pending[pif];
> > > > -
> > > > -			if (!fwd)
> > > > -				continue;
> > > > -			fwd->count = 0;
> > > > -			fwd->sock_count = 0;
> > > > -		}
> > > > +		for (pif = 0; pif < PIF_NUM_TYPES; pif++)
> > > > +			fwd_rule_clear(c->fwd_pending[pif]);
> > > >  
> > > >  		/* FIXME: this could block indefinitely if the client doesn't
> > > >  		 * write as much as it should
> > > > diff --git a/fwd_rule.c b/fwd_rule.c
> > > > index 03e8e80..eb9a601 100644
> > > > --- a/fwd_rule.c
> > > > +++ b/fwd_rule.c
> > > > @@ -180,6 +180,66 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
> > > >  	return true;
> > > >  }
> > > >  
> > > > +/**
> > > > + * fwd_rule_clear() - Clear a forwarding table
> > > > + * @fwd:	Table to clear (might be NULL)
> > > > + */
> > > > +void fwd_rule_clear(struct fwd_table *fwd)
> > > > +{
> > > > +	if (!fwd)
> > > > +		return;
> > > > +    
> > > 
> > > Not essential, but I wonder if it would be wise to verify that there
> > > are no currently open sockets associated with any of the rules.  
> > 
> > With a loop, I suppose. I can add it as a TODO comment because I guess
> > it would be good to handle that case (open sockets left) for
> > fwd_rule_del() as well, and a part of the implementation can probably
> > be common.
> >   
> > > > +	fwd->count = 0;
> > > > +	fwd->sock_count = 0;
> > > > +}
> > > > +
> > > > +/**
> > > > + * fwd_rule_del() - Partially validate and delete a rule from a forwarding table
> > > > + * @fwd:	Table to delete from
> > > > + * @rule:	Rule to delete (must match an existing rule)
> > > > + *
> > > > + * Return: 0 on success, negative error code on failure (-ENOENT if not found)
> > > > + *
> > > > + * NOTE: This function can't be used for a forwarding table with valid sockets
> > > > + * stored in fwd->rulesocks.    
> > > 
> > > The exact meaning of this isn't very clear to me.  Does "valid" mean
> > > "open" or something else?  
> > 
> > It means valid at some point, not necessarily open right now. I'll
> > change it to "open" for clarity.  
> 
> I'm not sure what "valid at some point" means, either.

That it was a valid socket file descriptor (an open one) at some point.

> > > I think what you're getting at is that every entry in fwd->socks[]
> > > must be -1.  Or at least every entry with index in [0,sock_count)  
> > 
> > Yes.
> >   
> > > > + */
> > > > +static int fwd_rule_del(struct fwd_table *fwd, const struct fwd_rule *rule)
> > > > +{
> > > > +	unsigned num, i;
> > > > +
> > > > +	for (i = 0; i < fwd->count; i++) {
> > > > +		if (fwd_rule_conflicts(rule, &fwd->rules[i]))
> > > > +			break;
> > > > +	}    
> > > 
> > > So, this deletes any conflicting rule, not only exact matches.  That's
> > > not very clear from the description of @rule.  
> > 
> > It deletes the first one  
> 
> Oh, good point.  Which actually elevates this to a bug, not just a
> debate about the best semantics, because...
> 
> > (but given that fwd_rule_conflicts() is called
> > on insertion, there should be a single one).  
> 
> ... that's not correct.  "conflicts" is not transitive, so (for
> example) in the cases below:
> 	-t 1000-2000 -t 4000-5000 --delete -t 500-5500
> 	-t 127.0.0.1/100 -t 127.0.0.2/100 --delete -t 100
> The deleted rule conflicts with both the added rules, but they don't
> conflict with each other.

Right, yes, for partially overlapping rules that's true. But that's not
what Podman needs right now, so I think it can be fixed later.

> I don't think "delete all conflicting rules" is a great either, since
> it means that:
> 	-t 1000-1999 -t 2000-2999 --delete -t 1500-2500
> maps nothing at all, which seems pretty surprising.
> 
> > It's good enough for our purposes right now, even though we might want
> > to make that more sophisticated in the future. I'll change the
> > description of @rule.  
> 
> I really think the current behaviour is too confusing.  Only deleting
> exact matches (and giving an error if there's a conflict that's not an
> exact match) I think *is* good enough for now, so that's what I'd
> suggest.

...except that it's not implemented by any function and it's not exactly
trivial either, and delaying the implementation further makes this
useless (at least for Podman, which we can approximate to "essentially
useless"), so I'd rather go with something that doesn't take care of
partially overlapping ranges, rather than no feature at all.

-- 
Stefano


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

* Re: [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto
  2026-05-06  8:52         ` David Gibson
@ 2026-05-06  9:11           ` Laurent Vivier
  2026-05-06 12:11             ` Stefano Brivio
  0 siblings, 1 reply; 44+ messages in thread
From: Laurent Vivier @ 2026-05-06  9:11 UTC (permalink / raw)
  To: David Gibson, Stefano Brivio; +Cc: passt-dev, Jon Maloy

On 5/6/26 10:52, David Gibson wrote:
> On Wed, May 06, 2026 at 10:49:55AM +0200, Stefano Brivio wrote:
>> On Wed, 6 May 2026 10:39:30 +0200
>> Stefano Brivio <sbrivio@redhat.com> wrote:
>>
>>> On Wed, 6 May 2026 10:12:21 +0200
>>> Laurent Vivier <lvivier@redhat.com> wrote:
>>>
>>>> On 5/6/26 01:47, Stefano Brivio wrote:
>>>>> From: David Gibson <david@gibson.dropbear.id.au>
>>>>>
>>>>> 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>
>>>>> [sbrivio: In fwd_listen_switch(), use the destination size as argument
>>>>>    to memcpy(), instead of sizeof(tmp), as suggested by Laurent]
>>>>> Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
>>>>> ---
>>>>>    conf.c |  5 ++---
>>>>>    fwd.c  | 34 ++++++++++++++++++++++++++++++++++
>>>>>    fwd.h  |  1 +
>>>>>    3 files changed, 37 insertions(+), 3 deletions(-)
>>>>>
>>>>> diff --git a/conf.c b/conf.c
>>>>> index 76344da..3f48793 100644
>>>>> --- a/conf.c
>>>>> +++ b/conf.c
>>>>> @@ -2160,15 +2160,14 @@ void conf_handler(struct ctx *c, uint32_t events)
>>>>>    			fwd_rules_dump(info, fwd->rules, fwd->count,
>>>>>    				       "    ", "");
>>>>>    		}
>>>>> +
>>>>> +		fwd_listen_switch(c);
>>>>>    	}
>>>>>    
>>>>>    	if (events & EPOLLHUP) {
>>>>>    		debug("Configuration client hangup");
>>>>> -		goto close;
>>>>>    	}
>>>>>    
>>>>> -	return;
>>>>> -
>>>>>    close:
>>>>>    	conf_close(c);
>>>>>    
>>>>> diff --git a/fwd.c b/fwd.c
>>>>> index d93d2e5..0697435 100644
>>>>> --- a/fwd.c
>>>>> +++ b/fwd.c
>>>>> @@ -534,6 +534,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);
>>>>> +		fwd->count = fwd->sock_count = 0;
>>>>> +	}
>>>>> +
>>>>> +	/* Swap active and pending tables */
>>>>> +	static_assert(sizeof(tmp) == sizeof(c->fwd) &&
>>>>> +		      sizeof(tmp) == sizeof(c->fwd_pending),
>>>>> +		      "Temporary has wrong size");
>>>>
>>>> At this point:
>>>>
>>>> c->fwd[PIF_HOST] = &fwd_in;
>>>> c->fwd[PIF_SPLICE] = &fwd_out;
>>>>
>>>> c->fwd_pending[PIF_HOST] = &fwd_in_pending;
>>>> c->fwd_pending[PIF_SPLICE] = &fwd_out_pending;
>>>>    
>>>>> +	memcpy(&tmp, (void *)c->fwd, sizeof(tmp));
>>>>> +	memcpy((void *)c->fwd, (void *)c->fwd_pending, sizeof(c->fwd));
>>>>> +	memcpy((void *)c->fwd_pending, &tmp, sizeof(c->fwd_pending));
>>>>
>>>> At this point:
>>>>
>>>> c->fwd[PIF_HOST] = &fwd_in_pending;
>>>> c->fwd[PIF_SPLICE] = &fwd_out_pending;
>>>>
>>>> c->fwd_pending[PIF_HOST] = &fwd_in;
>>>> c->fwd_pending[PIF_SPLICE] = &fwd_out;
>>>
>>> Yeah, makes sense, I can change that in v9.
>>>
>>>> Perhaps it should be noted somewhere to avoid confusion in the future?
>>>
>>> What do you think should be noted exactly, and where? Can you show a
>>> practical example of the change you're proposing?
>>
>> ...I'm leaving like it is in v9 to make sure I'm not misinterpreting
>> you, and also because the current (v8) version is obviously correct and
>> I also tested it fairly heavily by now.
>>
>> I'd suggest optimising this (and commenting as needed) in a separate
>> patch later.
> 
> As noted in another branch of this thread, I think all it really needs
> is renaming the globals that c->fwd and c->fwd_pending point to.  They
> should just be fwd_in_[12] (or an array of 2 tables), instead of
> implying a semantic difference between the plain and "pending" copies.
> 

I agree with that.

Thanks,
Laurent


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

* Re: [PATCH v8 15/19] pesto: Parse and add new rules from command line
  2026-05-06  7:13   ` Laurent Vivier
@ 2026-05-06  9:15     ` Stefano Brivio
  0 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-06  9:15 UTC (permalink / raw)
  To: Laurent Vivier; +Cc: passt-dev, Jon Maloy, David Gibson

On Wed, 6 May 2026 09:13:19 +0200
Laurent Vivier <lvivier@redhat.com> wrote:

> On 5/6/26 01:47, Stefano Brivio wrote:
> > From: David Gibson <david@gibson.dropbear.id.au>
> > 
> > This adds parsing of options using fwd_rule_parse(), validates them and
> > adds them to the existing rules. It doesn't yet send those rules back to
> > passt or pasta.
> > 
> > Message-ID: <20260322141843.4095972-3-sbrivio@redhat.com>
> > [dwg: Based on an early draft by Stefano]
> > Signed-off-by: David Gibson <david@gibson.dropbear.id.au>
> > [sbrivio: Recycled usage messages for -T and -U from conf.c as
> >   suggested by Laurent, dropped unrelated whitespace change]
> > [sbrivio: Add description of -t, -u, -T, -U to pesto.1]  
> 
> And -s, --show ?

Oops, I forgot that one as well. Added in v9.

-- 
Stefano


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

* Re: [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules
  2026-05-06  8:56         ` Stefano Brivio
@ 2026-05-06  9:22           ` David Gibson
  2026-05-06 12:52           ` Stefano Brivio
  1 sibling, 0 replies; 44+ messages in thread
From: David Gibson @ 2026-05-06  9:22 UTC (permalink / raw)
  To: Stefano Brivio; +Cc: passt-dev, Jon Maloy, Laurent Vivier

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

On Wed, May 06, 2026 at 10:56:02AM +0200, Stefano Brivio wrote:
> On Wed, 6 May 2026 18:48:10 +1000
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > On Wed, May 06, 2026 at 10:22:20AM +0200, Stefano Brivio wrote:
> > > On Wed, 6 May 2026 16:45:27 +1000
> > > David Gibson <david@gibson.dropbear.id.au> wrote:
> > >   
> > > > On Wed, May 06, 2026 at 01:47:19AM +0200, Stefano Brivio wrote:  
> > > > > Instead of just being able to replace the existing forwarding table,    
> > > > 
> > > > As of my last version, we already added, rather than replacing.  
> > > 
> > > Right, I noticed that, but this isn't the default behaviour we
> > > discussed, so I assumed it was accidental, and planned to go back and
> > > check the reason why.
> > > 
> > > Given that it wasn't accidental, I'll simply adjust this part of the
> > > commit message.
> > >   
> > > > > implement --add and --delete options to maintain the table and add
> > > > > or delete specific ports.
> > > > > 
> > > > > The option --clear PIF forces the clearing of a table, instead.
> > > > > 
> > > > > These options can be combined arbitrarily and are handled as
> > > > > sequential commands, as now described in pesto(1).
> > > > > 
> > > > > If no option is given before forwarding specifiers for a matching
> > > > > table, the command line is interpreted as a replacement of the
> > > > > existing rules.
> > > > > 
> > > > > To this end:
> > > > > 
> > > > > - there's no protocol change, as pesto is anyway sending updated
> > > > >   copies of the table
> > > > > 
> > > > > - the forwarding table functions now include a new fwd_rule_del(),
> > > > >   which deletes existing rule only if a matching one is found
> > > > > 
> > > > > - a trivial fwd_rule_clear() is factored out from the existing
> > > > >   conf_handler() implementation, so that it can be directly used
> > > > >   in pesto
> > > > > 
> > > > > The entry points for parsing of port specifiers now take an additional
> > > > > 'del' parameter which is passed down all the way before reaching the
> > > > > fwd_rule_add() implementation. If a rule should be deleted, at that
> > > > > point, fwd_rule_del() is called instead.
> > > > > 
> > > > > Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> > > > > ---
> > > > >  conf.c     | 26 ++++++----------
> > > > >  fwd_rule.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++-------
> > > > >  fwd_rule.h |  4 ++-
> > > > >  pesto.1    | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++
> > > > >  pesto.c    | 53 ++++++++++++++++++++++++++++---
> > > > >  5 files changed, 227 insertions(+), 32 deletions(-)
> > > > > 
> > > > > diff --git a/conf.c b/conf.c
> > > > > index 3f48793..909c34c 100644
> > > > > --- a/conf.c
> > > > > +++ b/conf.c
> > > > > @@ -1849,16 +1849,16 @@ void conf(struct ctx *c, int argc, char **argv)
> > > > >  
> > > > >  		if (name == 't') {
> > > > >  			opt_t = true;
> > > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> > > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
> > > > >  		} else if (name == 'u') {
> > > > >  			opt_u = true;
> > > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> > > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
> > > > >  		} else if (name == 'T') {
> > > > >  			opt_T = true;
> > > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> > > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
> > > > >  		} else if (name == 'U') {
> > > > >  			opt_U = true;
> > > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> > > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
> > > > >  		}
> > > > >  	} while (name != -1);
> > > > >  
> > > > > @@ -1910,13 +1910,13 @@ void conf(struct ctx *c, int argc, char **argv)
> > > > >  
> > > > >  	if (c->mode == MODE_PASTA) {
> > > > >  		if (!opt_t)
> > > > > -			fwd_rule_parse('t', "auto", c->fwd[PIF_HOST]);
> > > > > +			fwd_rule_parse('t', false, "auto", c->fwd[PIF_HOST]);
> > > > >  		if (!opt_T)
> > > > > -			fwd_rule_parse('T', "auto", c->fwd[PIF_SPLICE]);
> > > > > +			fwd_rule_parse('T', false, "auto", c->fwd[PIF_SPLICE]);
> > > > >  		if (!opt_u)
> > > > > -			fwd_rule_parse('u', "auto", c->fwd[PIF_HOST]);
> > > > > +			fwd_rule_parse('u', false, "auto", c->fwd[PIF_HOST]);
> > > > >  		if (!opt_U)
> > > > > -			fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]);
> > > > > +			fwd_rule_parse('U', false, "auto", c->fwd[PIF_SPLICE]);
> > > > >  	}
> > > > >  
> > > > >  	conf_sock_listen(c);
> > > > > @@ -2135,14 +2135,8 @@ void conf_handler(struct ctx *c, uint32_t events)
> > > > >  		unsigned pif;
> > > > >  
> > > > >  		/* Clear pending tables */
> > > > > -		for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> > > > > -			struct fwd_table *fwd = c->fwd_pending[pif];
> > > > > -
> > > > > -			if (!fwd)
> > > > > -				continue;
> > > > > -			fwd->count = 0;
> > > > > -			fwd->sock_count = 0;
> > > > > -		}
> > > > > +		for (pif = 0; pif < PIF_NUM_TYPES; pif++)
> > > > > +			fwd_rule_clear(c->fwd_pending[pif]);
> > > > >  
> > > > >  		/* FIXME: this could block indefinitely if the client doesn't
> > > > >  		 * write as much as it should
> > > > > diff --git a/fwd_rule.c b/fwd_rule.c
> > > > > index 03e8e80..eb9a601 100644
> > > > > --- a/fwd_rule.c
> > > > > +++ b/fwd_rule.c
> > > > > @@ -180,6 +180,66 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
> > > > >  	return true;
> > > > >  }
> > > > >  
> > > > > +/**
> > > > > + * fwd_rule_clear() - Clear a forwarding table
> > > > > + * @fwd:	Table to clear (might be NULL)
> > > > > + */
> > > > > +void fwd_rule_clear(struct fwd_table *fwd)
> > > > > +{
> > > > > +	if (!fwd)
> > > > > +		return;
> > > > > +    
> > > > 
> > > > Not essential, but I wonder if it would be wise to verify that there
> > > > are no currently open sockets associated with any of the rules.  
> > > 
> > > With a loop, I suppose. I can add it as a TODO comment because I guess
> > > it would be good to handle that case (open sockets left) for
> > > fwd_rule_del() as well, and a part of the implementation can probably
> > > be common.
> > >   
> > > > > +	fwd->count = 0;
> > > > > +	fwd->sock_count = 0;
> > > > > +}
> > > > > +
> > > > > +/**
> > > > > + * fwd_rule_del() - Partially validate and delete a rule from a forwarding table
> > > > > + * @fwd:	Table to delete from
> > > > > + * @rule:	Rule to delete (must match an existing rule)
> > > > > + *
> > > > > + * Return: 0 on success, negative error code on failure (-ENOENT if not found)
> > > > > + *
> > > > > + * NOTE: This function can't be used for a forwarding table with valid sockets
> > > > > + * stored in fwd->rulesocks.    
> > > > 
> > > > The exact meaning of this isn't very clear to me.  Does "valid" mean
> > > > "open" or something else?  
> > > 
> > > It means valid at some point, not necessarily open right now. I'll
> > > change it to "open" for clarity.  
> > 
> > I'm not sure what "valid at some point" means, either.
> 
> That it was a valid socket file descriptor (an open one) at some point.
> 
> > > > I think what you're getting at is that every entry in fwd->socks[]
> > > > must be -1.  Or at least every entry with index in [0,sock_count)  
> > > 
> > > Yes.
> > >   
> > > > > + */
> > > > > +static int fwd_rule_del(struct fwd_table *fwd, const struct fwd_rule *rule)
> > > > > +{
> > > > > +	unsigned num, i;
> > > > > +
> > > > > +	for (i = 0; i < fwd->count; i++) {
> > > > > +		if (fwd_rule_conflicts(rule, &fwd->rules[i]))
> > > > > +			break;
> > > > > +	}    
> > > > 
> > > > So, this deletes any conflicting rule, not only exact matches.  That's
> > > > not very clear from the description of @rule.  
> > > 
> > > It deletes the first one  
> > 
> > Oh, good point.  Which actually elevates this to a bug, not just a
> > debate about the best semantics, because...
> > 
> > > (but given that fwd_rule_conflicts() is called
> > > on insertion, there should be a single one).  
> > 
> > ... that's not correct.  "conflicts" is not transitive, so (for
> > example) in the cases below:
> > 	-t 1000-2000 -t 4000-5000 --delete -t 500-5500
> > 	-t 127.0.0.1/100 -t 127.0.0.2/100 --delete -t 100
> > The deleted rule conflicts with both the added rules, but they don't
> > conflict with each other.
> 
> Right, yes, for partially overlapping rules that's true. But that's not
> what Podman needs right now, so I think it can be fixed later.

The second example involves no ranges at all.

> > I don't think "delete all conflicting rules" is a great either, since
> > it means that:
> > 	-t 1000-1999 -t 2000-2999 --delete -t 1500-2500
> > maps nothing at all, which seems pretty surprising.
> > 
> > > It's good enough for our purposes right now, even though we might want
> > > to make that more sophisticated in the future. I'll change the
> > > description of @rule.  
> > 
> > I really think the current behaviour is too confusing.  Only deleting
> > exact matches (and giving an error if there's a conflict that's not an
> > exact match) I think *is* good enough for now, so that's what I'd
> > suggest.
> 
> ...except that it's not implemented by any function and it's not exactly
> trivial either, and delaying the implementation further makes this
> useless (at least for Podman, which we can approximate to "essentially
> useless"), so I'd rather go with something that doesn't take care of
> partially overlapping ranges, rather than no feature at all.
> 
> -- 
> 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] 44+ messages in thread

* Re: [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto
  2026-05-06  9:11           ` Laurent Vivier
@ 2026-05-06 12:11             ` Stefano Brivio
  0 siblings, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-06 12:11 UTC (permalink / raw)
  To: Laurent Vivier; +Cc: David Gibson, passt-dev, Jon Maloy

On Wed, 6 May 2026 11:11:05 +0200
Laurent Vivier <lvivier@redhat.com> wrote:

> On 5/6/26 10:52, David Gibson wrote:
> > On Wed, May 06, 2026 at 10:49:55AM +0200, Stefano Brivio wrote:  
> >> On Wed, 6 May 2026 10:39:30 +0200
> >> Stefano Brivio <sbrivio@redhat.com> wrote:
> >>  
> >>> On Wed, 6 May 2026 10:12:21 +0200
> >>> Laurent Vivier <lvivier@redhat.com> wrote:
> >>>  
> >>>> On 5/6/26 01:47, Stefano Brivio wrote:  
> >>>>> From: David Gibson <david@gibson.dropbear.id.au>
> >>>>>
> >>>>> 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>
> >>>>> [sbrivio: In fwd_listen_switch(), use the destination size as argument
> >>>>>    to memcpy(), instead of sizeof(tmp), as suggested by Laurent]
> >>>>> Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> >>>>> ---
> >>>>>    conf.c |  5 ++---
> >>>>>    fwd.c  | 34 ++++++++++++++++++++++++++++++++++
> >>>>>    fwd.h  |  1 +
> >>>>>    3 files changed, 37 insertions(+), 3 deletions(-)
> >>>>>
> >>>>> diff --git a/conf.c b/conf.c
> >>>>> index 76344da..3f48793 100644
> >>>>> --- a/conf.c
> >>>>> +++ b/conf.c
> >>>>> @@ -2160,15 +2160,14 @@ void conf_handler(struct ctx *c, uint32_t events)
> >>>>>    			fwd_rules_dump(info, fwd->rules, fwd->count,
> >>>>>    				       "    ", "");
> >>>>>    		}
> >>>>> +
> >>>>> +		fwd_listen_switch(c);
> >>>>>    	}
> >>>>>    
> >>>>>    	if (events & EPOLLHUP) {
> >>>>>    		debug("Configuration client hangup");
> >>>>> -		goto close;
> >>>>>    	}
> >>>>>    
> >>>>> -	return;
> >>>>> -
> >>>>>    close:
> >>>>>    	conf_close(c);
> >>>>>    
> >>>>> diff --git a/fwd.c b/fwd.c
> >>>>> index d93d2e5..0697435 100644
> >>>>> --- a/fwd.c
> >>>>> +++ b/fwd.c
> >>>>> @@ -534,6 +534,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);
> >>>>> +		fwd->count = fwd->sock_count = 0;
> >>>>> +	}
> >>>>> +
> >>>>> +	/* Swap active and pending tables */
> >>>>> +	static_assert(sizeof(tmp) == sizeof(c->fwd) &&
> >>>>> +		      sizeof(tmp) == sizeof(c->fwd_pending),
> >>>>> +		      "Temporary has wrong size");  
> >>>>
> >>>> At this point:
> >>>>
> >>>> c->fwd[PIF_HOST] = &fwd_in;
> >>>> c->fwd[PIF_SPLICE] = &fwd_out;
> >>>>
> >>>> c->fwd_pending[PIF_HOST] = &fwd_in_pending;
> >>>> c->fwd_pending[PIF_SPLICE] = &fwd_out_pending;
> >>>>      
> >>>>> +	memcpy(&tmp, (void *)c->fwd, sizeof(tmp));
> >>>>> +	memcpy((void *)c->fwd, (void *)c->fwd_pending, sizeof(c->fwd));
> >>>>> +	memcpy((void *)c->fwd_pending, &tmp, sizeof(c->fwd_pending));  
> >>>>
> >>>> At this point:
> >>>>
> >>>> c->fwd[PIF_HOST] = &fwd_in_pending;
> >>>> c->fwd[PIF_SPLICE] = &fwd_out_pending;
> >>>>
> >>>> c->fwd_pending[PIF_HOST] = &fwd_in;
> >>>> c->fwd_pending[PIF_SPLICE] = &fwd_out;  
> >>>
> >>> Yeah, makes sense, I can change that in v9.
> >>>  
> >>>> Perhaps it should be noted somewhere to avoid confusion in the future?  
> >>>
> >>> What do you think should be noted exactly, and where? Can you show a
> >>> practical example of the change you're proposing?  
> >>
> >> ...I'm leaving like it is in v9 to make sure I'm not misinterpreting
> >> you, and also because the current (v8) version is obviously correct and
> >> I also tested it fairly heavily by now.
> >>
> >> I'd suggest optimising this (and commenting as needed) in a separate
> >> patch later.  
> > 
> > As noted in another branch of this thread, I think all it really needs
> > is renaming the globals that c->fwd and c->fwd_pending point to.  They
> > should just be fwd_in_[12] (or an array of 2 tables), instead of
> > implying a semantic difference between the plain and "pending" copies.
> 
> I agree with that.

Okay, I see, and I agree as well. I'd still keep that as a separate
patch (there are quite a few follow-ups we'll need anyway) as it's not
really fundamental right now.

-- 
Stefano


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

* Re: [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules
  2026-05-06  8:56         ` Stefano Brivio
  2026-05-06  9:22           ` David Gibson
@ 2026-05-06 12:52           ` Stefano Brivio
  1 sibling, 0 replies; 44+ messages in thread
From: Stefano Brivio @ 2026-05-06 12:52 UTC (permalink / raw)
  To: David Gibson; +Cc: passt-dev, Jon Maloy, Laurent Vivier

On Wed, 6 May 2026 10:56:01 +0200
Stefano Brivio <sbrivio@redhat.com> wrote:

> On Wed, 6 May 2026 18:48:10 +1000
> David Gibson <david@gibson.dropbear.id.au> wrote:
> 
> > On Wed, May 06, 2026 at 10:22:20AM +0200, Stefano Brivio wrote:  
> > > On Wed, 6 May 2026 16:45:27 +1000
> > > David Gibson <david@gibson.dropbear.id.au> wrote:
> > >     
> > > > On Wed, May 06, 2026 at 01:47:19AM +0200, Stefano Brivio wrote:    
> > > > > Instead of just being able to replace the existing forwarding table,      
> > > > 
> > > > As of my last version, we already added, rather than replacing.    
> > > 
> > > Right, I noticed that, but this isn't the default behaviour we
> > > discussed, so I assumed it was accidental, and planned to go back and
> > > check the reason why.
> > > 
> > > Given that it wasn't accidental, I'll simply adjust this part of the
> > > commit message.
> > >     
> > > > > implement --add and --delete options to maintain the table and add
> > > > > or delete specific ports.
> > > > > 
> > > > > The option --clear PIF forces the clearing of a table, instead.
> > > > > 
> > > > > These options can be combined arbitrarily and are handled as
> > > > > sequential commands, as now described in pesto(1).
> > > > > 
> > > > > If no option is given before forwarding specifiers for a matching
> > > > > table, the command line is interpreted as a replacement of the
> > > > > existing rules.
> > > > > 
> > > > > To this end:
> > > > > 
> > > > > - there's no protocol change, as pesto is anyway sending updated
> > > > >   copies of the table
> > > > > 
> > > > > - the forwarding table functions now include a new fwd_rule_del(),
> > > > >   which deletes existing rule only if a matching one is found
> > > > > 
> > > > > - a trivial fwd_rule_clear() is factored out from the existing
> > > > >   conf_handler() implementation, so that it can be directly used
> > > > >   in pesto
> > > > > 
> > > > > The entry points for parsing of port specifiers now take an additional
> > > > > 'del' parameter which is passed down all the way before reaching the
> > > > > fwd_rule_add() implementation. If a rule should be deleted, at that
> > > > > point, fwd_rule_del() is called instead.
> > > > > 
> > > > > Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
> > > > > ---
> > > > >  conf.c     | 26 ++++++----------
> > > > >  fwd_rule.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++-------
> > > > >  fwd_rule.h |  4 ++-
> > > > >  pesto.1    | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++
> > > > >  pesto.c    | 53 ++++++++++++++++++++++++++++---
> > > > >  5 files changed, 227 insertions(+), 32 deletions(-)
> > > > > 
> > > > > diff --git a/conf.c b/conf.c
> > > > > index 3f48793..909c34c 100644
> > > > > --- a/conf.c
> > > > > +++ b/conf.c
> > > > > @@ -1849,16 +1849,16 @@ void conf(struct ctx *c, int argc, char **argv)
> > > > >  
> > > > >  		if (name == 't') {
> > > > >  			opt_t = true;
> > > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> > > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
> > > > >  		} else if (name == 'u') {
> > > > >  			opt_u = true;
> > > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
> > > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_HOST]);
> > > > >  		} else if (name == 'T') {
> > > > >  			opt_T = true;
> > > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> > > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
> > > > >  		} else if (name == 'U') {
> > > > >  			opt_U = true;
> > > > > -			fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
> > > > > +			fwd_rule_parse(name, false, optarg, c->fwd[PIF_SPLICE]);
> > > > >  		}
> > > > >  	} while (name != -1);
> > > > >  
> > > > > @@ -1910,13 +1910,13 @@ void conf(struct ctx *c, int argc, char **argv)
> > > > >  
> > > > >  	if (c->mode == MODE_PASTA) {
> > > > >  		if (!opt_t)
> > > > > -			fwd_rule_parse('t', "auto", c->fwd[PIF_HOST]);
> > > > > +			fwd_rule_parse('t', false, "auto", c->fwd[PIF_HOST]);
> > > > >  		if (!opt_T)
> > > > > -			fwd_rule_parse('T', "auto", c->fwd[PIF_SPLICE]);
> > > > > +			fwd_rule_parse('T', false, "auto", c->fwd[PIF_SPLICE]);
> > > > >  		if (!opt_u)
> > > > > -			fwd_rule_parse('u', "auto", c->fwd[PIF_HOST]);
> > > > > +			fwd_rule_parse('u', false, "auto", c->fwd[PIF_HOST]);
> > > > >  		if (!opt_U)
> > > > > -			fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]);
> > > > > +			fwd_rule_parse('U', false, "auto", c->fwd[PIF_SPLICE]);
> > > > >  	}
> > > > >  
> > > > >  	conf_sock_listen(c);
> > > > > @@ -2135,14 +2135,8 @@ void conf_handler(struct ctx *c, uint32_t events)
> > > > >  		unsigned pif;
> > > > >  
> > > > >  		/* Clear pending tables */
> > > > > -		for (pif = 0; pif < PIF_NUM_TYPES; pif++) {
> > > > > -			struct fwd_table *fwd = c->fwd_pending[pif];
> > > > > -
> > > > > -			if (!fwd)
> > > > > -				continue;
> > > > > -			fwd->count = 0;
> > > > > -			fwd->sock_count = 0;
> > > > > -		}
> > > > > +		for (pif = 0; pif < PIF_NUM_TYPES; pif++)
> > > > > +			fwd_rule_clear(c->fwd_pending[pif]);
> > > > >  
> > > > >  		/* FIXME: this could block indefinitely if the client doesn't
> > > > >  		 * write as much as it should
> > > > > diff --git a/fwd_rule.c b/fwd_rule.c
> > > > > index 03e8e80..eb9a601 100644
> > > > > --- a/fwd_rule.c
> > > > > +++ b/fwd_rule.c
> > > > > @@ -180,6 +180,66 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
> > > > >  	return true;
> > > > >  }
> > > > >  
> > > > > +/**
> > > > > + * fwd_rule_clear() - Clear a forwarding table
> > > > > + * @fwd:	Table to clear (might be NULL)
> > > > > + */
> > > > > +void fwd_rule_clear(struct fwd_table *fwd)
> > > > > +{
> > > > > +	if (!fwd)
> > > > > +		return;
> > > > > +      
> > > > 
> > > > Not essential, but I wonder if it would be wise to verify that there
> > > > are no currently open sockets associated with any of the rules.    
> > > 
> > > With a loop, I suppose. I can add it as a TODO comment because I guess
> > > it would be good to handle that case (open sockets left) for
> > > fwd_rule_del() as well, and a part of the implementation can probably
> > > be common.
> > >     
> > > > > +	fwd->count = 0;
> > > > > +	fwd->sock_count = 0;
> > > > > +}
> > > > > +
> > > > > +/**
> > > > > + * fwd_rule_del() - Partially validate and delete a rule from a forwarding table
> > > > > + * @fwd:	Table to delete from
> > > > > + * @rule:	Rule to delete (must match an existing rule)
> > > > > + *
> > > > > + * Return: 0 on success, negative error code on failure (-ENOENT if not found)
> > > > > + *
> > > > > + * NOTE: This function can't be used for a forwarding table with valid sockets
> > > > > + * stored in fwd->rulesocks.      
> > > > 
> > > > The exact meaning of this isn't very clear to me.  Does "valid" mean
> > > > "open" or something else?    
> > > 
> > > It means valid at some point, not necessarily open right now. I'll
> > > change it to "open" for clarity.    
> > 
> > I'm not sure what "valid at some point" means, either.  
> 
> That it was a valid socket file descriptor (an open one) at some point.
> 
> > > > I think what you're getting at is that every entry in fwd->socks[]
> > > > must be -1.  Or at least every entry with index in [0,sock_count)    
> > > 
> > > Yes.
> > >     
> > > > > + */
> > > > > +static int fwd_rule_del(struct fwd_table *fwd, const struct fwd_rule *rule)
> > > > > +{
> > > > > +	unsigned num, i;
> > > > > +
> > > > > +	for (i = 0; i < fwd->count; i++) {
> > > > > +		if (fwd_rule_conflicts(rule, &fwd->rules[i]))
> > > > > +			break;
> > > > > +	}      
> > > > 
> > > > So, this deletes any conflicting rule, not only exact matches.  That's
> > > > not very clear from the description of @rule.    
> > > 
> > > It deletes the first one    
> > 
> > Oh, good point.  Which actually elevates this to a bug, not just a
> > debate about the best semantics, because...
> >   
> > > (but given that fwd_rule_conflicts() is called
> > > on insertion, there should be a single one).    
> > 
> > ... that's not correct.  "conflicts" is not transitive, so (for
> > example) in the cases below:
> > 	-t 1000-2000 -t 4000-5000 --delete -t 500-5500
> > 	-t 127.0.0.1/100 -t 127.0.0.2/100 --delete -t 100
> > The deleted rule conflicts with both the added rules, but they don't
> > conflict with each other.  
> 
> Right, yes, for partially overlapping rules that's true. But that's not
> what Podman needs right now, so I think it can be fixed later.
> 
> > I don't think "delete all conflicting rules" is a great either, since
> > it means that:
> > 	-t 1000-1999 -t 2000-2999 --delete -t 1500-2500
> > maps nothing at all, which seems pretty surprising.
> >   
> > > It's good enough for our purposes right now, even though we might want
> > > to make that more sophisticated in the future. I'll change the
> > > description of @rule.    
> > 
> > I really think the current behaviour is too confusing.  Only deleting
> > exact matches (and giving an error if there's a conflict that's not an
> > exact match) I think *is* good enough for now, so that's what I'd
> > suggest.  
> 
> ...except that it's not implemented by any function and it's not exactly
> trivial either, and delaying the implementation further makes this
> useless (at least for Podman, which we can approximate to "essentially
> useless"), so I'd rather go with something that doesn't take care of
> partially overlapping ranges, rather than no feature at all.

...but I ended up implementing exactly that in v10 as a re-spin was
needed anyway and I realised that the implementation is absolutely
trivial.

-- 
Stefano


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

end of thread, other threads:[~2026-05-06 22:20 UTC | newest]

Thread overview: 44+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-05-05 23:47 [PATCH v8 00/19] Dynamic configuration update implementation Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 01/19] conf, fwd: Stricter rule checking in fwd_rule_add() Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 02/19] fwd_rule: Move ephemeral port probing to fwd_rule.c Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 03/19] fwd, conf: Move rule parsing code to fwd_rule.[ch] Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 04/19] fwd_rule: Move conflict checking back within fwd_rule_add() Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 05/19] fwd: Generalise fwd_rules_info() Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 06/19] pif: Limit pif names to 128 bytes Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 07/19] fwd_rule: Fix some format specifiers Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 08/19] pesto: Introduce stub configuration tool Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 09/19] pesto, log: Share log.h (but not log.c) with pesto tool Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 10/19] pesto, conf: Have pesto connect to passt and check versions Stefano Brivio
2026-05-06  5:38   ` David Gibson
2026-05-06  7:06     ` Laurent Vivier
2026-05-06  7:41       ` David Gibson
2026-05-06  7:55     ` Stefano Brivio
2026-05-06  8:21       ` David Gibson
2026-05-06  8:30         ` Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 11/19] pesto: Expose list of pifs to pesto and display them Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 12/19] ip: Prepare ip.[ch] for sharing with pesto tool Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 13/19] inany: Prepare inany.[ch] " Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 14/19] pesto: Read current ruleset from passt/pasta and optionally display it Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 15/19] pesto: Parse and add new rules from command line Stefano Brivio
2026-05-06  7:13   ` Laurent Vivier
2026-05-06  9:15     ` Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 16/19] pesto, conf: Send updated rules from pesto back to passt/pasta Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 17/19] conf, fwd: Allow switching to new rules received from pesto Stefano Brivio
2026-05-06  7:15   ` Laurent Vivier
2026-05-06  8:12   ` Laurent Vivier
2026-05-06  8:23     ` David Gibson
2026-05-06  8:39     ` Stefano Brivio
2026-05-06  8:49       ` Stefano Brivio
2026-05-06  8:52         ` David Gibson
2026-05-06  9:11           ` Laurent Vivier
2026-05-06 12:11             ` Stefano Brivio
2026-05-05 23:47 ` [PATCH v8 18/19] fwd_rule: Fix static checkers warnings in fwd_rule_add() Stefano Brivio
2026-05-06  7:18   ` Laurent Vivier
2026-05-05 23:47 ` [PATCH v8 19/19] pesto, conf, fwd_rule: Add options and modes to add, delete, clear rules Stefano Brivio
2026-05-06  6:45   ` David Gibson
2026-05-06  8:22     ` Stefano Brivio
2026-05-06  8:48       ` David Gibson
2026-05-06  8:56         ` Stefano Brivio
2026-05-06  9:22           ` David Gibson
2026-05-06 12:52           ` Stefano Brivio
2026-05-06  6:53 ` [PATCH v8 00/19] Dynamic configuration update implementation 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).