[PATCH v4 03/17] fwd, conf: Move rule parsing code to fwd_rule.[ch]

David Gibson
2026-04-21T04:42:23Z

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 | 376 +------------------------------------------
fwd.c | 94 -----------
fwd.h | 33 ----
fwd_rule.c | 464 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
fwd_rule.h | 36 ++++-
5 files changed, 502 insertions(+), 501 deletions(-)

diff --git a/conf.c b/conf.c
index b470b0d8..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,365 +65,6 @@

const char *pasta_default_ifn = "tap0";

-/**
- * port_range() - Represents a non-empty range of ports
- * @first: First port number in the range
- * @last: Last port number in the range (inclusive)
- *
- * Invariant: @last >= @first
- */
-struct port_range {
- in_port_t first, last;
-};
-
-/**
- * parse_port_range() - Parse a range of port numbers '<first>[-<last>]'
- * @s: String to parse
- * @endptr: Update to the character after the parsed range (similar to
- * strtol() etc.)
- * @range: Update with the parsed values on success
- *
- * Return: -EINVAL on parsing error, -ERANGE on out of range port
- * numbers, 0 on success
- */
-static int parse_port_range(const char *s, const char **endptr,
- struct port_range *range)
-{
- unsigned long first, last;
- char *ep;
-
- last = first = strtoul(s, &ep, 10);
- if (ep == s) /* Parsed nothing */
- return -EINVAL;
- if (*ep == '-') { /* we have a last value too */
- const char *lasts = ep + 1;
- last = strtoul(lasts, &ep, 10);
- if (ep == lasts) /* Parsed nothing */
- return -EINVAL;
- }
-
- if ((last < first) || (last >= NUM_PORTS))
- return -ERANGE;
-
- range->first = first;
- range->last = last;
- *endptr = ep;
-
- return 0;
-}
-
-/**
- * parse_keyword() - Parse a literal keyword
- * @s: String to parse
- * @endptr: Update to the character after the keyword
- * @kw: Keyword to accept
- *
- * Return: 0, if @s starts with @kw, -EINVAL if it does not
- */
-static int parse_keyword(const char *s, const char **endptr, const char *kw)
-{
- size_t len = strlen(kw);
-
- if (strlen(s) < len)
- return -EINVAL;
-
- if (memcmp(s, kw, len))
- return -EINVAL;
-
- *endptr = s + len;
- return 0;
-}
-
-/**
- * conf_ports_range_except() - Set up forwarding for a range of ports minus a
- * bitmap of exclusions
- * @fwd: Forwarding table to be updated
- * @proto: Protocol to forward
- * @addr: Listening address
- * @ifname: Listening interface
- * @first: First port to forward
- * @last: Last port to forward
- * @exclude: Bitmap of ports to exclude (may be NULL)
- * @to: Port to translate @first to when forwarding
- * @flags: Flags for forwarding entries
- */
-static void conf_ports_range_except(struct fwd_table *fwd, uint8_t proto,
- const union inany_addr *addr,
- const char *ifname,
- uint16_t first, uint16_t last,
- const uint8_t *exclude, uint16_t to,
- uint8_t flags)
-{
- struct fwd_rule rule = {
- .addr = addr ? *addr : inany_any6,
- .ifname = { 0 },
- .proto = proto,
- .flags = flags,
- };
- char rulestr[FWD_RULE_STRLEN];
- unsigned delta = to - first;
- unsigned base, i;
-
- if (!addr)
- rule.flags |= FWD_DUAL_STACK_ANY;
- if (ifname) {
- int ret;
-
- ret = snprintf(rule.ifname, sizeof(rule.ifname),
- "%s", ifname);
- if (ret <= 0 || (size_t)ret >= sizeof(rule.ifname))
- die("Invalid interface name: %s", ifname);
- }
-
- for (base = first; base <= last; base++) {
- if (exclude && bitmap_isset(exclude, base))
- continue;
-
- for (i = base; i <= last; i++) {
- if (exclude && bitmap_isset(exclude, i))
- break;
- }
-
- rule.first = base;
- rule.last = i - 1;
- rule.to = base + delta;
-
- fwd_rule_conflict_check(&rule, fwd->rules, fwd->count);
- if (fwd_rule_add(fwd, &rule) < 0)
- goto fail;
-
- base = i - 1;
- }
- return;
-
-fail:
- die("Unable to add rule %s",
- fwd_rule_fmt(&rule, rulestr, sizeof(rulestr)));
-}
-
-/*
- * for_each_chunk - Step through delimited chunks of a string
- * @p_: Pointer to start of each chunk (updated)
- * @ep_: Pointer to end of each chunk (updated)
- * @s_: String to step through
- * @sep_: String of all allowed delimiters
- */
-#define for_each_chunk(p_, ep_, s_, sep_) \
- for ((p_) = (s_); \
- (ep_) = (p_) + strcspn((p_), (sep_)), *(p_); \
- (p_) = *(ep_) ? (ep_) + 1 : (ep_))
-
-/**
- * conf_ports_spec() - Parse port range(s) specifier
- * @fwd: Forwarding table to be updated
- * @proto: Protocol to forward
- * @addr: Listening address for forwarding
- * @ifname: Interface name for listening
- * @spec: Port range(s) specifier
- */
-static void conf_ports_spec(struct fwd_table *fwd, uint8_t proto,
- const union inany_addr *addr, const char *ifname,
- const char *spec)
-{
- uint8_t exclude[PORT_BITMAP_SIZE] = { 0 };
- bool exclude_only = true;
- const char *p, *ep;
- uint8_t flags = 0;
- unsigned i;
-
- if (!strcmp(spec, "all")) {
- /* Treat "all" as equivalent to "": all non-ephemeral ports */
- spec = "";
- }
-
- /* Parse excluded ranges and "auto" in the first pass */
- for_each_chunk(p, ep, spec, ",") {
- struct port_range xrange;
-
- if (isdigit(*p)) {
- /* Include range, parse later */
- exclude_only = false;
- continue;
- }
-
- if (parse_keyword(p, &p, "auto") == 0) {
- if (p != ep) /* Garbage after the keyword */
- goto bad;
-
- if (!(fwd->caps & FWD_CAP_SCAN)) {
- die(
-"'auto' port forwarding is only allowed for pasta");
- }
-
- flags |= FWD_SCAN;
- continue;
- }
-
- /* Should be an exclude range */
- if (*p != '~')
- goto bad;
- p++;
-
- if (parse_port_range(p, &p, &xrange))
- goto bad;
- if (p != ep) /* Garbage after the range */
- goto bad;
-
- for (i = xrange.first; i <= xrange.last; i++)
- bitmap_set(exclude, i);
- }
-
- if (exclude_only) {
- /* Exclude ephemeral ports */
- fwd_port_map_ephemeral(exclude);
-
- conf_ports_range_except(fwd, proto, addr, ifname,
- 1, NUM_PORTS - 1, exclude,
- 1, flags | FWD_WEAK);
- return;
- }
-
- /* Now process base ranges, skipping exclusions */
- for_each_chunk(p, ep, spec, ",") {
- struct port_range orig_range, mapped_range;
-
- if (!isdigit(*p))
- /* Already parsed */
- continue;
-
- if (parse_port_range(p, &p, &orig_range))
- goto bad;
-
- if (*p == ':') { /* There's a range to map to as well */
- if (parse_port_range(p + 1, &p, &mapped_range))
- goto bad;
- if ((mapped_range.last - mapped_range.first) !=
- (orig_range.last - orig_range.first))
- goto bad;
- } else {
- mapped_range = orig_range;
- }
-
- if (p != ep) /* Garbage after the ranges */
- goto bad;
-
- conf_ports_range_except(fwd, proto, addr, ifname,
- orig_range.first, orig_range.last,
- exclude,
- mapped_range.first, flags);
- }
-
- return;
-bad:
- die("Invalid port specifier '%s'", spec);
-}
-
-/**
- * conf_ports() - Parse port configuration options, initialise UDP/TCP sockets
- * @optname: Short option name, t, T, u, or U
- * @optarg: Option argument (port specification)
- * @fwd: Forwarding table to be updated
- */
-static void conf_ports(char optname, const char *optarg, struct fwd_table *fwd)
-{
- union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
- char buf[BUFSIZ], *spec, *ifname = NULL;
- uint8_t proto;
-
- if (optname == 't' || optname == 'T')
- proto = IPPROTO_TCP;
- else if (optname == 'u' || optname == 'U')
- proto = IPPROTO_UDP;
- else
- assert(0);
-
- if (!strcmp(optarg, "none")) {
- unsigned i;
-
- for (i = 0; i < fwd->count; i++) {
- if (fwd->rules[i].proto == proto) {
- die("-%c none conflicts with previous options",
- optname);
- }
- }
- return;
- }
-
- strncpy(buf, optarg, sizeof(buf) - 1);
-
- if ((spec = strchr(buf, '/'))) {
- *spec = 0;
- spec++;
-
- if (optname != 't' && optname != 'u')
- die("Listening address not allowed for -%c %s",
- optname, optarg);
-
- if ((ifname = strchr(buf, '%'))) {
- *ifname = 0;
- ifname++;
-
- /* spec is already advanced one past the '/',
- * so the length of the given ifname is:
- * (spec - ifname - 1)
- */
- if (spec - ifname - 1 >= IFNAMSIZ) {
- die("Interface name '%s' is too long (max %u)",
- ifname, IFNAMSIZ - 1);
- }
- }
-
- if (ifname == buf + 1) { /* Interface without address */
- addr = NULL;
- } else {
- char *p = buf;
-
- /* Allow square brackets for IPv4 too for convenience */
- if (*p == '[' && p[strlen(p) - 1] == ']') {
- p[strlen(p) - 1] = '\0';
- p++;
- }
-
- if (!inany_pton(p, addr))
- die("Bad forwarding address '%s'", p);
- }
- } else {
- spec = buf;
-
- addr = NULL;
- }
-
- if (optname == 'T' || optname == 'U') {
- assert(!addr && !ifname);
-
- if (!(fwd->caps & FWD_CAP_IFNAME)) {
- warn(
-"SO_BINDTODEVICE unavailable, forwarding only 127.0.0.1 and ::1 for '-%c %s'",
- optname, optarg);
-
- if (fwd->caps & FWD_CAP_IPV4) {
- conf_ports_spec(fwd, proto,
- &inany_loopback4, NULL, spec);
- }
- if (fwd->caps & FWD_CAP_IPV6) {
- conf_ports_spec(fwd, proto,
- &inany_loopback6, NULL, spec);
- }
- return;
- }
-
- ifname = "lo";
- }
-
- if (ifname && !(fwd->caps & FWD_CAP_IFNAME)) {
- die(
-"Device binding for '-%c %s' unsupported (requires kernel 5.7+)",
- optname, optarg);
- }
-
- conf_ports_spec(fwd, proto, addr, ifname, spec);
-}
-
/**
* add_dns4() - Possibly add the IPv4 address of a DNS resolver to configuration
* @c: Execution context
@@ -2160,16 +1800,16 @@ void conf(struct ctx *c, int argc, char **argv)

if (name == 't') {
opt_t = true;
- conf_ports(name, optarg, c->fwd[PIF_HOST]);
+ fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
} else if (name == 'u') {
opt_u = true;
- conf_ports(name, optarg, c->fwd[PIF_HOST]);
+ fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]);
} else if (name == 'T') {
opt_T = true;
- conf_ports(name, optarg, c->fwd[PIF_SPLICE]);
+ fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
} else if (name == 'U') {
opt_U = true;
- conf_ports(name, optarg, c->fwd[PIF_SPLICE]);
+ fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]);
}
} while (name != -1);

@@ -2221,13 +1861,13 @@ void conf(struct ctx *c, int argc, char **argv)

if (c->mode == MODE_PASTA) {
if (!opt_t)
- conf_ports('t', "auto", c->fwd[PIF_HOST]);
+ fwd_rule_parse('t', "auto", c->fwd[PIF_HOST]);
if (!opt_T)
- conf_ports('T', "auto", c->fwd[PIF_SPLICE]);
+ fwd_rule_parse('T', "auto", c->fwd[PIF_SPLICE]);
if (!opt_u)
- conf_ports('u', "auto", c->fwd[PIF_HOST]);
+ fwd_rule_parse('u', "auto", c->fwd[PIF_HOST]);
if (!opt_U)
- conf_ports('U', "auto", c->fwd[PIF_SPLICE]);
+ fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]);
}

if (!c->quiet)
diff --git a/fwd.c b/fwd.c
index a6d75b74..728a783c 100644
--- a/fwd.c
+++ b/fwd.c
@@ -275,100 +275,6 @@ void fwd_rule_init(struct ctx *c)
c->fwd[PIF_SPLICE] = &fwd_out;
}

-/**
- * fwd_rule_add() - Validate and add a rule to a forwarding table
- * @fwd: Table to add to
- * @new: Rule to add
- *
- * Return: 0 on success, negative error code on failure
- */
-int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
-{
- /* Flags which can be set from the caller */
- const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
- unsigned num = (unsigned)new->last - new->first + 1;
- unsigned port;
-
- if (new->first > new->last) {
- warn("Rule has invalid port range %u-%u",
- new->first, new->last);
- return -EINVAL;
- }
- if (!new->first) {
- warn("Forwarding rule attempts to map from port 0");
- return -EINVAL;
- }
- if (!new->to ||
- (in_port_t)(new->to + new->last - new->first) < new->to) {
- warn("Forwarding rule attempts to map to port 0");
- return -EINVAL;
- }
- if (new->flags & ~allowed_flags) {
- warn("Rule has invalid flags 0x%hhx",
- new->flags & ~allowed_flags);
- return -EINVAL;
- }
- if (new->flags & FWD_DUAL_STACK_ANY) {
- if (!inany_equals(&new->addr, &inany_any6)) {
- char astr[INANY_ADDRSTRLEN];
-
- warn("Dual stack rule has non-wildcard address %s",
- inany_ntop(&new->addr, astr, sizeof(astr)));
- return -EINVAL;
- }
- if (!(fwd->caps & FWD_CAP_IPV4)) {
- warn("Dual stack forward, but IPv4 not enabled");
- return -EINVAL;
- }
- if (!(fwd->caps & FWD_CAP_IPV6)) {
- warn("Dual stack forward, but IPv6 not enabled");
- return -EINVAL;
- }
- } else {
- if (inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV4)) {
- warn("IPv4 forward, but IPv4 not enabled");
- return -EINVAL;
- }
- if (!inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV6)) {
- warn("IPv6 forward, but IPv6 not enabled");
- return -EINVAL;
- }
- }
- if (new->proto == IPPROTO_TCP) {
- if (!(fwd->caps & FWD_CAP_TCP)) {
- warn("Can't add TCP forwarding rule, TCP not enabled");
- return -EINVAL;
- }
- } else if (new->proto == IPPROTO_UDP) {
- if (!(fwd->caps & FWD_CAP_UDP)) {
- warn("Can't add UDP forwarding rule, UDP not enabled");
- return -EINVAL;
- }
- } else {
- warn("Unsupported protocol 0x%hhx (%s) for forwarding rule",
- new->proto, ipproto_name(new->proto));
- return -EINVAL;
- }
-
- if (fwd->count >= ARRAY_SIZE(fwd->rules)) {
- warn("Too many rules (maximum %u)", ARRAY_SIZE(fwd->rules));
- return -ENOSPC;
- }
- if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) {
- warn("Rules require too many listening sockets (maximum %u)",
- ARRAY_SIZE(fwd->socks));
- return -ENOSPC;
- }
-
- fwd->rulesocks[fwd->count] = &fwd->socks[fwd->sock_count];
- for (port = new->first; port <= new->last; port++)
- fwd->rulesocks[fwd->count][port - new->first] = -1;
-
- fwd->rules[fwd->count++] = *new;
- fwd->sock_count += num;
- return 0;
-}
-
/**
* fwd_rule_match() - Does a prospective flow match a given forwarding rule?
* @rule: Forwarding rule
diff --git a/fwd.h b/fwd.h
index 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..cd3dec04 100644
--- a/fwd_rule.c
+++ b/fwd_rule.c
@@ -15,6 +15,7 @@
* Author: David Gibson <david@gibson.dropbear.id.au>
*/

