From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=none (p=none dis=none) header.from=gibson.dropbear.id.au Authentication-Results: passt.top; dkim=pass (2048-bit key; secure) header.d=gibson.dropbear.id.au header.i=@gibson.dropbear.id.au header.a=rsa-sha256 header.s=202602 header.b=KgSUsC+K; dkim-atps=neutral Received: from mail.ozlabs.org (mail.ozlabs.org [IPv6:2404:9400:2221:ea00::3]) by passt.top (Postfix) with ESMTPS id E17805A0776 for ; Fri, 10 Apr 2026 03:03:25 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gibson.dropbear.id.au; s=202602; t=1775782993; bh=e/9d8LLe8dBnCK/0tgzrmY9XkppiOdsk6nppL95neJo=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=KgSUsC+KeTcwWzczNCkHx14xHkdbQDFK9gHVcO4b0B5wnCEqt55KPjAaR+cxCNfqL EMtTXx+99kzXt5rzk3+gqpu1qtI9QG8hjxYvdp+so3kKZMkF5+TzfKw8TEe7xdxSnu f4Um1iHk0/8KuSFFUZ+GHVIj0B8WqZj8enBicuUv9coliYwDUSDYWbQS/wRyjsuW8n 3CKmv7ugFt7RriDmXyYATE/Baiq+YHNJQqWFM2yrtGjJP9GxAwFLrA3Zz2JvmZA1od N/9DkTFIdoCre45+UjGKnk+LbG085OabXAuh6M0ygPT45xZPmuWteg1+XPnRpLN9AY CX5RdNSrk+4Bg== Received: by gandalf.ozlabs.org (Postfix, from userid 1007) id 4fsJSx1qpgz4wxF; Fri, 10 Apr 2026 11:03:13 +1000 (AEST) From: David Gibson To: passt-dev@passt.top, Stefano Brivio Subject: [PATCH v2 23/23] fwd, conf: Move rule parsing code to fwd_rule.[ch] Date: Fri, 10 Apr 2026 11:03:09 +1000 Message-ID: <20260410010309.736855-24-david@gibson.dropbear.id.au> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260410010309.736855-1-david@gibson.dropbear.id.au> References: <20260410010309.736855-1-david@gibson.dropbear.id.au> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Message-ID-Hash: 5VD7UCH2UKMSWQ36AKMRVSL7FVFHKPLJ X-Message-ID-Hash: 5VD7UCH2UKMSWQ36AKMRVSL7FVFHKPLJ X-MailFrom: dgibson@gandalf.ozlabs.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: David Gibson X-Mailman-Version: 3.3.8 Precedence: list List-Id: Development discussion and patches for passt Archived-At: Archived-At: List-Archive: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- conf.c | 378 +------------------------------------------ fwd.c | 93 ----------- fwd.h | 33 ---- fwd_rule.c | 465 ++++++++++++++++++++++++++++++++++++++++++++++++++++- fwd_rule.h | 36 ++++- 5 files changed, 503 insertions(+), 502 deletions(-) diff --git a/conf.c b/conf.c index 31627209..5f38a287 100644 --- a/conf.c +++ b/conf.c @@ -13,7 +13,6 @@ */ #include -#include #include #include #include @@ -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 '[-]' - * @s: String to parse - * @endptr: Update to the character after the parsed range (similar to - * strtol() etc.) - * @range: Update with the parsed values on success - * - * Return: -EINVAL on parsing error, -ERANGE on out of range port - * numbers, 0 on success - */ -static int parse_port_range(const char *s, const char **endptr, - struct port_range *range) -{ - unsigned long first, last; - char *ep; - - last = first = strtoul(s, &ep, 10); - if (ep == s) /* Parsed nothing */ - return -EINVAL; - if (*ep == '-') { /* we have a last value too */ - const char *lasts = ep + 1; - last = strtoul(lasts, &ep, 10); - if (ep == lasts) /* Parsed nothing */ - return -EINVAL; - } - - if ((last < first) || (last >= NUM_PORTS)) - return -ERANGE; - - range->first = first; - range->last = last; - *endptr = ep; - - return 0; -} - -/** - * parse_keyword() - Parse a literal keyword - * @s: String to parse - * @endptr: Update to the character after the keyword - * @kw: Keyword to accept - * - * Return: 0, if @s starts with @kw, -EINVAL if it does not - */ -static int parse_keyword(const char *s, const char **endptr, const char *kw) -{ - size_t len = strlen(kw); - - if (strlen(s) < len) - return -EINVAL; - - if (memcmp(s, kw, len)) - return -EINVAL; - - *endptr = s + len; - return 0; -} - -/** - * conf_ports_range_except() - Set up forwarding for a range of ports minus a - * bitmap of exclusions - * @fwd: Forwarding table to be updated - * @proto: Protocol to forward - * @addr: Listening address - * @ifname: Listening interface - * @first: First port to forward - * @last: Last port to forward - * @exclude: Bitmap of ports to exclude (may be NULL) - * @to: Port to translate @first to when forwarding - * @flags: Flags for forwarding entries - */ -static void conf_ports_range_except(struct fwd_table *fwd, uint8_t proto, - const union inany_addr *addr, - const char *ifname, - uint16_t first, uint16_t last, - const uint8_t *exclude, uint16_t to, - uint8_t flags) -{ - struct fwd_rule rule = { - .addr = addr ? *addr : inany_any6, - .ifname = { 0 }, - .proto = proto, - .flags = flags, - }; - char rulestr[FWD_RULE_STRLEN]; - unsigned delta = to - first; - unsigned base, i; - - if (!addr) - rule.flags |= FWD_DUAL_STACK_ANY; - if (ifname) { - int ret; - - ret = snprintf(rule.ifname, sizeof(rule.ifname), - "%s", ifname); - if (ret <= 0 || (size_t)ret >= sizeof(rule.ifname)) - die("Invalid interface name: %s", ifname); - } - - assert(first != 0); - - for (base = first; base <= last; base++) { - if (exclude && bitmap_isset(exclude, base)) - continue; - - for (i = base; i <= last; i++) { - if (exclude && bitmap_isset(exclude, i)) - break; - } - - rule.first = base; - rule.last = i - 1; - rule.to = base + delta; - - fwd_rule_conflict_check(&rule, fwd->rules, fwd->count); - if (fwd_rule_add(fwd, &rule) < 0) - goto fail; - - base = i - 1; - } - return; - -fail: - die("Unable to add rule %s", - fwd_rule_fmt(&rule, rulestr, sizeof(rulestr))); -} - -/* - * for_each_chunk - Step through delimited chunks of a string - * @p_: Pointer to start of each chunk (updated) - * @ep_: Pointer to end of each chunk (updated) - * @s_: String to step through - * @sep_: String of all allowed delimiters - */ -#define for_each_chunk(p_, ep_, s_, sep_) \ - for ((p_) = (s_); \ - (ep_) = (p_) + strcspn((p_), (sep_)), *(p_); \ - (p_) = *(ep_) ? (ep_) + 1 : (ep_)) - -/** - * conf_ports_spec() - Parse port range(s) specifier - * @fwd: Forwarding table to be updated - * @proto: Protocol to forward - * @addr: Listening address for forwarding - * @ifname: Interface name for listening - * @spec: Port range(s) specifier - */ -static void conf_ports_spec(struct fwd_table *fwd, uint8_t proto, - const union inany_addr *addr, const char *ifname, - const char *spec) -{ - uint8_t exclude[PORT_BITMAP_SIZE] = { 0 }; - bool exclude_only = true; - const char *p, *ep; - uint8_t flags = 0; - unsigned i; - - if (!strcmp(spec, "all")) { - /* Treat "all" as equivalent to "": all non-ephemeral ports */ - spec = ""; - } - - /* Parse excluded ranges and "auto" in the first pass */ - for_each_chunk(p, ep, spec, ",") { - struct port_range xrange; - - if (isdigit(*p)) { - /* Include range, parse later */ - exclude_only = false; - continue; - } - - if (parse_keyword(p, &p, "auto") == 0) { - if (p != ep) /* Garbage after the keyword */ - goto bad; - - if (!(fwd->caps & FWD_CAP_SCAN)) { - die( -"'auto' port forwarding is only allowed for pasta"); - } - - flags |= FWD_SCAN; - continue; - } - - /* Should be an exclude range */ - if (*p != '~') - goto bad; - p++; - - if (parse_port_range(p, &p, &xrange)) - goto bad; - if (p != ep) /* Garbage after the range */ - goto bad; - - for (i = xrange.first; i <= xrange.last; i++) - bitmap_set(exclude, i); - } - - if (exclude_only) { - /* Exclude ephemeral ports */ - fwd_port_map_ephemeral(exclude); - - conf_ports_range_except(fwd, proto, addr, ifname, - 1, NUM_PORTS - 1, exclude, - 1, flags | FWD_WEAK); - return; - } - - /* Now process base ranges, skipping exclusions */ - for_each_chunk(p, ep, spec, ",") { - struct port_range orig_range, mapped_range; - - if (!isdigit(*p)) - /* Already parsed */ - continue; - - if (parse_port_range(p, &p, &orig_range)) - goto bad; - - if (*p == ':') { /* There's a range to map to as well */ - if (parse_port_range(p + 1, &p, &mapped_range)) - goto bad; - if ((mapped_range.last - mapped_range.first) != - (orig_range.last - orig_range.first)) - goto bad; - } else { - mapped_range = orig_range; - } - - if (p != ep) /* Garbage after the ranges */ - goto bad; - - conf_ports_range_except(fwd, proto, addr, ifname, - orig_range.first, orig_range.last, - exclude, - mapped_range.first, flags); - } - - return; -bad: - die("Invalid port specifier '%s'", spec); -} - -/** - * conf_ports() - Parse port configuration options, initialise UDP/TCP sockets - * @optname: Short option name, t, T, u, or U - * @optarg: Option argument (port specification) - * @fwd: Forwarding table to be updated - */ -static void conf_ports(char optname, const char *optarg, struct fwd_table *fwd) -{ - union inany_addr addr_buf = inany_any6, *addr = &addr_buf; - char buf[BUFSIZ], *spec, *ifname = NULL; - uint8_t proto; - - if (optname == 't' || optname == 'T') - proto = IPPROTO_TCP; - else if (optname == 'u' || optname == 'U') - proto = IPPROTO_UDP; - else - assert(0); - - if (!strcmp(optarg, "none")) { - unsigned i; - - for (i = 0; i < fwd->count; i++) { - if (fwd->rules[i].proto == proto) { - die("-%c none conflicts with previous options", - optname); - } - } - return; - } - - strncpy(buf, optarg, sizeof(buf) - 1); - - if ((spec = strchr(buf, '/'))) { - *spec = 0; - spec++; - - if (optname != 't' && optname != 'u') - die("Listening address not allowed for -%c %s", - optname, optarg); - - if ((ifname = strchr(buf, '%'))) { - *ifname = 0; - ifname++; - - /* spec is already advanced one past the '/', - * so the length of the given ifname is: - * (spec - ifname - 1) - */ - if (spec - ifname - 1 >= IFNAMSIZ) { - die("Interface name '%s' is too long (max %u)", - ifname, IFNAMSIZ - 1); - } - } - - if (ifname == buf + 1) { /* Interface without address */ - addr = NULL; - } else { - char *p = buf; - - /* Allow square brackets for IPv4 too for convenience */ - if (*p == '[' && p[strlen(p) - 1] == ']') { - p[strlen(p) - 1] = '\0'; - p++; - } - - if (!inany_pton(p, addr)) - die("Bad forwarding address '%s'", p); - } - } else { - spec = buf; - - addr = NULL; - } - - if (optname == 'T' || optname == 'U') { - assert(!addr && !ifname); - - if (!(fwd->caps & FWD_CAP_IFNAME)) { - warn( -"SO_BINDTODEVICE unavailable, forwarding only 127.0.0.1 and ::1 for '-%c %s'", - optname, optarg); - - if (fwd->caps & FWD_CAP_IPV4) { - conf_ports_spec(fwd, proto, - &inany_loopback4, NULL, spec); - } - if (fwd->caps & FWD_CAP_IPV6) { - conf_ports_spec(fwd, proto, - &inany_loopback6, NULL, spec); - } - return; - } - - ifname = "lo"; - } - - if (ifname && !(fwd->caps & FWD_CAP_IFNAME)) { - die( -"Device binding for '-%c %s' unsupported (requires kernel 5.7+)", - optname, optarg); - } - - conf_ports_spec(fwd, proto, addr, ifname, spec); -} - /** * add_dns4() - Possibly add the IPv4 address of a DNS resolver to configuration * @c: Execution context @@ -2163,16 +1801,16 @@ void conf(struct ctx *c, int argc, char **argv) if (name == 't') { opt_t = true; - conf_ports(name, optarg, c->fwd[PIF_HOST]); + fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]); } else if (name == 'u') { opt_u = true; - conf_ports(name, optarg, c->fwd[PIF_HOST]); + fwd_rule_parse(name, optarg, c->fwd[PIF_HOST]); } else if (name == 'T') { opt_T = true; - conf_ports(name, optarg, c->fwd[PIF_SPLICE]); + fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]); } else if (name == 'U') { opt_U = true; - conf_ports(name, optarg, c->fwd[PIF_SPLICE]); + fwd_rule_parse(name, optarg, c->fwd[PIF_SPLICE]); } } while (name != -1); @@ -2224,13 +1862,13 @@ void conf(struct ctx *c, int argc, char **argv) if (c->mode == MODE_PASTA) { if (!opt_t) - conf_ports('t', "auto", c->fwd[PIF_HOST]); + fwd_rule_parse('t', "auto", c->fwd[PIF_HOST]); if (!opt_T) - conf_ports('T', "auto", c->fwd[PIF_SPLICE]); + fwd_rule_parse('T', "auto", c->fwd[PIF_SPLICE]); if (!opt_u) - conf_ports('u', "auto", c->fwd[PIF_HOST]); + fwd_rule_parse('u', "auto", c->fwd[PIF_HOST]); if (!opt_U) - conf_ports('U', "auto", c->fwd[PIF_SPLICE]); + fwd_rule_parse('U', "auto", c->fwd[PIF_SPLICE]); } if (!c->quiet) diff --git a/fwd.c b/fwd.c index f2f7b648..728a783c 100644 --- a/fwd.c +++ b/fwd.c @@ -275,99 +275,6 @@ void fwd_rule_init(struct ctx *c) c->fwd[PIF_SPLICE] = &fwd_out; } -/** - * fwd_rule_add() - Validate and add a rule to a forwarding table - * @fwd: Table to add to - * @new: Rule to add - * - * Return: 0 on success, negative error code on failure - */ -int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new) -{ - /* Flags which can be set from the caller */ - const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY; - unsigned num = (unsigned)new->last - new->first + 1; - unsigned port; - - if (new->first > new->last) { - warn("Rule has invalid port range %u-%u", - new->first, new->last); - return -EINVAL; - } - if (!new->first) { - warn("Forwarding rule atttempts to map from port 0"); - return -EINVAL; - } - if (!new->to || (new->to + new->last - new->first) < new->to) { - warn("Forwarding rule atttempts to map to port 0"); - return -EINVAL; - } - if (new->flags & ~allowed_flags) { - warn("Rule has invalid flags 0x%hhx", - new->flags & ~allowed_flags); - return -EINVAL; - } - if (new->flags & FWD_DUAL_STACK_ANY) { - if (!inany_equals(&new->addr, &inany_any6)) { - char astr[INANY_ADDRSTRLEN]; - - warn("Dual stack rule has non-wildcard address %s", - inany_ntop(&new->addr, astr, sizeof(astr))); - return -EINVAL; - } - if (!(fwd->caps & FWD_CAP_IPV4)) { - warn("Dual stack forward, but IPv4 not enabled"); - return -EINVAL; - } - if (!(fwd->caps & FWD_CAP_IPV6)) { - warn("Dual stack forward, but IPv6 not enabled"); - return -EINVAL; - } - } else { - if (inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV4)) { - warn("IPv4 forward, but IPv4 not enabled"); - return -EINVAL; - } - if (!inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV6)) { - warn("IPv6 forward, but IPv6 not enabled"); - return -EINVAL; - } - } - if (new->proto == IPPROTO_TCP) { - if (!(fwd->caps & FWD_CAP_TCP)) { - warn("Can't add TCP forwarding rule, TCP not enabled"); - return -EINVAL; - } - } else if (new->proto == IPPROTO_UDP) { - if (!(fwd->caps & FWD_CAP_UDP)) { - warn("Can't add UDP forwarding rule, UDP not enabled"); - return -EINVAL; - } - } else { - warn("Unsupported protocol 0x%hhx (%s) for forwarding rule", - new->proto, ipproto_name(new->proto)); - return -EINVAL; - } - - if (fwd->count >= ARRAY_SIZE(fwd->rules)) { - warn("Too many rules (maximum %u)", ARRAY_SIZE(fwd->rules)); - return -ENOSPC; - } - if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) { - warn("Rules require too many listening sockets (maximum %u)", - ARRAY_SIZE(fwd->socks)); - return -ENOSPC; - } - - fwd->rulesocks[fwd->count] = &fwd->socks[fwd->sock_count]; - for (port = new->first; port <= new->last; port++) - fwd->rulesocks[fwd->count][port - new->first] = -1; - - fwd->rules[fwd->count++] = *new; - fwd->sock_count += num; - return 0; -} - /** * fwd_rule_match() - Does a prospective flow match a given forwarding rule? * @rule: Forwarding rule diff --git a/fwd.h b/fwd.h index e664d1d0..8f845d09 100644 --- a/fwd.h +++ b/fwd.h @@ -20,8 +20,6 @@ struct flowside; -#define FWD_RULE_BITS 8 -#define MAX_FWD_RULES MAX_FROM_BITS(FWD_RULE_BITS) #define FWD_NO_HINT (-1) /** @@ -36,36 +34,6 @@ struct fwd_listen_ref { unsigned rule :FWD_RULE_BITS; }; -/* Maximum number of listening sockets (per pif) - * - * Rationale: This lets us listen on every port for two addresses and two - * protocols (which we need for -T auto -U auto without SO_BINDTODEVICE), plus a - * comfortable number of extras. - */ -#define MAX_LISTEN_SOCKS (NUM_PORTS * 5) - -/** - * struct fwd_table - Forwarding state (per initiating pif) - * @caps: Forwarding capabilities for this initiating pif - * @count: Number of forwarding rules - * @rules: Array of forwarding rules - * @rulesocks: Parallel array of @rules (@count valid entries) of pointers to - * @socks entries giving the start of the corresponding rule's - * sockets within the larger array - * @sock_count: Number of entries used in @socks (for all rules combined) - * @socks: Listening sockets for forwarding - */ -struct fwd_table { - uint32_t caps; - unsigned count; - struct fwd_rule rules[MAX_FWD_RULES]; - int *rulesocks[MAX_FWD_RULES]; - unsigned sock_count; - int socks[MAX_LISTEN_SOCKS]; -}; - -#define PORT_BITMAP_SIZE DIV_ROUND_UP(NUM_PORTS, 8) - /** * struct fwd_scan - Port scanning state for a protocol+direction * @scan4: /proc/net fd to scan for IPv4 ports when in AUTO mode @@ -81,7 +49,6 @@ struct fwd_scan { #define FWD_PORT_SCAN_INTERVAL 1000 /* ms */ void fwd_rule_init(struct ctx *c); -int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new); const struct fwd_rule *fwd_rule_search(const struct fwd_table *fwd, const struct flowside *ini, uint8_t proto, int hint); diff --git a/fwd_rule.c b/fwd_rule.c index 9d489827..2b0bd4db 100644 --- a/fwd_rule.c +++ b/fwd_rule.c @@ -15,6 +15,7 @@ * Author: David Gibson */ +#include #include #include #include @@ -89,7 +90,7 @@ parse_err: * fwd_port_map_ephemeral() - Mark ephemeral ports in a bitmap * @map: Bitmap to update */ -void fwd_port_map_ephemeral(uint8_t *map) +static void fwd_port_map_ephemeral(uint8_t *map) { unsigned port; @@ -123,6 +124,7 @@ const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule) */ __attribute__((noinline)) #endif +/* cppcheck-suppress staticFunction */ const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size) { const char *percent = *rule->ifname ? "%" : ""; @@ -199,8 +201,8 @@ static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule * * @rules: Existing rules against which to test * @count: Number of rules in @rules */ -void fwd_rule_conflict_check(const struct fwd_rule *new, - const struct fwd_rule *rules, size_t count) +static void fwd_rule_conflict_check(const struct fwd_rule *new, + const struct fwd_rule *rules, size_t count) { unsigned i; @@ -215,3 +217,460 @@ void fwd_rule_conflict_check(const struct fwd_rule *new, fwd_rule_fmt(&rules[i], rulestr, sizeof(rulestr))); } } + +/** + * fwd_rule_add() - Validate and add a rule to a forwarding table + * @fwd: Table to add to + * @new: Rule to add + * + * Return: 0 on success, negative error code on failure + */ +static int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new) +{ + /* Flags which can be set from the caller */ + const uint8_t allowed_flags = FWD_WEAK | FWD_SCAN | FWD_DUAL_STACK_ANY; + unsigned num = (unsigned)new->last - new->first + 1; + unsigned port; + + if (new->first > new->last) { + warn("Rule has invalid port range %u-%u", + new->first, new->last); + return -EINVAL; + } + if (!new->first) { + warn("Forwarding rule atttempts to map from port 0"); + return -EINVAL; + } + if (!new->to || (new->to + new->last - new->first) < new->to) { + warn("Forwarding rule atttempts to map to port 0"); + return -EINVAL; + } + if (new->flags & ~allowed_flags) { + warn("Rule has invalid flags 0x%hhx", + new->flags & ~allowed_flags); + return -EINVAL; + } + if (new->flags & FWD_DUAL_STACK_ANY) { + if (!inany_equals(&new->addr, &inany_any6)) { + char astr[INANY_ADDRSTRLEN]; + + warn("Dual stack rule has non-wildcard address %s", + inany_ntop(&new->addr, astr, sizeof(astr))); + return -EINVAL; + } + if (!(fwd->caps & FWD_CAP_IPV4)) { + warn("Dual stack forward, but IPv4 not enabled"); + return -EINVAL; + } + if (!(fwd->caps & FWD_CAP_IPV6)) { + warn("Dual stack forward, but IPv6 not enabled"); + return -EINVAL; + } + } else { + if (inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV4)) { + warn("IPv4 forward, but IPv4 not enabled"); + return -EINVAL; + } + if (!inany_v4(&new->addr) && !(fwd->caps & FWD_CAP_IPV6)) { + warn("IPv6 forward, but IPv6 not enabled"); + return -EINVAL; + } + } + if (new->proto == IPPROTO_TCP) { + if (!(fwd->caps & FWD_CAP_TCP)) { + warn("Can't add TCP forwarding rule, TCP not enabled"); + return -EINVAL; + } + } else if (new->proto == IPPROTO_UDP) { + if (!(fwd->caps & FWD_CAP_UDP)) { + warn("Can't add UDP forwarding rule, UDP not enabled"); + return -EINVAL; + } + } else { + warn("Unsupported protocol 0x%hhx (%s) for forwarding rule", + new->proto, ipproto_name(new->proto)); + return -EINVAL; + } + + if (fwd->count >= ARRAY_SIZE(fwd->rules)) { + warn("Too many rules (maximum %u)", ARRAY_SIZE(fwd->rules)); + return -ENOSPC; + } + if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) { + warn("Rules require too many listening sockets (maximum %u)", + ARRAY_SIZE(fwd->socks)); + return -ENOSPC; + } + + fwd->rulesocks[fwd->count] = &fwd->socks[fwd->sock_count]; + for (port = new->first; port <= new->last; port++) + fwd->rulesocks[fwd->count][port - new->first] = -1; + + fwd->rules[fwd->count++] = *new; + fwd->sock_count += num; + return 0; +} + +/** + * port_range() - Represents a non-empty range of ports + * @first: First port number in the range + * @last: Last port number in the range (inclusive) + * + * Invariant: @last >= @first + */ +struct port_range { + in_port_t first, last; +}; + +/** + * parse_port_range() - Parse a range of port numbers '[-]' + * @s: String to parse + * @endptr: Update to the character after the parsed range (similar to + * strtol() etc.) + * @range: Update with the parsed values on success + * + * Return: -EINVAL on parsing error, -ERANGE on out of range port + * numbers, 0 on success + */ +static int parse_port_range(const char *s, const char **endptr, + struct port_range *range) +{ + unsigned long first, last; + char *ep; + + last = first = strtoul(s, &ep, 10); + if (ep == s) /* Parsed nothing */ + return -EINVAL; + if (*ep == '-') { /* we have a last value too */ + const char *lasts = ep + 1; + last = strtoul(lasts, &ep, 10); + if (ep == lasts) /* Parsed nothing */ + return -EINVAL; + } + + if ((last < first) || (last >= NUM_PORTS)) + return -ERANGE; + + range->first = first; + range->last = last; + *endptr = ep; + + return 0; +} + +/** + * parse_keyword() - Parse a literal keyword + * @s: String to parse + * @endptr: Update to the character after the keyword + * @kw: Keyword to accept + * + * Return: 0, if @s starts with @kw, -EINVAL if it does not + */ +static int parse_keyword(const char *s, const char **endptr, const char *kw) +{ + size_t len = strlen(kw); + + if (strlen(s) < len) + return -EINVAL; + + if (memcmp(s, kw, len)) + return -EINVAL; + + *endptr = s + len; + return 0; +} + +/** + * fwd_rule_range_except() - Set up forwarding for a range of ports minus a + * bitmap of exclusions + * @fwd: Forwarding table to be updated + * @proto: Protocol to forward + * @addr: Listening address + * @ifname: Listening interface + * @first: First port to forward + * @last: Last port to forward + * @exclude: Bitmap of ports to exclude (may be NULL) + * @to: Port to translate @first to when forwarding + * @flags: Flags for forwarding entries + */ +static void fwd_rule_range_except(struct fwd_table *fwd, uint8_t proto, + const union inany_addr *addr, + const char *ifname, + uint16_t first, uint16_t last, + const uint8_t *exclude, uint16_t to, + uint8_t flags) +{ + struct fwd_rule rule = { + .addr = addr ? *addr : inany_any6, + .ifname = { 0 }, + .proto = proto, + .flags = flags, + }; + char rulestr[FWD_RULE_STRLEN]; + unsigned delta = to - first; + unsigned base, i; + + if (!addr) + rule.flags |= FWD_DUAL_STACK_ANY; + if (ifname) { + int ret; + + ret = snprintf(rule.ifname, sizeof(rule.ifname), + "%s", ifname); + if (ret <= 0 || (size_t)ret >= sizeof(rule.ifname)) + die("Invalid interface name: %s", ifname); + } + + assert(first != 0); + + for (base = first; base <= last; base++) { + if (exclude && bitmap_isset(exclude, base)) + continue; + + for (i = base; i <= last; i++) { + if (exclude && bitmap_isset(exclude, i)) + break; + } + + rule.first = base; + rule.last = i - 1; + rule.to = base + delta; + + fwd_rule_conflict_check(&rule, fwd->rules, fwd->count); + if (fwd_rule_add(fwd, &rule) < 0) + goto fail; + + base = i - 1; + } + return; + +fail: + die("Unable to add rule %s", + fwd_rule_fmt(&rule, rulestr, sizeof(rulestr))); +} + +/* + * for_each_chunk - Step through delimited chunks of a string + * @p_: Pointer to start of each chunk (updated) + * @ep_: Pointer to end of each chunk (updated) + * @s_: String to step through + * @sep_: String of all allowed delimiters + */ +#define for_each_chunk(p_, ep_, s_, sep_) \ + for ((p_) = (s_); \ + (ep_) = (p_) + strcspn((p_), (sep_)), *(p_); \ + (p_) = *(ep_) ? (ep_) + 1 : (ep_)) + +/** + * fwd_rule_parse_ports() - Parse port range(s) specifier + * @fwd: Forwarding table to be updated + * @proto: Protocol to forward + * @addr: Listening address for forwarding + * @ifname: Interface name for listening + * @spec: Port range(s) specifier + */ +static void fwd_rule_parse_ports(struct fwd_table *fwd, uint8_t proto, + const union inany_addr *addr, + const char *ifname, + const char *spec) +{ + uint8_t exclude[PORT_BITMAP_SIZE] = { 0 }; + bool exclude_only = true; + const char *p, *ep; + uint8_t flags = 0; + unsigned i; + + if (!strcmp(spec, "all")) { + /* Treat "all" as equivalent to "": all non-ephemeral ports */ + spec = ""; + } + + /* Parse excluded ranges and "auto" in the first pass */ + for_each_chunk(p, ep, spec, ",") { + struct port_range xrange; + + if (isdigit(*p)) { + /* Include range, parse later */ + exclude_only = false; + continue; + } + + if (parse_keyword(p, &p, "auto") == 0) { + if (p != ep) /* Garbage after the keyword */ + goto bad; + + if (!(fwd->caps & FWD_CAP_SCAN)) { + die( +"'auto' port forwarding is only allowed for pasta"); + } + + flags |= FWD_SCAN; + continue; + } + + /* Should be an exclude range */ + if (*p != '~') + goto bad; + p++; + + if (parse_port_range(p, &p, &xrange)) + goto bad; + if (p != ep) /* Garbage after the range */ + goto bad; + + for (i = xrange.first; i <= xrange.last; i++) + bitmap_set(exclude, i); + } + + if (exclude_only) { + /* Exclude ephemeral ports */ + fwd_port_map_ephemeral(exclude); + + fwd_rule_range_except(fwd, proto, addr, ifname, + 1, NUM_PORTS - 1, exclude, + 1, flags | FWD_WEAK); + return; + } + + /* Now process base ranges, skipping exclusions */ + for_each_chunk(p, ep, spec, ",") { + struct port_range orig_range, mapped_range; + + if (!isdigit(*p)) + /* Already parsed */ + continue; + + if (parse_port_range(p, &p, &orig_range)) + goto bad; + + if (*p == ':') { /* There's a range to map to as well */ + if (parse_port_range(p + 1, &p, &mapped_range)) + goto bad; + if ((mapped_range.last - mapped_range.first) != + (orig_range.last - orig_range.first)) + goto bad; + } else { + mapped_range = orig_range; + } + + if (p != ep) /* Garbage after the ranges */ + goto bad; + + fwd_rule_range_except(fwd, proto, addr, ifname, + orig_range.first, orig_range.last, + exclude, + mapped_range.first, flags); + } + + return; +bad: + die("Invalid port specifier '%s'", spec); +} + +/** + * fwd_rule_parse() - Parse port configuration option + * @optname: Short option name, t, T, u, or U + * @optarg: Option argument (port specification) + * @fwd: Forwarding table to be updated + */ +void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd) +{ + union inany_addr addr_buf = inany_any6, *addr = &addr_buf; + char buf[BUFSIZ], *spec, *ifname = NULL; + uint8_t proto; + + if (optname == 't' || optname == 'T') + proto = IPPROTO_TCP; + else if (optname == 'u' || optname == 'U') + proto = IPPROTO_UDP; + else + assert(0); + + if (!strcmp(optarg, "none")) { + unsigned i; + + for (i = 0; i < fwd->count; i++) { + if (fwd->rules[i].proto == proto) { + die("-%c none conflicts with previous options", + optname); + } + } + return; + } + + strncpy(buf, optarg, sizeof(buf) - 1); + + if ((spec = strchr(buf, '/'))) { + *spec = 0; + spec++; + + if (optname != 't' && optname != 'u') + die("Listening address not allowed for -%c %s", + optname, optarg); + + if ((ifname = strchr(buf, '%'))) { + *ifname = 0; + ifname++; + + /* spec is already advanced one past the '/', + * so the length of the given ifname is: + * (spec - ifname - 1) + */ + if (spec - ifname - 1 >= IFNAMSIZ) { + die("Interface name '%s' is too long (max %u)", + ifname, IFNAMSIZ - 1); + } + } + + if (ifname == buf + 1) { /* Interface without address */ + addr = NULL; + } else { + char *p = buf; + + /* Allow square brackets for IPv4 too for convenience */ + if (*p == '[' && p[strlen(p) - 1] == ']') { + p[strlen(p) - 1] = '\0'; + p++; + } + + if (!inany_pton(p, addr)) + die("Bad forwarding address '%s'", p); + } + } else { + spec = buf; + + addr = NULL; + } + + if (optname == 'T' || optname == 'U') { + assert(!addr && !ifname); + + if (!(fwd->caps & FWD_CAP_IFNAME)) { + warn( +"SO_BINDTODEVICE unavailable, forwarding only 127.0.0.1 and ::1 for '-%c %s'", + optname, optarg); + + if (fwd->caps & FWD_CAP_IPV4) { + fwd_rule_parse_ports(fwd, proto, + &inany_loopback4, NULL, + spec); + } + if (fwd->caps & FWD_CAP_IPV6) { + fwd_rule_parse_ports(fwd, proto, + &inany_loopback6, NULL, + spec); + } + return; + } + + ifname = "lo"; + } + + if (ifname && !(fwd->caps & FWD_CAP_IFNAME)) { + die( +"Device binding for '-%c %s' unsupported (requires kernel 5.7+)", + optname, optarg); + } + + fwd_rule_parse_ports(fwd, proto, addr, ifname, spec); +} diff --git a/fwd_rule.h b/fwd_rule.h index 5c7b67aa..f0f4efda 100644 --- a/fwd_rule.h +++ b/fwd_rule.h @@ -19,6 +19,7 @@ /* Number of ports for both TCP and UDP */ #define NUM_PORTS (1U << 16) +#define PORT_BITMAP_SIZE DIV_ROUND_UP(NUM_PORTS, 8) /* Forwarding capability bits */ #define FWD_CAP_IPV4 BIT(0) @@ -54,8 +55,38 @@ struct fwd_rule { uint8_t flags; }; +#define FWD_RULE_BITS 8 +#define MAX_FWD_RULES MAX_FROM_BITS(FWD_RULE_BITS) + +/* Maximum number of listening sockets (per pif) + * + * Rationale: This lets us listen on every port for two addresses and two + * protocols (which we need for -T auto -U auto without SO_BINDTODEVICE), plus a + * comfortable number of extras. + */ +#define MAX_LISTEN_SOCKS (NUM_PORTS * 5) + +/** + * struct fwd_table - Forwarding state (per initiating pif) + * @caps: Forwarding capabilities for this initiating pif + * @count: Number of forwarding rules + * @rules: Array of forwarding rules + * @rulesocks: Parallel array of @rules (@count valid entries) of pointers to + * @socks entries giving the start of the corresponding rule's + * sockets within the larger array + * @sock_count: Number of entries used in @socks (for all rules combined) + * @socks: Listening sockets for forwarding + */ +struct fwd_table { + uint32_t caps; + unsigned count; + struct fwd_rule rules[MAX_FWD_RULES]; + int *rulesocks[MAX_FWD_RULES]; + unsigned sock_count; + int socks[MAX_LISTEN_SOCKS]; +}; + void fwd_probe_ephemeral(void); -void fwd_port_map_ephemeral(uint8_t *map); #define FWD_RULE_STRLEN \ (IPPROTO_STRLEN - 1 \ @@ -67,7 +98,6 @@ void fwd_port_map_ephemeral(uint8_t *map); const union inany_addr *fwd_rule_addr(const struct fwd_rule *rule); const char *fwd_rule_fmt(const struct fwd_rule *rule, char *dst, size_t size); void fwd_rules_info(const struct fwd_rule *rules, size_t count); -void fwd_rule_conflict_check(const struct fwd_rule *new, - const struct fwd_rule *rules, size_t count); +void fwd_rule_parse(char optname, const char *optarg, struct fwd_table *fwd); #endif /* FWD_RULE_H */ -- 2.53.0