public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
From: Laurent Vivier <lvivier@redhat.com>
To: David Gibson <david@gibson.dropbear.id.au>,
	passt-dev@passt.top, Stefano Brivio <sbrivio@redhat.com>
Subject: Re: [PATCH v3 09/11] fwd, conf: Move rule parsing code to fwd_rule.[ch]
Date: Mon, 20 Apr 2026 19:06:42 +0200	[thread overview]
Message-ID: <c75fe864-0c1a-475a-bec2-b9f003a0242e@redhat.com> (raw)
In-Reply-To: <20260417050520.102247-10-david@gibson.dropbear.id.au>

On 4/17/26 07:05, David Gibson wrote:
> 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>

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

> ---
>   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 3b373b22..5aacfe0f 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
> @@ -2162,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);
>   
> @@ -2223,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 9a7053fd..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 attempts to map from port 0");
> -		return -EINVAL;
> -	}
> -	if (!new->to || (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 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..7bba2602 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 attempts to map from port 0");
> +		return -EINVAL;
> +	}
> +	if (!new->to || (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);
> +	}
> +
> +	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 */


  reply	other threads:[~2026-04-20 17:06 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-17  5:05 [PATCH v3 00/11] Rework forwarding option parsing David Gibson
2026-04-17  5:05 ` [PATCH v3 01/11] doc: Rework man page description of port specifiers David Gibson
2026-04-20 13:31   ` Laurent Vivier
2026-04-17  5:05 ` [PATCH v3 02/11] conf: Move "all" handling to port specifier David Gibson
2026-04-20 13:44   ` Laurent Vivier
2026-04-17  5:05 ` [PATCH v3 03/11] conf: Allow user-specified auto-scanned port forwarding ranges David Gibson
2026-04-20 14:45   ` Laurent Vivier
2026-04-17  5:05 ` [PATCH v3 04/11] conf: Move SO_BINDTODEVICE workaround to conf_ports() David Gibson
2026-04-20 15:06   ` Laurent Vivier
2026-04-17  5:05 ` [PATCH v3 05/11] conf: Don't pass raw commandline argument to conf_ports_spec() David Gibson
2026-04-20 16:11   ` Laurent Vivier
2026-04-17  5:05 ` [PATCH v3 06/11] fwd, conf: Add capabilities bits to each forwarding table David Gibson
2026-04-20 16:17   ` Laurent Vivier
2026-04-17  5:05 ` [PATCH v3 07/11] conf, fwd: Stricter rule checking in fwd_rule_add() David Gibson
2026-04-20 16:48   ` Laurent Vivier
2026-04-17  5:05 ` [PATCH v3 08/11] fwd_rule: Move ephemeral port probing to fwd_rule.c David Gibson
2026-04-20 16:52   ` Laurent Vivier
2026-04-17  5:05 ` [PATCH v3 09/11] fwd, conf: Move rule parsing code to fwd_rule.[ch] David Gibson
2026-04-20 17:06   ` Laurent Vivier [this message]
2026-04-17  5:05 ` [PATCH v3 10/11] fwd_rule: Move conflict checking back within fwd_rule_add() David Gibson
2026-04-20 17:15   ` Laurent Vivier
2026-04-17  5:05 ` [PATCH v3 11/11] fwd: Generalise fwd_rules_info() David Gibson
2026-04-20 17:21   ` Laurent Vivier

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=c75fe864-0c1a-475a-bec2-b9f003a0242e@redhat.com \
    --to=lvivier@redhat.com \
    --cc=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).