public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
From: Stefano Brivio <sbrivio@redhat.com>
To: Anshu Kumari <anskuma@redhat.com>
Cc: passt-dev@passt.top, david@gibson.dropbear.id.au,
	lvivier@redhat.com, jmaloy@redhat.com
Subject: Re: [PATCH v2 1/2] dhcpv6: Add --dhcpv6-opt with option type table and value parser
Date: Wed, 01 Jul 2026 18:59:49 +0200 (CEST)	[thread overview]
Message-ID: <20260701185948.03f1557a@elisabeth> (raw)
In-Reply-To: <20260618120529.1768765-2-anskuma@redhat.com>

On Thu, 18 Jun 2026 17:35:28 +0530
Anshu Kumari <anskuma@redhat.com> wrote:

> Introduce the --dhcpv6-opt flag that allows setting arbitrary DHCPv6
> options from command-line in the form [--dhcpv6-opt CODE,VALUE].
> 
> Add a type lookup table mapping option codes to value types (IPv6,
> IPv6 list, integer, string, vendor class, length-prefixed string
> list) and dhcpv6_opt_parse() to convert CLI strings to binary wire
> format. If the same option code is given more than once, the
> last value wins.
> 
> Link: https://bugs.passt.top/show_bug.cgi?id=192
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> ---
> v2:
>   - Renamed custom_v6opts to dhcpv6_opts, MAX_CUSTOM_DHCPV6_OPTS
>     to MAX_DHCPV6_OPTS.
>   - Dropped val/len from ctx struct.
>   - Moved dhcpv6_add_option() to conf.c as static
>     conf_dhcpv6_option().
>   - Made dhcpv6_opt_parse() non-static, declared in dhcpv6.h
>   - Omitted explicit [256] from dhcpv6_opt_types[].
>   - Moved chunk declaration into while block.
>   - Removed redundant !slen check in DHCPV6_OPT_STR case.
>   - All errors in dhcpv6_opt_parse() return -1, removed die()
>     calls.
> ---
>  conf.c   |  60 +++++++++++++++-
>  dhcpv6.c | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
>  dhcpv6.h |   2 +
>  passt.1  |  31 +++++++++
>  passt.h  |  12 ++++
>  5 files changed, 313 insertions(+), 1 deletion(-)
> 
> diff --git a/conf.c b/conf.c
> index cd05adf..3981c1b 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -47,6 +47,7 @@
>  #include "lineread.h"
>  #include "isolation.h"
>  #include "log.h"
> +#include "dhcpv6.h"
>  #include "vhost_user.h"
>  #include "epoll_ctl.h"
>  #include "conf.h"
> @@ -616,7 +617,8 @@ static void usage(const char *name, FILE *f, int status)
>  		"  -S, --search LIST	Space-separated list, search domains\n"
>  		"    a single, empty option disables the DNS search list\n"
>  		"  -H, --hostname NAME 	Hostname to configure client with\n"
> -		"  --fqdn NAME		FQDN to configure client with\n");
> +		"  --fqdn NAME		FQDN to configure client with\n"
> +		"  --dhcpv6-opt CODE,VAL	Set DHCPv6 option CODE to VAL\n");
>  	if (strstr(name, "pasta"))
>  		FPRINTF(f, "    default: don't use any search list\n");
>  	else
> @@ -884,6 +886,10 @@ static void conf_print(const struct ctx *c)
>  		info("    our link-local: %s",
>  		     inet_ntop(AF_INET6, &c->ip6.our_tap_ll,
>  			       buf, sizeof(buf)));
> +		for (i = 0; i < c->dhcpv6_opts_count; i++)
> +			info("    v6 option %u: %s",
> +			     c->dhcpv6_opts[i].code,
> +			     c->dhcpv6_opts[i].str);
>  
>  dns6:
>  		for (i = 0; i < ARRAY_SIZE(c->ip6.dns); i++) {
> @@ -1150,6 +1156,41 @@ static void conf_sock_listen(const struct ctx *c)
>  		die_perror("Couldn't add configuration socket to epoll");
>  }
>  
> +/**
> + * conf_dhcpv6_option() - Set value for a DHCPv6 option in configuration
> + * @c:		Execution context
> + * @code:	DHCPv6 option code
> + * @val_str:	Value string from command line
> + */
> +static void conf_dhcpv6_option(struct ctx *c, uint16_t code,
> +			       const char *val_str)
> +{
> +	uint8_t tmp[255];

For DHCPv6, we have 16 bits of options length, so this should be
UINT16_MAX.

> +	int idx;
> +
> +	if (dhcpv6_opt_parse(code, val_str, tmp, sizeof(tmp)) < 0)
> +		die("Invalid value for DHCPv6 option %u: %s", code, val_str);
> +
> +	for (idx = 0; idx < c->dhcpv6_opts_count; idx++) {
> +		if (c->dhcpv6_opts[idx].code == code)
> +			break;
> +	}
> +
> +	if (idx == c->dhcpv6_opts_count) {
> +		if (c->dhcpv6_opts_count >= MAX_DHCPV6_OPTS)

Same as my question about MAX_CUSTOM_DHCP_OPTS in the review of 1/6 of
the v3 for DHCP options:

  https://archives.passt.top/passt-dev/20260612010426.319bc57d@elisabeth/

Unlike DHCP, DHCPv6 doesn't specify a minimum size of the option field
(312 bytes for DHCP, RFC 2131, Section 2) that a client or server needs
to be ready to accept, implying that, as long as a message fits the
MTU, it's acceptable.

And our default MTU is 65520 bytes, meaning we have 65472 bytes
available for DHCP messages (accounting for 40 bytes of IPv6 header and
for 8 bytes of UDP header). So, actually, 32 is likely to be more
restrictive than needed in this case.

But I see you have that dhcpv6_opts[MAX_DHCPV6_OPTS] in struct ctx...
see below for some considerations about that.

> +			die("Too many --dhcpv6-opt entries (max %d)",
> +			    MAX_DHCPV6_OPTS);
> +		c->dhcpv6_opts_count++;
> +	}
> +
> +	c->dhcpv6_opts[idx].code = code;
> +
> +	if (snprintf_check(c->dhcpv6_opts[idx].str,
> +			   sizeof(c->dhcpv6_opts[0].str),
> +			   "%s", val_str))
> +		die("DHCPv6 option value too long: %s", val_str);
> +}
> +
>  /**
>   * conf() - Process command-line arguments and set configuration
>   * @c:		Execution context
> @@ -1233,6 +1274,7 @@ void conf(struct ctx *c, int argc, char **argv)
>  		{"migrate-no-linger", no_argument,	NULL,		30 },
>  		{"stats", required_argument,		NULL,		31 },
>  		{"conf-path",	required_argument,	NULL,		'c' },
> +		{"dhcpv6-opt", required_argument,	NULL,		35 },
>  		{ 0 },
>  	};
>  	const char *optstring = "+dqfel:hs:c:F:I:p:P:m:a:n:M:g:i:o:D:S:H:461t:u:T:U:";
> @@ -1248,10 +1290,13 @@ void conf(struct ctx *c, int argc, char **argv)
>  	uint8_t prefix_len_from_opt = 0;
>  	unsigned int ifi4 = 0, ifi6 = 0;
>  	const char *logfile = NULL;
> +	unsigned long v6optcode;

I'm not suggesting to make this look like Java but... 'v6' isn't really
explanatory of what this is. Our 'v6' short-hands typically represent
IPv6, not DHCPv6. What about dhcpv6_opt_code? It doesn't make any of
the lines you're adding below wrap.

>  	char *runas = NULL;
>  	size_t logsize = 0;
> +	const char *comma;
>  	long fd_tap_opt;
>  	int name, ret;
> +	char *end;
>  	uid_t uid;
>  	gid_t gid;
>  
> @@ -1467,6 +1512,19 @@ void conf(struct ctx *c, int argc, char **argv)
>  				die("Can't display statistics if not running in foreground");
>  			c->stats = strtol(optarg, NULL, 0);
>  			break;
> +		case 35:
> +			comma = strchr(optarg, ',');
> +			if (!comma)
> +				die("--dhcpv6-opt requires CODE,VALUE format");
> +
> +			errno = 0;
> +			v6optcode = strtoul(optarg, &end, 0);
> +			if (end != comma || errno || v6optcode < 1)
> +				die("Invalid DHCPv6 option code: %s",
> +				    optarg);
> +
> +			conf_dhcpv6_option(c, v6optcode, comma + 1);
> +			break;
>  		case 'd':
>  			c->debug = 1;
>  			c->quiet = 0;
> diff --git a/dhcpv6.c b/dhcpv6.c
> index 97c04e2..1e1dd0d 100644
> --- a/dhcpv6.c
> +++ b/dhcpv6.c
> @@ -25,6 +25,7 @@
>  #include <string.h>
>  #include <time.h>
>  #include <limits.h>
> +#include <errno.h>
>  
>  #include "packet.h"
>  #include "util.h"
> @@ -278,6 +279,214 @@ static struct resp_not_on_link_t {
>  	{ 0, },
>  };
>  
> +/**
> + * enum dhcpv6_opt_type - DHCPv6 option value types
> + * @DHCPV6_OPT_NONE:		Unsupported or unknown option
> + * @DHCPV6_OPT_STR:		Variable-length string
> + * @DHCPV6_OPT_IPV6:		Single IPv6 address
> + * @DHCPV6_OPT_IPV6_LIST:	Multiple IPv6 addresses, comma-separated
> + * @DHCPV6_OPT_INT8:		Unsigned 8-bit integer
> + * @DHCPV6_OPT_INT16:		Unsigned 16-bit integer
> + * @DHCPV6_OPT_INT32:		Unsigned 32-bit integer
> + * @DHCPV6_OPT_VENDOR_CLASS:	Enterprise number + length-prefixed data
> + * @DHCPV6_OPT_LEN_STR_LIST:	Length-prefixed string list

By the way, I just realised that RFC 7227 (Best Current Practice,
nothing we have to take care of here) has a nice summary of option
types in Section 5, if it helps cross-checking things.

> + */
> +enum dhcpv6_opt_type {
> +	DHCPV6_OPT_NONE,
> +	DHCPV6_OPT_STR,
> +	DHCPV6_OPT_IPV6,
> +	DHCPV6_OPT_IPV6_LIST,
> +	DHCPV6_OPT_INT8,
> +	DHCPV6_OPT_INT16,
> +	DHCPV6_OPT_INT32,
> +	DHCPV6_OPT_VENDOR_CLASS,
> +	DHCPV6_OPT_LEN_STR_LIST,
> +};
> +
> +/**
> + * dhcpv6_opt_types - Maps DHCPv6 option code to value type, indexed by code
> + * RFC 8415 Options: 7, 15, 16, 17, 32, 82, 83
> + * RFC 5970 Options: 59, 60
> + * RFC 4075 Options: 31
> + */
> +static const enum dhcpv6_opt_type dhcpv6_opt_types[] = {
> +	[7]   = DHCPV6_OPT_INT8,		/* Preference */
> +	[15]  = DHCPV6_OPT_LEN_STR_LIST,	/* User Class */
> +	[16]  = DHCPV6_OPT_VENDOR_CLASS,	/* Vendor Class */
> +	[17]  = DHCPV6_OPT_VENDOR_CLASS,	/* Vendor Opts */
> +	[31]  = DHCPV6_OPT_IPV6_LIST,		/* SNTP Servers */
> +	[32]  = DHCPV6_OPT_INT32,		/* Information Refresh Time */
> +	[59]  = DHCPV6_OPT_STR,		/* Boot File URL */
> +	[60]  = DHCPV6_OPT_LEN_STR_LIST,	/* Boot File Params */
> +	[82]  = DHCPV6_OPT_INT32,		/* SOL_MAX_RT */
> +	[83]  = DHCPV6_OPT_INT32,		/* INF_MAX_RT */
> +};
> +
> +/**
> + * dhcpv6_opt_parse() - Parse a DHCPv6 option value string into binary
> + * @code:	DHCPv6 option code
> + * @str:	Value string from command line
> + * @buf:	Output buffer for binary value
> + * @buf_len:	Size of output buffer
> + *
> + * Return: number of bytes written to @buf, or -1 on error
> + */
> +int dhcpv6_opt_parse(uint16_t code, const char *str,
> +		     uint8_t *buf, size_t buf_len)
> +{
> +	enum dhcpv6_opt_type type;
> +	unsigned long val;
> +	unsigned int i;
> +	uint8_t width;
> +	size_t slen;
> +	char *end;
> +	int len;
> +
> +	if (!*str)
> +		return -1;
> +
> +	if (code >= ARRAY_SIZE(dhcpv6_opt_types))
> +		return -1;
> +
> +	type = dhcpv6_opt_types[code];
> +
> +	switch (type) {
> +	case DHCPV6_OPT_NONE:
> +		return -1;
> +	case DHCPV6_OPT_IPV6:
> +	case DHCPV6_OPT_IPV6_LIST:
> +		len = 0;
> +
> +		while (*str) {
> +			char chunk[INET6_ADDRSTRLEN];
> +			size_t clen;
> +
> +			clen = strcspn(str, ",");
> +			if (!clen || clen >= sizeof(chunk))
> +				return -1;
> +
> +			if (len + (int)sizeof(struct in6_addr) > (int)buf_len)
> +				return -1;
> +
> +			memcpy(chunk, str, clen);
> +			chunk[clen] = '\0';
> +
> +			if (inet_pton(AF_INET6, chunk, buf + len) != 1)
> +				return -1;
> +
> +			len += sizeof(struct in6_addr);
> +
> +			if (type == DHCPV6_OPT_IPV6) {
> +				if (str[clen] == ',')
> +					return -1;
> +				break;
> +			}
> +
> +			str += clen + (str[clen] == ',');
> +		}
> +
> +		if (!len)
> +			return -1;
> +
> +		return len;
> +	case DHCPV6_OPT_INT8:
> +	case DHCPV6_OPT_INT16:
> +	case DHCPV6_OPT_INT32:
> +		if (type == DHCPV6_OPT_INT8)
> +			width = 1;
> +		else if (type == DHCPV6_OPT_INT16)
> +			width = 2;
> +		else
> +			width = 4;
> +
> +		if (buf_len < width)
> +			return -1;
> +
> +		errno = 0;
> +		val = strtoul(str, &end, 0);
> +
> +		if (*end || errno || val >= (1ULL << (width * 8)))
> +			return -1;
> +
> +		for (i = width; i > 0; i--) {
> +			buf[i - 1] = val & 0xff;
> +			val >>= 8;
> +		}

This looks correct to me, and it's safe regardless of the underlying
endianness because you're not addressing 'val' byte by byte. However,
it's in part a reimplementation of htons() and htonl().

Given that you already have explicit conditions setting the width
above, _maybe_ you could try to shuffle things around and turn those
into:

		if (type == DHCPV6_OPT_INT8) {
			width = 1;
		} else if (type == DHCPV6_OPT_INT16) {
			width = 2;
			*(uint16_t *)(buf + i) = htons(val);
		} else {
			width = 4;
			*(uint32_t *)(buf + i) = htons(val);
		}

but it needs to be combined with the checking you already added. I'm not
sure if it's worth it.

> +
> +		return width;
> +	case DHCPV6_OPT_STR:
> +		slen = strlen(str);
> +
> +		if (slen >= buf_len)
> +			return -1;
> +
> +		memcpy(buf, str, slen);
> +
> +		return slen;
> +	case DHCPV6_OPT_VENDOR_CLASS: {
> +		const char *colon;
> +		uint32_t ent;
> +
> +		colon = strchr(str, ':');
> +		if (!colon)
> +			return -1;
> +
> +		errno = 0;
> +		val = strtoul(str, &end, 0);
> +		if (end != colon || errno || val > UINT32_MAX)
> +			return -1;
> +
> +		slen = strlen(colon + 1);
> +		if (!slen)
> +			return -1;
> +
> +		len = sizeof(uint32_t) + sizeof(uint16_t) + slen;
> +		if ((size_t)len > buf_len)
> +			return -1;
> +
> +		ent = htonl(val);
> +		memcpy(buf, &ent, sizeof(ent));
> +
> +		buf[4] = slen >> 8;
> +		buf[5] = slen & 0xff;

Similar to my comment above: this could be done with htons().

> +
> +		memcpy(buf + sizeof(uint32_t) + sizeof(uint16_t),
> +		       colon + 1, slen);
> +
> +		return len;
> +	}
> +	case DHCPV6_OPT_LEN_STR_LIST:
> +		len = 0;
> +
> +		while (*str) {
> +			slen = strcspn(str, ",");
> +			if (!slen)
> +				return -1;
> +
> +			if (len + (int)(sizeof(uint16_t) + slen) > (int)buf_len)
> +				return -1;
> +
> +			buf[len]     = slen >> 8;
> +			buf[len + 1] = slen & 0xff;

Same here.

> +			len += sizeof(uint16_t);
> +
> +			memcpy(buf + len, str, slen);
> +			len += slen;
> +
> +			str += slen;
> +			if (*str == ',')
> +				str++;
> +		}
> +
> +		if (!len)
> +			return -1;
> +
> +		return len;
> +	}
> +
> +	return -1;
> +}
> +
>  /**
>   * dhcpv6_opt() - Get option from DHCPv6 message
>   * @data:	Buffer with options, set to matching option on return
> diff --git a/dhcpv6.h b/dhcpv6.h
> index c706dfd..2da1c76 100644
> --- a/dhcpv6.h
> +++ b/dhcpv6.h
> @@ -9,5 +9,7 @@
>  int dhcpv6(struct ctx *c, struct iov_tail *data,
>  	   struct in6_addr *saddr, struct in6_addr *daddr);
>  void dhcpv6_init(const struct ctx *c);
> +int dhcpv6_opt_parse(uint16_t code, const char *str,
> +		     uint8_t *buf, size_t buf_len);
>  
>  #endif /* DHCPV6_H */
> diff --git a/passt.1 b/passt.1
> index 908fd4a..0f771cb 100644
> --- a/passt.1
> +++ b/passt.1
> @@ -430,6 +430,37 @@ Send \fIname\fR as DHCP option 12 (hostname).
>  FQDN to configure the client with.
>  Send \fIname\fR as Client FQDN: DHCP option 81 and DHCPv6 option 39.
>  
> +.TP
> +.BR \-\-dhcpv6-opt " " \fICODE\fR,\fIVALUE\fR
> +Set DHCPv6 option \fICODE\fR to \fIVALUE\fR.  The value format depends
> +on the option type and is determined automatically from the option code.
> +Multiple IPv6 addresses are comma-separated.
> +This option can be specified multiple times.  If the same option code is
> +given more than once, the last value wins.
> +.RS
> +.TP
> +.B String options
> +59 (Boot File URL, RFC 5970)
> +.TP
> +.B Length-prefixed string list options (comma-separated entries)
> +15 (User Class, RFC 8415), 60 (Boot File Params, RFC 5970).
> +Each comma-separated entry is encoded with a 2-byte length prefix.
> +Example: \fB\-\-dhcpv6-opt 15,class1,class2\fR.
> +.TP
> +.B Vendor class options (ENTERPRISE:DATA format)
> +16 (Vendor Class, RFC 8415), 17 (Vendor-specific Info, RFC 8415).
> +VALUE is \fIENTERPRISE\fR:\fIDATA\fR where \fIENTERPRISE\fR is the IANA
> +Private Enterprise Number and \fIDATA\fR is the vendor class string.
> +Example: \fB\-\-dhcpv6-opt 16,0:HTTPClient\fR for UEFI HTTP Boot.
> +.TP
> +.B IPv6 address list options (comma-separated)
> +31 (SNTP Servers)
> +.TP
> +.B Integer options
> +7 (Preference, 8-bit), 32 (Information Refresh Time, 32-bit),
> +82 (SOL_MAX_RT, 32-bit), 83 (INF_MAX_RT, 32-bit)
> +.RE
> +
>  .TP
>  .BR \-t ", " \-\-tcp-ports " " \fIspec
>  Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of:
> diff --git a/passt.h b/passt.h
> index 3a07294..8f4ddef 100644
> --- a/passt.h
> +++ b/passt.h
> @@ -182,6 +182,10 @@ struct ip6_ctx {
>   * @dns_search:		DNS search list
>   * @hostname:		Guest hostname
>   * @fqdn:		Guest FQDN
> + * @dhcpv6_opts:	User-specified DHCPv6 options from --dhcpv6-opt
> + * @dhcpv6_opts.code:	DHCPv6 option code
> + * @dhcpv6_opts.str:	String value from command line
> + * @dhcpv6_opts_count:	Number of entries in @dhcpv6_opts
>   * @ifi6:		Template interface for IPv6, -1: none, 0: IPv6 disabled
>   * @ip6:		IPv6 configuration
>   * @pasta_ifn:		Name of namespace interface for pasta
> @@ -264,6 +268,14 @@ struct ctx {
>  	char hostname[PASST_MAXDNAME];
>  	char fqdn[PASST_MAXDNAME];
>  
> +#define MAX_DHCPV6_OPTS	32
> +
> +	struct {
> +		uint16_t code;
> +		char str[255];

Same as above: this needs to be UINT16_MAX, which, if we want to
support, say, 255 options, makes this approximately 16 MiB.

On the other hand, as long as the user doesn't use items in this array,
that memory isn't actually allocated anywhere, so the real size would
be very reasonable in practice.

Unlike DHCP, though, DHCPv6 has 16 bits of option code, and adopting
the same approach as for DHCP means 4 GiB of potential memory. Yes,
it's not allocated if not used, but it starts looking scary, and it
will give us problems with dynamic memory checkers such as valgrind.

Realistically, while a maximum number of 32 options looks exceedingly
restrictive to me, IANA allocated only up to option 150, and, given
that we only accept options we know about, we could limit this to the
current count of known options, and then, same as for IPv4, index those
options directly.

There are a few tricks to get, here, the size of the array as
initialised. You can't just move the enum to dhcpv6.h and add an extern
declaration such as, say:

  extern const enum dhcpv6_opt_type dhcpv6_opt_types[];

because, with that, the storage size isn't known. I think the least
ugly way is to move the actual initialisation of the array in the
header file, and use the __weak__ attribute, so that it will be defined
only once (and not as many times as the header is included, which would
lead to duplicate definitions).

That is, you could move the whole thing to dhcpv6.h like this:

/**
 * enum dhcpv6_opt_type - DHCPv6 option value types
 * @DHCPV6_OPT_NONE:           Unsupported or unknown option
 ...
 */
enum dhcpv6_opt_type {
	DHCPV6_OPT_NONE,
	...
};

/**
 * dhcpv6_opt_types - Maps DHCPv6 option code to value type, indexed by code
 * RFC 8415 Options: 7, 15, 16, 17, 32, 82, 83
 * RFC 5970 Options: 59, 60
 * RFC 4075 Options: 31
 */
__attribute__ ((weak)) const enum dhcpv6_opt_type dhcpv6_opt_types[] = {
	[7]   = DHCPV6_OPT_INT8,		/* Preference */
	...
};

and then include dhcpv6.h from passt.h. At that point, you could use something
like this here:

	struct {
		char str[UINT16_MAX - 4 /* code and length */];
	} dhcpv6_opts[ARRAY_SIZE(dhcpv6_opt_types)];

...and you don't need 'code' anymore because the position in the array
represents that.

That being said, I would rather suggest to use this to store the parsed value,
not the original value, so that we avoid parsing stuff twice. See my follow-up
comment to 2/2.

> +	} dhcpv6_opts[MAX_DHCPV6_OPTS];
> +	int dhcpv6_opts_count;
> +
>  	int ifi6;
>  	struct ip6_ctx ip6;
>  

-- 
Stefano


  parent reply	other threads:[~2026-07-01 16:59 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-18 12:05 [PATCH v2 0/2] dhcpv6: Add --dhcpv6-opt for custom DHCPv6 options Anshu Kumari
2026-06-18 12:05 ` [PATCH v2 1/2] dhcpv6: Add --dhcpv6-opt with option type table and value parser Anshu Kumari
2026-06-19  4:00   ` David Gibson
2026-07-01 16:59   ` Stefano Brivio [this message]
2026-06-18 12:05 ` [PATCH v2 2/2] dhcpv6: Inject custom options into DHCPv6 replies Anshu Kumari
2026-06-19  4:03   ` David Gibson
2026-07-01 17:00     ` Stefano Brivio
2026-07-01 16:59   ` Stefano Brivio

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=20260701185948.03f1557a@elisabeth \
    --to=sbrivio@redhat.com \
    --cc=anskuma@redhat.com \
    --cc=david@gibson.dropbear.id.au \
    --cc=jmaloy@redhat.com \
    --cc=lvivier@redhat.com \
    --cc=passt-dev@passt.top \
    /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).