This series makes a number of significant reworks to how we process forwarding options (-t, -u, -T and -U) in passt & pasta. This is largely motivated by moving towards being able to share this code with a configuration update tool. However, along the way it also enables some forwarding configurations that were technically possible with the forwarding table but couldn't be specified on the command line, in particular bug 180. There is still a bunch of work needed to make the parsing code truly shareable with pesto, but this is a solid start. v2: * Assorted minor changes based on Stefano's review, including * Explicitly state "guest or namespace" in the manpage * Clearer description of @rulesocks field * Worked around a gcc < 15 bug causing a false positive warning * Update man page and usage() for new capabilities * Additional patches moving rule parsing out of conf.c David Gibson (23): conf: Split parsing of port specifiers from the rest of -[tuTU] parsing conf: Simplify handling of default forwarding mode conf: Move first pass handling of -[TU] next to handling of -[tu] doc: Consolidate -[tu] option descriptions for passt and pasta conf: Permit -[tTuU] all in pasta mode fwd: Better split forwarding rule specification from associated sockets fwd_rule: Move forwarding rule formatting conf: Pass protocol explicitly to conf_ports_range_except() fwd: Split rule building from rule adding fwd_rule: Move rule conflict checking from fwd_rule_add() to caller fwd: Improve error handling in fwd_rule_add() conf: Don't be strict about exclusivity of forwarding mode conf: Rework stepping through chunks of port specifiers conf: Rework checking for garbage after a range doc: Rework man page description of port specifiers conf: Move "all" handling to port specifier conf: Allow user-specified auto-scanned port forwarding ranges conf: Move SO_BINDTODEVICE workaround to conf_ports() conf: Don't pass raw commandline argument to conf_ports_spec() fwd, conf: Add capabilities bits to each forwarding table conf, fwd: Stricter rule checking in fwd_rule_add() fwd_rule: Move ephemeral port probing to fwd_rule.c fwd, conf: Move rule parsing code to fwd_rule.[ch] Makefile | 10 +- conf.c | 524 ++++++----------------------------------- fwd.c | 261 +++------------------ fwd.h | 46 ---- fwd_rule.c | 676 +++++++++++++++++++++++++++++++++++++++++++++++++++++ fwd_rule.h | 59 +++++ passt.1 | 288 ++++++++++------------- 7 files changed, 973 insertions(+), 891 deletions(-) create mode 100644 fwd_rule.c -- 2.53.0
conf_ports() is extremely long, but we want to refactor it so that parts
can be shared with the upcoming configuration client. Make a small start
by separating out the section that parses just the port specification
(not including address and/or interface).
This also allows us to constify a few extra things, and while we're there
replace a few vague error messages with more specific ones.
Signed-off-by: David Gibson
The forwarding options -[tTuU] can't be fully handled in our first pass
over the command line options, they need to wait until the second, once
we know more about the interface configuration. However, we do need stub
handling, so they don't cause an error. For historical reasons the
-[TU] options are handled a fair way apart from the -[tu] options. Move
them next to each other for clarity.
Signed-off-by: David Gibson
For passt, the default forwarding mode is "none", which falls out naturally
from the other handling: if we don't get any options, we get empty
forwarding tables, which corresponds to "none" behaviour. However, for
pasta the default is "auto". This is handled a bit oddly: in conf_ports()
we set the mode variable, but don't set up the rules we need for "auto"
mode. Instead we want until nearly the end of conf() and if the mode is
FWD_MODE_AUTO or unset, we make conf_ports_range_except() calls to set up
the "auto" rules.
Simplify this a bit, by creating the rules within conf_ports() itself when
we parse -[tuTU] auto. For the case of no forwarding options we call
into conf_ports() itself with synthetic arguments. As well as making the
code a little shorter, this makes it more obvious that giving no arguments
really is equivalent to -[tuTU] auto.
Signed-off-by: David Gibson
The man page currently has two fairly large, near-identical sections
separately describing the -t and -u options for passt and pasta. This is
bulky and potentially confusing. It will make this information more
tedious to update as we alter what's possible here with the forwarding
table. Consolidate both descriptions to a single one in the common
options, noting the few passt/pasta difference inline.
There's similar duplication usage(), consolidate that as well.
Signed-off-by: David Gibson
6dad076df037 ("fwd: Split forwarding rule specification from its
implementation state") created struct fwd_rule_state with a forwarding rule
plus the table of sockets used for its implementation. It turns out this
is quite awkward for sharing rule parsing code between passt and the
upcoming configuration client.
Instead keep the index of listening sockets in a parallel array in
struct fwd_table.
Signed-off-by: David Gibson
fwd_rule_add() sanity checks the given rule, however all errors are fatal:
either they're assert()s in the case of things that callers should have
already verified, or die()s if we run out of space for the new rule.
This won't suffice any more when we allow rule updates from a
configuration client. We don't want to trust the input we get from
the client any more than we have to.
Replace the assert()s and die()s with a return value. Also include warn()s
so that the user gets a more specific idea of the problem in the logs or
stderr.
Signed-off-by: David Gibson
In order to be shared with the upcoming configuration client, we want to
split code which deals with forwarding rules as standalone objects from
those which deal with forwarding rules as they're actually used to
implement forwarding in passt/pasta.
Create fwd_rule.c to contain code from the first category, and start off
by moving code to format rules into text for human display into it. While
we're at it, we rework that formatting code a little to make it more
flexible.
Signed-off-by: David Gibson
Amongst other checks, fwd_rule_add() checks that the newly added rule
doesn't conflict with any existing rules. However, unlike the other things
we verify, this isn't really required for safe operation. Rule conflicts
are a useful thing for the user to know about, but the forwarding logic
is perfectly sound with conflicting rules (the first one will win).
In order to support dynamic rule updates, we want fwd_rule_add() to become
a more low-level function, only checking the things it really needs to.
So, move rule conflict checking to its caller via new helpers in
fwd_rule.c.
Signed-off-by: David Gibson
Currently conf_ports_range_except() deduces the protocol to use from the
option name. This is correct, but a DRY violation, since we check the
option name at several points in the callchain.
Instead pass the protocol explicitly to conf_ports_range_except() and
conf_ports_spec(), computing it from the option name in conf_ports(). This
is redundant for now, but means the optname and optarg parameters to the
lower-level functions are used only for debugging output and will be
removable in future.
Signed-off-by: David Gibson
Currently as well as building the forwarding tables, conf() maintains a
"forwarding mode" value for each protocol and direction. This prevents,
for example "-t all" and "-t 40000" being given on the same command line.
This restriction predates the forwarding table and is no longer really
necessary. Remove the restriction, instead doing our best to apply all the
given options simultaneously.
* Many combinations previously disallowed will still be disallowed because
of conflicts between the specific generated rules, e.g.
-t all -t 8888
(because -t all already listens on port 8888)
* Some new combinations are now allowed and will work, e.g.
-t all -t 40000
because 'all' excludes ephemeral ports (which includes 40000 on default
Linux configurations).
* We remove our mode variables, but keep boolean variables to track if
any forwarding config option has been given. This is needed in order to
correctly default to -t auto -T auto -u auto -U auto for pasta.
* -[tTuU] none after any other rules is still considered an error.
However -t none *before* other rules is allowed. This is potentially
confusing, but is awkward to avoid for the time being.
Signed-off-by: David Gibson
After parsing port ranges conf_ports_spec() checks if we've reached a
chunk delimiter (',') to verify that there isn't extra garbage there.
Rework how we do this to use the recently introduced chunk-end
pointer. This has two advantages:
1) Small, but practical: we don't need to repeat what the valid delimiters
are, that's already handled in the chunk splitting code.
2) Large, if theoretical: this will also give an error if port parsing
overruns a chunk boundary. We don't really expect that to happen,
but it would be very confusing if it did. strtoul(3), on which
parse_port_range() is based does say it may accept thousands
separators based on locale which means we can't be sure it will
only accept strings of digits.
Signed-off-by: David Gibson
Currently we explicitly forbid -[tTuU] all in pasta mode. While these are
primarily useful for passt, there's no particular reason they can't be
used in pasta mode as well. Indeed you can do the same thing in pasta
by using "-t ~32768-60999" (assuming default Linux configuration of
ephemeral ports). For consistency, permit "all" for pasta as well.
Signed-off-by: David Gibson
Currently fwd_rule_add() both builds the struct fwd_rule and inserts it
into the table. This will be inconvenient when we want to dynamically add
rules from a configuration client. Alter fwd_rule_add() to take a
pre-constructed struct fwd_rule, which we build in the caller.
Signed-off-by: David Gibson
Port specifier strings are made up of ',' separated chunks. Rework the
logic we use to step through the chunks.
Specifically, maintain a pointer to the end of each chunk as well as the
start. This is not really used yet, but will be useful in future.
This also has side effect on semantics. Previously an empty specifier (0
chunks) was not accepted. Now it is, and will be treated as an "exclude
only" spec which excludes only ephemeral ports. This seems a bit odd, and
I don't expect it to be (directly) used in practice. However, it falls
naturally out of the existing semantics, and will combine well with some
upcoming changes.
Signed-off-by: David Gibson
Currently the man page describes the internal syntax of port specifiers
in prose, which isn't particularly easy to follow. Rework it to use
more syntax "diagrams" to show how it works. This will also allow us to
more easily update the manual page for some coming changes in syntax.
usage() output is updated similarly, though more briefly.
Signed-off-by: David Gibson
The forwarding table now allows for arbitrary port ranges to be marked as
FWD_SCAN, meaning we don't open sockets for every port, but only those we
scan as listening on the target side. However, there's currently no way
to create such rules, except -[tTuU] auto which always scans every port
with an unspecified listening address and interface.
Allow user-specified "auto" ranges by moving the parsing of the "auto"
keyword from conf_ports(), to conf_ports_spec() as part of the port
specified. "auto" can be combined freely with other port ranges, e.g.
-t 127.0.0.1/auto
-u %lo/5000-7000,auto
-T auto,12345
-U auto,~1-9000
Note that any address and interface given only affects where the automatic
forwards listen, not what addresses we consider when scanning. That is,
if the target side is listening on *any* address, we will create a forward
on the specified address.
Link: https://bugs.passt.top/show_bug.cgi?id=180
Signed-off-by: David Gibson
Currently -[tTuU] all is handled separately in conf_ports() before calling
conf_ports_spec(). Earlier changes mean we can now move this handling to
conf_ports_spec(). This makes the code slightly simpler, but more
importantly it allows some useful combinations we couldn't previously do,
such as
-t 127.0.0.1/all
or
-u %eth2/all
Signed-off-by: David Gibson
For historical reasons we apply our workaround for -[TU] handling when
SO_BINDTODEVICE is unavailable inside conf_ports_range_except(). We've
now removed the reasons it had to be there, so it can move to conf_ports(),
the caller's caller.
Signed-off-by: David Gibson
We only use the optname and optarg parameters for printing error messages,
and they're not even particularly necessary there. Remove them.
Signed-off-by: David Gibson
conf_ports_spec() and conf_ports() take the global context structure, but
their only use for it is seeing if various things are possible: which
protocols and address formats are allowed in formatting rules. Localise
that information into the forwarding table, with a capabilities bitmap.
For now we set that caps map to the same thing for all tables, but keep it
per-table to allow for the possibility of different pif types in future
that might have different capabilities (e.g. if we add a forwarding table
for the tap interface, it won't be able to accept interface names to bind).
Use this information to remove the global context parameter from
conf_ports() and conf_ports_spec().
Signed-off-by: David Gibson
Although fwd_rule_add() performs some sanity checks on the rule it is
given, there are invalid rules we don't check for, assuming that its
callers will do that.
That won't be enough when we can get rules inserted by a dynamic update
client without going through the existing parsing code. So, add stricter
checks to fwd_rule_add(), which is now possible thanks to the capabilities
bits in the struct fwd_table. Where those duplicate existing checks in the
callers, remove the old copies.
Signed-off-by: David Gibson
We want to move parsing of forward rule options to fwd_rule.c so it can
eventually be shared with a configuration client. As a preliminary step,
move the ephemeral port probing there, which that will need to use.
Signed-off-by: David Gibson
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
On Fri, 10 Apr 2026 11:02:56 +1000
David Gibson
Amongst other checks, fwd_rule_add() checks that the newly added rule doesn't conflict with any existing rules. However, unlike the other things we verify, this isn't really required for safe operation. Rule conflicts are a useful thing for the user to know about, but the forwarding logic is perfectly sound with conflicting rules (the first one will win).
In order to support dynamic rule updates, we want fwd_rule_add() to become a more low-level function, only checking the things it really needs to. So, move rule conflict checking to its caller via new helpers in fwd_rule.c.
Signed-off-by: David Gibson
--- conf.c | 5 +++++ fwd.c | 26 +------------------------- fwd_rule.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ fwd_rule.h | 2 ++ 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/conf.c b/conf.c index 027bbac9..b871646f 100644 --- a/conf.c +++ b/conf.c @@ -205,13 +205,18 @@ static void conf_ports_range_except(const struct ctx *c, char optname,
if (c->ifi4) { rulev.addr = inany_loopback4; + fwd_rule_conflict_check(&rulev, + fwd->rules, fwd->count); fwd_rule_add(fwd, &rulev); } if (c->ifi6) { rulev.addr = inany_loopback6; + fwd_rule_conflict_check(&rulev, + fwd->rules, fwd->count); fwd_rule_add(fwd, &rulev); } } else { + fwd_rule_conflict_check(&rule, fwd->rules, fwd->count); fwd_rule_add(fwd, &rule); } base = i - 1; diff --git a/fwd.c b/fwd.c index c05107d1..c9637525 100644 --- a/fwd.c +++ b/fwd.c @@ -341,7 +341,7 @@ void 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 i, port; + unsigned port;
assert(!(new->flags & ~allowed_flags)); /* Passing a non-wildcard address with DUAL_STACK_ANY is a bug */ @@ -354,30 +354,6 @@ void fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new) if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) die("Too many listening sockets");
- /* Check for any conflicting entries */ - for (i = 0; i < fwd->count; i++) { - char newstr[INANY_ADDRSTRLEN], rulestr[INANY_ADDRSTRLEN]; - const struct fwd_rule *rule = &fwd->rules[i]; - - if (new->proto != rule->proto) - /* Non-conflicting protocols */ - continue; - - if (!inany_matches(fwd_rule_addr(new), fwd_rule_addr(rule))) - /* Non-conflicting addresses */ - continue; - - if (new->last < rule->first || rule->last < new->first) - /* Port ranges don't overlap */ - continue; - - die("Forwarding configuration conflict: %s/%u-%u versus %s/%u-%u", - inany_ntop(fwd_rule_addr(new), newstr, sizeof(newstr)), - new->first, new->last, - inany_ntop(fwd_rule_addr(rule), rulestr, sizeof(rulestr)), - rule->first, rule->last); - } - 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; diff --git a/fwd_rule.c b/fwd_rule.c index a034d5d1..5bc94efe 100644 --- a/fwd_rule.c +++ b/fwd_rule.c @@ -93,3 +93,48 @@ void fwd_rules_info(const struct fwd_rule *rules, size_t count) info(" %s", fwd_rule_fmt(&rules[i], buf, sizeof(buf))); } } + +/** + * fwd_rule_conflicts() - Test if two rules conflict with each other + * @a, @b: Rules to test + */ +static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *b) +{ + if (a->proto != b->proto) + /* Non-conflicting protocols */ + return false; + + if (!inany_matches(fwd_rule_addr(a), fwd_rule_addr(b))) + /* Non-conflicting addresses */ + return false; + + assert(a->first <= a->last && b->first <= b->last);
I expected this assert() to be gone by the end of the series, like the ones dropped in 11/23, but it's still there. Is this something that the client can't specifically trigger for some reason? -- Stefano
On Fri, 10 Apr 2026 11:02:57 +1000
David Gibson
fwd_rule_add() sanity checks the given rule, however all errors are fatal: either they're assert()s in the case of things that callers should have already verified, or die()s if we run out of space for the new rule.
This won't suffice any more when we allow rule updates from a configuration client. We don't want to trust the input we get from the client any more than we have to.
Replace the assert()s and die()s with a return value. Also include warn()s so that the user gets a more specific idea of the problem in the logs or stderr.
Signed-off-by: David Gibson
--- conf.c | 15 ++++++++++++--- fwd.c | 41 +++++++++++++++++++++++++++++++---------- fwd.h | 2 +- fwd_rule.c | 3 +-- fwd_rule.h | 1 + 5 files changed, 46 insertions(+), 16 deletions(-) diff --git a/conf.c b/conf.c index b871646f..5c913820 100644 --- a/conf.c +++ b/conf.c @@ -157,6 +157,7 @@ static void conf_ports_range_except(const struct ctx *c, char optname, .proto = proto, .flags = flags, }; + char rulestr[FWD_RULE_STRLEN]; unsigned delta = to - first; unsigned base, i;
@@ -207,20 +208,28 @@ static void conf_ports_range_except(const struct ctx *c, char optname, rulev.addr = inany_loopback4; fwd_rule_conflict_check(&rulev, fwd->rules, fwd->count); - fwd_rule_add(fwd, &rulev); + if (fwd_rule_add(fwd, &rulev) < 0) + goto fail; } if (c->ifi6) { rulev.addr = inany_loopback6; fwd_rule_conflict_check(&rulev, fwd->rules, fwd->count); - fwd_rule_add(fwd, &rulev); + if (fwd_rule_add(fwd, &rulev) < 0) + goto fail; } } else { fwd_rule_conflict_check(&rule, fwd->rules, fwd->count); - fwd_rule_add(fwd, &rule); + 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)));
Similar to my comment to 10/23: I expected this die() to be gone by the end of the series, but it's still there. Is it just a placeholder die() that will go once pesto(1) is added? -- Stefano
On Fri, 10 Apr 2026 11:03:01 +1000
David Gibson
Currently the man page describes the internal syntax of port specifiers in prose, which isn't particularly easy to follow. Rework it to use more syntax "diagrams" to show how it works. This will also allow us to more easily update the manual page for some coming changes in syntax.
usage() output is updated similarly, though more briefly.
Signed-off-by: David Gibson
--- conf.c | 10 +++++----- passt.1 | 32 ++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/conf.c b/conf.c index c3655824..5d6517c3 100644 --- a/conf.c +++ b/conf.c @@ -1041,11 +1041,11 @@ static void usage(const char *name, FILE *f, int status) " 'none': don't forward any ports\n" " 'all': forward all unbound, non-ephemeral ports\n" "%s" - " a comma-separated list, optionally ranged with '-'\n" - " and optional target ports after ':', with optional\n" - " address specification suffixed by '/' and optional\n" - " interface prefixed by '%%'. Ranges can be reduced by\n" - " excluding ports or ranges prefixed by '~'\n" + " [ADDR[%%IFACE]/]PORTS: forward specific ports\n" + " PORTS is a comma-separated list of ports, optionally\n" + " ranged with '-' and optional target ports after ':'.\n" + " Ranges can be reduced by excluding ports or ranges\n" + " prefixed by '~'\n" " Examples:\n" " -t 22 Forward local port 22 to 22 on %s\n" " -t 22:23 Forward local port 22 to 23 on %s\n" diff --git a/passt.1 b/passt.1 index 7da4fe5f..d329f8f0 100644 --- a/passt.1 +++ b/passt.1 @@ -447,16 +447,28 @@ periodically derived (every second) from listening sockets reported by \fI/proc/net/tcp\fR and \fI/proc/net/tcp6\fR, see \fBproc\fR(5).
.TP -.BR ports -A comma-separated list of ports, optionally ranged with \fI-\fR, and, -optionally, with target ports after \fI:\fR, if they differ. Specific addresses -can be bound as well, separated by \fI/\fR, and also, since Linux 5.7, limited -to specific interfaces, prefixed by \fI%\fR. Within given ranges, selected ports -and ranges can be excluded by an additional specification prefixed by \fI~\fR. - -Specifying excluded ranges only implies that all other ports are forwarded. In -this case, no failures are reported for unavailable ports, unless no ports could -be forwarded at all. +[\fIaddress\fR[\fB%\fR\fIinterface\fR]\fB/\fR]\fIports\fR ... +Specific ports to forward. Optionally, a specific listening address +and interface name (since Linux 5.7) can be specified. \fIports\fR is +a comma-separated list of entries which may be any of: +.RS +.TP +\fIfirst\fR[\fB-\fR\fIlast\fR][\fB:\fR\fItofirst\fR[\fB-\fR\fItolast\fR]] +Include range. Forward port numbers between \fIfirst\fR and \fIlast\fR +(inclusive) to ports between \fItofirst\fR and \fItolast\fR. If +\fItofirst\fR and \fItolast\fR are omitted, assume the same as +\fIfirst\fR and \fIlast\fR. If \fIlast\fR is omitted, assume the same +as \fIfirst\fR. + +.TP +\fB~\fR\fIfirst\fR[\fB-\fR\fIlast\fR] +Exclude range. Exclude port numbers between \fIfirst\fR and +\fIlast\fR from. This takes precedences over include ranges.
..."from the set of all non-ephemeral ports permitted by current capabilities"? Or simply drop " from", because it should be clear from the paragraph below?
+.RE + +Specifying excluded ranges only implies that all other non-ephemeral +ports are forwarded. In this case, no failures are reported for +unavailable ports, unless no ports could be forwarded at all.
Examples: .RS
-- Stefano
On Fri, 10 Apr 2026 11:03:02 +1000
David Gibson
Currently -[tTuU] all is handled separately in conf_ports() before calling conf_ports_spec(). Earlier changes mean we can now move this handling to conf_ports_spec(). This makes the code slightly simpler, but more importantly it allows some useful combinations we couldn't previously do, such as -t 127.0.0.1/all or -u %eth2/all
Signed-off-by: David Gibson
--- conf.c | 25 ++++++++++--------------- passt.1 | 28 ++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/conf.c b/conf.c index 5d6517c3..f62109b5 100644 --- a/conf.c +++ b/conf.c @@ -251,6 +251,11 @@ static void conf_ports_spec(const struct ctx *c, const char *p, *ep; unsigned i;
+ if (!strcmp(spec, "all")) { + /* Treat "all" as equivalent to "": all non-ephemeral ports */ + spec = ""; + } + /* Mark all exclusions first, they might be given after base ranges */ for_each_chunk(p, ep, spec, ",") { struct port_range xrange; @@ -372,19 +377,6 @@ static void conf_ports(const struct ctx *c, char optname, const char *optarg, return; }
- if (!strcmp(optarg, "all")) { - uint8_t exclude[PORT_BITMAP_SIZE] = { 0 }; - - /* Exclude ephemeral ports */ - fwd_port_map_ephemeral(exclude); - - conf_ports_range_except(c, optname, optarg, fwd, - proto, NULL, NULL, - 1, NUM_PORTS - 1, exclude, - 1, FWD_WEAK); - return; - } - strncpy(buf, optarg, sizeof(buf) - 1);
if ((spec = strchr(buf, '/'))) { @@ -1039,14 +1031,17 @@ static void usage(const char *name, FILE *f, int status) " can be specified multiple times\n" " SPEC can be:\n" " 'none': don't forward any ports\n" - " 'all': forward all unbound, non-ephemeral ports\n" "%s" " [ADDR[%%IFACE]/]PORTS: forward specific ports\n" - " PORTS is a comma-separated list of ports, optionally\n" + " PORTS is either 'all' (forward all unbound, non-ephemeral\n" + " ports), or a comma-separated list of ports, optionally\n" " ranged with '-' and optional target ports after ':'.\n" " Ranges can be reduced by excluding ports or ranges\n" " prefixed by '~'\n" " Examples:\n" + " -t all Forward all ports\n"
Nit: the examples below have a tab as a separator, which makes it slightly easier to ensure we indent them properly.
+ " -t 127.0.0.1/all Forward all ports from local address\n" + " 127.0.0.1\n"
This makes things pretty hard on eyes as it's not consistent with the rest of the "table". Could we perhaps do: " -t ::1/all Forward all ports from ::1\n" ?
" -t 22 Forward local port 22 to 22 on %s\n" " -t 22:23 Forward local port 22 to 23 on %s\n" " -t 22,25 Forward ports 22, 25 to ports 22, 25\n" diff --git a/passt.1 b/passt.1 index d329f8f0..3ba447d5 100644 --- a/passt.1 +++ b/passt.1 @@ -434,12 +434,6 @@ Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of: .BR none Don't forward any ports
-.TP -.BR all -Forward all unbound, non-ephemeral ports, as permitted by current capabilities. -For low (< 1024) ports, see \fBNOTES\fR. No failures are reported for -unavailable ports, unless no ports could be forwarded at all. - .TP .BR auto " " (\fBpasta\fR " " only) Dynamically forward ports bound in the namespace. The list of ports is @@ -449,10 +443,20 @@ periodically derived (every second) from listening sockets reported by .TP [\fIaddress\fR[\fB%\fR\fIinterface\fR]\fB/\fR]\fIports\fR ... Specific ports to forward. Optionally, a specific listening address -and interface name (since Linux 5.7) can be specified. \fIports\fR is -a comma-separated list of entries which may be any of: +and interface name (since Linux 5.7) can be specified. \fIports\fR +may be either: .RS .TP +\fBall\fR +Forward all unbound, non-ephemeral ports, as permitted by current +capabilities. For low (< 1024) ports, see \fBNOTES\fR. No failures +are reported for unavailable ports, unless no ports could be forwarded +at all. +.RE + +.RS +or a comma-separated list of entries which may be any of: +.TP \fIfirst\fR[\fB-\fR\fIlast\fR][\fB:\fR\fItofirst\fR[\fB-\fR\fItolast\fR]] Include range. Forward port numbers between \fIfirst\fR and \fIlast\fR (inclusive) to ports between \fItofirst\fR and \fItolast\fR. If @@ -473,6 +477,14 @@ unavailable ports, unless no ports could be forwarded at all. Examples: .RS .TP +-t all +Forward all unbound, non-ephemeral ports as permitted by current +capabilities to the corresponding port on the guest or namespace +.TP +-t 127.0.0.1/all +For the local address 127.0.0.1, forward all unbound, non-ephemeral +ports as permitted by current capabilities.
Nit: all the other examples have no dot at the end (I tend to think it fits better this type of list, but all I care about is that it's consistent).
+.TP -t 22 Forward local port 22 to port 22 on the guest or namespace .TP
-- Stefano
On Fri, 10 Apr 2026 11:03:03 +1000
David Gibson
The forwarding table now allows for arbitrary port ranges to be marked as FWD_SCAN, meaning we don't open sockets for every port, but only those we scan as listening on the target side. However, there's currently no way to create such rules, except -[tTuU] auto which always scans every port with an unspecified listening address and interface.
Allow user-specified "auto" ranges by moving the parsing of the "auto" keyword from conf_ports(), to conf_ports_spec() as part of the port specified. "auto" can be combined freely with other port ranges, e.g. -t 127.0.0.1/auto -u %lo/5000-7000,auto -T auto,12345 -U auto,~1-9000
Note that any address and interface given only affects where the automatic forwards listen, not what addresses we consider when scanning. That is, if the target side is listening on *any* address, we will create a forward on the specified address.
Link: https://bugs.passt.top/show_bug.cgi?id=180
Signed-off-by: David Gibson
--- conf.c | 85 ++++++++++++++++++++++++++++++++++++++++++--------------- passt.1 | 30 ++++++++++++++------ 2 files changed, 85 insertions(+), 30 deletions(-) diff --git a/conf.c b/conf.c index f62109b5..8e3b4b20 100644 --- a/conf.c +++ b/conf.c @@ -13,6 +13,7 @@ */
#include
+#include #include #include #include @@ -112,6 +113,28 @@ static int parse_port_range(const char *s, const char **endptr, 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 @@ -249,6 +272,7 @@ static void conf_ports_spec(const struct ctx *c, 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")) { @@ -256,15 +280,32 @@ static void conf_ports_spec(const struct ctx *c, spec = ""; }
- /* Mark all exclusions first, they might be given after base ranges */ + /* Parse excluded ranges and "auto" in the first pass */ for_each_chunk(p, ep, spec, ",") { struct port_range xrange;
- if (*p != '~') { - /* Not an exclude range, parse later */ + 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 (c->mode != MODE_PASTA) { + 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)) @@ -283,7 +324,7 @@ static void conf_ports_spec(const struct ctx *c, conf_ports_range_except(c, optname, optarg, fwd, proto, addr, ifname, 1, NUM_PORTS - 1, exclude, - 1, FWD_WEAK); + 1, flags | FWD_WEAK); return; }
@@ -291,8 +332,8 @@ static void conf_ports_spec(const struct ctx *c, for_each_chunk(p, ep, spec, ",") { struct port_range orig_range, mapped_range;
- if (*p == '~') - /* Exclude range, already parsed */ + if (!isdigit(*p)) + /* Already parsed */ continue;
if (parse_port_range(p, &p, &orig_range)) @@ -320,7 +361,7 @@ static void conf_ports_spec(const struct ctx *c, proto, addr, ifname, orig_range.first, orig_range.last, exclude, - mapped_range.first, 0); + mapped_range.first, flags); }
return; @@ -366,17 +407,6 @@ static void conf_ports(const struct ctx *c, char optname, const char *optarg, if (proto == IPPROTO_UDP && c->no_udp) die("UDP port forwarding requested but UDP is disabled");
- if (!strcmp(optarg, "auto")) { - if (c->mode != MODE_PASTA) - die("'auto' port forwarding is only allowed for pasta"); - - conf_ports_range_except(c, optname, optarg, fwd, - proto, NULL, NULL, - 1, NUM_PORTS - 1, NULL, 1, FWD_SCAN); - - return; - } - strncpy(buf, optarg, sizeof(buf) - 1);
if ((spec = strchr(buf, '/'))) { @@ -1031,13 +1061,13 @@ static void usage(const char *name, FILE *f, int status) " can be specified multiple times\n" " SPEC can be:\n" " 'none': don't forward any ports\n" - "%s" " [ADDR[%%IFACE]/]PORTS: forward specific ports\n" " PORTS is either 'all' (forward all unbound, non-ephemeral\n" " ports), or a comma-separated list of ports, optionally\n" " ranged with '-' and optional target ports after ':'.\n" " Ranges can be reduced by excluding ports or ranges\n" - " prefixed by '~'\n" + " prefixed by '~'.\n" + "%s" " Examples:\n" " -t all Forward all ports\n" " -t 127.0.0.1/all Forward all ports from local address\n" @@ -1051,15 +1081,26 @@ static void usage(const char *name, FILE *f, int status) " -t 192.0.2.1/5 Bind port 5 of 192.0.2.1 to %s\n" " -t 5-25,~10-20 Forward ports 5 to 9, and 21 to 25\n" " -t ~25 Forward all ports except for 25\n" + "%s" " default: %s\n" " -u, --udp-ports SPEC UDP port forwarding to %s\n" " SPEC is as described for TCP above\n" " default: %s\n", guest, strstr(name, "pasta") ? - " 'auto': forward all ports currently bound in namespace\n" + " The 'auto' keyword may be given to only forward\n" + " ports which are bound in the target namespace\n" + : "", + guest, guest, guest, + strstr(name, "pasta") ? + " -t auto Forward all ports bound in namespace\n" + " -t 192.0.2.2/auto Forward ports from 192.0.2.2 if\n" + " they are bound in the namespace\n" + " -t 8000-8010,auto Forward ports 8000-8010 if they\n" + " are bound in the namespace\n"
The whole thing is now rendered as: Examples: -t all Forward all ports -t 127.0.0.1/all Forward all ports from local address 127.0.0.1 -t 22 Forward local port 22 to 22 on namespace -t 22:23 Forward local port 22 to 23 on namespace -t 22,25 Forward ports 22, 25 to ports 22, 25 -t 22-80 Forward ports 22 to 80 -t 22-80:32-90 Forward ports 22 to 80 to corresponding port numbers plus 10 -t 192.0.2.1/5 Bind port 5 of 192.0.2.1 to namespace -t 5-25,~10-20 Forward ports 5 to 9, and 21 to 25 -t ~25 Forward all ports except for 25 -t auto Forward all ports bound in namespace -t 192.0.2.2/auto Forward ports from 192.0.2.2 if they are bound in the namespace -t 8000-8010,auto Forward ports 8000-8010 if they are bound in the namespace I think this could be: " -t auto\t Forward all ports bound in namespace\n" " -t ::1/auto Forward ports from ::1 if bound in\n" " namespace\n" " -t 80-82,auto Forward ports 80-82 if bound in\n" " namespace\n" instead.
: "", - guest, guest, guest, fwd_default, guest, fwd_default); + + fwd_default, guest, fwd_default);
if (strstr(name, "pasta")) goto pasta_opts; diff --git a/passt.1 b/passt.1 index 3ba447d5..eeecc0fb 100644 --- a/passt.1 +++ b/passt.1 @@ -434,12 +434,6 @@ Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of: .BR none Don't forward any ports
-.TP -.BR auto " " (\fBpasta\fR " " only) -Dynamically forward ports bound in the namespace. The list of ports is -periodically derived (every second) from listening sockets reported by -\fI/proc/net/tcp\fR and \fI/proc/net/tcp6\fR, see \fBproc\fR(5). - .TP [\fIaddress\fR[\fB%\fR\fIinterface\fR]\fB/\fR]\fIports\fR ... Specific ports to forward. Optionally, a specific listening address @@ -468,11 +462,20 @@ as \fIfirst\fR. \fB~\fR\fIfirst\fR[\fB-\fR\fIlast\fR] Exclude range. Exclude port numbers between \fIfirst\fR and \fIlast\fR from. This takes precedences over include ranges. + +.TP +.BR auto +(\fBpasta\fR " " only). Only forward ports in the specified set if +the target ports are bound in the namespace. The list of ports is +periodically derived (every second) from listening sockets reported by +\fI/proc/net/tcp\fR and \fI/proc/net/tcp6\fR, see \fBproc\fR(5). .RE
Specifying excluded ranges only implies that all other non-ephemeral -ports are forwarded. In this case, no failures are reported for -unavailable ports, unless no ports could be forwarded at all. +ports are forwarded. Specifying no ranges at all implies forwarding +all non-ephemeral ports permitted by current capabilities. In this +case, no failures are reported for unavailable ports, unless no ports +could be forwarded at all.
Examples: .RS @@ -519,6 +522,17 @@ and 30 .TP -t ~20000-20010 Forward all ports to the guest, except for the range from 20000 to 20010 +.TP +-t auto +Automatically forward any ports which are bound in the namespace. +.TP +-t 192.0.2.2/auto +Automatically forward any ports which are bound in the namespace, +listening only on local port 192.0.2.2. +.TP +-t 8000-8010,auto +Forward ports in the range 8000-8010 if and only if they are bound in +the namespace. .RE
Default is \fBnone\fR for \fBpasst\fR and \fBauto\fR for \fBpasta\fR.
-- Stefano
On Fri, 10 Apr 2026 11:03:07 +1000
David Gibson
Although fwd_rule_add() performs some sanity checks on the rule it is given, there are invalid rules we don't check for, assuming that its callers will do that.
That won't be enough when we can get rules inserted by a dynamic update client without going through the existing parsing code. So, add stricter checks to fwd_rule_add(), which is now possible thanks to the capabilities bits in the struct fwd_table. Where those duplicate existing checks in the callers, remove the old copies.
Signed-off-by: David Gibson
--- conf.c | 19 ------------------- fwd.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/conf.c b/conf.c index b7a4459e..31627209 100644 --- a/conf.c +++ b/conf.c @@ -310,10 +310,6 @@ static void conf_ports_spec(struct fwd_table *fwd, uint8_t proto, if (p != ep) /* Garbage after the ranges */ goto bad;
- if (orig_range.first == 0) { - die("Can't forward port 0 included in '%s'", spec); - } - conf_ports_range_except(fwd, proto, addr, ifname, orig_range.first, orig_range.last, exclude, @@ -356,11 +352,6 @@ static void conf_ports(char optname, const char *optarg, struct fwd_table *fwd) return; }
- if (proto == IPPROTO_TCP && !(fwd->caps & FWD_CAP_TCP)) - die("TCP port forwarding requested but TCP is disabled"); - if (proto == IPPROTO_UDP && !(fwd->caps & FWD_CAP_UDP)) - die("UDP port forwarding requested but UDP is disabled"); - strncpy(buf, optarg, sizeof(buf) - 1);
if ((spec = strchr(buf, '/'))) { @@ -405,16 +396,6 @@ static void conf_ports(char optname, const char *optarg, struct fwd_table *fwd) addr = NULL; }
- if (addr) { - if (!(fwd->caps & FWD_CAP_IPV4) && inany_v4(addr)) { - die("IPv4 is disabled, can't use -%c %s", - optname, optarg); - } else if (!(fwd->caps & FWD_CAP_IPV6) && !inany_v4(addr)) { - die("IPv6 is disabled, can't use -%c %s", - optname, optarg); - } - } - if (optname == 'T' || optname == 'U') { assert(!addr && !ifname);
diff --git a/fwd.c b/fwd.c index c7fd1a9d..98b04d0c 100644 --- a/fwd.c +++ b/fwd.c @@ -367,17 +367,58 @@ int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new) 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");
Nit: attempts
+ 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 && - !inany_equals(&new->addr, &inany_any6)) { - char astr[INANY_ADDRSTRLEN]; + 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))); + 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; }
-- Stefano
On Fri, 10 Apr 2026 11:02:46 +1000
David Gibson
This series makes a number of significant reworks to how we process forwarding options (-t, -u, -T and -U) in passt & pasta. This is largely motivated by moving towards being able to share this code with a configuration update tool. However, along the way it also enables some forwarding configurations that were technically possible with the forwarding table but couldn't be specified on the command line, in particular bug 180.
There is still a bunch of work needed to make the parsing code truly shareable with pesto, but this is a solid start.
v2: * Assorted minor changes based on Stefano's review, including * Explicitly state "guest or namespace" in the manpage * Clearer description of @rulesocks field * Worked around a gcc < 15 bug causing a false positive warning * Update man page and usage() for new capabilities * Additional patches moving rule parsing out of conf.c
I applied up to 14/23 as I had comments on patches after that one. I also have pending comments on 10/23 and 11/23 but I'm fairly sure they would need to be addressed in subsequent patches anyway (if at all). -- Stefano
On Thu, Apr 16, 2026 at 12:04:28AM +0200, Stefano Brivio wrote:
On Fri, 10 Apr 2026 11:02:56 +1000 David Gibson
wrote: Amongst other checks, fwd_rule_add() checks that the newly added rule doesn't conflict with any existing rules. However, unlike the other things we verify, this isn't really required for safe operation. Rule conflicts are a useful thing for the user to know about, but the forwarding logic is perfectly sound with conflicting rules (the first one will win).
In order to support dynamic rule updates, we want fwd_rule_add() to become a more low-level function, only checking the things it really needs to. So, move rule conflict checking to its caller via new helpers in fwd_rule.c.
Signed-off-by: David Gibson
--- conf.c | 5 +++++ fwd.c | 26 +------------------------- fwd_rule.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ fwd_rule.h | 2 ++ 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/conf.c b/conf.c index 027bbac9..b871646f 100644 --- a/conf.c +++ b/conf.c @@ -205,13 +205,18 @@ static void conf_ports_range_except(const struct ctx *c, char optname,
if (c->ifi4) { rulev.addr = inany_loopback4; + fwd_rule_conflict_check(&rulev, + fwd->rules, fwd->count); fwd_rule_add(fwd, &rulev); } if (c->ifi6) { rulev.addr = inany_loopback6; + fwd_rule_conflict_check(&rulev, + fwd->rules, fwd->count); fwd_rule_add(fwd, &rulev); } } else { + fwd_rule_conflict_check(&rule, fwd->rules, fwd->count); fwd_rule_add(fwd, &rule); } base = i - 1; diff --git a/fwd.c b/fwd.c index c05107d1..c9637525 100644 --- a/fwd.c +++ b/fwd.c @@ -341,7 +341,7 @@ void 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 i, port; + unsigned port;
assert(!(new->flags & ~allowed_flags)); /* Passing a non-wildcard address with DUAL_STACK_ANY is a bug */ @@ -354,30 +354,6 @@ void fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new) if ((fwd->sock_count + num) > ARRAY_SIZE(fwd->socks)) die("Too many listening sockets");
- /* Check for any conflicting entries */ - for (i = 0; i < fwd->count; i++) { - char newstr[INANY_ADDRSTRLEN], rulestr[INANY_ADDRSTRLEN]; - const struct fwd_rule *rule = &fwd->rules[i]; - - if (new->proto != rule->proto) - /* Non-conflicting protocols */ - continue; - - if (!inany_matches(fwd_rule_addr(new), fwd_rule_addr(rule))) - /* Non-conflicting addresses */ - continue; - - if (new->last < rule->first || rule->last < new->first) - /* Port ranges don't overlap */ - continue; - - die("Forwarding configuration conflict: %s/%u-%u versus %s/%u-%u", - inany_ntop(fwd_rule_addr(new), newstr, sizeof(newstr)), - new->first, new->last, - inany_ntop(fwd_rule_addr(rule), rulestr, sizeof(rulestr)), - rule->first, rule->last); - } - 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; diff --git a/fwd_rule.c b/fwd_rule.c index a034d5d1..5bc94efe 100644 --- a/fwd_rule.c +++ b/fwd_rule.c @@ -93,3 +93,48 @@ void fwd_rules_info(const struct fwd_rule *rules, size_t count) info(" %s", fwd_rule_fmt(&rules[i], buf, sizeof(buf))); } } + +/** + * fwd_rule_conflicts() - Test if two rules conflict with each other + * @a, @b: Rules to test + */ +static bool fwd_rule_conflicts(const struct fwd_rule *a, const struct fwd_rule *b) +{ + if (a->proto != b->proto) + /* Non-conflicting protocols */ + return false; + + if (!inany_matches(fwd_rule_addr(a), fwd_rule_addr(b))) + /* Non-conflicting addresses */ + return false; + + assert(a->first <= a->last && b->first <= b->last);
I expected this assert() to be gone by the end of the series, like the ones dropped in 11/23, but it's still there. Is this something that the client can't specifically trigger for some reason?
Oops, yeah, that's a mistake. What I had in mind is that the rule is first checked for internal consistency (including first <= last), and only then do we conflict check. But then I changed things so that's no longer the case. I'll fix it with followup patches in the next spin. -- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Thu, Apr 16, 2026 at 12:04:33AM +0200, Stefano Brivio wrote:
On Fri, 10 Apr 2026 11:02:57 +1000 David Gibson
wrote: fwd_rule_add() sanity checks the given rule, however all errors are fatal: either they're assert()s in the case of things that callers should have already verified, or die()s if we run out of space for the new rule.
This won't suffice any more when we allow rule updates from a configuration client. We don't want to trust the input we get from the client any more than we have to.
Replace the assert()s and die()s with a return value. Also include warn()s so that the user gets a more specific idea of the problem in the logs or stderr.
Signed-off-by: David Gibson
--- conf.c | 15 ++++++++++++--- fwd.c | 41 +++++++++++++++++++++++++++++++---------- fwd.h | 2 +- fwd_rule.c | 3 +-- fwd_rule.h | 1 + 5 files changed, 46 insertions(+), 16 deletions(-) diff --git a/conf.c b/conf.c index b871646f..5c913820 100644 --- a/conf.c +++ b/conf.c @@ -157,6 +157,7 @@ static void conf_ports_range_except(const struct ctx *c, char optname, .proto = proto, .flags = flags, }; + char rulestr[FWD_RULE_STRLEN]; unsigned delta = to - first; unsigned base, i;
@@ -207,20 +208,28 @@ static void conf_ports_range_except(const struct ctx *c, char optname, rulev.addr = inany_loopback4; fwd_rule_conflict_check(&rulev, fwd->rules, fwd->count); - fwd_rule_add(fwd, &rulev); + if (fwd_rule_add(fwd, &rulev) < 0) + goto fail; } if (c->ifi6) { rulev.addr = inany_loopback6; fwd_rule_conflict_check(&rulev, fwd->rules, fwd->count); - fwd_rule_add(fwd, &rulev); + if (fwd_rule_add(fwd, &rulev) < 0) + goto fail; } } else { fwd_rule_conflict_check(&rule, fwd->rules, fwd->count); - fwd_rule_add(fwd, &rule); + 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)));
Similar to my comment to 10/23: I expected this die() to be gone by the end of the series, but it's still there. Is it just a placeholder die() that will go once pesto(1) is added?
No, this one's intended to stay in pesto, at least to begin with. This is in the parsing path, not the rule insertion path. So once pesto is involved this will die() *in pesto*, which is correct[0]. When we receive new rules from pesto in passt we bypass this giong direct to fwd_rule_add(). -- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Thu, Apr 16, 2026 at 12:04:51AM +0200, Stefano Brivio wrote:
On Fri, 10 Apr 2026 11:03:03 +1000 David Gibson
wrote: The forwarding table now allows for arbitrary port ranges to be marked as FWD_SCAN, meaning we don't open sockets for every port, but only those we scan as listening on the target side. However, there's currently no way to create such rules, except -[tTuU] auto which always scans every port with an unspecified listening address and interface.
Allow user-specified "auto" ranges by moving the parsing of the "auto" keyword from conf_ports(), to conf_ports_spec() as part of the port specified. "auto" can be combined freely with other port ranges, e.g. -t 127.0.0.1/auto -u %lo/5000-7000,auto -T auto,12345 -U auto,~1-9000
Note that any address and interface given only affects where the automatic forwards listen, not what addresses we consider when scanning. That is, if the target side is listening on *any* address, we will create a forward on the specified address.
Link: https://bugs.passt.top/show_bug.cgi?id=180
Signed-off-by: David Gibson
--- conf.c | 85 ++++++++++++++++++++++++++++++++++++++++++--------------- passt.1 | 30 ++++++++++++++------ 2 files changed, 85 insertions(+), 30 deletions(-) diff --git a/conf.c b/conf.c index f62109b5..8e3b4b20 100644 --- a/conf.c +++ b/conf.c @@ -13,6 +13,7 @@ */
#include
+#include #include #include #include @@ -112,6 +113,28 @@ static int parse_port_range(const char *s, const char **endptr, 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 @@ -249,6 +272,7 @@ static void conf_ports_spec(const struct ctx *c, 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")) { @@ -256,15 +280,32 @@ static void conf_ports_spec(const struct ctx *c, spec = ""; }
- /* Mark all exclusions first, they might be given after base ranges */ + /* Parse excluded ranges and "auto" in the first pass */ for_each_chunk(p, ep, spec, ",") { struct port_range xrange;
- if (*p != '~') { - /* Not an exclude range, parse later */ + 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 (c->mode != MODE_PASTA) { + 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)) @@ -283,7 +324,7 @@ static void conf_ports_spec(const struct ctx *c, conf_ports_range_except(c, optname, optarg, fwd, proto, addr, ifname, 1, NUM_PORTS - 1, exclude, - 1, FWD_WEAK); + 1, flags | FWD_WEAK); return; }
@@ -291,8 +332,8 @@ static void conf_ports_spec(const struct ctx *c, for_each_chunk(p, ep, spec, ",") { struct port_range orig_range, mapped_range;
- if (*p == '~') - /* Exclude range, already parsed */ + if (!isdigit(*p)) + /* Already parsed */ continue;
if (parse_port_range(p, &p, &orig_range)) @@ -320,7 +361,7 @@ static void conf_ports_spec(const struct ctx *c, proto, addr, ifname, orig_range.first, orig_range.last, exclude, - mapped_range.first, 0); + mapped_range.first, flags); }
return; @@ -366,17 +407,6 @@ static void conf_ports(const struct ctx *c, char optname, const char *optarg, if (proto == IPPROTO_UDP && c->no_udp) die("UDP port forwarding requested but UDP is disabled");
- if (!strcmp(optarg, "auto")) { - if (c->mode != MODE_PASTA) - die("'auto' port forwarding is only allowed for pasta"); - - conf_ports_range_except(c, optname, optarg, fwd, - proto, NULL, NULL, - 1, NUM_PORTS - 1, NULL, 1, FWD_SCAN); - - return; - } - strncpy(buf, optarg, sizeof(buf) - 1);
if ((spec = strchr(buf, '/'))) { @@ -1031,13 +1061,13 @@ static void usage(const char *name, FILE *f, int status) " can be specified multiple times\n" " SPEC can be:\n" " 'none': don't forward any ports\n" - "%s" " [ADDR[%%IFACE]/]PORTS: forward specific ports\n" " PORTS is either 'all' (forward all unbound, non-ephemeral\n" " ports), or a comma-separated list of ports, optionally\n" " ranged with '-' and optional target ports after ':'.\n" " Ranges can be reduced by excluding ports or ranges\n" - " prefixed by '~'\n" + " prefixed by '~'.\n" + "%s" " Examples:\n" " -t all Forward all ports\n" " -t 127.0.0.1/all Forward all ports from local address\n" @@ -1051,15 +1081,26 @@ static void usage(const char *name, FILE *f, int status) " -t 192.0.2.1/5 Bind port 5 of 192.0.2.1 to %s\n" " -t 5-25,~10-20 Forward ports 5 to 9, and 21 to 25\n" " -t ~25 Forward all ports except for 25\n" + "%s" " default: %s\n" " -u, --udp-ports SPEC UDP port forwarding to %s\n" " SPEC is as described for TCP above\n" " default: %s\n", guest, strstr(name, "pasta") ? - " 'auto': forward all ports currently bound in namespace\n" + " The 'auto' keyword may be given to only forward\n" + " ports which are bound in the target namespace\n" + : "", + guest, guest, guest, + strstr(name, "pasta") ? + " -t auto Forward all ports bound in namespace\n" + " -t 192.0.2.2/auto Forward ports from 192.0.2.2 if\n" + " they are bound in the namespace\n" + " -t 8000-8010,auto Forward ports 8000-8010 if they\n" + " are bound in the namespace\n"
The whole thing is now rendered as:
Examples: -t all Forward all ports -t 127.0.0.1/all Forward all ports from local address 127.0.0.1 -t 22 Forward local port 22 to 22 on namespace -t 22:23 Forward local port 22 to 23 on namespace -t 22,25 Forward ports 22, 25 to ports 22, 25 -t 22-80 Forward ports 22 to 80 -t 22-80:32-90 Forward ports 22 to 80 to corresponding port numbers plus 10 -t 192.0.2.1/5 Bind port 5 of 192.0.2.1 to namespace -t 5-25,~10-20 Forward ports 5 to 9, and 21 to 25 -t ~25 Forward all ports except for 25 -t auto Forward all ports bound in namespace -t 192.0.2.2/auto Forward ports from 192.0.2.2 if they are bound in the namespace -t 8000-8010,auto Forward ports 8000-8010 if they are bound in the namespace
I think this could be:
" -t auto\t Forward all ports bound in namespace\n" " -t ::1/auto Forward ports from ::1 if bound in\n" " namespace\n" " -t 80-82,auto Forward ports 80-82 if bound in\n" " namespace\n"
instead.
Good ideas, done. -- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Thu, Apr 16, 2026 at 12:04:58AM +0200, Stefano Brivio wrote:
On Fri, 10 Apr 2026 11:03:07 +1000 David Gibson
wrote: Although fwd_rule_add() performs some sanity checks on the rule it is given, there are invalid rules we don't check for, assuming that its callers will do that.
That won't be enough when we can get rules inserted by a dynamic update client without going through the existing parsing code. So, add stricter checks to fwd_rule_add(), which is now possible thanks to the capabilities bits in the struct fwd_table. Where those duplicate existing checks in the callers, remove the old copies.
Signed-off-by: David Gibson
--- conf.c | 19 ------------------- fwd.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/conf.c b/conf.c index b7a4459e..31627209 100644 --- a/conf.c +++ b/conf.c @@ -310,10 +310,6 @@ static void conf_ports_spec(struct fwd_table *fwd, uint8_t proto, if (p != ep) /* Garbage after the ranges */ goto bad;
- if (orig_range.first == 0) { - die("Can't forward port 0 included in '%s'", spec); - } - conf_ports_range_except(fwd, proto, addr, ifname, orig_range.first, orig_range.last, exclude, @@ -356,11 +352,6 @@ static void conf_ports(char optname, const char *optarg, struct fwd_table *fwd) return; }
- if (proto == IPPROTO_TCP && !(fwd->caps & FWD_CAP_TCP)) - die("TCP port forwarding requested but TCP is disabled"); - if (proto == IPPROTO_UDP && !(fwd->caps & FWD_CAP_UDP)) - die("UDP port forwarding requested but UDP is disabled"); - strncpy(buf, optarg, sizeof(buf) - 1);
if ((spec = strchr(buf, '/'))) { @@ -405,16 +396,6 @@ static void conf_ports(char optname, const char *optarg, struct fwd_table *fwd) addr = NULL; }
- if (addr) { - if (!(fwd->caps & FWD_CAP_IPV4) && inany_v4(addr)) { - die("IPv4 is disabled, can't use -%c %s", - optname, optarg); - } else if (!(fwd->caps & FWD_CAP_IPV6) && !inany_v4(addr)) { - die("IPv6 is disabled, can't use -%c %s", - optname, optarg); - } - } - if (optname == 'T' || optname == 'U') { assert(!addr && !ifname);
diff --git a/fwd.c b/fwd.c index c7fd1a9d..98b04d0c 100644 --- a/fwd.c +++ b/fwd.c @@ -367,17 +367,58 @@ int fwd_rule_add(struct fwd_table *fwd, const struct fwd_rule *new) 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");
Nit: attempts
Oops, fixed.
+ 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 && - !inany_equals(&new->addr, &inany_any6)) { - char astr[INANY_ADDRSTRLEN]; + 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))); + 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; }
-- Stefano
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Thu, Apr 16, 2026 at 12:04:38AM +0200, Stefano Brivio wrote:
On Fri, 10 Apr 2026 11:03:01 +1000 David Gibson
wrote: Currently the man page describes the internal syntax of port specifiers in prose, which isn't particularly easy to follow. Rework it to use more syntax "diagrams" to show how it works. This will also allow us to more easily update the manual page for some coming changes in syntax.
usage() output is updated similarly, though more briefly.
Signed-off-by: David Gibson
--- conf.c | 10 +++++----- passt.1 | 32 ++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/conf.c b/conf.c index c3655824..5d6517c3 100644 --- a/conf.c +++ b/conf.c @@ -1041,11 +1041,11 @@ static void usage(const char *name, FILE *f, int status) " 'none': don't forward any ports\n" " 'all': forward all unbound, non-ephemeral ports\n" "%s" - " a comma-separated list, optionally ranged with '-'\n" - " and optional target ports after ':', with optional\n" - " address specification suffixed by '/' and optional\n" - " interface prefixed by '%%'. Ranges can be reduced by\n" - " excluding ports or ranges prefixed by '~'\n" + " [ADDR[%%IFACE]/]PORTS: forward specific ports\n" + " PORTS is a comma-separated list of ports, optionally\n" + " ranged with '-' and optional target ports after ':'.\n" + " Ranges can be reduced by excluding ports or ranges\n" + " prefixed by '~'\n" " Examples:\n" " -t 22 Forward local port 22 to 22 on %s\n" " -t 22:23 Forward local port 22 to 23 on %s\n" diff --git a/passt.1 b/passt.1 index 7da4fe5f..d329f8f0 100644 --- a/passt.1 +++ b/passt.1 @@ -447,16 +447,28 @@ periodically derived (every second) from listening sockets reported by \fI/proc/net/tcp\fR and \fI/proc/net/tcp6\fR, see \fBproc\fR(5).
.TP -.BR ports -A comma-separated list of ports, optionally ranged with \fI-\fR, and, -optionally, with target ports after \fI:\fR, if they differ. Specific addresses -can be bound as well, separated by \fI/\fR, and also, since Linux 5.7, limited -to specific interfaces, prefixed by \fI%\fR. Within given ranges, selected ports -and ranges can be excluded by an additional specification prefixed by \fI~\fR. - -Specifying excluded ranges only implies that all other ports are forwarded. In -this case, no failures are reported for unavailable ports, unless no ports could -be forwarded at all. +[\fIaddress\fR[\fB%\fR\fIinterface\fR]\fB/\fR]\fIports\fR ... +Specific ports to forward. Optionally, a specific listening address +and interface name (since Linux 5.7) can be specified. \fIports\fR is +a comma-separated list of entries which may be any of: +.RS +.TP +\fIfirst\fR[\fB-\fR\fIlast\fR][\fB:\fR\fItofirst\fR[\fB-\fR\fItolast\fR]] +Include range. Forward port numbers between \fIfirst\fR and \fIlast\fR +(inclusive) to ports between \fItofirst\fR and \fItolast\fR. If +\fItofirst\fR and \fItolast\fR are omitted, assume the same as +\fIfirst\fR and \fIlast\fR. If \fIlast\fR is omitted, assume the same +as \fIfirst\fR. + +.TP +\fB~\fR\fIfirst\fR[\fB-\fR\fIlast\fR] +Exclude range. Exclude port numbers between \fIfirst\fR and +\fIlast\fR from. This takes precedences over include ranges.
..."from the set of all non-ephemeral ports permitted by current capabilities"?
Or simply drop " from", because it should be clear from the paragraph below?
Uh, yeah, not entirely sure what I was going for there. I've changed this to \fB~\fR\fIfirst\fR[\fB-\fR\fIlast\fR] Exclude range. Don't forward port numbers between \fIfirst\fR and \fIlast\fR. This takes precedences over include ranges.
+.RE + +Specifying excluded ranges only implies that all other non-ephemeral +ports are forwarded. In this case, no failures are reported for +unavailable ports, unless no ports could be forwarded at all.
Examples: .RS
-- Stefano
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Thu, Apr 16, 2026 at 12:04:43AM +0200, Stefano Brivio wrote:
On Fri, 10 Apr 2026 11:03:02 +1000 David Gibson
wrote: Currently -[tTuU] all is handled separately in conf_ports() before calling conf_ports_spec(). Earlier changes mean we can now move this handling to conf_ports_spec(). This makes the code slightly simpler, but more importantly it allows some useful combinations we couldn't previously do, such as -t 127.0.0.1/all or -u %eth2/all
Signed-off-by: David Gibson
--- conf.c | 25 ++++++++++--------------- passt.1 | 28 ++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/conf.c b/conf.c index 5d6517c3..f62109b5 100644 --- a/conf.c +++ b/conf.c @@ -251,6 +251,11 @@ static void conf_ports_spec(const struct ctx *c, const char *p, *ep; unsigned i;
+ if (!strcmp(spec, "all")) { + /* Treat "all" as equivalent to "": all non-ephemeral ports */ + spec = ""; + } + /* Mark all exclusions first, they might be given after base ranges */ for_each_chunk(p, ep, spec, ",") { struct port_range xrange; @@ -372,19 +377,6 @@ static void conf_ports(const struct ctx *c, char optname, const char *optarg, return; }
- if (!strcmp(optarg, "all")) { - uint8_t exclude[PORT_BITMAP_SIZE] = { 0 }; - - /* Exclude ephemeral ports */ - fwd_port_map_ephemeral(exclude); - - conf_ports_range_except(c, optname, optarg, fwd, - proto, NULL, NULL, - 1, NUM_PORTS - 1, exclude, - 1, FWD_WEAK); - return; - } - strncpy(buf, optarg, sizeof(buf) - 1);
if ((spec = strchr(buf, '/'))) { @@ -1039,14 +1031,17 @@ static void usage(const char *name, FILE *f, int status) " can be specified multiple times\n" " SPEC can be:\n" " 'none': don't forward any ports\n" - " 'all': forward all unbound, non-ephemeral ports\n" "%s" " [ADDR[%%IFACE]/]PORTS: forward specific ports\n" - " PORTS is a comma-separated list of ports, optionally\n" + " PORTS is either 'all' (forward all unbound, non-ephemeral\n" + " ports), or a comma-separated list of ports, optionally\n" " ranged with '-' and optional target ports after ':'.\n" " Ranges can be reduced by excluding ports or ranges\n" " prefixed by '~'\n" " Examples:\n" + " -t all Forward all ports\n"
Nit: the examples below have a tab as a separator, which makes it slightly easier to ensure we indent them properly.
Oops, fixed.
+ " -t 127.0.0.1/all Forward all ports from local address\n" + " 127.0.0.1\n"
This makes things pretty hard on eyes as it's not consistent with the rest of the "table". Could we perhaps do:
" -t ::1/all Forward all ports from ::1\n"
?
Ah, good idea. Done.
" -t 22 Forward local port 22 to 22 on %s\n" " -t 22:23 Forward local port 22 to 23 on %s\n" " -t 22,25 Forward ports 22, 25 to ports 22, 25\n" diff --git a/passt.1 b/passt.1 index d329f8f0..3ba447d5 100644 --- a/passt.1 +++ b/passt.1 @@ -434,12 +434,6 @@ Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of: .BR none Don't forward any ports
-.TP -.BR all -Forward all unbound, non-ephemeral ports, as permitted by current capabilities. -For low (< 1024) ports, see \fBNOTES\fR. No failures are reported for -unavailable ports, unless no ports could be forwarded at all. - .TP .BR auto " " (\fBpasta\fR " " only) Dynamically forward ports bound in the namespace. The list of ports is @@ -449,10 +443,20 @@ periodically derived (every second) from listening sockets reported by .TP [\fIaddress\fR[\fB%\fR\fIinterface\fR]\fB/\fR]\fIports\fR ... Specific ports to forward. Optionally, a specific listening address -and interface name (since Linux 5.7) can be specified. \fIports\fR is -a comma-separated list of entries which may be any of: +and interface name (since Linux 5.7) can be specified. \fIports\fR +may be either: .RS .TP +\fBall\fR +Forward all unbound, non-ephemeral ports, as permitted by current +capabilities. For low (< 1024) ports, see \fBNOTES\fR. No failures +are reported for unavailable ports, unless no ports could be forwarded +at all. +.RE + +.RS +or a comma-separated list of entries which may be any of: +.TP \fIfirst\fR[\fB-\fR\fIlast\fR][\fB:\fR\fItofirst\fR[\fB-\fR\fItolast\fR]] Include range. Forward port numbers between \fIfirst\fR and \fIlast\fR (inclusive) to ports between \fItofirst\fR and \fItolast\fR. If @@ -473,6 +477,14 @@ unavailable ports, unless no ports could be forwarded at all. Examples: .RS .TP +-t all +Forward all unbound, non-ephemeral ports as permitted by current +capabilities to the corresponding port on the guest or namespace +.TP +-t 127.0.0.1/all +For the local address 127.0.0.1, forward all unbound, non-ephemeral +ports as permitted by current capabilities.
Nit: all the other examples have no dot at the end (I tend to think it fits better this type of list, but all I care about is that it's consistent).
Fixed. Also changed to ::1 for consistency with the usage() example.
+.TP -t 22 Forward local port 22 to port 22 on the guest or namespace .TP
-- Stefano
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
participants (2)
-
David Gibson
-
Stefano Brivio