* [PATCH v4 0/2] [PATCH 0/2] dhcpv6: Add --dhcpv6-opt for custom DHCPv6 options
@ 2026-10-06 9:04 Anshu Kumari
2026-10-06 9:04 ` [PATCH v4 1/2] dhcpv6: Add --dhcpv6-opt with option type table and value parser Anshu Kumari
2026-10-06 9:04 ` [PATCH v4 2/2] dhcpv6: Inject custom options into DHCPv6 replies Anshu Kumari
0 siblings, 2 replies; 5+ messages in thread
From: Anshu Kumari @ 2026-10-06 9:04 UTC (permalink / raw)
To: sbrivio, passt-dev; +Cc: lvivier, anskuma
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 | 26 +++-
dhcpv6.c | 375 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
dhcpv6.h | 2 +
passt.1 | 31 +++++
4 files changed, 433 insertions(+), 1 deletion(-)
--
2.55.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v4 1/2] dhcpv6: Add --dhcpv6-opt with option type table and value parser
2026-10-06 9:04 [PATCH v4 0/2] [PATCH 0/2] dhcpv6: Add --dhcpv6-opt for custom DHCPv6 options Anshu Kumari
@ 2026-10-06 9:04 ` Anshu Kumari
2026-10-25 12:01 ` Stefano Brivio
2026-10-06 9:04 ` [PATCH v4 2/2] dhcpv6: Inject custom options into DHCPv6 replies Anshu Kumari
1 sibling, 1 reply; 5+ messages in thread
From: Anshu Kumari @ 2026-10-06 9:04 UTC (permalink / raw)
To: sbrivio, passt-dev; +Cc: lvivier, anskuma
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>
---
v4:
- The dhcpv6_opt_code < 1 check was replaced with proper validation:
dhcpv6_opt_code == 0 || dhcpv6_opt_code > UINT16_MAX in conf.c
- All uint16_t/uint32_t writes to byte buffers now use intermediate
variables + memcpy() instead of direct pointer casts.
- The DHCPV6_OPT_STR case now uses slen > buf_len instead of slen >= buf_len.
- Option 17 (Vendor-specific Info) excluded.
- A separate char optbuf[512] was added in conf_print() instead of
reusing the small INANY_ADDRSTRLEN-sized buf for option rendering.
- inet_ntop() return value checked.
- Uses off += strlen(buf + off) instead of off = strlen(buf) in
the IPv6 list rendering loop.
- Iterator macro added — for_each_in6_off(off, len).
- The addr and af variables are declared at function scope
in dhcpv6_opt_parse() rather than inside block wrappers.
- snprintf() alignment and line formatting.
v3:
- Use parse_unsigned(), parse_literal(), parse_eoi() from parse.c
instead of manual strtoul()/errno/strchr() in dhcpv6_opt_parse()
for UINT and VENDOR_CLASS cases.
- Store options as pre-parsed binary in a static array indexed by
option code, instead of storing raw strings.
- Add dhcpv6_opt_to_str() to render binary option values back to
printable strings for startup logging in conf_print().
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 | 26 ++++-
dhcpv6.c | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
dhcpv6.h | 2 +
passt.1 | 31 +++++
4 files changed, 397 insertions(+), 1 deletion(-)
diff --git a/conf.c b/conf.c
index faf26814..dba62bf8 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"
@@ -632,7 +633,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
@@ -800,6 +802,7 @@ enum passt_modes conf_mode(int argc, char *argv[])
static void conf_print(const struct ctx *c)
{
char buf[INANY_ADDRSTRLEN];
+ char optbuf[512];
int i;
if (c->fd_control_listen >= 0)
@@ -900,6 +903,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 = 1; i < UINT16_MAX; i++) {
+ if (dhcpv6_opt_to_str(i, optbuf, sizeof(optbuf)))
+ info(" option %u: %s", i, optbuf);
+ }
dns6:
for (i = 0; i < ARRAY_SIZE(c->ip6.dns); i++) {
@@ -1348,6 +1355,7 @@ void conf(struct ctx *c, int argc, char **argv)
{"stats", required_argument, NULL, 31 },
{"conf-path", required_argument, NULL, 'c' },
{"chroot-fallback", no_argument, NULL, 32 },
+ {"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:";
@@ -1589,6 +1597,22 @@ void conf(struct ctx *c, int argc, char **argv)
case 32:
c->chroot_fallback = true;
break;
+ case 35: {
+ unsigned long dhcpv6_opt_code;
+
+ p = optarg;
+
+ if (!parse_unsigned(&p, 0, &dhcpv6_opt_code) ||
+ !parse_literal(&p, ","))
+ die("--dhcpv6-opt requires CODE,VALUE format");
+
+ if (dhcpv6_opt_code == 0 ||
+ dhcpv6_opt_code > UINT16_MAX)
+ die("Invalid DHCPv6 option code: %s", optarg);
+
+ dhcpv6_set_opt(dhcpv6_opt_code, p);
+ break;
+ }
case 'd':
c->debug = 1;
c->quiet = 0;
diff --git a/dhcpv6.c b/dhcpv6.c
index 29c7e320..abc45813 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -31,6 +31,8 @@
#include "passt.h"
#include "tap.h"
#include "log.h"
+#include "inany.h"
+#include "parse.h"
/**
* struct opt_hdr - DHCPv6 option header
@@ -63,6 +65,11 @@ struct opt_hdr {
#define OPT_MAX_SIZE IPV6_MIN_MTU - (sizeof(struct ipv6hdr) + \
UDP_MSG_HDR_SIZE)
+#define for_each_in6_off(off, len) \
+ for ((off) = 0; \
+ (off) + sizeof(struct in6_addr) <= (unsigned int)(len); \
+ (off) += sizeof(struct in6_addr))
+
/**
* struct opt_client_id - DHCPv6 Client Identifier option
* @hdr: Option header
@@ -278,6 +285,338 @@ 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_UINT8: Unsigned 8-bit integer
+ * @DHCPV6_OPT_UINT16: Unsigned 16-bit integer
+ * @DHCPV6_OPT_UINT32: 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_UINT8,
+ DHCPV6_OPT_UINT16,
+ DHCPV6_OPT_UINT32,
+ 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, 32, 82, 83
+ * RFC 5970 Options: 59, 60
+ * RFC 4075 Options: 31
+ */
+static const enum dhcpv6_opt_type dhcpv6_opt_types[] = {
+ [7] = DHCPV6_OPT_UINT8, /* Preference */
+ [15] = DHCPV6_OPT_LEN_STR_LIST, /* User Class */
+ [16] = DHCPV6_OPT_VENDOR_CLASS, /* Vendor Class */
+ [31] = DHCPV6_OPT_IPV6_LIST, /* SNTP Servers */
+ [32] = DHCPV6_OPT_UINT32, /* Information Refresh Time */
+ [59] = DHCPV6_OPT_STR, /* Boot File URL */
+ [60] = DHCPV6_OPT_LEN_STR_LIST, /* Boot File Params */
+ [82] = DHCPV6_OPT_UINT32, /* SOL_MAX_RT */
+ [83] = DHCPV6_OPT_UINT32, /* 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
+ */
+static int dhcpv6_opt_parse(uint16_t code, const char *str,
+ uint8_t *buf, size_t buf_len)
+{
+ enum dhcpv6_opt_type type;
+ union inany_addr addr;
+ unsigned long val;
+ uint16_t slen_net;
+ sa_family_t af;
+ uint8_t width;
+ uint32_t ent;
+ size_t slen;
+ 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;
+
+ do {
+ if (len + sizeof(struct in6_addr) > buf_len)
+ return -1;
+
+ if (!parse_inany_(&str, &addr, &af) || af != AF_INET6)
+ return -1;
+
+ memcpy(buf + len, &addr.a6, sizeof(struct in6_addr));
+ len += sizeof(struct in6_addr);
+
+ if (type == DHCPV6_OPT_IPV6)
+ break;
+ } while (parse_literal(&str, ","));
+
+ if (!len || !parse_eoi(str))
+ return -1;
+
+ return len;
+ case DHCPV6_OPT_UINT8:
+ case DHCPV6_OPT_UINT16:
+ case DHCPV6_OPT_UINT32:
+ if (type == DHCPV6_OPT_UINT8)
+ width = 1;
+ else if (type == DHCPV6_OPT_UINT16)
+ width = 2;
+ else
+ width = 4;
+
+ if (buf_len < width)
+ return -1;
+
+ if (!parse_unsigned(&str, 0, &val) || !parse_eoi(str) ||
+ val >= (1ULL << (width * 8)))
+ return -1;
+
+ if (type == DHCPV6_OPT_UINT16) {
+ uint16_t v16 = htons(val);
+ memcpy(buf, &v16, sizeof(v16));
+ } else if (type == DHCPV6_OPT_UINT32) {
+ uint32_t v32 = htonl(val);
+ memcpy(buf, &v32, sizeof(v32));
+ } else {
+ buf[0] = val;
+ }
+
+ 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:
+ if (!parse_unsigned(&str, 0, &val) ||
+ !parse_literal(&str, ":") || val > UINT32_MAX)
+ return -1;
+
+ slen = strlen(str);
+ 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));
+
+ slen_net = htons(slen);
+ memcpy(buf + sizeof(uint32_t), &slen_net, sizeof(slen_net));
+
+ memcpy(buf + sizeof(uint32_t) + sizeof(uint16_t), str, 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;
+
+ slen_net = htons(slen);
+ memcpy(buf + len, &slen_net, sizeof(slen_net));
+ len += sizeof(uint16_t);
+
+ memcpy(buf + len, str, slen);
+ len += slen;
+
+ str += slen;
+ if (*str == ',')
+ str++;
+ }
+
+ if (!len)
+ return -1;
+
+ return len;
+ }
+
+ return -1;
+}
+
+/**
+ * struct dhcpv6_user_opts - User-specified DHCPv6 options from --dhcpv6-opt
+ * @val: Binary option value in wire format
+ * @len: Length of @val in bytes, 0 if not set
+ */
+static struct {
+ uint8_t val[UINT16_MAX];
+ int len;
+} dhcpv6_user_opts[ARRAY_SIZE(dhcpv6_opt_types)];
+
+/**
+ * dhcpv6_opt_to_str() - Render a binary DHCPv6 option value to printable string
+ * @code: DHCPv6 option code
+ * @buf: Output string buffer
+ * @buf_len: Size of output buffer
+ *
+ * Return: pointer to @buf if option is set, NULL otherwise
+ */
+const char *dhcpv6_opt_to_str(uint16_t code, char *buf, size_t buf_len)
+{
+ enum dhcpv6_opt_type type;
+ unsigned int i;
+ uint16_t slen;
+ int off = 0;
+
+ if (code >= ARRAY_SIZE(dhcpv6_user_opts) ||
+ !dhcpv6_user_opts[code].len)
+ return NULL;
+
+ type = dhcpv6_opt_types[code];
+
+ switch (type) {
+ case DHCPV6_OPT_IPV6:
+ case DHCPV6_OPT_IPV6_LIST:
+ for_each_in6_off(i, dhcpv6_user_opts[code].len) {
+ if (off) {
+ if (off + 1 >= (int)buf_len)
+ return NULL;
+ buf[off++] = ',';
+ }
+ if (!inet_ntop(AF_INET6,
+ dhcpv6_user_opts[code].val + i,
+ buf + off, buf_len - off))
+ return NULL;
+ off += strlen(buf + off);
+ }
+ return buf;
+ case DHCPV6_OPT_UINT8:
+ case DHCPV6_OPT_UINT16:
+ case DHCPV6_OPT_UINT32: {
+ uint32_t val = 0;
+
+ if (dhcpv6_user_opts[code].len == 1) {
+ val = dhcpv6_user_opts[code].val[0];
+ } else if (dhcpv6_user_opts[code].len == 2) {
+ uint16_t v16;
+ memcpy(&v16, dhcpv6_user_opts[code].val, sizeof(v16));
+ val = ntohs(v16);
+ } else if (dhcpv6_user_opts[code].len == 4) {
+ memcpy(&val, dhcpv6_user_opts[code].val, sizeof(val));
+ val = ntohl(val);
+ }
+
+ (void)snprintf(buf, buf_len, "%u", val);
+ return buf;
+ }
+ case DHCPV6_OPT_STR:
+ (void)snprintf(buf, buf_len, "%.*s",
+ dhcpv6_user_opts[code].len,
+ dhcpv6_user_opts[code].val);
+ return buf;
+ case DHCPV6_OPT_VENDOR_CLASS: {
+ uint32_t ent;
+ int doff;
+
+ if (dhcpv6_user_opts[code].len <
+ (int)(sizeof(uint32_t) + sizeof(uint16_t)))
+ return NULL;
+
+ memcpy(&ent, dhcpv6_user_opts[code].val, sizeof(ent));
+ memcpy(&slen, dhcpv6_user_opts[code].val + sizeof(uint32_t),
+ sizeof(slen));
+ doff = sizeof(uint32_t) + sizeof(uint16_t);
+
+ (void)snprintf(buf, buf_len, "%u:%.*s",
+ ntohl(ent), ntohs(slen),
+ dhcpv6_user_opts[code].val + doff);
+ return buf;
+ }
+ case DHCPV6_OPT_LEN_STR_LIST:
+ off = 0;
+ i = 0;
+
+ while (i + sizeof(uint16_t) <=
+ (unsigned int)dhcpv6_user_opts[code].len) {
+ memcpy(&slen, dhcpv6_user_opts[code].val + i,
+ sizeof(slen));
+ slen = ntohs(slen);
+ i += sizeof(uint16_t);
+
+ if (i + slen > (unsigned int)dhcpv6_user_opts[code].len)
+ break;
+
+ if (off) {
+ if (off + 1 >= (int)buf_len)
+ return NULL;
+ buf[off++] = ',';
+ }
+ if (off + slen >= (int)buf_len)
+ return NULL;
+ memcpy(buf + off, dhcpv6_user_opts[code].val + i,
+ slen);
+ off += slen;
+ i += slen;
+ }
+ buf[off] = '\0';
+ return buf;
+ default:
+ return NULL;
+ }
+}
+
+/**
+ * dhcpv6_set_opt() - Parse and store a user-specified DHCPv6 option
+ * @code: DHCPv6 option code
+ * @val_str: Value string from command line
+ */
+void dhcpv6_set_opt(uint16_t code, const char *val_str)
+{
+ int ret;
+
+ if (code >= ARRAY_SIZE(dhcpv6_opt_types) ||
+ dhcpv6_opt_types[code] == DHCPV6_OPT_NONE)
+ die("DHCPv6 option %u is not supported", code);
+
+ ret = dhcpv6_opt_parse(code, val_str,
+ dhcpv6_user_opts[code].val,
+ sizeof(dhcpv6_user_opts[code].val));
+ if (ret < 0)
+ die("Invalid value for DHCPv6 option %u: %s", code, val_str);
+
+ dhcpv6_user_opts[code].len = ret;
+}
+
/**
* 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 1015a1a7..973c477c 100644
--- a/dhcpv6.h
+++ b/dhcpv6.h
@@ -9,5 +9,7 @@
int dhcpv6(struct ctx *c, struct iov_tail *data,
const struct in6_addr *saddr, const struct in6_addr *daddr);
void dhcpv6_init(const struct ctx *c);
+void dhcpv6_set_opt(uint16_t code, const char *val_str);
+const char *dhcpv6_opt_to_str(uint16_t code, char *buf, size_t buf_len);
#endif /* DHCPV6_H */
diff --git a/passt.1 b/passt.1
index 53e072a9..249ed5cf 100644
--- a/passt.1
+++ b/passt.1
@@ -440,6 +440,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).
+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 either:
--
2.55.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v4 2/2] dhcpv6: Inject custom options into DHCPv6 replies
2026-10-06 9:04 [PATCH v4 0/2] [PATCH 0/2] dhcpv6: Add --dhcpv6-opt for custom DHCPv6 options Anshu Kumari
2026-10-06 9:04 ` [PATCH v4 1/2] dhcpv6: Add --dhcpv6-opt with option type table and value parser Anshu Kumari
@ 2026-10-06 9:04 ` Anshu Kumari
2026-10-25 12:01 ` Stefano Brivio
1 sibling, 1 reply; 5+ messages in thread
From: Anshu Kumari @ 2026-10-06 9:04 UTC (permalink / raw)
To: sbrivio, passt-dev; +Cc: lvivier, anskuma
Append user-specified options from --dhcpv6-opt to DHCPv6 reply
messages. Options that exceed available buffer space are skipped
with a warning.
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
v4:
- warn() instead of debug() when options don't fit in the reply.
- doc comment for dhcpv6_user_opts_fill() updated.
v3:
- rename dhcpv6_custom_opts_fill() to dhcpv6_user_opts_fill().
- Iterate directly over the pre-parsed binary dhcpv6_user_opts[]
array instead of re-parsing string values at reply time.
v2:
- Updated dhcpv6_custom_opts_fill() to parse str at reply time
using dhcpv6_opt_parse() instead of copying cached val/len.
---
dhcpv6.c | 36 ++++++++++++++++++++++++++++++++++++
1 file changed, 36 insertions(+)
diff --git a/dhcpv6.c b/dhcpv6.c
index abc45813..a0545406 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -879,6 +879,41 @@ static size_t dhcpv6_client_fqdn_fill(const struct iov_tail *data,
return offset + sizeof(struct opt_hdr) + opt_len;
}
+/**
+ * dhcpv6_user_opts_fill() - Append user-specified options to reply
+ * @buf: Response message buffer
+ * @offset: Current offset in buffer
+ *
+ * Return: offset after appended user options
+ */
+static size_t dhcpv6_user_opts_fill(char *buf, size_t offset)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(dhcpv6_user_opts); i++) {
+ int vlen = dhcpv6_user_opts[i].len;
+ struct opt_hdr *hdr;
+
+ if (!vlen)
+ continue;
+
+ if (offset + sizeof(struct opt_hdr) + vlen > OPT_MAX_SIZE) {
+ warn("DHCPv6: option %u doesn't fit, skipping", i);
+ continue;
+ }
+
+ hdr = (struct opt_hdr *)(buf + offset);
+ hdr->t = htons(i);
+ hdr->l = htons(vlen);
+ offset += sizeof(struct opt_hdr);
+
+ memcpy(buf + offset, dhcpv6_user_opts[i].val, vlen);
+ offset += vlen;
+ }
+
+ return offset;
+}
+
/**
* dhcpv6() - Check if this is a DHCPv6 message, reply as needed
* @c: Execution context
@@ -1017,6 +1052,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_user_opts_fill((char *)&resp, n);
resp.hdr.xid = mh->xid;
--
2.55.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v4 1/2] dhcpv6: Add --dhcpv6-opt with option type table and value parser
2026-10-06 9:04 ` [PATCH v4 1/2] dhcpv6: Add --dhcpv6-opt with option type table and value parser Anshu Kumari
@ 2026-10-25 12:01 ` Stefano Brivio
0 siblings, 0 replies; 5+ messages in thread
From: Stefano Brivio @ 2026-10-25 12:01 UTC (permalink / raw)
To: Anshu Kumari; +Cc: passt-dev, lvivier
On Tue, 6 Oct 2026 14:34:52 +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>
> ---
> v4:
> - The dhcpv6_opt_code < 1 check was replaced with proper validation:
> dhcpv6_opt_code == 0 || dhcpv6_opt_code > UINT16_MAX in conf.c
> - All uint16_t/uint32_t writes to byte buffers now use intermediate
> variables + memcpy() instead of direct pointer casts.
> - The DHCPV6_OPT_STR case now uses slen > buf_len instead of slen >= buf_len.
> - Option 17 (Vendor-specific Info) excluded.
> - A separate char optbuf[512] was added in conf_print() instead of
> reusing the small INANY_ADDRSTRLEN-sized buf for option rendering.
> - inet_ntop() return value checked.
> - Uses off += strlen(buf + off) instead of off = strlen(buf) in
> the IPv6 list rendering loop.
> - Iterator macro added — for_each_in6_off(off, len).
> - The addr and af variables are declared at function scope
> in dhcpv6_opt_parse() rather than inside block wrappers.
> - snprintf() alignment and line formatting.
>
> v3:
> - Use parse_unsigned(), parse_literal(), parse_eoi() from parse.c
> instead of manual strtoul()/errno/strchr() in dhcpv6_opt_parse()
> for UINT and VENDOR_CLASS cases.
> - Store options as pre-parsed binary in a static array indexed by
> option code, instead of storing raw strings.
> - Add dhcpv6_opt_to_str() to render binary option values back to
> printable strings for startup logging in conf_print().
>
> 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 | 26 ++++-
> dhcpv6.c | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
> dhcpv6.h | 2 +
> passt.1 | 31 +++++
> 4 files changed, 397 insertions(+), 1 deletion(-)
Note: this will have a trivial conflict with --no-pidns in conf(), will
need a rebase.
> diff --git a/conf.c b/conf.c
> index faf26814..dba62bf8 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"
> @@ -632,7 +633,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
> @@ -800,6 +802,7 @@ enum passt_modes conf_mode(int argc, char *argv[])
> static void conf_print(const struct ctx *c)
> {
> char buf[INANY_ADDRSTRLEN];
> + char optbuf[512];
We could actually pack about 4k IPv6 addresses in a DHCPv6 option, and
they need up to 46 bytes (IPv4-mapped address, with separator) to be
represented, but having ~ 190 KiB on the stack for that isn't great.
This doesn't make any practical sense for the moment by the way, as all
we can encode as list of addresses is SNTP servers (and we won't have
12 of them), but maybe in the future we might want to support a larger
number of addresses for some other option.
So you could do this: use a reasonable multiple of INANY_ADDRSTRLEN
here (say, INANY_ADDRSTRLEN * 2, note that it already includes a
terminator which you can use as separator), and then:
> int i;
>
> if (c->fd_control_listen >= 0)
> @@ -900,6 +903,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 = 1; i < UINT16_MAX; i++) {
> + if (dhcpv6_opt_to_str(i, optbuf, sizeof(optbuf)))
...pass an offset, starting from 0, opaque to the caller, and turn this
into a loop, say:
while (dhcpv6_opt_to_str(i, buf, sizeof(buf)), &off)
and dhcpv6_opt_to_str() will use that offset to go over addresses
(see comments there).
With that, you would print several "option" lines in case you exceed
the buffer, but it's actually better than the alternative, I think.
By limiting the buffer to 2 addresses, the output could be even easier
to read.
> + info(" option %u: %s", i, optbuf);
> + }
>
> dns6:
> for (i = 0; i < ARRAY_SIZE(c->ip6.dns); i++) {
> @@ -1348,6 +1355,7 @@ void conf(struct ctx *c, int argc, char **argv)
> {"stats", required_argument, NULL, 31 },
> {"conf-path", required_argument, NULL, 'c' },
> {"chroot-fallback", no_argument, NULL, 32 },
> + {"dhcpv6-opt", required_argument, NULL, 35 },
At some point it wasn't done consistently anymore, but I think it would
be nice to not break this further (and maybe make this all consistent
at some point): the 'has_arg' fields were aligned with tabs where
possible, that is:
{"dhcpv6-opt", required_argument, NULL, 35 },
instead.
> { 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:";
> @@ -1589,6 +1597,22 @@ void conf(struct ctx *c, int argc, char **argv)
> case 32:
> c->chroot_fallback = true;
> break;
> + case 35: {
Same comment as for the DHCP patches about extra / artificial code
blocks.
> + unsigned long dhcpv6_opt_code;
> +
> + p = optarg;
> +
> + if (!parse_unsigned(&p, 0, &dhcpv6_opt_code) ||
> + !parse_literal(&p, ","))
> + die("--dhcpv6-opt requires CODE,VALUE format");
> +
> + if (dhcpv6_opt_code == 0 ||
> + dhcpv6_opt_code > UINT16_MAX)
> + die("Invalid DHCPv6 option code: %s", optarg);
> +
> + dhcpv6_set_opt(dhcpv6_opt_code, p);
> + break;
> + }
> case 'd':
> c->debug = 1;
> c->quiet = 0;
> diff --git a/dhcpv6.c b/dhcpv6.c
> index 29c7e320..abc45813 100644
> --- a/dhcpv6.c
> +++ b/dhcpv6.c
> @@ -31,6 +31,8 @@
> #include "passt.h"
> #include "tap.h"
> #include "log.h"
> +#include "inany.h"
> +#include "parse.h"
>
> /**
> * struct opt_hdr - DHCPv6 option header
> @@ -63,6 +65,11 @@ struct opt_hdr {
> #define OPT_MAX_SIZE IPV6_MIN_MTU - (sizeof(struct ipv6hdr) + \
> UDP_MSG_HDR_SIZE)
>
> +#define for_each_in6_off(off, len) \
> + for ((off) = 0; \
> + (off) + sizeof(struct in6_addr) <= (unsigned int)(len); \
> + (off) += sizeof(struct in6_addr))
> +
> /**
> * struct opt_client_id - DHCPv6 Client Identifier option
> * @hdr: Option header
> @@ -278,6 +285,338 @@ 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_UINT8: Unsigned 8-bit integer
> + * @DHCPV6_OPT_UINT16: Unsigned 16-bit integer
> + * @DHCPV6_OPT_UINT32: 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_UINT8,
> + DHCPV6_OPT_UINT16,
> + DHCPV6_OPT_UINT32,
> + DHCPV6_OPT_VENDOR_CLASS,
> + DHCPV6_OPT_LEN_STR_LIST,
> +};
> +
> +/**
> + * dhcpv6_opt_types - Maps DHCPv6 option code to value type, indexed by code
It's an enum, so this should say " * enum ...". And "map" (imperative)
instead of "maps" (third-person indicative) as it's the convention we
use in all code comments.
About the name: I was about to suggest dhcpv6_opt_type, because the
fact it's an array makes it plural ('s' -> '[]'), but it conflicts with
the enum describing types. What about calling this dhcpv6_opt in a more
general sense? Say:
* enum dhcpv6_opt - Description of DHCPv6 options (value type), indexed by code
?
> + * RFC 8415 Options: 7, 15, 16, 32, 82, 83
I was mentioning it here a while ago:
https://archives.passt.top/passt-dev/20260620000930.62f30fee@elisabeth/
...RFC 8415 was obsoleted by RFC 9915. Option codes remain the same.
> + * RFC 5970 Options: 59, 60
> + * RFC 4075 Options: 31
> + */
> +static const enum dhcpv6_opt_type dhcpv6_opt_types[] = {
> + [7] = DHCPV6_OPT_UINT8, /* Preference */
> + [15] = DHCPV6_OPT_LEN_STR_LIST, /* User Class */
> + [16] = DHCPV6_OPT_VENDOR_CLASS, /* Vendor Class */
> + [31] = DHCPV6_OPT_IPV6_LIST, /* SNTP Servers */
> + [32] = DHCPV6_OPT_UINT32, /* Information Refresh Time */
> + [59] = DHCPV6_OPT_STR, /* Boot File URL */
> + [60] = DHCPV6_OPT_LEN_STR_LIST, /* Boot File Params */
> + [82] = DHCPV6_OPT_UINT32, /* SOL_MAX_RT */
> + [83] = DHCPV6_OPT_UINT32, /* 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
> + */
> +static int dhcpv6_opt_parse(uint16_t code, const char *str,
> + uint8_t *buf, size_t buf_len)
> +{
> + enum dhcpv6_opt_type type;
> + union inany_addr addr;
> + unsigned long val;
> + uint16_t slen_net;
> + sa_family_t af;
> + uint8_t width;
> + uint32_t ent;
> + size_t slen;
> + 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;
> +
> + do {
> + if (len + sizeof(struct in6_addr) > buf_len)
> + return -1;
> +
> + if (!parse_inany_(&str, &addr, &af) || af != AF_INET6)
> + return -1;
> +
> + memcpy(buf + len, &addr.a6, sizeof(struct in6_addr));
> + len += sizeof(struct in6_addr);
> +
> + if (type == DHCPV6_OPT_IPV6)
> + break;
> + } while (parse_literal(&str, ","));
> +
> + if (!len || !parse_eoi(str))
> + return -1;
> +
> + return len;
> + case DHCPV6_OPT_UINT8:
> + case DHCPV6_OPT_UINT16:
> + case DHCPV6_OPT_UINT32:
> + if (type == DHCPV6_OPT_UINT8)
> + width = 1;
> + else if (type == DHCPV6_OPT_UINT16)
> + width = 2;
> + else
> + width = 4;
> +
> + if (buf_len < width)
> + return -1;
> +
> + if (!parse_unsigned(&str, 0, &val) || !parse_eoi(str) ||
> + val >= (1ULL << (width * 8)))
> + return -1;
> +
> + if (type == DHCPV6_OPT_UINT16) {
> + uint16_t v16 = htons(val);
> + memcpy(buf, &v16, sizeof(v16));
> + } else if (type == DHCPV6_OPT_UINT32) {
> + uint32_t v32 = htonl(val);
> + memcpy(buf, &v32, sizeof(v32));
> + } else {
> + buf[0] = val;
> + }
> +
> + 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:
> + if (!parse_unsigned(&str, 0, &val) ||
> + !parse_literal(&str, ":") || val > UINT32_MAX)
> + return -1;
> +
> + slen = strlen(str);
> + 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));
> +
> + slen_net = htons(slen);
> + memcpy(buf + sizeof(uint32_t), &slen_net, sizeof(slen_net));
> +
> + memcpy(buf + sizeof(uint32_t) + sizeof(uint16_t), str, 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;
> +
> + slen_net = htons(slen);
> + memcpy(buf + len, &slen_net, sizeof(slen_net));
> + len += sizeof(uint16_t);
> +
> + memcpy(buf + len, str, slen);
> + len += slen;
> +
> + str += slen;
> + if (*str == ',')
> + str++;
> + }
> +
> + if (!len)
> + return -1;
> +
> + return len;
> + }
> +
> + return -1;
> +}
> +
> +/**
> + * struct dhcpv6_user_opts - User-specified DHCPv6 options from --dhcpv6-opt
> + * @val: Binary option value in wire format
> + * @len: Length of @val in bytes, 0 if not set
> + */
> +static struct {
> + uint8_t val[UINT16_MAX];
> + int len;
> +} dhcpv6_user_opts[ARRAY_SIZE(dhcpv6_opt_types)];
> +
> +/**
> + * dhcpv6_opt_to_str() - Render a binary DHCPv6 option value to printable string
> + * @code: DHCPv6 option code
> + * @buf: Output string buffer
> + * @buf_len: Size of output buffer
> + *
> + * Return: pointer to @buf if option is set, NULL otherwise
If you change this to accept an offset that can be used for
DHCPV6_OPT_IPV6_LIST, you can either keep returning a pointer to @buf
*if this function wrote something* (the comment would need to be
updated), or return a number of bytes written, which is perhaps less
surprising. Both would work though.
> + */
> +const char *dhcpv6_opt_to_str(uint16_t code, char *buf, size_t buf_len)
> +{
> + enum dhcpv6_opt_type type;
> + unsigned int i;
> + uint16_t slen;
> + int off = 0;
> +
> + if (code >= ARRAY_SIZE(dhcpv6_user_opts) ||
> + !dhcpv6_user_opts[code].len)
This fits on one line now.
> + return NULL;
> +
> + type = dhcpv6_opt_types[code];
> +
> + switch (type) {
> + case DHCPV6_OPT_IPV6:
> + case DHCPV6_OPT_IPV6_LIST:
> + for_each_in6_off(i, dhcpv6_user_opts[code].len) {
...to implement an opaque offset ("opaque" because the caller doesn't
need to know what it is about, and just passes it as a pointer), this
function can use that as initial value for 'i', and update it from 'i'
before returning.
You could call that parameter 'off', and do, before this loop:
i = *off;
and rename the current 'off' to 'buf_off' or similar.
The rest would remain the same, I think.
> + if (off) {
> + if (off + 1 >= (int)buf_len)
> + return NULL;
> + buf[off++] = ',';
> + }
> + if (!inet_ntop(AF_INET6,
> + dhcpv6_user_opts[code].val + i,
> + buf + off, buf_len - off))
> + return NULL;
> + off += strlen(buf + off);
> + }
And before returning, you would update *off = i;.
> + return buf;
> + case DHCPV6_OPT_UINT8:
> + case DHCPV6_OPT_UINT16:
> + case DHCPV6_OPT_UINT32: {
Usual comment about extra code blocks (I'd suggest to just add a
function-wide 'uint32_t val' and also use it for 'ent' below).
> + uint32_t val = 0;
> +
> + if (dhcpv6_user_opts[code].len == 1) {
> + val = dhcpv6_user_opts[code].val[0];
> + } else if (dhcpv6_user_opts[code].len == 2) {
> + uint16_t v16;
> + memcpy(&v16, dhcpv6_user_opts[code].val, sizeof(v16));
> + val = ntohs(v16);
> + } else if (dhcpv6_user_opts[code].len == 4) {
> + memcpy(&val, dhcpv6_user_opts[code].val, sizeof(val));
> + val = ntohl(val);
> + }
> +
> + (void)snprintf(buf, buf_len, "%u", val);
> + return buf;
> + }
> + case DHCPV6_OPT_STR:
> + (void)snprintf(buf, buf_len, "%.*s",
> + dhcpv6_user_opts[code].len,
> + dhcpv6_user_opts[code].val);
These two lines should be intended with three tabs plus spaces.
> + return buf;
> + case DHCPV6_OPT_VENDOR_CLASS: {
> + uint32_t ent;
> + int doff;
...and you could use 'off' for this one instead. It's also an offset.
> +
> + if (dhcpv6_user_opts[code].len <
> + (int)(sizeof(uint32_t) + sizeof(uint16_t)))
> + return NULL;
> +
> + memcpy(&ent, dhcpv6_user_opts[code].val, sizeof(ent));
> + memcpy(&slen, dhcpv6_user_opts[code].val + sizeof(uint32_t),
> + sizeof(slen));
> + doff = sizeof(uint32_t) + sizeof(uint16_t);
> +
> + (void)snprintf(buf, buf_len, "%u:%.*s",
> + ntohl(ent), ntohs(slen),
> + dhcpv6_user_opts[code].val + doff);
Same here, three tabs plus spaces.
> + return buf;
> + }
> + case DHCPV6_OPT_LEN_STR_LIST:
> + off = 0;
> + i = 0;
> +
> + while (i + sizeof(uint16_t) <=
> + (unsigned int)dhcpv6_user_opts[code].len) {
If you use a pointer to refer to dhcpv6_user_opts[code].len (unsigned
int *opt_len maybe), this becomes more readable.
> + memcpy(&slen, dhcpv6_user_opts[code].val + i,
Same for .val, here...
> + sizeof(slen));
> + slen = ntohs(slen);
> + i += sizeof(uint16_t);
> +
> + if (i + slen > (unsigned int)dhcpv6_user_opts[code].len)
(same here for .len)
> + break;
> +
> + if (off) {
> + if (off + 1 >= (int)buf_len)
> + return NULL;
> + buf[off++] = ',';
> + }
> + if (off + slen >= (int)buf_len)
> + return NULL;
> + memcpy(buf + off, dhcpv6_user_opts[code].val + i,
...and here.
> + slen);
> + off += slen;
> + i += slen;
> + }
> + buf[off] = '\0';
> + return buf;
> + default:
> + return NULL;
> + }
> +}
> +
> +/**
> + * dhcpv6_set_opt() - Parse and store a user-specified DHCPv6 option
> + * @code: DHCPv6 option code
> + * @val_str: Value string from command line
> + */
> +void dhcpv6_set_opt(uint16_t code, const char *val_str)
> +{
> + int ret;
> +
> + if (code >= ARRAY_SIZE(dhcpv6_opt_types) ||
> + dhcpv6_opt_types[code] == DHCPV6_OPT_NONE)
> + die("DHCPv6 option %u is not supported", code);
> +
> + ret = dhcpv6_opt_parse(code, val_str,
> + dhcpv6_user_opts[code].val,
> + sizeof(dhcpv6_user_opts[code].val));
> + if (ret < 0)
> + die("Invalid value for DHCPv6 option %u: %s", code, val_str);
> +
> + dhcpv6_user_opts[code].len = ret;
> +}
> +
> /**
> * 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 1015a1a7..973c477c 100644
> --- a/dhcpv6.h
> +++ b/dhcpv6.h
> @@ -9,5 +9,7 @@
> int dhcpv6(struct ctx *c, struct iov_tail *data,
> const struct in6_addr *saddr, const struct in6_addr *daddr);
> void dhcpv6_init(const struct ctx *c);
> +void dhcpv6_set_opt(uint16_t code, const char *val_str);
> +const char *dhcpv6_opt_to_str(uint16_t code, char *buf, size_t buf_len);
>
> #endif /* DHCPV6_H */
> diff --git a/passt.1 b/passt.1
> index 53e072a9..249ed5cf 100644
> --- a/passt.1
> +++ b/passt.1
> @@ -440,6 +440,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).
Same as above about RFC 8415, here...
> +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).
and here.
> +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 either:
--
Stefano
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v4 2/2] dhcpv6: Inject custom options into DHCPv6 replies
2026-10-06 9:04 ` [PATCH v4 2/2] dhcpv6: Inject custom options into DHCPv6 replies Anshu Kumari
@ 2026-10-25 12:01 ` Stefano Brivio
0 siblings, 0 replies; 5+ messages in thread
From: Stefano Brivio @ 2026-10-25 12:01 UTC (permalink / raw)
To: Anshu Kumari; +Cc: passt-dev, lvivier
On Tue, 6 Oct 2026 14:34:53 +0530
Anshu Kumari <anskuma@redhat.com> wrote:
> Append user-specified options from --dhcpv6-opt to DHCPv6 reply
> messages. Options that exceed available buffer space are skipped
> with a warning.
>
> Link: https://bugs.passt.top/show_bug.cgi?id=192
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> ---
> v4:
> - warn() instead of debug() when options don't fit in the reply.
> - doc comment for dhcpv6_user_opts_fill() updated.
>
> v3:
> - rename dhcpv6_custom_opts_fill() to dhcpv6_user_opts_fill().
> - Iterate directly over the pre-parsed binary dhcpv6_user_opts[]
> array instead of re-parsing string values at reply time.
>
> v2:
> - Updated dhcpv6_custom_opts_fill() to parse str at reply time
> using dhcpv6_opt_parse() instead of copying cached val/len.
> ---
> dhcpv6.c | 36 ++++++++++++++++++++++++++++++++++++
> 1 file changed, 36 insertions(+)
>
> diff --git a/dhcpv6.c b/dhcpv6.c
> index abc45813..a0545406 100644
> --- a/dhcpv6.c
> +++ b/dhcpv6.c
> @@ -879,6 +879,41 @@ static size_t dhcpv6_client_fqdn_fill(const struct iov_tail *data,
> return offset + sizeof(struct opt_hdr) + opt_len;
> }
>
> +/**
> + * dhcpv6_user_opts_fill() - Append user-specified options to reply
> + * @buf: Response message buffer
> + * @offset: Current offset in buffer
> + *
> + * Return: offset after appended user options
> + */
> +static size_t dhcpv6_user_opts_fill(char *buf, size_t offset)
> +{
> + unsigned int i;
> +
> + for (i = 0; i < ARRAY_SIZE(dhcpv6_user_opts); i++) {
> + int vlen = dhcpv6_user_opts[i].len;
> + struct opt_hdr *hdr;
> +
> + if (!vlen)
> + continue;
> +
> + if (offset + sizeof(struct opt_hdr) + vlen > OPT_MAX_SIZE) {
> + warn("DHCPv6: option %u doesn't fit, skipping", i);
I've seen Laurent's comment in:
https://archives.passt.top/passt-dev/6fcca48c-a992-436f-a308-061ae0017d4f@redhat.com/
but the issue with a warn() call here is that the guest could try to
flood logs that way. You could rate-limit it by passing the timestamp
to here via dhcpv6(), it's just one caller to modify, it doesn't look
that bad.
> + continue;
> + }
> +
> + hdr = (struct opt_hdr *)(buf + offset);
> + hdr->t = htons(i);
> + hdr->l = htons(vlen);
> + offset += sizeof(struct opt_hdr);
> +
> + memcpy(buf + offset, dhcpv6_user_opts[i].val, vlen);
> + offset += vlen;
> + }
> +
> + return offset;
> +}
> +
> /**
> * dhcpv6() - Check if this is a DHCPv6 message, reply as needed
> * @c: Execution context
> @@ -1017,6 +1052,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_user_opts_fill((char *)&resp, n);
>
> resp.hdr.xid = mh->xid;
>
--
Stefano
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-25 12:01 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-06 9:04 [PATCH v4 0/2] [PATCH 0/2] dhcpv6: Add --dhcpv6-opt for custom DHCPv6 options Anshu Kumari
2026-10-06 9:04 ` [PATCH v4 1/2] dhcpv6: Add --dhcpv6-opt with option type table and value parser Anshu Kumari
2026-10-25 12:01 ` Stefano Brivio
2026-10-06 9:04 ` [PATCH v4 2/2] dhcpv6: Inject custom options into DHCPv6 replies Anshu Kumari
2026-10-25 12:01 ` 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).