// SPDX-License-Identifier: GPL-2.0-or-later /* PASST - Plug A Simple Socket Transport * for qemu/UNIX domain socket mode * * PASTA - Pack A Subtle Tap Abstraction * for network namespace/tap device mode * * PESTO - Programmable Extensible Socket Translation Orchestrator * front-end for passt(1) and pasta(1) forwarding configuration * * ports.c - Parse port options * * Copyright (c) 2026 Red Hat GmbH * Author: Stefano Brivio * Author: David Gibson */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common.h" #include "util.h" #include "ip.h" #include "passt.h" #include "common.h" #include "pesto.h" #include "ports.h" /** * next_chunk() - Return the next piece of a string delimited by a character * @s: String to search * @c: Delimiter character * * Return: if another @c is found in @s, returns a pointer to the * character *after* the delimiter, if no further @c is in @s, * return NULL */ static char *next_chunk(const char *s, char c) { char *sep = strchr(s, c); return sep ? sep + 1 : NULL; } /** * 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, char **endptr, struct port_range *range) { unsigned long first, last; last = first = strtoul(s, endptr, 10); if (*endptr == s) /* Parsed nothing */ return -EINVAL; if (**endptr == '-') { /* we have a last value too */ const char *lasts = *endptr + 1; last = strtoul(lasts, endptr, 10); if (*endptr == lasts) /* Parsed nothing */ return -EINVAL; } if ((last < first) || (last >= NUM_PORTS)) return -ERANGE; range->first = first; range->last = last; return 0; } /** * conf_ports_range_except() - Set up forwarding for a range of ports minus a * bitmap of exclusions * @c: Execution context * @optname: Short option name, t, T, u, or U * @optarg: Option argument (port specification) * @fwd: Forwarding table to be updated * @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 */ void conf_ports_range_except(const struct ctx *c, char optname, const char *optarg, struct fwd_table *fwd, const union inany_addr *addr, const char *ifname, uint16_t first, uint16_t last, const uint8_t *exclude, uint16_t to, uint8_t flags) { unsigned delta = to - first; unsigned base, i; uint8_t proto; if (first == 0) { die("Can't forward port 0 for option '-%c %s'", optname, optarg); } if (optname == 't' || optname == 'T') proto = IPPROTO_TCP; else if (optname == 'u' || optname == 'U') proto = IPPROTO_UDP; else assert(0); if (addr) { if (!c->ifi4 && inany_v4(addr)) { die("IPv4 is disabled, can't use -%c %s", optname, optarg); } else if (!c->ifi6 && !inany_v4(addr)) { die("IPv6 is disabled, can't use -%c %s", optname, optarg); } } 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; } if ((optname == 'T' || optname == 'U') && c->no_bindtodevice) { /* FIXME: Once the fwd bitmaps are removed, move this * workaround to the caller */ assert(!addr && ifname && !strcmp(ifname, "lo")); warn( "SO_BINDTODEVICE unavailable, forwarding only 127.0.0.1 and ::1 for '-%c %s'", optname, optarg); if (c->ifi4) { fwd_rule_add(fwd, proto, flags, &inany_loopback4, NULL, base, i - 1, base + delta); } if (c->ifi6) { fwd_rule_add(fwd, proto, flags, &inany_loopback6, NULL, base, i - 1, base + delta); } } else { fwd_rule_add(fwd, proto, flags, addr, ifname, base, i - 1, base + delta); } base = i - 1; } } /** * conf_ports() - Parse port configuration options, initialise UDP/TCP sockets * @c: Execution context * @optname: Short option name, t, T, u, or U * @optarg: Option argument (port specification) * @fwd: Forwarding table to be updated * @mode: Overall port forwarding mode (updated) */ void conf_ports(const struct ctx *c, char optname, const char *optarg, struct fwd_table *fwd, enum fwd_mode *mode) { union inany_addr addr_buf = inany_any6, *addr = &addr_buf; char buf[BUFSIZ], *spec, *ifname = NULL, *p; uint8_t exclude[PORT_BITMAP_SIZE] = { 0 }; bool exclude_only = true; unsigned i; if (!strcmp(optarg, "none")) { if (*mode) goto mode_conflict; *mode = FWD_MODE_NONE; return; } if ((optname == 't' || optname == 'T') && c->no_tcp) die("TCP port forwarding requested but TCP is disabled"); if ((optname == 'u' || optname == 'U') && c->no_udp) die("UDP port forwarding requested but UDP is disabled"); if (!strcmp(optarg, "auto")) { if (*mode) goto mode_conflict; if (c->mode != MODE_PASTA) die("'auto' port forwarding is only allowed for pasta"); if ((optname == 'T' || optname == 'U') && c->no_bindtodevice) { warn( "'-%c auto' enabled without unprivileged SO_BINDTODEVICE", optname); warn( "Forwarding from addresses other than 127.0.0.1 will not work"); } *mode = FWD_MODE_AUTO; return; } if (!strcmp(optarg, "all")) { if (*mode) goto mode_conflict; if (c->mode == MODE_PASTA) die("'all' port forwarding is only allowed for passt"); *mode = FWD_MODE_ALL; /* Exclude ephemeral ports */ for (i = 0; i < NUM_PORTS; i++) if (fwd_port_is_ephemeral(i)) bitmap_set(exclude, i); conf_ports_range_except(c, optname, optarg, fwd, NULL, NULL, 1, NUM_PORTS - 1, exclude, 1, FWD_WEAK); return; } if (*mode > FWD_MODE_SPEC) die("Specific ports cannot be specified together with all/none/auto"); *mode = FWD_MODE_SPEC; strncpy(buf, optarg, sizeof(buf) - 1); if ((spec = strchr(buf, '/'))) { *spec = 0; spec++; if (optname != 't' && optname != 'u') goto bad; 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) goto bad; } if (ifname == buf + 1) { /* Interface without address */ addr = NULL; } else { 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)) goto bad; } } else { spec = buf; addr = NULL; } /* Mark all exclusions first, they might be given after base ranges */ p = spec; do { struct port_range xrange; if (*p != '~') { /* Not an exclude range, parse later */ exclude_only = false; continue; } p++; if (parse_port_range(p, &p, &xrange)) goto bad; if ((*p != '\0') && (*p != ',')) /* Garbage after the range */ goto bad; for (i = xrange.first; i <= xrange.last; i++) { if (bitmap_isset(exclude, i)) die("Overlapping excluded ranges %s", optarg); bitmap_set(exclude, i); } } while ((p = next_chunk(p, ','))); if (ifname && c->no_bindtodevice) { die( "Device binding for '-%c %s' unsupported (requires kernel 5.7+)", optname, optarg); } /* Outbound forwards come from guest loopback */ if ((optname == 'T' || optname == 'U') && !ifname) ifname = "lo"; if (exclude_only) { /* Exclude ephemeral ports */ for (i = 0; i < NUM_PORTS; i++) if (fwd_port_is_ephemeral(i)) bitmap_set(exclude, i); conf_ports_range_except(c, optname, optarg, fwd, addr, ifname, 1, NUM_PORTS - 1, exclude, 1, FWD_WEAK); return; } /* Now process base ranges, skipping exclusions */ p = spec; do { struct port_range orig_range, mapped_range; if (*p == '~') /* Exclude range, 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 != '\0') && (*p != ',')) /* Garbage after the ranges */ goto bad; conf_ports_range_except(c, optname, optarg, fwd, addr, ifname, orig_range.first, orig_range.last, exclude, mapped_range.first, 0); } while ((p = next_chunk(p, ','))); return; bad: die("Invalid port specifier %s", optarg); mode_conflict: die("Port forwarding mode '%s' conflicts with previous mode", optarg); }