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 */
next prev parent 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).