aboutgitcodebugslistschat
path: root/fwd_rule.c
diff options
context:
space:
mode:
Diffstat (limited to 'fwd_rule.c')
-rw-r--r--fwd_rule.c464
1 files changed, 461 insertions, 3 deletions
diff --git a/fwd_rule.c b/fwd_rule.c
index 9d48982..cd3dec0 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);
+}