public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
* [PATCH v2 0/2] dhcpv6: Add --dhcpv6-opt for custom DHCPv6 options
@ 2026-06-18 12:05 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-18 12:05 ` [PATCH v2 2/2] dhcpv6: Inject custom options into DHCPv6 replies Anshu Kumari
  0 siblings, 2 replies; 5+ messages in thread
From: Anshu Kumari @ 2026-06-18 12:05 UTC (permalink / raw)
  To: sbrivio, anskuma, passt-dev; +Cc: david, lvivier, jmaloy

This series adds a --dhcpv6-opt CODE,VALUE command-line option to
inject custom options into DHCPv6 replies, complementing the existing
--dhcp-opt support for DHCPv4.

The primary use case is UEFI HTTP Boot, which requires Vendor Class
(option 16) with the correct enterprise-number + length-prefixed wire
encoding per RFC 8415 Section 21.16, and Boot File URL (option 59).

Value formats are determined automatically from the option code via a
type table. Supported types include plain strings, IPv6
addresses (single and list), 8/16/32-bit integers, vendor class
(ENTERPRISE:DATA), and length-prefixed string lists.

Anshu Kumari (2):
  dhcpv6: Add --dhcpv6-opt with option type table and value parser
  dhcpv6: Inject custom options into DHCPv6 replies

 conf.c   |  60 ++++++++++++-
 dhcpv6.c | 253 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 dhcpv6.h |   2 +
 passt.1  |  31 +++++++
 passt.h  |  12 +++
 5 files changed, 357 insertions(+), 1 deletion(-)

-- 
2.54.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v2 1/2] dhcpv6: Add --dhcpv6-opt with option type table and value parser
  2026-06-18 12:05 [PATCH v2 0/2] dhcpv6: Add --dhcpv6-opt for custom DHCPv6 options Anshu Kumari
@ 2026-06-18 12:05 ` Anshu Kumari
  2026-06-19  4:00   ` David Gibson
  2026-06-18 12:05 ` [PATCH v2 2/2] dhcpv6: Inject custom options into DHCPv6 replies Anshu Kumari
  1 sibling, 1 reply; 5+ messages in thread
From: Anshu Kumari @ 2026-06-18 12:05 UTC (permalink / raw)
  To: sbrivio, anskuma, passt-dev; +Cc: david, lvivier, jmaloy

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];
+	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)
+			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;
 	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
+ */
+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;
+		}
+
+		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;
+
+		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;
+			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];
+	} dhcpv6_opts[MAX_DHCPV6_OPTS];
+	int dhcpv6_opts_count;
+
 	int ifi6;
 	struct ip6_ctx ip6;
 
-- 
2.54.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v2 2/2] dhcpv6: Inject custom options into DHCPv6 replies
  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-18 12:05 ` Anshu Kumari
  2026-06-19  4:03   ` David Gibson
  1 sibling, 1 reply; 5+ messages in thread
From: Anshu Kumari @ 2026-06-18 12:05 UTC (permalink / raw)
  To: sbrivio, anskuma, passt-dev; +Cc: david, lvivier, jmaloy

Append user-specified options from --dhcpv6-opt to DHCPv6 reply
messages.  Options are parsed from the stored string value at reply
time using dhcpv6_opt_parse(), and skipped with a debug message if
they exceed the available space.

Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
v2:
  - Updated dhcpv6_custom_opts_fill() to parse str at reply time
    using dhcpv6_opt_parse() instead of copying cached val/len.
---
 dhcpv6.c | 44 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 44 insertions(+)

diff --git a/dhcpv6.c b/dhcpv6.c
index 1e1dd0d..6b1e90b 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -748,6 +748,49 @@ static size_t dhcpv6_client_fqdn_fill(const struct iov_tail *data,
 	return offset + sizeof(struct opt_hdr) + opt_len;
 }
 
