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; 8+ 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] 8+ 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-07-01 16:59   ` Stefano Brivio
  2026-06-18 12:05 ` [PATCH v2 2/2] dhcpv6: Inject custom options into DHCPv6 replies Anshu Kumari
  1 sibling, 2 replies; 8+ 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] 8+ 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
  2026-07-01 16:59   ` Stefano Brivio
  1 sibling, 2 replies; 8+ 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] 8+ 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
  2026-07-01 16:59   ` Stefano Brivio
  1 sibling, 0 replies; 8+ 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] 8+ 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
  2026-07-01 17:00     ` Stefano Brivio
  2026-07-01 16:59   ` Stefano Brivio
  1 sibling, 1 reply; 8+ 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] 8+ 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
@ 2026-07-01 16:59   ` Stefano Brivio
  1 sibling, 0 replies; 8+ messages in thread
From: Stefano Brivio @ 2026-07-01 16:59 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: passt-dev, david, lvivier, jmaloy

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


^ permalink raw reply	[flat|nested] 8+ 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
@ 2026-07-01 16:59   ` Stefano Brivio
  1 sibling, 0 replies; 8+ messages in thread
From: Stefano Brivio @ 2026-07-01 16:59 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: passt-dev, david, lvivier, jmaloy

On Thu, 18 Jun 2026 17:35:29 +0530
Anshu Kumari <anskuma@redhat.com> 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.
> ---
>  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];

This should be UINT16_MAX. Other than that, this looks good to me,
except for pending comments from David and also my considerations on
top of that.

-- 
Stefano


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

* Re: [PATCH v2 2/2] dhcpv6: Inject custom options into DHCPv6 replies
  2026-06-19  4:03   ` David Gibson
@ 2026-07-01 17:00     ` Stefano Brivio
  0 siblings, 0 replies; 8+ messages in thread
From: Stefano Brivio @ 2026-07-01 17:00 UTC (permalink / raw)
  To: David Gibson; +Cc: Anshu Kumari, passt-dev, lvivier, jmaloy

On Fri, 19 Jun 2026 14:03:10 +1000
David Gibson <david@gibson.dropbear.id.au> wrote:

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

With this revision, they're not stored, but still parsed twice, and:

> The existing
> data structures for DHCPv6 are different, so maybe there's not an
> obvious place to store pre-parsed options.

...yes, I guess this is one reason, but as we only let users specify
options we have in the table, we could actually make space for that by
declaring an array of 64 KiB * 84 entries.

The highest option number we currently support is 83, and IANA only
registered up to number 150, so I would expect it to remain within a
reasonable range, and in any case only memory initialised here is
actually used / allocated.

Another bit missing would be the rendering of binary values back to
human-readable ones for conf_print() purposes. That looks like a bit
more effort.

On the other hand, one day we'll probably want to have pesto(1)
configure this stuff at runtime, and sharing binary values between
processes looks more practical than sharing configuration strings... so
I'd tend to say that it's effort we will need anyway at some point.

If it's too complicated for whatever reason, I'm fine with the current
approach as well. I'd just suggest to make enough room for all the
options we support, because that part is simple.

-- 
Stefano


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

end of thread, other threads:[~2026-07-01 17:00 UTC | newest]

Thread overview: 8+ 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-07-01 16:59   ` Stefano Brivio
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

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