+#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
@@ -89,7 +90,7 @@ parse_err:
* fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap
* @map: Bitmap to update
*/
-void fwd_port_map_ephemeral(uint8_t *map)
+static void fwd_port_map_ephemeral(uint8_t *map)
{
unsigned port;

@@ -123,6 +124,7 @@ const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule)
*/
__attribute__((noinline))
#endif
+/* cppcheck-suppress staticFunction */
const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size)
{
const char *percent = *rule->ifname ? "%" : "";
@@ -199,8 +201,8 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *
* @rules: Existing rules against which to test
* @count: Number of rules in @rules
*/
-void fwd_rule_conflict_check(const struct fwd_rule *new,
- const struct fwd_rule *rules, size_t count)
+static void fwd_rule_conflict_check(const struct fwd_rule *new,
+ const struct fwd_rule *rules, size_t count)
{
unsigned i;

@@ -215,3 +217,459 @@ void fwd_rule_conflict_check(const struct fwd_rule *new,
fwd_rule_fmt(&rules[i], rulestr, sizeof(rulestr)));
}
}
+
+/**
+ * fwd_rule_add() - Validate and add a rule to a forwarding table
+ * @fwd: Table to add to
+ * @new: Rule to add
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+static int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new)
+{
+ /* Flags which can be set from the caller */
+ const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY;
+ unsigned num = (unsigned)new->last - new->first + 1;
+ unsigned port;
+
+ if (new->first > new->last) {
+ warn("Rule has invalid port range %u-%u",
+ new->first, new->last);
+ return -EINVAL;
+ }
+ if (!new->first) {
+ warn("Forwarding rule attempts to map from port 0");
+ return -EINVAL;
+ }
+ if (!new->to ||
+ (in_port_t)(new->to + new->last - new->first) < new->to) {
+ warn("Forwarding rule attempts to map to port 0");
+ return -EINVAL;
+ }
+ if (new->flags & ~allowed_flags) {
+ warn("Rule has invalid flags 0x%hhx",
+ new->flags & ~allowed_flags);
+ return -EINVAL;
+ }
+ if (new->flags & FWD_DUAL_STACK_ANY) {
+ if (!inany_equals(&new->addr, &inany_any6)) {
+ char astr[INANY_ADDRSTRLEN];
+
+ warn("Dual stack rule has non-wildcard address %s",
+ inany_ntop(&new->addr, astr, sizeof(astr)));
+ return -EINVAL;
+ }
+ if (!(fwd->caps & FWD_CAP_IPV4)) {
+ warn("Dual stack forward, but IPv4 not enabled");
+ return -EINVAL;
+ }
+ if (!(fwd->caps & FWD_CAP_IPV6)) {
+ warn("Dual stack forward, but IPv6 not enabled");
+ return -EINVAL;
+ }
+ } else {
+ if (inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV4)) {
+ warn("IPv4 forward, but IPv4 not enabled");
+ return -EINVAL;
+ }
+ if (!inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV6)) {
+ warn("IPv6 forward, but IPv6 not enabled");
+ return -EINVAL;
+ }
+ }
+ if (new->proto == IPPROTO_TCP) {
+ if (!(fwd->caps & FWD_CAP_TCP)) {
+ warn("Can't add TCP forwarding rule, TCP not enabled");
+ return -EINVAL;
+ }
+ } else if (new->proto == IPPROTO_UDP) {
+ if (!(fwd->caps & FWD_CAP_UDP)) {
+ warn("Can't add UDP forwarding rule, UDP not enabled");
+ return -EINVAL;
+ }
+ } else {
+ warn("Unsupported protocol 0x%hhx (%s) for forwarding rule",
+ new->proto, ipproto_name(new->proto));
+ return -EINVAL;
+ }
+
+ if (fwd->count >= ARRAY_SIZE(fwd->rules)) {
+ warn("Too many rules (maximum %u)", ARRAY_SIZE(fwd->rules));
+ return -ENOSPC;
+ }
+ if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) {
+ warn("Rules require too many listening sockets (maximum %u)",
+ ARRAY_SIZE(fwd->socks));
+ return -ENOSPC;
+ }
+
+ fwd->rulesocks[fwd->count] = &fwd->socks[fwd->sock_count];
+ for (port = new->first; port <= new->last; port++)
+ fwd->rulesocks[fwd->count][port - new->first] = -1;
+
+ fwd->rules[fwd->count++] = *new;
+ fwd->sock_count += num;
+ return 0;
+}
+
+/**
+ * port_range() - Represents a non-empty range of ports
+ * @first: First port number in the range
+ * @last: Last port number in the range (inclusive)
+ *
+ * Invariant: @last >= @first
+ */
+struct port_range {
+ in_port_t first, last;
+};
+
+/**
+ * parse_port_range() - Parse a range of port numbers '<first>[-<last>]'
+ * @s: String to parse
+ * @endptr: Update to the character after the parsed range (similar to
+ * strtol() etc.)
+ * @range: Update with the parsed values on success
+ *
+ * Return: -EINVAL on parsing error, -ERANGE on out of range port
+ * numbers, 0 on success
+ */
+static int parse_port_range(const char *s, const char **endptr,
+ struct port_range *range)
+{
+ unsigned long first, last;
+ char *ep;
+
+ last = first = strtoul(s, &ep, 10);
+ if (ep == s) /* Parsed nothing */
+ return -EINVAL;
+ if (*ep == '-') { /* we have a last value too */
+ const char *lasts = ep + 1;
+ last = strtoul(lasts, &ep, 10);
+ if (ep == lasts) /* Parsed nothing */
+ return -EINVAL;
+ }
+
+ if ((last < first) || (last >= NUM_PORTS))
+ return -ERANGE;
+
+ range->first = first;
+ range->last = last;
+ *endptr = ep;
+
+ return 0;
+}
+
+/**
+ * parse_keyword() - Parse a literal keyword
+ * @s: String to parse
+ * @endptr: Update to the character after the keyword
+ * @kw: Keyword to accept
+ *
+ * Return: 0, if @s starts with @kw, -EINVAL if it does not
+ */
+static int parse_keyword(const char *s, const char **endptr, const char *kw)
+{
+ size_t len = strlen(kw);
+
+ if (strlen(s) < len)
+ return -EINVAL;
+
+ if (memcmp(s, kw, len))
+ return -EINVAL;
+
+ *endptr = s + len;
+ return 0;
+}
+
+/**
+ * fwd_rule_range_except() - Set up forwarding for a range of ports minus a
+ * bitmap of exclusions
+ * @fwd: Forwarding table to be updated
+ * @proto: Protocol to forward
+ * @addr: Listening address
+ * @ifname: Listening interface
+ * @first: First port to forward
+ * @last: Last port to forward
+ * @exclude: Bitmap of ports to exclude (may be NULL)
+ * @to: Port to translate @first to when forwarding
+ * @flags: Flags for forwarding entries
+ */
+static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto,
+ const union inany_addr *addr,
+ const char *ifname,
+ uint16_t first, uint16_t last,
+ const uint8_t *exclude, uint16_t to,
+ uint8_t flags)
+{
+ struct fwd_rule rule = {
+ .addr = addr ? *addr : inany_any6,
+ .ifname = { 0 },
+ .proto = proto,
+ .flags = flags,
+ };
+ char rulestr[FWD_RULE_STRLEN];
+ unsigned delta = to - first;
+ unsigned base, i;
+
+ if (!addr)
+ rule.flags |= FWD_DUAL_STACK_ANY;
+ if (ifname) {
+ int ret;
+
+ ret = snprintf(rule.ifname, sizeof(rule.ifname),
+ "%s", ifname);
+ if (ret <= 0 || (size_t)ret >= sizeof(rule.ifname))
+ die("Invalid interface name: %s", ifname);
+ }
+
+ for (base = first; base <= last; base++) {
+ if (exclude && bitmap_isset(exclude, base))
+ continue;
+
+ for (i = base; i <= last; i++) {
+ if (exclude && bitmap_isset(exclude, i))
+ break;
+ }
+
+ rule.first = base;
+ rule.last = i - 1;
+ rule.to = base + delta;
+
+ fwd_rule_conflict_check(&rule, fwd->rules, fwd->count);
+ if (fwd_rule_add(fwd, &rule) < 0)
+ goto fail;
+
+ base = i - 1;
+ }
+ return;
+
+fail:
+ die("Unable to add rule %s",
+ fwd_rule_fmt(&rule, rulestr, sizeof(rulestr)));
+}
+
+/*
+ * for_each_chunk - Step through delimited chunks of a string
+ * @p_: Pointer to start of each chunk (updated)
+ * @ep_: Pointer to end of each chunk (updated)
+ * @s_: String to step through
+ * @sep_: String of all allowed delimiters
+ */
+#define for_each_chunk(p_, ep_, s_, sep_) \
+ for ((p_) = (s_); \
+ (ep_) = (p_) + strcspn((p_), (sep_)), *(p_); \
+ (p_) = *(ep_) ? (ep_) + 1 : (ep_))
+
+/**
+ * fwd_rule_parse_ports() - Parse port range(s) specifier
+ * @fwd: Forwarding table to be updated
+ * @proto: Protocol to forward
+ * @addr: Listening address for forwarding
+ * @ifname: Interface name for listening
+ * @spec: Port range(s) specifier
+ */
+static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto,
+ const union inany_addr *addr,
+ const char *ifname,
+ const char *spec)
+{
+ uint8_t exclude[PORT_BITMAP_SIZE] = { 0 };
+ bool exclude_only = true;
+ const char *p, *ep;
+ uint8_t flags = 0;
+ unsigned i;
+
+ if (!strcmp(spec, "all")) {
+ /* Treat "all" as equivalent to "": all non-ephemeral ports */
+ spec = "";
+ }
+
+ /* Parse excluded ranges and "auto" in the first pass */
+ for_each_chunk(p, ep, spec, ",") {
+ struct port_range xrange;
+
+ if (isdigit(*p)) {
+ /* Include range, parse later */
+ exclude_only = false;
+ continue;
+ }
+
+ if (parse_keyword(p, &p, "auto") == 0) {
+ if (p != ep) /* Garbage after the keyword */
+ goto bad;
+
+ if (!(fwd->caps & FWD_CAP_SCAN)) {
+ die(
+"'auto' port forwarding is only allowed for pasta");
+ }
+
+ flags |= FWD_SCAN;
+ continue;
+ }
+
+ /* Should be an exclude range */
+ if (*p != '~')
+ goto bad;
+ p++;
+
+ if (parse_port_range(p, &p, &xrange))
+ goto bad;
+ if (p != ep) /* Garbage after the range */
+ goto bad;
+
+ for (i = xrange.first; i <= xrange.last; i++)
+ bitmap_set(exclude, i);
+ }
+
+ if (exclude_only) {
+ /* Exclude ephemeral ports */
+ fwd_port_map_ephemeral(exclude);
+
+ fwd_rule_range_except(fwd, proto, addr, ifname,
+ 1, NUM_PORTS - 1, exclude,
+ 1, flags | FWD_WEAK);
+ return;
+ }
+
+ /* Now process base ranges, skipping exclusions */
+ for_each_chunk(p, ep, spec, ",") {
+ struct port_range orig_range, mapped_range;
+
+ if (!isdigit(*p))
+ /* Already parsed */
+ continue;
+
+ if (parse_port_range(p, &p, &orig_range))
+ goto bad;
+
+ if (*p == ':') { /* There's a range to map to as well */
+ if (parse_port_range(p + 1, &p, &mapped_range))
+ goto bad;
+ if ((mapped_range.last - mapped_range.first) !=
+ (orig_range.last - orig_range.first))
+ goto bad;
+ } else {
+ mapped_range = orig_range;
+ }
+
+ if (p != ep) /* Garbage after the ranges */
+ goto bad;
+
+ fwd_rule_range_except(fwd, proto, addr, ifname,
+ orig_range.first, orig_range.last,
+ exclude,
+ mapped_range.first, flags);
+ }
+
+ return;
+bad:
+ die("Invalid port specifier '%s'", spec);
+}
+
+/**
+ * fwd_rule_parse() - Parse port configuration option
+ * @optname: Short option name, t, T, u, or U
+ * @optarg: Option argument (port specification)
+ * @fwd: Forwarding table to be updated
+ */
+void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd)
+{
+ union inany_addr addr_buf = inany_any6, *addr = &addr_buf;
+ char buf[BUFSIZ], *spec, *ifname = NULL;
+ uint8_t proto;
+
+ if (optname == 't' || optname == 'T')
+ proto = IPPROTO_TCP;
+ else if (optname == 'u' || optname == 'U')
+ proto = IPPROTO_UDP;
+ else
+ assert(0);
+
+ if (!strcmp(optarg, "none")) {
+ unsigned i;
+
+ for (i = 0; i < fwd->count; i++) {
+ if (fwd->rules[i].proto == proto) {
+ die("-%c none conflicts with previous options",
+ optname);
+ }
+ }
+ return;
+ }
+
+ strncpy(buf, optarg, sizeof(buf) - 1);
+
+ if ((spec = strchr(buf, '/'))) {
+ *spec = 0;
+ spec++;
+
+ if (optname != 't' && optname != 'u')
+ die("Listening address not allowed for -%c %s",
+ optname, optarg);
+
+ if ((ifname = strchr(buf, '%'))) {
+ *ifname = 0;
+ ifname++;
+
+ /* spec is already advanced one past the '/',
+ * so the length of the given ifname is:
+ * (spec - ifname - 1)
+ */
+ if (spec - ifname - 1 >= IFNAMSIZ) {
+ die("Interface name '%s' is too long (max %u)",
+ ifname, IFNAMSIZ - 1);
+ }
+ }
+
+ if (ifname == buf + 1) { /* Interface without address */
+ addr = NULL;
+ } else {
+ char *p = buf;
+
+ /* Allow square brackets for IPv4 too for convenience */
+ if (*p == '[' && p[strlen(p) - 1] == ']') {
+ p[strlen(p) - 1] = '\0';
+ p++;
+ }
+
+ if (!inany_pton(p, addr))
+ die("Bad forwarding address '%s'", p);
+ }
+ } else {
+ spec = buf;
+
+ addr = NULL;
+ }
+
+ if (optname == 'T' || optname == 'U') {
+ assert(!addr && !ifname);
+
+ if (!(fwd->caps & FWD_CAP_IFNAME)) {
+ warn(
+"SO_BINDTODEVICE unavailable, forwarding only 127.0.0.1 and ::1 for '-%c %s'",
+ optname, optarg);
+
+ if (fwd->caps & FWD_CAP_IPV4) {
+ fwd_rule_parse_ports(fwd, proto,
+ &inany_loopback4, NULL,
+ spec);
+ }
+ if (fwd->caps & FWD_CAP_IPV6) {
+ fwd_rule_parse_ports(fwd, proto,
+ &inany_loopback6, NULL,
+ spec);
+ }
+ return;
+ }
+
+ ifname = "lo";
+ }
+
+ if (ifname && !(fwd->caps & FWD_CAP_IFNAME)) {
+ die(
+"Device binding for '-%c %s' unsupported (requires kernel 5.7+)",
+ optname, optarg);
+ }
+
+ fwd_rule_parse_ports(fwd, proto, addr, ifname, spec);
+}
diff --git a/fwd_rule.h b/fwd_rule.h
index 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