+/**
+ * dhcpv6_custom_opts_fill() - Append user-specified custom options to reply
+ * @c:		Execution context
+ * @buf:	Response message buffer
+ * @offset:	Current offset in buffer
+ *
+ * Return: updated offset after appending custom options
+ */
+static size_t dhcpv6_custom_opts_fill(const struct ctx *c,
+				      char *buf, int offset)
+{
+	int i;
+
+	for (i = 0; i < c->dhcpv6_opts_count; i++) {
+		uint16_t code = c->dhcpv6_opts[i].code;
+		struct opt_hdr *hdr;
+		uint8_t val[255];
+		int vlen;
+
+		vlen = dhcpv6_opt_parse(code, c->dhcpv6_opts[i].str,
+					val, sizeof(val));
+		if (vlen < 0)
+			continue;
+
+		if ((size_t)offset + sizeof(struct opt_hdr) + vlen >
+		    OPT_MAX_SIZE) {
+			debug("DHCPv6: custom option %u doesn't fit,"
+			      " skipping", code);
+			continue;
+		}
+
+		hdr = (struct opt_hdr *)(buf + offset);
+		hdr->t = htons(code);
+		hdr->l = htons(vlen);
+		offset += sizeof(struct opt_hdr);
+
+		memcpy(buf + offset, val, vlen);
+		offset += vlen;
+	}
+
+	return offset;
+}
+
 /**
  * dhcpv6() - Check if this is a DHCPv6 message, reply as needed
  * @c:		Execution context
@@ -886,6 +929,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
 	    sizeof(struct opt_hdr) + ntohs(client_id->l);
 	n = dhcpv6_dns_fill(c, (char *)&resp, n);
 	n = dhcpv6_client_fqdn_fill(data, c, (char *)&resp, n);
+	n = dhcpv6_custom_opts_fill(c, (char *)&resp, n);
 
 	resp.hdr.xid = mh->xid;
 
-- 
2.54.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH v2 1/2] dhcpv6: Add --dhcpv6-opt with option type table and value parser
  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
  0 siblings, 0 replies; 5+ messages in thread
From: David Gibson @ 2026-06-19  4:00 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy

[-- Attachment #1: Type: text/plain, Size: 14850 bytes --]

On Thu, Jun 18, 2026 at 05:35:28PM +0530, Anshu Kumari 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];
> +	int idx;
> +
> +	if (dhcpv6_opt_parse(code, val_str, tmp, sizeof(tmp)) < 0)
> +		die("Invalid value for DHCPv6 option %u: %s", code, val_str);

As for DHCPv4, I wonder if we can avoid double parsing each option.  I
realise the data structures in dhcpv6.c are different from those in
dhcp.c, so it might not reasonable here, even if it is there.

> +	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)
> +			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;
>  	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
> + */
> +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) {

A number of these types result in identical parsing to DHCPv4.  Could
we have a unified parser function?  Obviously things like IPv4 address
and IPv6 address would need different types, but we should be able to
share at least the integer and string code.

> +	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;
> +		}
> +
> +		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;
> +
> +		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;
> +			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];
> +	} dhcpv6_opts[MAX_DHCPV6_OPTS];
> +	int dhcpv6_opts_count;
> +
>  	int ifi6;
>  	struct ip6_ctx ip6;
>  
> -- 
> 2.54.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 --]

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH v2 2/2] dhcpv6: Inject custom options into DHCPv6 replies
  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
  0 siblings, 0 replies; 5+ messages in thread
From: David Gibson @ 2026-06-19  4:03 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy

[-- Attachment #1: Type: text/plain, Size: 3094 bytes --]

On Thu, Jun 18, 2026 at 05:35:29PM +0530, Anshu Kumari wrote:
> Append user-specified options from --dhcpv6-opt to DHCPv6 reply
> messages.  Options are parsed from the stored string value at reply
> time using dhcpv6_opt_parse(), and skipped with a debug message if
> they exceed the available space.
> 
> Link: https://bugs.passt.top/show_bug.cgi?id=192
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> ---
> v2:
>   - Updated dhcpv6_custom_opts_fill() to parse str at reply time
>     using dhcpv6_opt_parse() instead of copying cached val/len.

As with DHCPv4, it's not that I'm against storing the parsed values
persistently, just that I'm against storing them twice.  The existing
data structures for DHCPv6 are different, so maybe there's not an
obvious place to store pre-parsed options.

> ---
>  dhcpv6.c | 44 ++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 44 insertions(+)
> 
> diff --git a/dhcpv6.c b/dhcpv6.c
> index 1e1dd0d..6b1e90b 100644
> --- a/dhcpv6.c
> +++ b/dhcpv6.c
> @@ -748,6 +748,49 @@ static size_t dhcpv6_client_fqdn_fill(const struct iov_tail *data,
>  	return offset + sizeof(struct opt_hdr) + opt_len;
>  }
>  
> +/**
> + * dhcpv6_custom_opts_fill() - Append user-specified custom options to reply

As discussed elsewhere "custom" isn't a great name.  "user" might be
better, or just drop it.


> + * @c:		Execution context
> + * @buf:	Response message buffer
> + * @offset:	Current offset in buffer
> + *
> + * Return: updated offset after appending custom options
> + */
> +static size_t dhcpv6_custom_opts_fill(const struct ctx *c,
> +				      char *buf, int offset)
> +{
> +	int i;
> +
> +	for (i = 0; i < c->dhcpv6_opts_count; i++) {
> +		uint16_t code = c->dhcpv6_opts[i].code;
> +		struct opt_hdr *hdr;
> +		uint8_t val[255];
> +		int vlen;
> +
> +		vlen = dhcpv6_opt_parse(code, c->dhcpv6_opts[i].str,
> +					val, sizeof(val));
> +		if (vlen < 0)
> +			continue;
> +
> +		if ((size_t)offset + sizeof(struct opt_hdr) + vlen >
> +		    OPT_MAX_SIZE) {
> +			debug("DHCPv6: custom option %u doesn't fit,"
> +			      " skipping", code);
> +			continue;
> +		}
> +
> +		hdr = (struct opt_hdr *)(buf + offset);
> +		hdr->t = htons(code);
> +		hdr->l = htons(vlen);
> +		offset += sizeof(struct opt_hdr);
> +
> +		memcpy(buf + offset, val, vlen);
> +		offset += vlen;
> +	}
> +
> +	return offset;
> +}
> +
>  /**
>   * dhcpv6() - Check if this is a DHCPv6 message, reply as needed
>   * @c:		Execution context
> @@ -886,6 +929,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
>  	    sizeof(struct opt_hdr) + ntohs(client_id->l);
>  	n = dhcpv6_dns_fill(c, (char *)&resp, n);
>  	n = dhcpv6_client_fqdn_fill(data, c, (char *)&resp, n);
> +	n = dhcpv6_custom_opts_fill(c, (char *)&resp, n);
>  
>  	resp.hdr.xid = mh->xid;
>  
> -- 
> 2.54.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 --]

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-06-19  4:03 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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-06-18 12:05 ` [PATCH v2 2/2] dhcpv6: Inject custom options into DHCPv6 replies Anshu Kumari
2026-06-19  4:03   ` David Gibson

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).