public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
From: David Gibson <david@gibson.dropbear.id.au>
To: passt-dev@passt.top, Stefano Brivio <sbrivio@redhat.com>
Cc: David Gibson <david@gibson.dropbear.id.au>
Subject: [PATCH v2 23/23] fwd, conf: Move rule parsing code to fwd_rule.[ch]
Date: Fri, 10 Apr 2026 11:03:09 +1000	[thread overview]
Message-ID: <20260410010309.736855-24-david@gibson.dropbear.id.au> (raw)
In-Reply-To: <20260410010309.736855-1-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>
---
 conf.c     | 378 +------------------------------------------
 fwd.c      |  93 -----------
 fwd.h      |  33 ----
 fwd_rule.c | 465 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 fwd_rule.h |  36 ++++-
 5 files changed, 503 insertions(+), 502 deletions(-)

diff --git a/conf.c b/conf.c
index 31627209..5f38a287 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,367 +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);
-	}
-
-	assert(first != 0);
-
-	for (base = first; base <= last; base++) {
-		if (exclude && bitmap_isset(exclude, base))
-			continue;
-
-		for (i = base; i <= last; i++) {
-			if (exclude && bitmap_isset(exclude, i))
-				break;
-		}
-
-		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
@@ -2163,16 +1801,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);
 
@@ -2224,13 +1862,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 f2f7b648..728a783c 100644
--- a/fwd.c
+++ b/fwd.c
@@ -275,99 +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 atttempts to map from port 0");
-		return -EINVAL;
-	}
-	if (!new->to || (new->to + new->last - new->first) < new->to) {
-		warn("Forwarding rule atttempts 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 e664d1d0..8f845d09 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 9d489827..2b0bd4db 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,460 @@ 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 atttempts to map from port 0");
+		return -EINVAL;
+	}
+	if (!new->to || (new->to + new->last - new->first) < new->to) {
+		warn("Forwarding rule atttempts 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);
+	}
+
+	assert(first != 0);
+
+	for (base = first; base <= last; base++) {
+		if (exclude && bitmap_isset(exclude, base))
+			continue;
+
+		for (i = base; i <= last; i++) {
+			if (exclude && bitmap_isset(exclude, i))
+				break;
+		}
+
+		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 5c7b67aa..f0f4efda 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.53.0


      parent reply	other threads:[~2026-04-10  1:03 UTC|newest]

Thread overview: 24+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-10  1:02 [PATCH v2 00/23] Rework forwarding option parsing David Gibson
2026-04-10  1:02 ` [PATCH v2 01/23] conf: Split parsing of port specifiers from the rest of -[tuTU] parsing David Gibson
2026-04-10  1:02 ` [PATCH v2 02/23] conf: Simplify handling of default forwarding mode David Gibson
2026-04-10  1:02 ` [PATCH v2 03/23] conf: Move first pass handling of -[TU] next to handling of -[tu] David Gibson
2026-04-10  1:02 ` [PATCH v2 04/23] doc: Consolidate -[tu] option descriptions for passt and pasta David Gibson
2026-04-10  1:02 ` [PATCH v2 05/23] conf: Permit -[tTuU] all in pasta mode David Gibson
2026-04-10  1:02 ` [PATCH v2 06/23] fwd: Better split forwarding rule specification from associated sockets David Gibson
2026-04-10  1:02 ` [PATCH v2 07/23] fwd_rule: Move forwarding rule formatting David Gibson
2026-04-10  1:02 ` [PATCH v2 08/23] conf: Pass protocol explicitly to conf_ports_range_except() David Gibson
2026-04-10  1:02 ` [PATCH v2 09/23] fwd: Split rule building from rule adding David Gibson
2026-04-10  1:02 ` [PATCH v2 10/23] fwd_rule: Move rule conflict checking from fwd_rule_add() to caller David Gibson
2026-04-10  1:02 ` [PATCH v2 11/23] fwd: Improve error handling in fwd_rule_add() David Gibson
2026-04-10  1:02 ` [PATCH v2 12/23] conf: Don't be strict about exclusivity of forwarding mode David Gibson
2026-04-10  1:02 ` [PATCH v2 13/23] conf: Rework stepping through chunks of port specifiers David Gibson
2026-04-10  1:03 ` [PATCH v2 14/23] conf: Rework checking for garbage after a range David Gibson
2026-04-10  1:03 ` [PATCH v2 15/23] doc: Rework man page description of port specifiers David Gibson
2026-04-10  1:03 ` [PATCH v2 16/23] conf: Move "all" handling to port specifier David Gibson
2026-04-10  1:03 ` [PATCH v2 17/23] conf: Allow user-specified auto-scanned port forwarding ranges David Gibson
2026-04-10  1:03 ` [PATCH v2 18/23] conf: Move SO_BINDTODEVICE workaround to conf_ports() David Gibson
2026-04-10  1:03 ` [PATCH v2 19/23] conf: Don't pass raw commandline argument to conf_ports_spec() David Gibson
2026-04-10  1:03 ` [PATCH v2 20/23] fwd, conf: Add capabilities bits to each forwarding table David Gibson
2026-04-10  1:03 ` [PATCH v2 21/23] conf, fwd: Stricter rule checking in fwd_rule_add() David Gibson
2026-04-10  1:03 ` [PATCH v2 22/23] fwd_rule: Move ephemeral port probing to fwd_rule.c David Gibson
2026-04-10  1:03 ` David Gibson [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260410010309.736855-24-david@gibson.dropbear.id.au \
    --to=david@gibson.dropbear.id.au \
    --cc=passt-dev@passt.top \
    --cc=sbrivio@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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).