From: David Gibson <david@gibson.dropbear.id.au>
To: Jon Maloy <jmaloy@redhat.com>
Cc: sbrivio@redhat.com, dgibson@redhat.com, passt-dev@passt.top
Subject: Re: [PATCH v4] conf: Support CIDR notation for -a/--address option
Date: Mon, 12 Jan 2026 14:16:31 +1100 [thread overview]
Message-ID: <aWRnjyFk7l6Zv5Qu@zatzit> (raw)
In-Reply-To: <20260110225936.3200705-1-jmaloy@redhat.com>
[-- Attachment #1: Type: text/plain, Size: 11301 bytes --]
On Sat, Jan 10, 2026 at 05:59:36PM -0500, Jon Maloy wrote:
> Extend the -a/--address option to accept addresses in CIDR notation
> (e.g., 192.168.1.1/24 or 2001:db8::1/64) as an alternative to using
> separate -a and -n options.
>
> We add a new conf_addr_prefix_len() helper function that:
> - Parses address strings with optional /prefix_len suffix
> - Validates prefix length based on address family (0-32 for IPv4,
> 0-128 for IPv6), including handling of IPv4-to-IPv6 mapping case.
> - Returns address family via union inany_addr output parameter
>
> For IPv4, the prefix length is stored in ip4.prefix_len when provided.
> Mixing -n and CIDR notation results in an error to catch likely user
> mistakes.
>
> Also fix a bug in conf_ip4_prefix() that was incorrectly using the
> global 'optarg' instead of its 'arg' parameter.
>
> Signed-off-by: Jon Maloy <jmaloy@redhat.com>
>
> ---
> v3: Fixes after feedback from Laurent, David and Stefano
> Notably, updated man page for the -a option
>
> v4: Fixes based on feedback from David G:
> - Handling prefix length adjustment when IPv4-to-IPv6 mapping
> - Removed redundant !IN6_IS_ADDR_V4MAPPED(&addr.a6) test
> - Simplified tests of acceptable address types
> - Merged documentation and code commits
> - Some documentation text clarifications
> ---
> conf.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++----------
> inany.c | 29 ++++++++++++++++++
> inany.h | 1 +
> passt.1 | 17 +++++++----
> 4 files changed, 118 insertions(+), 20 deletions(-)
>
> diff --git a/conf.c b/conf.c
> index 2942c8c..b36abf3 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -682,7 +682,7 @@ static int conf_ip4_prefix(const char *arg)
> return -1;
> } else {
> errno = 0;
> - len = strtoul(optarg, NULL, 0);
> + len = strtoul(arg, NULL, 0);
> if (len > 32 || errno)
> return -1;
> }
> @@ -690,6 +690,50 @@ static int conf_ip4_prefix(const char *arg)
> return len;
> }
>
> +/**
> + * conf_addr_prefix_len() - Parse address with optional prefix length
> + * @arg: Address string, optionally with /prefix_len suffix (modified)
> + * @addr: Output for parsed address
> + * @prefix_len: Output for prefix length (0 if not specified)
> + *
> + * Return: AF_INET for IPv4, AF_INET6 for IPv6, -1 on error
> + */
> +static int conf_addr_prefix_len(char *arg, union inany_addr *addr,
> + int *prefix_len)
> +{
> + char *slash;
> +
> + *prefix_len = 0;
> +
> + /* Check for /prefix_len suffix */
> + slash = strchr(arg, '/');
> + if (slash) {
> + unsigned long len;
> + char *end;
> +
> + *slash = '\0';
> + errno = 0;
> + len = strtoul(slash + 1, &end, 10);
> + if (errno || *end)
> + return -1;
> +
> + *prefix_len = len;
> + }
> +
> + if (!inany_prefix_pton(arg, addr, prefix_len))
> + return -1;
The handling of prefix lengths is correct now, but in a really
confusing way. This function now returns addresses in a uniform way
(essentially as IPv6), but the prefix has a different meaning
depending on what type of address it is internally. We parse the
prefix length here but the actualy address elsewhere.
I think what would make more sense would be to make
inany_prefix_pton() parse a full CIDR address itself, with
prefix_length becoming a pure output parameter. prefix_length should
always be returned in IPv6 form - so add 96 if the input is an IPv4
address.
Yes, that means re-correcting it when going from the v4-mapped inany
address to the v4-only fields in ctx.ip4, but I think that's
preferable to having non-consistent semantics in the interface of a
single function.
> +
> + if (inany_v4(addr)) {
> + if (*prefix_len > 32)
> + return -1;
> + return AF_INET;
> + }
> +
> + if (*prefix_len > 128)
> + return -1;
> + return AF_INET6;
> +}
> +
> /**
> * conf_ip4() - Verify or detect IPv4 support, get relevant addresses
> * @ifi: Host interface to attempt (0 to determine one)
> @@ -896,7 +940,7 @@ static void usage(const char *name, FILE *f, int status)
> " a zero value disables assignment\n"
> " default: 65520: maximum 802.3 MTU minus 802.3 header\n"
> " length, rounded to 32 bits (IPv4 words)\n"
> - " -a, --address ADDR Assign IPv4 or IPv6 address ADDR\n"
> + " -a, --address ADDR Assign IPv4 or IPv6 address ADDR[/PREFIXLEN]\n"
> " can be specified zero to two times (for IPv4 and IPv6)\n"
> " default: use addresses from interface with default route\n"
> " -n, --netmask MASK Assign IPv4 MASK, dot-decimal or bits\n"
> @@ -1499,6 +1543,7 @@ void conf(struct ctx *c, int argc, char **argv)
> const char *logname = (c->mode == MODE_PASTA) ? "pasta" : "passt";
> char userns[PATH_MAX] = { 0 }, netns[PATH_MAX] = { 0 };
> bool copy_addrs_opt = false, copy_routes_opt = false;
> + bool prefix_from_cidr = false, prefix_from_opt = false;
> enum fwd_ports_mode fwd_default = FWD_NONE;
> bool v4_only = false, v6_only = false;
> unsigned dns4_idx = 0, dns6_idx = 0;
> @@ -1808,23 +1853,36 @@ void conf(struct ctx *c, int argc, char **argv)
> c->mtu = mtu;
> break;
> }
> - case 'a':
> - if (inet_pton(AF_INET6, optarg, &c->ip6.addr) &&
> - !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr) &&
> - !IN6_IS_ADDR_LOOPBACK(&c->ip6.addr) &&
> - !IN6_IS_ADDR_V4MAPPED(&c->ip6.addr) &&
> - !IN6_IS_ADDR_V4COMPAT(&c->ip6.addr) &&
> - !IN6_IS_ADDR_MULTICAST(&c->ip6.addr)) {
> + case 'a': {
> + union inany_addr addr;
> + const struct in_addr *a4;
> + int prefix_len = 0;
> + int af;
> +
> + af = conf_addr_prefix_len(optarg, &addr, &prefix_len);
> +
> + if (inany_is_unspecified(&addr) ||
> + inany_is_multicast(&addr) ||
> + inany_is_loopback(&addr) ||
> + IN6_IS_ADDR_V4COMPAT(&addr.a6))
> + die("Invalid address: %s", optarg);
> +
> + if (af == AF_INET6) {
> + c->ip6.addr = addr.a6;
> if (c->mode == MODE_PASTA)
> c->ip6.no_copy_addrs = true;
Is there a reason not to do the prefix_from_cidr / prefix_from_out
checks in the IPv6 case?
> break;
> }
>
> - if (inet_pton(AF_INET, optarg, &c->ip4.addr) &&
> - !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.addr) &&
> - !IN4_IS_ADDR_BROADCAST(&c->ip4.addr) &&
> - !IN4_IS_ADDR_LOOPBACK(&c->ip4.addr) &&
> - !IN4_IS_ADDR_MULTICAST(&c->ip4.addr)) {
> + a4 = inany_v4(&addr);
> + if (af == AF_INET && a4) {
> + c->ip4.addr = *a4;
> + if (prefix_len) {
> + if (prefix_from_opt)
> + die("Can't mix CIDR with -n");
> + c->ip4.prefix_len = prefix_len;
> + prefix_from_cidr = true;
> + }
> if (c->mode == MODE_PASTA)
> c->ip4.no_copy_addrs = true;
> break;
> @@ -1832,11 +1890,14 @@ void conf(struct ctx *c, int argc, char **argv)
>
> die("Invalid address: %s", optarg);
> break;
> + }
> case 'n':
> + if (prefix_from_cidr)
> + die("Can't use both -n and CIDR prefix length");
> c->ip4.prefix_len = conf_ip4_prefix(optarg);
> if (c->ip4.prefix_len < 0)
> die("Invalid netmask: %s", optarg);
> -
> + prefix_from_opt = true;
> break;
> case 'M':
> parse_mac(c->our_tap_mac, optarg);
> diff --git a/inany.c b/inany.c
> index 7680439..f142a76 100644
> --- a/inany.c
> +++ b/inany.c
> @@ -57,3 +57,32 @@ int inany_pton(const char *src, union inany_addr *dst)
>
> return 0;
> }
> +
> +/** inany_prefix_pton - Parse an IPv[46] address with prefix length adjustment
> + * @src: IPv[46] address string
> + * @dst: output buffer, filled with parsed address
> + * @prefix_len: pointer to prefix length
> + *
> + * Return: on success, 1, if no parseable address is found, 0
> + */
> +int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len)
> +{
> + /* First try parsing as plain IPv4 */
> + if (inet_pton(AF_INET, src, &dst->v4mapped.a4)) {
> + memset(&dst->v4mapped.zero, 0, sizeof(dst->v4mapped.zero));
> + memset(&dst->v4mapped.one, 0xff, sizeof(dst->v4mapped.one));
> + return 1;
> + }
> +
> + /* Try parsing as IPv6, adjust prefix length if mapped IPv4 address */
> + if (inet_pton(AF_INET6, src, &dst->a6)) {
> + if (inany_v4(dst) && prefix_len && *prefix_len > 0) {
> + if (*prefix_len < 96)
> + return 0;
> + *prefix_len -= 96;
> + }
> + return 1;
> + }
> +
> + return 0;
> +}
> diff --git a/inany.h b/inany.h
> index 61b36fb..36865f9 100644
> --- a/inany.h
> +++ b/inany.h
> @@ -295,5 +295,6 @@ static inline void inany_siphash_feed(struct siphash_state *state,
>
> const char *inany_ntop(const union inany_addr *src, char *dst, socklen_t size);
> int inany_pton(const char *src, union inany_addr *dst);
> +int inany_prefix_pton(const char *src, union inany_addr *dst, int *prefix_len);
>
> #endif /* INANY_H */
> diff --git a/passt.1 b/passt.1
> index db0d662..7ca03be 100644
> --- a/passt.1
> +++ b/passt.1
> @@ -156,10 +156,14 @@ By default, the advertised MTU is 65520 bytes, that is, the maximum 802.3 MTU
> minus the length of a 802.3 header, rounded to 32 bits (IPv4 words).
>
> .TP
> -.BR \-a ", " \-\-address " " \fIaddr
> +.BR \-a ", " \-\-address " " \fIaddr\fR[\fB/\fR\fIprefix_len\fR]
> Assign IPv4 \fIaddr\fR via DHCP (\fByiaddr\fR), or \fIaddr\fR via DHCPv6 (option
> 5) and an \fIaddr\fR-based prefix via NDP Router Advertisement (option type 3)
> for an IPv6 \fIaddr\fR.
> +An optional \fB/\fR\fIprefix_len\fR (0-32 for IPv4, 0-128 for IPv6) can be
> +appended in CIDR notation (e.g., 192.168.1.1/24). This is an alternative to
> +using the \fB-n\fR, \fB--netmask\fR option. Mixing CIDR notation with
> +\fB-n\fR results in an error.
> This option can be specified zero (for defaults) to two times (once for IPv4,
> once for IPv6).
> By default, assigned IPv4 and IPv6 addresses are taken from the host interfaces
> @@ -172,10 +176,13 @@ is assigned for IPv4, and no additional address will be assigned for IPv6.
> .TP
> .BR \-n ", " \-\-netmask " " \fImask
> Assign IPv4 netmask \fImask\fR, expressed as dot-decimal or number of bits, via
> -DHCP (option 1).
> -By default, the netmask associated to the host address matching the assigned one
> -is used. If there's no matching address on the host, the netmask is determined
> -according to the CIDR block of the assigned address (RFC 4632).
> +DHCP (option 1). Alternatively, the prefix length can be specified using CIDR
> +notation with the \fB-a\fR, \fB--address\fR option (e.g., \fB-a\fR 192.168.1.1/24).
> +Mixing \fB-n\fR with CIDR notation results in an error.
> +If no address is indicated, the netmask associated with the adopted host address,
> +if any, is used. If an address is indicated, but without a prefix length, the
> +netmask is determined based on the corresponding network class. In all other
> +cases, the netmask is determined by using the indicated prefix length.
>
> .TP
> .BR \-M ", " \-\-mac-addr " " \fIaddr
> --
> 2.52.0
>
--
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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
prev parent reply other threads:[~2026-01-12 3:16 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-01-10 22:59 Jon Maloy
2026-01-12 3:16 ` David Gibson [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=aWRnjyFk7l6Zv5Qu@zatzit \
--to=david@gibson.dropbear.id.au \
--cc=dgibson@redhat.com \
--cc=jmaloy@redhat.com \
--cc=passt-dev@passt.top \
--cc=sbrivio@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://passt.top/passt
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).