public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
* [PATCH v5 0/7] Add --dhcp-boot and --dhcp-opt options
@ 2026-07-17 17:56 Anshu Kumari
  2026-07-17 17:56 ` [PATCH v5 1/7] dhcp: Refactor fill_one() to operate on a generic buffer Anshu Kumari
                   ` (6 more replies)
  0 siblings, 7 replies; 17+ messages in thread
From: Anshu Kumari @ 2026-07-17 17:56 UTC (permalink / raw)
  To: anskuma, sbrivio, passt-dev; +Cc: lvivier, david, jmaloy

This series adds support for custom DHCP options in passt, enabling
network boot (PXE/UEFI HTTP Boot) and arbitrary DHCP option injection.

Two new command-line flags are introduced:

  --dhcp-boot URL    Sets the boot file URL (DHCP option 67 and the
                     legacy boot file field)

  --dhcp-opt CODE,VALUE
                     Sets any DHCP option by numeric code, with
                     type-aware parsing per RFC 2132

The DHCP reply path is extended with option overload support (RFC 2132
option 52), allowing options to overflow into the file and sname fields
when the standard options area is full.

RFC 3396 option splitting for concatenation-requiring options has also
been introduce as a separate patch.

Anshu Kumari (7):
  dhcp: Refactor fill_one() to operate on a generic buffer
  dhcp: Add option state management with enum opt_state
  dhcp: Add option overload
  dhcp: Add --dhcp-opt with option table and value parser
  dhcp: Add --dhcp-boot command-line option
  dhcp: Add RFC 3396 option splitting for concatenation-requiring
    options
  dhcp: Handle FQDN option with RFC 3396 concatenation

 conf.c  |  30 ++-
 dhcp.c  | 591 +++++++++++++++++++++++++++++++++++++++++++++++++++-----
 dhcp.h  |   2 +
 passt.1 |  49 +++++
 4 files changed, 621 insertions(+), 51 deletions(-)

-- 
2.54.0


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

* [PATCH v5 1/7] dhcp: Refactor fill_one() to operate on a generic buffer
  2026-07-17 17:56 [PATCH v5 0/7] Add --dhcp-boot and --dhcp-opt options Anshu Kumari
@ 2026-07-17 17:56 ` Anshu Kumari
  2026-07-20  1:18   ` David Gibson
  2026-07-17 17:56 ` [PATCH v5 2/7] dhcp: Add option state management with enum opt_state Anshu Kumari
                   ` (5 subsequent siblings)
  6 siblings, 1 reply; 17+ messages in thread
From: Anshu Kumari @ 2026-07-17 17:56 UTC (permalink / raw)
  To: anskuma, sbrivio, passt-dev; +Cc: lvivier, david, jmaloy

Change fill_one() to accept a buffer pointer and capacity instead of
a struct msg pointer.  This is a pure refactor with no behavior change,
preparing for option overload support where fill_one() will also write
into the file and sname fields.

Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
v5:
  - Restore debug messages for skipped options in fill()
  - Add /* code and length of option */ comment in size check
  - Use buf + *offset instead of &buf[*offset] for memcpy

v3:
  - Restored removed comments: "If we don't have space to write the
    option, then just skip" and "Move to option".

v2:
  - Renamed parameter cap → size.
---
 dhcp.c | 27 ++++++++++++++-------------
 1 file changed, 14 insertions(+), 13 deletions(-)

diff --git a/dhcp.c b/dhcp.c
index c3c7422..bb72b72 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -131,28 +131,29 @@ struct msg {
 } __attribute__((__packed__));
 
 /**
- * fill_one() - Fill a single option in message
- * @m:		Message to fill
+ * fill_one() - Fill a single option into a buffer
+ * @buf:	Buffer to write option
+ * @size:	Usable size of @buf (excluding end marker)
  * @o:		Option number
- * @offset:	Current offset within options field, updated on insertion
+ * @offset:	Current offset within @buf, updated on insertion
  *
- * Return: false if m has space to write the option, true otherwise
+ * Return: false if @buf has space to write the option, true otherwise
  */
-static bool fill_one(struct msg *m, int o, int *offset)
+static bool fill_one(uint8_t *buf, size_t size, int o, int *offset)
 {
 	size_t slen = opts[o].slen;
 
 	/* If we don't have space to write the option, then just skip */
-	if (*offset + 2 /* code and length of option */ + slen > OPT_MAX)
+	if (*offset + 2 /* code and length of option */ + slen > size)
 		return true;
 
-	m->o[*offset] = o;
-	m->o[*offset + 1] = slen;
+	buf[*offset] = o;
+	buf[*offset + 1] = slen;
 
 	/* Move to option */
 	*offset += 2;
 
-	memcpy(&m->o[*offset], opts[o].s, slen);
+	memcpy(buf + *offset, opts[o].s, slen);
 
 	opts[o].sent = 1;
 	*offset += slen;
@@ -177,19 +178,19 @@ static int fill(struct msg *m)
 	 * Put it there explicitly, unless requested via option 55.
 	 */
 	if (opts[55].clen > 0 && !memchr(opts[55].c, 53, opts[55].clen))
-		if (fill_one(m, 53, &offset))
-			 debug("DHCP: skipping option 53");
+		if (fill_one(m->o, OPT_MAX, 53, &offset))
+			debug("DHCP: skipping option 53");
 
 	for (i = 0; i < opts[55].clen; i++) {
 		o = opts[55].c[i];
 		if (opts[o].slen != -1)
-			if (fill_one(m, o, &offset))
+			if (fill_one(m->o, OPT_MAX, o, &offset))
 				debug("DHCP: skipping option %i", o);
 	}
 
 	for (o = 0; o < 255; o++) {
 		if (opts[o].slen != -1 && !opts[o].sent)
-			if (fill_one(m, o, &offset))
+			if (fill_one(m->o, OPT_MAX, o, &offset))
 				debug("DHCP: skipping option %i", o);
 	}
 
-- 
2.54.0


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

* [PATCH v5 2/7] dhcp: Add option state management with enum opt_state
  2026-07-17 17:56 [PATCH v5 0/7] Add --dhcp-boot and --dhcp-opt options Anshu Kumari
  2026-07-17 17:56 ` [PATCH v5 1/7] dhcp: Refactor fill_one() to operate on a generic buffer Anshu Kumari
@ 2026-07-17 17:56 ` Anshu Kumari
  2026-07-20  1:36   ` David Gibson
  2026-07-17 17:56 ` [PATCH v5 3/7] dhcp: Add option overload Anshu Kumari
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 17+ messages in thread
From: Anshu Kumari @ 2026-07-17 17:56 UTC (permalink / raw)
  To: anskuma, sbrivio, passt-dev; +Cc: lvivier, david, jmaloy

Introduce enum opt_state to track each DHCP option instead
of overloading slen = -1 for "not set".

OPT_UNSET means the option is not configured.
OPT_DEFAULT means the option was set from host configuration.

This replaces all slen = -1 / slen != -1 checks with
state = OPT_UNSET / state != OPT_UNSET, and sets state = OPT_DEFAULT
for options initialised in dhcp_init() and at reply time in dhcp().

Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
v5:
  - New patch: introduce enum opt_state { OPT_UNSET, OPT_DEFAULT } to replace slen = -1 for tracking option state
  - Replace all slen = -1 / slen != -1 checks with state = OPT_UNSET / state != OPT_UNSET
  - Set OPT_DEFAULT for options initialised in dhcp_init() and at reply time

---
 dhcp.c | 57 ++++++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 40 insertions(+), 17 deletions(-)

diff --git a/dhcp.c b/dhcp.c
index bb72b72..e5d89fc 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -33,13 +33,24 @@
 #include "log.h"
 #include "dhcp.h"
 
+/**
+ * enum opt_state - DHCP option state
+ * @OPT_UNSET:		Option not configured
+ * @OPT_DEFAULT:	Option set from host config
+ */
+enum opt_state {
+	OPT_UNSET,
+	OPT_DEFAULT,
+};
+
 /**
  * struct opt - DHCP option
  * @sent:	Convenience flag, set while filling replies
- * @slen:	Length of option defined for server, -1 if not going to be sent
+ * @slen:	Length of option defined for server
  * @s:		Option payload from server
  * @clen:	Length of option received from client, -1 if not received
  * @c:		Option payload from client
+ * @state:	Option state (unset or default)
  */
 struct opt {
 	int sent;
@@ -47,6 +58,7 @@ struct opt {
 	uint8_t s[255];
 	int clen;
 	uint8_t c[255];
+	enum opt_state state;
 };
 
 static struct opt opts[256];
@@ -76,16 +88,19 @@ void dhcp_init(void)
 	int i;
 
 	for (i = 0; i < ARRAY_SIZE(opts); i++)
-		opts[i].slen = -1;
-
-	opts[1]  = (struct opt) { 0, 4, {     0 }, 0, { 0 }, };	/* Mask */
-	opts[3]  = (struct opt) { 0, 4, {     0 }, 0, { 0 }, };	/* Router */
-	opts[51] = (struct opt) { 0, 4, {  0xff,
-					   0xff,
-					   0xff,
-					   0xff }, 0, { 0 }, };	/* Lease time */
-	opts[53] = (struct opt) { 0, 1, {     0 }, 0, { 0 }, };	/* Type */
-	opts[54] = (struct opt) { 0, 4, {     0 }, 0, { 0 }, };	/* Server ID */
+		opts[i].state = OPT_UNSET;
+
+	/* Mask */
+	opts[1]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
+	/* Router */
+	opts[3]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
+	/* Lease time */
+	opts[51] = (struct opt) { 0, 4, { 0xff, 0xff, 0xff, 0xff },
+				 0, { 0 }, OPT_DEFAULT, };
+	/* Type */
+	opts[53] = (struct opt) { 0, 1, {  0 }, 0, { 0 }, OPT_DEFAULT, };
+	/* Server ID */
+	opts[54] = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
 }
 
 /**
@@ -183,13 +198,13 @@ static int fill(struct msg *m)
 
 	for (i = 0; i < opts[55].clen; i++) {
 		o = opts[55].c[i];
-		if (opts[o].slen != -1)
+		if (opts[o].state != OPT_UNSET)
 			if (fill_one(m->o, OPT_MAX, o, &offset))
 				debug("DHCP: skipping option %i", o);
 	}
 
 	for (o = 0; o < 255; o++) {
-		if (opts[o].slen != -1 && !opts[o].sent)
+		if (opts[o].state != OPT_UNSET && !opts[o].sent)
 			if (fill_one(m->o, OPT_MAX, o, &offset))
 				debug("DHCP: skipping option %i", o);
 	}
@@ -243,6 +258,7 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
 	int i;
 
 	opts[119].slen = 0;
+	opts[119].state = OPT_DEFAULT;
 
 	for (i = 0; i < 255; i++)
 		max_len -= opts[i].slen;
@@ -291,7 +307,7 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
 	}
 
 	if (!opts[119].slen)
-		opts[119].slen = -1;
+		opts[119].state = OPT_UNSET;
 }
 
 /**
@@ -389,7 +405,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 		iov_drop_header(data, *olen);
 	}
 
-	opts[80].slen = -1;
+	opts[80].state = OPT_UNSET;
 	if (opts[53].clen > 0 && opts[53].c[0] == DHCPDISCOVER) {
 		if (opts[80].clen == -1) {
 			info("DHCP: offer to discover");
@@ -398,6 +414,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 			info("DHCP: ack to discover (Rapid Commit)");
 			opts[53].s[0] = DHCPACK;
 			opts[80].slen = 0;
+			opts[80].state = OPT_DEFAULT;
 		}
 	} else if (opts[53].clen <= 0 || opts[53].c[0] == DHCPREQUEST) {
 		info("%s: ack to request", /* DHCP needs a valid message type */
@@ -421,6 +438,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	    != (c->ip4.guest_gw.s_addr & mask.s_addr)) {
 		/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
 		opts[121].slen = 14;
+		opts[121].state = OPT_DEFAULT;
 		opts[121].s[0] = 32;
 		memcpy(opts[121].s + 1,
 		       &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
@@ -430,6 +448,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 
 	if (c->mtu) {
 		opts[26].slen = 2;
+		opts[26].state = OPT_DEFAULT;
 		opts[26].s[0] = c->mtu / 256;
 		opts[26].s[1] = c->mtu % 256;
 	}
@@ -441,12 +460,15 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 		((struct in_addr *)opts[6].s)[i] = c->ip4.dns[i];
 		opts[6].slen += sizeof(uint32_t);
 	}
-	if (!opts[6].slen)
-		opts[6].slen = -1;
+	if (opts[6].slen)
+		opts[6].state = OPT_DEFAULT;
+	else
+		opts[6].state = OPT_UNSET;
 
 	opt_len = strlen(c->hostname);
 	if (opt_len > 0) {
 		opts[12].slen = opt_len;
+		opts[12].state = OPT_DEFAULT;
 		memcpy(opts[12].s, &c->hostname, opt_len);
 	}
 
@@ -463,6 +485,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 			encode_domain_name((char *)opts[81].s + 3, c->fqdn);
 
 			opts[81].slen = opt_len;
+			opts[81].state = OPT_DEFAULT;
 		} else {
 			debug("DHCP: client FQDN option doesn't fit, skipping");
 		}
-- 
2.54.0


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

* [PATCH v5 3/7] dhcp: Add option overload
  2026-07-17 17:56 [PATCH v5 0/7] Add --dhcp-boot and --dhcp-opt options Anshu Kumari
  2026-07-17 17:56 ` [PATCH v5 1/7] dhcp: Refactor fill_one() to operate on a generic buffer Anshu Kumari
  2026-07-17 17:56 ` [PATCH v5 2/7] dhcp: Add option state management with enum opt_state Anshu Kumari
@ 2026-07-17 17:56 ` Anshu Kumari
  2026-07-20  2:03   ` David Gibson
  2026-07-17 17:56 ` [PATCH v5 4/7] dhcp: Add --dhcp-opt with option table and value parser Anshu Kumari
                   ` (3 subsequent siblings)
  6 siblings, 1 reply; 17+ messages in thread
From: Anshu Kumari @ 2026-07-17 17:56 UTC (permalink / raw)
  To: anskuma, sbrivio, passt-dev; +Cc: lvivier, david, jmaloy

When the options field is full, overflow remaining DHCP options into
the sname and file fields per RFC 2132 option 52.

Per RFC 2132, Section 9.5, the boot file name is always placed in the
'file' header field.  When a boot file is set, the file field is
reserved from overload and overflow uses only the sname field.

Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
v5:
  - enhanced enum dhcp_overload to follow kernel-doc.
  - Inline fill_overflow() into fill()
  - Use state-based checks instead of slen

v4:
  - Converted overload #defines to enum dhcp_overload.
  - Fixed missing whitespace in comment before */.
  - Boot file name always placed in 'file' header field per RFC 2132,
    Section 9.5; file field reserved from overload when bootfile is
    set; option 67 suppressed from options area.

v3:
  - Added RFC 2132 Section 9.3 reference comment on overload
    constants.
  - Use ARRAY_SIZE(opts) instead of raw 255 in fill_overflow().
  - Swapped overflow order: try sname (64 bytes) first, then file
    (128 bytes) — better packing and keeps file field available for
    boot file name.
  - Removed '&' from &reply.file.
  - Removed '+1' from memcpy — reply.file already zeroed.
  - opt_set_dns_search() max_len: OPT_MAX - 3 instead of
    sizeof(m->o).

v2:
  - Added #define DHCP_OVERLOAD_FILE and #define DHCP_OVERLOAD_SNAME constants
  - Added comment documenting space reservation: /* Reserve 3 bytes for option 52 */
  - Fixed DNS search length: sizeof(m->o) only, not combined with file+sname
  - Removed dhcp_boot references — reply.file copy now reads from opts[67]
  - Used DHCP_OVERLOAD_FILE constant in reply.file guard

---
 dhcp.c | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 79 insertions(+), 9 deletions(-)

diff --git a/dhcp.c b/dhcp.c
index e5d89fc..39f7952 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -176,13 +176,31 @@ static bool fill_one(uint8_t *buf, size_t size, int o, int *offset)
 }
 
 /**
- * fill() - Fill options in message
- * @m:		Message to fill
+* enum dhcp_overload - DHCP option overload values (RFC 2132, Section 9.3)
+ * @DHCP_OVERLOAD_NONE:		No overload
+ * @DHCP_OVERLOAD_FILE:		file field carries options
+ * @DHCP_OVERLOAD_SNAME:	sname field carries options
+ */
+enum dhcp_overload {
+	DHCP_OVERLOAD_NONE,
+	DHCP_OVERLOAD_FILE,
+	DHCP_OVERLOAD_SNAME,
+};
+
+/**
+ * fill() - Fill options in message, with overload into file/sname if needed
+ * @m:			Message to fill
+ * @overload:		Set to option 52 value (0 if none, 1/2/3 per RFC 2132)
+ * @has_bootfile:	Reserve file field for boot file name
  *
  * Return: current size of options field
  */
-static int fill(struct msg *m)
+static int fill(struct msg *m, enum dhcp_overload *overload, bool has_bootfile)
 {
+	int sname_off = 0, file_off = 0;
+	*overload = DHCP_OVERLOAD_NONE;
+	/* Reserve 3 bytes for option 52 (overload) if needed */
+	size_t size = OPT_MAX - 3;
 	int i, o, offset = 0;
 
 	for (o = 0; o < 255; o++)
@@ -199,14 +217,47 @@ static int fill(struct msg *m)
 	for (i = 0; i < opts[55].clen; i++) {
 		o = opts[55].c[i];
 		if (opts[o].state != OPT_UNSET)
-			if (fill_one(m->o, OPT_MAX, o, &offset))
-				debug("DHCP: skipping option %i", o);
+			fill_one(m->o, size, o, &offset);
 	}
 
 	for (o = 0; o < 255; o++) {
 		if (opts[o].state != OPT_UNSET && !opts[o].sent)
-			if (fill_one(m->o, OPT_MAX, o, &offset))
-				debug("DHCP: skipping option %i", o);
+			fill_one(m->o, size, o, &offset);
+	}
+
+	/* Overflow unsent options into sname, then file */
+	for (o = 0; (size_t)o < ARRAY_SIZE(opts); o++) {
+		if (opts[o].state == OPT_UNSET || opts[o].sent)
+			continue;
+		fill_one(m->sname, sizeof(m->sname) - 1, o, &sname_off);
+	}
+
+	for (o = 0; (size_t)o < ARRAY_SIZE(opts); o++) {
+		if (opts[o].state == OPT_UNSET || opts[o].sent)
+			continue;
+
+		if (!has_bootfile &&
+			fill_one(m->file, sizeof(m->file) - 1, o,
+			         &file_off))
+				debug("DHCP: skipping option %i"
+				      " (overload full)", o);
+	}
+
+	if (sname_off) {
+		m->sname[sname_off] = 255;
+		*overload |= DHCP_OVERLOAD_SNAME;
+	}
+
+	if (file_off) {
+		m->file[file_off] = 255;
+		*overload |= DHCP_OVERLOAD_FILE;
+	}
+
+
+	if (*overload) {
+		m->o[offset++] = 52;
+		m->o[offset++] = 1;
+		m->o[offset++] = *overload;
 	}
 
 	m->o[offset++] = 255;
@@ -320,6 +371,7 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
 int dhcp(const struct ctx *c, struct iov_tail *data)
 {
 	char macstr[ETH_ADDRSTRLEN];
+	enum dhcp_overload overload;
 	size_t mlen, dlen, opt_len;
 	struct in_addr mask, dst;
 	struct ethhdr eh_storage;
@@ -328,9 +380,12 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	const struct ethhdr *eh;
 	const struct iphdr *iph;
 	const struct udphdr *uh;
+	uint8_t bootfile[128];
 	struct msg m_storage;
 	struct msg const *m;
+	bool has_bootfile;
 	struct msg reply;
+	int bootfile_len;
 	unsigned int i;
 
 	eh = IOV_REMOVE_HEADER(data, eh_storage);
@@ -492,9 +547,24 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	}
 
 	if (!c->no_dhcp_dns_search)
-		opt_set_dns_search(c, sizeof(m->o));
+		 /* 3 bytes reserved for option 52 (code, length, value) */
+		opt_set_dns_search(c, OPT_MAX - 3);
+
+	/* RFC 2132, Section 9.5: put boot file name in the 'file' header
+	 * field.  Suppress option 67 from the options area and reserve
+	 * the file field from overload.
+	 */
+	has_bootfile = opts[67].slen > 0 &&
+		       (size_t)opts[67].slen < sizeof(reply.file);
+	if (has_bootfile) {
+		memcpy(bootfile, opts[67].s, opts[67].slen);
+		bootfile_len = opts[67].slen;
+	}
+
+	dlen = offsetof(struct msg, o) + fill(&reply, &overload, has_bootfile);
 
-	dlen = offsetof(struct msg, o) + fill(&reply);
+	if (has_bootfile)
+		memcpy(reply.file, bootfile, bootfile_len);
 
 	if (m->flags & FLAG_BROADCAST)
 		dst = in4addr_broadcast;
-- 
2.54.0


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

* [PATCH v5 4/7] dhcp: Add --dhcp-opt with option table and value parser
  2026-07-17 17:56 [PATCH v5 0/7] Add --dhcp-boot and --dhcp-opt options Anshu Kumari
                   ` (2 preceding siblings ...)
  2026-07-17 17:56 ` [PATCH v5 3/7] dhcp: Add option overload Anshu Kumari
@ 2026-07-17 17:56 ` Anshu Kumari
  2026-07-20  3:58   ` David Gibson
  2026-07-17 17:56 ` [PATCH v5 5/7] dhcp: Add --dhcp-boot command-line option Anshu Kumari
                   ` (2 subsequent siblings)
  6 siblings, 1 reply; 17+ messages in thread
From: Anshu Kumari @ 2026-07-17 17:56 UTC (permalink / raw)
  To: anskuma, sbrivio, passt-dev; +Cc: lvivier, david, jmaloy

Introduce the --dhcp-opt flag that allows setting arbitrary DHCP
options from command-line in the form [--dhcp-opt CODE,VALUE].

Add a type lookup table mapping option codes to RFC 2132 value types
(IPv4, IPv4 list, integer, string) and dhcp_opt_parse() to convert
CLI strings to binary wire format.  Parsed options are stored in
struct opts[] and injected into DHCP replies.  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>
---
v5:
  - Rename enum values: INT8/INT16/INT32/SINT32 → UINT8/UINT16/UINT32/INT32
  - Change int len to size_t len in dhcp_opt_parse(), remove int casts
  - Make dhcp_opt_parse() static, add dhcp_set_opt() to store options 
    directly in opts[] at startup instead of double-parsing via ctx->dhcp_opts
  - Add OPT_USER to enum opt_state to protect user options from being
    overwritten by built-in defaults
  - Add dhcp_opt_to_str() for conf_print() display
  - Remove dhcp_opts[] from struct ctx in passt.h
  - Remove conf_dhcp_option() wrapper from conf.c

v4:
  - Renamed custom_opts to dhcp_opts, 256 entries indexed by option
    code, removed MAX_CUSTOM_DHCP_OPTS and count field.
  - Changed str buffer from 256 to 255 bytes.
  - Moved function to conf.c as static conf_dhcp_option(), renamed
    from dhcp_add_option().
  - Made dhcp_opt_parse() non-static, declared in dhcp.h
  - Dropped val/len from ctx struct; conf_dhcp_option() validates
    with temp buffer, dhcp() parses str directly into opts[] at
    reply time.
  - Replaced strtok_r() + 256-byte buffer with strcspn() +
    INET_ADDRSTRLEN buffer.
  - Added DHCP_OPT_SINT32 for option 2 (Time Offset), uses strtol()
    per RFC 2132 Section 8.2.
  - All errors in dhcp_opt_parse() return -1, removed die() calls;
    caller handles error message consistently.
  - Removed redundant !slen check in DHCP_OPT_STR case.
  - Omitted explicit array size for dhcp_opt_types[], arraydded bounds
    check before lookup.
  - Added errno = 0 + errno check for strtoul() in case 34.
  - Fixed usage text: "Set DHCP option CODE to VAL".
  - Improved man page: added format description and examples

v3:
  - Replaced DHCP_OPT_INTEGER with separate DHCP_OPT_INT8/INT16/INT32
    enums, removed dhcp_opt_int_width[] array.
  - Shared logic between DHCP_OPT_IPV4 and DHCP_OPT_IPV4_LIST — parse
    both as list, error if >1 in single case.
  - Added errno = 0 before strtoul() and check after.
  - Fixed range check: 1ULL << (width * 8) for all widths including
    width==4.
  - strncpy → memcpy for DHCP_OPT_STR.
  - Moved enum to dhcp.c since not used in other files.
  - Removed options 55, 61 (client-only), 119 (DNS compression, use
    --dhcp-search instead), 33 (IP pairs not supported).
  - DHCP_OPT_PARSE_BUF 1024 → char tmp[256].
  - Upgraded dhcp_add_option() to call dhcp_opt_parse() and populate
    val[]/len.
  - Aligned array entries for readability.
  - Added tab after @DHCP_OPT_IPV4_LIST: in kerneldoc.
  - Reject empty value strings before parsing
  - Reject leading/trailing/consecutive commas in IP list values.

v2:
  - Replaced struct lookup table + dhcp_opt_type_lookup() function with flat dhcp_opt_types[256] array indexed by code.
  - Consolidated DHCP_OPT_UINT8/UINT16/UINT32 into single DHCP_OPT_INTEGER with dhcp_opt_int_width[256] table.
  - Dropped DHCP_OPT_ROUTES / option 121 entirely.
  - Added kerneldoc for enum dhcp_opt_type values.
  - Removed curly braces from switch cases, declarations before switch.
  - Added newlines before return statements.
  - Changed IP list delimiter from space to comma (--dhcp-opt 6,1.1.1.1,8.8.8.8).
  - Defined DHCP_OPT_PARSE_BUF constant for bare 1024.
  - Added len and val[255] fields to struct here (moved from patch 1).
  - Added kerneldoc for @custom_opts.len and @custom_opts.val.
  - Wired dhcp_opt_parse() into case 32 (--dhcp-boot) to populate val/len.

---
 conf.c  |  25 ++++-
 dhcp.c  | 338 +++++++++++++++++++++++++++++++++++++++++++++++++++-----
 dhcp.h  |   2 +
 passt.1 |  42 +++++++
 4 files changed, 381 insertions(+), 26 deletions(-)

diff --git a/conf.c b/conf.c
index 5b6cc2b..e7f3ea0 100644
--- a/conf.c
+++ b/conf.c
@@ -47,6 +47,7 @@
 #include "lineread.h"
 #include "isolation.h"
 #include "log.h"
+#include "dhcp.h"
 #include "vhost_user.h"
 #include "epoll_ctl.h"
 #include "conf.h"
@@ -615,7 +616,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"
+		"  --dhcp-opt CODE,VAL	Set DHCP option CODE to VAL\n");
 	if (strstr(name, "pasta"))
 		FPRINTF(f, "    default: don't use any search list\n");
 	else
@@ -843,6 +845,9 @@ static void conf_print(const struct ctx *c)
 			info("    router: %s",
 			     inet_ntop(AF_INET, &c->ip4.guest_gw,
 				       buf, sizeof(buf)));
+			for (i = 1; i < 255; i++)
+				if (dhcp_opt_to_str(i, buf, sizeof(buf)))
+					info("    option %u: %s", i, buf);
 		}
 
 		for (i = 0; i < ARRAY_SIZE(c->ip4.dns); i++) {
@@ -1316,6 +1321,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 },
+		{"dhcp-opt", required_argument,		NULL,		34 },
 		{ 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:";
@@ -1330,10 +1336,13 @@ void conf(struct ctx *c, int argc, char **argv)
 	unsigned int ifi4 = 0, ifi6 = 0;
 	bool opt_a_is_prefix = false;
 	const char *logfile = NULL;
+	unsigned long optcode;
 	char *runas = NULL;
 	size_t logsize = 0;
+	const char *comma;
 	uint8_t opt_n = 0;
 	int name, ret;
+	char *end;
 	uid_t uid;
 	gid_t gid;
 
@@ -1557,6 +1566,20 @@ void conf(struct ctx *c, int argc, char **argv)
 		case 32:
 			c->chroot_fallback = true;
 			break;
+		case 34:
+			comma = strchr(optarg, ',');
+			if (!comma)
+				die("--dhcp-opt requires CODE,VALUE format");
+
+			errno = 0;
+			optcode = strtoul(optarg, &end, 0);
+			if (end != comma || errno ||
+			    optcode < 1 || optcode > 254)
+				die("DHCP option code must be 1-254: %s",
+				    optarg);
+
+			dhcp_set_opt(optcode, comma + 1);
+			break;
 		case 'd':
 			c->debug = 1;
 			c->quiet = 0;
diff --git a/dhcp.c b/dhcp.c
index 39f7952..cc910ee 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -23,6 +23,7 @@
 #include <unistd.h>
 #include <string.h>
 #include <limits.h>
+#include <errno.h>
 
 #include "util.h"
 #include "ip.h"
@@ -37,10 +38,12 @@
  * enum opt_state - DHCP option state
  * @OPT_UNSET:		Option not configured
  * @OPT_DEFAULT:	Option set from host config
+ * @OPT_USER:		Option set via --dhcp-opt command line
  */
 enum opt_state {
 	OPT_UNSET,
 	OPT_DEFAULT,
+	OPT_USER,
 };
 
 /**
@@ -50,7 +53,7 @@ enum opt_state {
  * @s:		Option payload from server
  * @clen:	Length of option received from client, -1 if not received
  * @c:		Option payload from client
- * @state:	Option state (unset or default)
+ * @state:	Option state (unset, default, or user)
  */
 struct opt {
 	int sent;
@@ -88,19 +91,20 @@ void dhcp_init(void)
 	int i;
 
 	for (i = 0; i < ARRAY_SIZE(opts); i++)
-		opts[i].state = OPT_UNSET;
-
-	/* Mask */
-	opts[1]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
-	/* Router */
-	opts[3]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
-	/* Lease time */
-	opts[51] = (struct opt) { 0, 4, { 0xff, 0xff, 0xff, 0xff },
+		if (opts[i].state != OPT_USER)
+			opts[i].state = OPT_UNSET;
+
+	if (opts[1].state != OPT_USER)			/* Mask */
+		opts[1]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
+	if (opts[3].state != OPT_USER)			/* Router */
+		opts[3]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
+	if (opts[51].state != OPT_USER)			/* Lease time */
+		opts[51] = (struct opt) { 0, 4, { 0xff, 0xff, 0xff, 0xff },
 				 0, { 0 }, OPT_DEFAULT, };
-	/* Type */
-	opts[53] = (struct opt) { 0, 1, {  0 }, 0, { 0 }, OPT_DEFAULT, };
-	/* Server ID */
-	opts[54] = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
+	if (opts[53].state != OPT_USER)			/* Type */
+		opts[53] = (struct opt) { 0, 1, {  0 }, 0, { 0 }, OPT_DEFAULT, };
+	if (opts[54].state != OPT_USER)			/* Server ID */
+		opts[54] = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
 }
 
 /**
@@ -145,6 +149,280 @@ struct msg {
 	uint8_t o[OPT_MAX + 1 /* End option */ ];
 } __attribute__((__packed__));
 
+/**
+ * enum dhcp_opt_type - DHCP option value types per RFC 2132
+ * @DHCP_OPT_NONE:	Unsupported or unknown option
+ * @DHCP_OPT_STR:	Variable-length string
+ * @DHCP_OPT_IPV4:	Single IPv4 address
+ * @DHCP_OPT_IPV4_LIST:	Multiple IPv4 addresses, comma-separated
+ * @DHCP_OPT_UINT8:	Unsigned 8-bit integer
+ * @DHCP_OPT_UINT16:	Unsigned 16-bit integer
+ * @DHCP_OPT_UINT32:	Unsigned 32-bit integer
+ * @DHCP_OPT_INT32:	Signed 32-bit integer
+ */
+enum dhcp_opt_type {
+	DHCP_OPT_NONE,
+	DHCP_OPT_STR,
+	DHCP_OPT_IPV4,
+	DHCP_OPT_IPV4_LIST,
+	DHCP_OPT_UINT8,
+	DHCP_OPT_UINT16,
+	DHCP_OPT_UINT32,
+	DHCP_OPT_INT32,
+};
+
+/**
+ * dhcp_opt_types - Maps option code to RFC 2132 value type, indexed by code
+ */
+static const enum dhcp_opt_type dhcp_opt_types[] = {
+	[1]   = DHCP_OPT_IPV4,		/* Subnet Mask */
+	[2]   = DHCP_OPT_INT32,		/* Time Offset */
+	[3]   = DHCP_OPT_IPV4_LIST,	/* Router */
+	[4]   = DHCP_OPT_IPV4_LIST,	/* Time Server */
+	[5]   = DHCP_OPT_IPV4_LIST,	/* Name Server */
+	[6]   = DHCP_OPT_IPV4_LIST,	/* Domain Name Server */
+	[7]   = DHCP_OPT_IPV4_LIST,	/* Log Server */
+	[8]   = DHCP_OPT_IPV4_LIST,	/* Cookie Server */
+	[9]   = DHCP_OPT_IPV4_LIST,	/* LPR Server */
+	[10]  = DHCP_OPT_IPV4_LIST,	/* Impress Server */
+	[11]  = DHCP_OPT_IPV4_LIST,	/* Resource Location Server */
+	[12]  = DHCP_OPT_STR,		/* Host Name */
+	[13]  = DHCP_OPT_UINT16,	/* Boot File Size */
+	[15]  = DHCP_OPT_STR,		/* Domain Name */
+	[16]  = DHCP_OPT_IPV4,		/* Swap Server */
+	[17]  = DHCP_OPT_STR,		/* Root Path */
+	[19]  = DHCP_OPT_UINT8,		/* IP Forwarding */
+	[23]  = DHCP_OPT_UINT8,		/* Default IP TTL */
+	[26]  = DHCP_OPT_UINT16,	/* Interface MTU */
+	[28]  = DHCP_OPT_IPV4,		/* Broadcast Address */
+	[37]  = DHCP_OPT_UINT8,		/* TCP Default TTL */
+	[38]  = DHCP_OPT_UINT32,	/* TCP Keepalive Interval */
+	[40]  = DHCP_OPT_STR,		/* NIS Domain Name */
+	[41]  = DHCP_OPT_IPV4_LIST,	/* NIS Servers */
+	[42]  = DHCP_OPT_IPV4_LIST,	/* NTP Servers */
+	[44]  = DHCP_OPT_IPV4_LIST,	/* NetBIOS Name Server */
+	[50]  = DHCP_OPT_IPV4,		/* Requested IP Address */
+	[51]  = DHCP_OPT_UINT32,	/* IP Address Lease Time */
+	[53]  = DHCP_OPT_UINT8,		/* DHCP Message Type */
+	[54]  = DHCP_OPT_IPV4,		/* Server Identifier */
+	[57]  = DHCP_OPT_UINT16,	/* Max DHCP Message Size */
+	[58]  = DHCP_OPT_UINT32,	/* Renewal (T1) Time */
+	[59]  = DHCP_OPT_UINT32,	/* Rebinding (T2) Time */
+	[60]  = DHCP_OPT_STR,		/* Vendor Class Identifier */
+	[66]  = DHCP_OPT_STR,		/* TFTP Server Name */
+	[67]  = DHCP_OPT_STR,		/* Bootfile Name */
+	[252] = DHCP_OPT_STR,		/* WPAD URL */
+};
+
+/**
+ * dhcp_opt_parse() - Parse a DHCP option value
+ * @code:	DHCP 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 dhcp_opt_parse(uint8_t code, const char *str,
+		   uint8_t *buf, size_t buf_len)
+{
+	enum dhcp_opt_type type;
+	unsigned long val;
+	unsigned int i;
+	uint8_t width;
+	size_t slen;
+	size_t len;
+	char *end;
+
+	if (code >= ARRAY_SIZE(dhcp_opt_types))
+		return -1;
+
+	type = dhcp_opt_types[code];
+
+	if (!*str)
+		return -1;
+
+	switch (type) {
+	case DHCP_OPT_NONE:
+		return -1;
+	case DHCP_OPT_IPV4:
+	case DHCP_OPT_IPV4_LIST:
+		len = 0;
+
+		while (*str) {
+			char ipbuf[INET_ADDRSTRLEN];
+			size_t chunk;
+
+			chunk = strcspn(str, ",");
+
+			if (!chunk || chunk >= sizeof(ipbuf))
+				return -1;
+
+			memcpy(ipbuf, str, chunk);
+			ipbuf[chunk] = '\0';
+
+			if (len + sizeof(struct in_addr) > buf_len)
+				return -1;
+
+			if (inet_pton(AF_INET, ipbuf, buf + len) != 1)
+				return -1;
+
+			len += sizeof(struct in_addr);
+
+			if (type == DHCP_OPT_IPV4) {
+				if (str[chunk] == ',')
+					return -1;
+				break;
+			}
+
+			str += chunk + (str[chunk] == ',');
+		}
+
+		if (!len)
+			return -1;
+
+		return len;
+	case DHCP_OPT_UINT8:
+	case DHCP_OPT_UINT16:
+	case DHCP_OPT_UINT32:
+	case DHCP_OPT_INT32:
+		if (type == DHCP_OPT_UINT8)
+			width = 1;
+		else if (type == DHCP_OPT_UINT16)
+			width = 2;
+		else
+			width = 4;
+
+		if (buf_len < width)
+			return -1;
+
+		errno = 0;
+		if (type == DHCP_OPT_INT32) {
+			long sval;
+
+			sval = strtol(str, &end, 0);
+			if (*end || errno ||
+			    sval < INT32_MIN || sval > INT32_MAX)
+				return -1;
+			val = (uint32_t)sval;
+		} else {
+			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 DHCP_OPT_STR:
+		slen = strlen(str);
+
+		if (slen >= buf_len)
+			return -1;
+
+		memcpy(buf, str, slen);
+
+		return slen;
+	}
+
+	return -1;
+}
+
+/**
+ * dhcp_set_opt() - Parse and store a user-specified DHCP option
+ * @code:	DHCP option code
+ * @val_str:	Value string from command line
+ */
+void dhcp_set_opt(uint8_t code, const char *val_str)
+{
+	int ret;
+
+	ret = dhcp_opt_parse(code, val_str, opts[code].s, sizeof(opts[code].s));
+	if (ret < 0)
+		die("Invalid value for DHCP option %u: %s", code, val_str);
+
+	opts[code].slen = ret;
+	opts[code].state = OPT_USER;
+}
+
+/**
+ * dhcp_opt_to_str() - Render a binary DHCP option value to a printable string
+ * @code:	DHCP option code
+ * @buf:	Output string buffer
+ * @buf_len:	Size of output buffer
+ *
+ * Return: pointer to @buf if option is user-set, NULL otherwise
+ */
+const char *dhcp_opt_to_str(uint8_t code, char *buf, size_t buf_len)
+{
+	enum dhcp_opt_type type;
+	int off = 0;
+	unsigned int i;
+
+	if (opts[code].state != OPT_USER || opts[code].slen < 0)
+		return NULL;
+
+	if (code >= ARRAY_SIZE(dhcp_opt_types))
+		return NULL;
+
+	type = dhcp_opt_types[code];
+
+	switch (type) {
+	case DHCP_OPT_IPV4:
+	case DHCP_OPT_IPV4_LIST:
+		for (i = 0; i + 4 <= (unsigned int)opts[code].slen; i += 4) {
+			if (off)
+				off += snprintf(buf + off, buf_len - off, ",");
+			inet_ntop(AF_INET, opts[code].s + i,
+				  buf + off, buf_len - off);
+			off = strlen(buf);
+		}
+		return buf;
+	case DHCP_OPT_UINT8:
+		if (snprintf(buf, buf_len, "%u",
+			     opts[code].s[0]) >= (int)buf_len)
+			return NULL;
+		return buf;
+	case DHCP_OPT_UINT16:
+		if (snprintf(buf, buf_len, "%u",
+			     ((unsigned)opts[code].s[0] << 8) |
+			      opts[code].s[1]) >= (int)buf_len)
+			return NULL;
+		return buf;
+	case DHCP_OPT_UINT32:
+		if (snprintf(buf, buf_len, "%u",
+			     ((unsigned)opts[code].s[0] << 24) |
+			     ((unsigned)opts[code].s[1] << 16) |
+			     ((unsigned)opts[code].s[2] << 8)  |
+			      opts[code].s[3]) >= (int)buf_len)
+			return NULL;
+		return buf;
+	case DHCP_OPT_INT32:
+		if (snprintf(buf, buf_len, "%d",
+			     (int)(((unsigned)opts[code].s[0] << 24) |
+				   ((unsigned)opts[code].s[1] << 16) |
+				   ((unsigned)opts[code].s[2] << 8)  |
+				    opts[code].s[3])) >= (int)buf_len)
+			return NULL;
+		return buf;
+	case DHCP_OPT_STR:
+		if ((size_t)opts[code].slen < buf_len) {
+			memcpy(buf, opts[code].s, opts[code].slen);
+			buf[opts[code].slen] = '\0';
+		} else {
+			memcpy(buf, opts[code].s, buf_len - 1);
+			buf[buf_len - 1] = '\0';
+		}
+		return buf;
+	default:
+		return NULL;
+	}
+}
+
 /**
  * fill_one() - Fill a single option into a buffer
  * @buf:	Buffer to write option
@@ -482,14 +760,19 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	info("    from %s", eth_ntop(m->chaddr, macstr, sizeof(macstr)));
 
 	mask.s_addr = htonl(0xffffffff << (32 - c->ip4.prefix_len));
-	memcpy(opts[1].s,  &mask,                sizeof(mask));
-	memcpy(opts[3].s,  &c->ip4.guest_gw,     sizeof(c->ip4.guest_gw));
-	memcpy(opts[54].s, &c->ip4.our_tap_addr, sizeof(c->ip4.our_tap_addr));
+	if (opts[1].state != OPT_USER)
+		memcpy(opts[1].s, &mask, sizeof(mask));
+	if (opts[3].state != OPT_USER)
+		memcpy(opts[3].s, &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
+	if (opts[54].state != OPT_USER)
+		memcpy(opts[54].s, &c->ip4.our_tap_addr,
+		       sizeof(c->ip4.our_tap_addr));
 
 	/* If the gateway is not on the assigned subnet, send an option 121
 	 * (Classless Static Routing) adding a dummy route to it.
 	 */
-	if ((c->ip4.addr.s_addr & mask.s_addr)
+	if (opts[121].state != OPT_USER &&
+	    (c->ip4.addr.s_addr & mask.s_addr)
 	    != (c->ip4.guest_gw.s_addr & mask.s_addr)) {
 		/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
 		opts[121].slen = 14;
@@ -501,27 +784,32 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 		       &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
 	}
 
-	if (c->mtu) {
+	if (opts[26].state != OPT_USER && c->mtu) {
 		opts[26].slen = 2;
 		opts[26].state = OPT_DEFAULT;
 		opts[26].s[0] = c->mtu / 256;
 		opts[26].s[1] = c->mtu % 256;
 	}
 
-	for (i = 0, opts[6].slen = 0;
-	     !c->no_dhcp_dns && i < ARRAY_SIZE(c->ip4.dns); i++) {
-		if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns[i]))
-			break;
-		((struct in_addr *)opts[6].s)[i] = c->ip4.dns[i];
-		opts[6].slen += sizeof(uint32_t);
+	if (opts[6].state != OPT_USER) {
+		for (i = 0, opts[6].slen = 0;
+		     !c->no_dhcp_dns && i < ARRAY_SIZE(c->ip4.dns); i++) {
+			if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns[i]))
+				break;
+			((struct in_addr *)opts[6].s)[i] = c->ip4.dns[i];
+			opts[6].slen += sizeof(uint32_t);
+		}
+		if (!opts[6].slen)
+			opts[6].slen = -1;
 	}
+
 	if (opts[6].slen)
 		opts[6].state = OPT_DEFAULT;
 	else
 		opts[6].state = OPT_UNSET;
 
 	opt_len = strlen(c->hostname);
-	if (opt_len > 0) {
+	if (opts[12].state != OPT_USER && opt_len > 0) {
 		opts[12].slen = opt_len;
 		opts[12].state = OPT_DEFAULT;
 		memcpy(opts[12].s, &c->hostname, opt_len);
diff --git a/dhcp.h b/dhcp.h
index cd50c99..80abda6 100644
--- a/dhcp.h
+++ b/dhcp.h
@@ -8,5 +8,7 @@
 
 int dhcp(const struct ctx *c, struct iov_tail *data);
 void dhcp_init(void);
+void dhcp_set_opt(uint8_t code, const char *val_str);
+const char *dhcp_opt_to_str(uint8_t code, char *buf, size_t buf_len);
 
 #endif /* DHCP_H */
diff --git a/passt.1 b/passt.1
index 995590a..215a289 100644
--- a/passt.1
+++ b/passt.1
@@ -440,6 +440,48 @@ 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 \-\-dhcp-opt " " \fICODE\fR,\fIVALUE\fR
+Set DHCP option \fICODE\fR (1\-254) to \fIVALUE\fR. The value format depends
+on the option type and is determined automatically from the option code.
+Multiple IPv4 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. Options set with
+\fB\-\-dhcp-opt\fR override built-in values.
+.PP
+Examples:
+.nf
+  \-\-dhcp-opt 6,8.8.8.8,4.4.4.4
+  \-\-dhcp-opt 12,myhostname
+.fi
+.PP
+Only the following option codes are supported (unsupported codes cause an error):
+.RS
+.TP
+.B IPv4 address options
+1 (Subnet Mask), 16 (Swap Server), 28 (Broadcast Address), 50 (Requested IP),
+54 (Server Identifier)
+.TP
+.B IPv4 address list options (comma-separated)
+3 (Router), 4 (Time Server), 5 (Name Server), 6 (DNS), 7 (Log Server),
+8 (Cookie Server), 9 (LPR Server), 10 (Impress Server),
+11 (Resource Location Server), 41 (NIS Servers),
+42 (NTP Servers), 44 (NetBIOS Name Server)
+.TP
+.B Integer options
+2 (Time Offset, 32-bit), 13 (Boot File Size, 16-bit), 19 (IP Forwarding, 8-bit),
+23 (Default IP TTL, 8-bit), 26 (Interface MTU, 16-bit),
+37 (TCP Default TTL, 8-bit), 38 (TCP Keepalive Interval, 32-bit),
+51 (IP Address Lease Time, 32-bit),
+53 (DHCP Message Type, 8-bit), 57 (Max DHCP Message Size, 16-bit),
+58 (Renewal Time, 32-bit), 59 (Rebinding Time, 32-bit)
+.TP
+.B String options
+12 (Host Name), 15 (Domain Name), 17 (Root Path), 40 (NIS Domain Name),
+60 (Vendor Class Identifier), 66 (TFTP Server Name),
+67 (Bootfile Name), 252 (WPAD URL)
+.RE
+
 .TP
 .BR \-t ", " \-\-tcp-ports " " \fIspec
 Configure TCP port forwarding to guest or namespace. \fIspec\fR can be either:
-- 
2.54.0


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

* [PATCH v5 5/7] dhcp: Add --dhcp-boot command-line option
  2026-07-17 17:56 [PATCH v5 0/7] Add --dhcp-boot and --dhcp-opt options Anshu Kumari
                   ` (3 preceding siblings ...)
  2026-07-17 17:56 ` [PATCH v5 4/7] dhcp: Add --dhcp-opt with option table and value parser Anshu Kumari
@ 2026-07-17 17:56 ` Anshu Kumari
  2026-07-20  4:06   ` David Gibson
  2026-07-17 17:56 ` [PATCH v5 6/7] dhcp: Add RFC 3396 option splitting for concatenation-requiring options Anshu Kumari
  2026-07-17 17:56 ` [PATCH v5 7/7] dhcp: Handle FQDN option with RFC 3396 concatenation Anshu Kumari
  6 siblings, 1 reply; 17+ messages in thread
From: Anshu Kumari @ 2026-07-17 17:56 UTC (permalink / raw)
  To: anskuma, sbrivio, passt-dev; +Cc: lvivier, david, jmaloy

Add a convenience shorthand --dhcp-boot FILE that sets the boot file
name (DHCP option 67) for network boot.  This is equivalent to
--dhcp-opt 67,FILE.

Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
v5:
  - replaced conf_dhcp_option() to dhcp_set_opt().

v4:
  - Changed argument name from URL to FILE in usage and man page.
  - Fixed UEFI HTTP boot wording in man page

v3:
  - case 32 now calls dhcp_add_option(c, 67, optarg).
  - Handles duplicate codes: --dhcp-boot and --dhcp-opt 67 coexist
    correctly, last value wins.

v2:
  - Removed separate dhcp_boot[PATH_MAX] field — --dhcp-boot foo now stores into custom_opts[] as code 67 (same as --dhcp-opt 67,foo)

---
 conf.c  | 5 +++++
 passt.1 | 7 +++++++
 2 files changed, 12 insertions(+)

diff --git a/conf.c b/conf.c
index e7f3ea0..eccf1c0 100644
--- a/conf.c
+++ b/conf.c
@@ -617,6 +617,7 @@ static void usage(const char *name, FILE *f, int status)
 		"    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"
+		"  --dhcp-boot FILE	Boot file name for network boot\n"
 		"  --dhcp-opt CODE,VAL	Set DHCP option CODE to VAL\n");
 	if (strstr(name, "pasta"))
 		FPRINTF(f, "    default: don't use any search list\n");
@@ -1321,6 +1322,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 },
+		{"dhcp-boot", required_argument,	NULL,		33 },
 		{"dhcp-opt", required_argument,		NULL,		34 },
 		{ 0 },
 	};
@@ -1566,6 +1568,9 @@ void conf(struct ctx *c, int argc, char **argv)
 		case 32:
 			c->chroot_fallback = true;
 			break;
+		case 33:
+			dhcp_set_opt(67, optarg);
+			break;
 		case 34:
 			comma = strchr(optarg, ',');
 			if (!comma)
diff --git a/passt.1 b/passt.1
index 215a289..bfc2111 100644
--- a/passt.1
+++ b/passt.1
@@ -440,6 +440,13 @@ 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 \-\-dhcp-boot " " \fIfile
+Convenience shorthand for \fB\-\-dhcp-opt\fR 67,\fIfile\fR.
+Sets the boot file name (DHCP option 67) for network boot.
+For UEFI HTTP boot, the vendor class identifier also needs to be set using
+\fB\-\-dhcp-opt\fR 60,HTTPClient.
+
 .TP
 .BR \-\-dhcp-opt " " \fICODE\fR,\fIVALUE\fR
 Set DHCP option \fICODE\fR (1\-254) to \fIVALUE\fR. The value format depends
-- 
2.54.0


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

* [PATCH v5 6/7] dhcp: Add RFC 3396 option splitting for concatenation-requiring options
  2026-07-17 17:56 [PATCH v5 0/7] Add --dhcp-boot and --dhcp-opt options Anshu Kumari
                   ` (4 preceding siblings ...)
  2026-07-17 17:56 ` [PATCH v5 5/7] dhcp: Add --dhcp-boot command-line option Anshu Kumari
@ 2026-07-17 17:56 ` Anshu Kumari
  2026-07-20  4:21   ` David Gibson
  2026-07-17 17:56 ` [PATCH v5 7/7] dhcp: Handle FQDN option with RFC 3396 concatenation Anshu Kumari
  6 siblings, 1 reply; 17+ messages in thread
From: Anshu Kumari @ 2026-07-17 17:56 UTC (permalink / raw)
  To: anskuma, sbrivio, passt-dev; +Cc: lvivier, david, jmaloy

Implement option splitting per RFC 3396 for options that may exceed
255 bytes.  A new DHCP_OPT_STR_CONCAT type marks concatenation-
requiring options (currently option 81, Client FQDN per RFC 4702).

The opts[].s buffer is resized from 255 to 496 bytes to hold the
maximum data that can be split across the options field, file field,
and sname field.

When a concatenation-requiring option does not fit as a single option
in any field, fill() splits it across fields in RFC 3396 order:
options field first, then file, then sname.

Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
v5:
  - New patch: implement option splitting per RFC 3396 for options exceeding 255 bytes
  - Add DHCP_OPT_STR_CONCAT type, is_concat_opt(), fill_split() helpers
  - Resize opts[].s from 255 to OPT_CONCAT_MAX (496) bytes
  - Add /* fallthrough */ between DHCP_OPT_STR and DHCP_OPT_STR_CONCAT case

---
 dhcp.c | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 106 insertions(+), 3 deletions(-)

diff --git a/dhcp.c b/dhcp.c
index cc910ee..6bebb5f 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -34,6 +34,11 @@
 #include "log.h"
 #include "dhcp.h"
 
+/* RFC 3396: maximum option data that can be split across options field,
+ * file field, and sname field (minus code+length overhead per portion).
+ */
+#define OPT_CONCAT_MAX	496
+
 /**
  * enum opt_state - DHCP option state
  * @OPT_UNSET:		Option not configured
@@ -58,7 +63,7 @@ enum opt_state {
 struct opt {
 	int sent;
 	int slen;
-	uint8_t s[255];
+	uint8_t s[OPT_CONCAT_MAX];
 	int clen;
 	uint8_t c[255];
 	enum opt_state state;
@@ -159,6 +164,7 @@ struct msg {
  * @DHCP_OPT_UINT16:	Unsigned 16-bit integer
  * @DHCP_OPT_UINT32:	Unsigned 32-bit integer
  * @DHCP_OPT_INT32:	Signed 32-bit integer
+ * @DHCP_OPT_STR_CONCAT:Concatenation-requiring string (RFC 3396)
  */
 enum dhcp_opt_type {
 	DHCP_OPT_NONE,
@@ -169,6 +175,7 @@ enum dhcp_opt_type {
 	DHCP_OPT_UINT16,
 	DHCP_OPT_UINT32,
 	DHCP_OPT_INT32,
+	DHCP_OPT_STR_CONCAT,
 };
 
 /**
@@ -319,6 +326,10 @@ static int dhcp_opt_parse(uint8_t code, const char *str,
 
 		return width;
 	case DHCP_OPT_STR:
+		if (strlen(str) > 255)
+			return -1;
+		/* fallthrough */
+	case DHCP_OPT_STR_CONCAT:
 		slen = strlen(str);
 
 		if (slen >= buf_len)
@@ -465,6 +476,53 @@ enum dhcp_overload {
 	DHCP_OVERLOAD_SNAME,
 };
 
+/**
+ * is_concat_opt() - Check if option requires RFC 3396 concatenation support
+ * @o:		Option number
+ *
+ * Return: true if option is a concatenation-requiring type
+ */
+static bool is_concat_opt(int o)
+{
+	if ((size_t)o >= ARRAY_SIZE(dhcp_opt_types))
+		return false;
+	return dhcp_opt_types[o] == DHCP_OPT_STR_CONCAT;
+}
+
+/**
+ * fill_split() - Write a split portion of an option into a buffer
+ * @buf:	Buffer to write into
+ * @size:	Usable size of @buf
+ * @o:		Option number (code)
+ * @offset:	Current offset within @buf, updated on write
+ * @data:	Pointer to remaining option data to write
+ * @remaining:	Bytes of option data still to write
+ *
+ * Return: number of data bytes written (excluding code+length header)
+ */
+static size_t fill_split(uint8_t *buf, size_t size, int o, int *offset,
+			 const uint8_t *data, size_t remaining)
+{
+	size_t avail, chunk;
+
+	if (*offset + 2 >= (int)size)
+		return 0;
+
+	avail = size - *offset - 2;
+	chunk = remaining < avail ? remaining : avail;
+	if (!chunk)
+		return 0;
+
+	buf[*offset] = o;
+	buf[*offset + 1] = chunk;
+	*offset += 2;
+
+	memcpy(buf + *offset, data, chunk);
+	*offset += chunk;
+
+	return chunk;
+}
+
 /**
  * fill() - Fill options in message, with overload into file/sname if needed
  * @m:			Message to fill
@@ -513,14 +571,59 @@ static int fill(struct msg *m, enum dhcp_overload *overload, bool has_bootfile)
 	for (o = 0; (size_t)o < ARRAY_SIZE(opts); o++) {
 		if (opts[o].state == OPT_UNSET || opts[o].sent)
 			continue;
-
 		if (!has_bootfile &&
 			fill_one(m->file, sizeof(m->file) - 1, o,
-			         &file_off))
+				 &file_off))
+			if (!is_concat_opt(o))
 				debug("DHCP: skipping option %i"
 				      " (overload full)", o);
 	}
 
+	/* RFC 3396: split concatenation-requiring options that didn't fit
+	 * as a single option.  Split order: options, file, sname.
+	 */
+	for (o = 0; (size_t)o < ARRAY_SIZE(opts); o++) {
+		size_t file_cap, sname_cap, total, written;
+
+		if (opts[o].state == OPT_UNSET || opts[o].sent ||
+		    !is_concat_opt(o))
+			continue;
+
+		sname_cap = sizeof(m->sname) - 1 > (size_t)sname_off ?
+			    sizeof(m->sname) - 1 - sname_off : 0;
+
+		if (has_bootfile || sizeof(m->file) - 1 <= (size_t)file_off)
+			file_cap = 0;
+		else
+			file_cap = sizeof(m->file) - 1 - file_off;
+
+		total = (size > (size_t)offset ? size - offset : 0)
+			+ file_cap + sname_cap;
+
+		if (total < (size_t)opts[o].slen) {
+			debug("DHCP: skipping option %i (no space to split)",
+			      o);
+			continue;
+		}
+
+		written = 0;
+		written += fill_split(m->o, size, o, &offset,
+				      opts[o].s, opts[o].slen);
+		if (written < (size_t)opts[o].slen && !has_bootfile)
+			written += fill_split(m->file,
+					      sizeof(m->file) - 1, o,
+					      &file_off,
+					      opts[o].s + written,
+					      opts[o].slen - written);
+		if (written < (size_t)opts[o].slen)
+			fill_split(m->sname,
+				   sizeof(m->sname) - 1, o,
+				   &sname_off,
+				   opts[o].s + written,
+				   opts[o].slen - written);
+		opts[o].sent = 1;
+	}
+
 	if (sname_off) {
 		m->sname[sname_off] = 255;
 		*overload |= DHCP_OVERLOAD_SNAME;
-- 
2.54.0


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

* [PATCH v5 7/7] dhcp: Handle FQDN option with RFC 3396 concatenation
  2026-07-17 17:56 [PATCH v5 0/7] Add --dhcp-boot and --dhcp-opt options Anshu Kumari
                   ` (5 preceding siblings ...)
  2026-07-17 17:56 ` [PATCH v5 6/7] dhcp: Add RFC 3396 option splitting for concatenation-requiring options Anshu Kumari
@ 2026-07-17 17:56 ` Anshu Kumari
  2026-07-20  4:23   ` David Gibson
  6 siblings, 1 reply; 17+ messages in thread
From: Anshu Kumari @ 2026-07-17 17:56 UTC (permalink / raw)
  To: anskuma, sbrivio, passt-dev; +Cc: lvivier, david, jmaloy

Mark Client FQDN (option 81) as concatenation-requiring per
RFC 4702, Section 2.  When the encoded FQDN exceeds 255 bytes,
fill() now splits it across DHCP message fields using the RFC 3396
splitting infrastructure instead of silently dropping it.

Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
v5:
  - New patch: mark Client FQDN (option 81) as concatenation-requiring per RFC 4702
---
 dhcp.c | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/dhcp.c b/dhcp.c
index 6bebb5f..a54ac58 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -484,9 +484,15 @@ enum dhcp_overload {
  */
 static bool is_concat_opt(int o)
 {
-	if ((size_t)o >= ARRAY_SIZE(dhcp_opt_types))
-		return false;
-	return dhcp_opt_types[o] == DHCP_OPT_STR_CONCAT;
+	if ((size_t)o < ARRAY_SIZE(dhcp_opt_types) &&
+	    dhcp_opt_types[o] == DHCP_OPT_STR_CONCAT)
+		return true;
+
+	/* RFC 4702, Section 2: Client FQDN option requires concatenation */
+	if (o == 81)
+		return true;
+
+	return false;
 }
 
 /**
-- 
2.54.0


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

* Re: [PATCH v5 1/7] dhcp: Refactor fill_one() to operate on a generic buffer
  2026-07-17 17:56 ` [PATCH v5 1/7] dhcp: Refactor fill_one() to operate on a generic buffer Anshu Kumari
@ 2026-07-20  1:18   ` David Gibson
  0 siblings, 0 replies; 17+ messages in thread
From: David Gibson @ 2026-07-20  1:18 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy

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

On Fri, Jul 17, 2026 at 11:26:38PM +0530, Anshu Kumari wrote:
> Change fill_one() to accept a buffer pointer and capacity instead of
> a struct msg pointer.  This is a pure refactor with no behavior change,
> preparing for option overload support where fill_one() will also write
> into the file and sname fields.
> 
> Link: https://bugs.passt.top/show_bug.cgi?id=192
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>

Reviewed-by: David Gibson <david@gibson.dropbear.id.au>

> ---
> v5:
>   - Restore debug messages for skipped options in fill()
>   - Add /* code and length of option */ comment in size check
>   - Use buf + *offset instead of &buf[*offset] for memcpy
> 
> v3:
>   - Restored removed comments: "If we don't have space to write the
>     option, then just skip" and "Move to option".
> 
> v2:
>   - Renamed parameter cap → size.
> ---
>  dhcp.c | 27 ++++++++++++++-------------
>  1 file changed, 14 insertions(+), 13 deletions(-)
> 
> diff --git a/dhcp.c b/dhcp.c
> index c3c7422..bb72b72 100644
> --- a/dhcp.c
> +++ b/dhcp.c
> @@ -131,28 +131,29 @@ struct msg {
>  } __attribute__((__packed__));
>  
>  /**
> - * fill_one() - Fill a single option in message
> - * @m:		Message to fill
> + * fill_one() - Fill a single option into a buffer
> + * @buf:	Buffer to write option
> + * @size:	Usable size of @buf (excluding end marker)
>   * @o:		Option number
> - * @offset:	Current offset within options field, updated on insertion
> + * @offset:	Current offset within @buf, updated on insertion
>   *
> - * Return: false if m has space to write the option, true otherwise
> + * Return: false if @buf has space to write the option, true otherwise
>   */
> -static bool fill_one(struct msg *m, int o, int *offset)
> +static bool fill_one(uint8_t *buf, size_t size, int o, int *offset)
>  {
>  	size_t slen = opts[o].slen;
>  
>  	/* If we don't have space to write the option, then just skip */
> -	if (*offset + 2 /* code and length of option */ + slen > OPT_MAX)
> +	if (*offset + 2 /* code and length of option */ + slen > size)
>  		return true;
>  
> -	m->o[*offset] = o;
> -	m->o[*offset + 1] = slen;
> +	buf[*offset] = o;
> +	buf[*offset + 1] = slen;
>  
>  	/* Move to option */
>  	*offset += 2;
>  
> -	memcpy(&m->o[*offset], opts[o].s, slen);
> +	memcpy(buf + *offset, opts[o].s, slen);
>  
>  	opts[o].sent = 1;
>  	*offset += slen;
> @@ -177,19 +178,19 @@ static int fill(struct msg *m)
>  	 * Put it there explicitly, unless requested via option 55.
>  	 */
>  	if (opts[55].clen > 0 && !memchr(opts[55].c, 53, opts[55].clen))
> -		if (fill_one(m, 53, &offset))
> -			 debug("DHCP: skipping option 53");
> +		if (fill_one(m->o, OPT_MAX, 53, &offset))
> +			debug("DHCP: skipping option 53");
>  
>  	for (i = 0; i < opts[55].clen; i++) {
>  		o = opts[55].c[i];
>  		if (opts[o].slen != -1)
> -			if (fill_one(m, o, &offset))
> +			if (fill_one(m->o, OPT_MAX, o, &offset))
>  				debug("DHCP: skipping option %i", o);
>  	}
>  
>  	for (o = 0; o < 255; o++) {
>  		if (opts[o].slen != -1 && !opts[o].sent)
> -			if (fill_one(m, o, &offset))
> +			if (fill_one(m->o, OPT_MAX, o, &offset))
>  				debug("DHCP: skipping option %i", o);
>  	}
>  
> -- 
> 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] 17+ messages in thread

* Re: [PATCH v5 2/7] dhcp: Add option state management with enum opt_state
  2026-07-17 17:56 ` [PATCH v5 2/7] dhcp: Add option state management with enum opt_state Anshu Kumari
@ 2026-07-20  1:36   ` David Gibson
  0 siblings, 0 replies; 17+ messages in thread
From: David Gibson @ 2026-07-20  1:36 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy

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

On Fri, Jul 17, 2026 at 11:26:39PM +0530, Anshu Kumari wrote:
> Introduce enum opt_state to track each DHCP option instead
> of overloading slen = -1 for "not set".
> 
> OPT_UNSET means the option is not configured.
> OPT_DEFAULT means the option was set from host configuration.
> 
> This replaces all slen = -1 / slen != -1 checks with
> state = OPT_UNSET / state != OPT_UNSET, and sets state = OPT_DEFAULT
> for options initialised in dhcp_init() and at reply time in dhcp().
> 
> Link: https://bugs.passt.top/show_bug.cgi?id=192
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>

This patch looks like it correctly does what it says here.  However,
the commit message doesn't really explain *why* that's a desirable
thing to do.

> ---
> v5:
>   - New patch: introduce enum opt_state { OPT_UNSET, OPT_DEFAULT } to replace slen = -1 for tracking option state
>   - Replace all slen = -1 / slen != -1 checks with state = OPT_UNSET / state != OPT_UNSET
>   - Set OPT_DEFAULT for options initialised in dhcp_init() and at reply time
> 
> ---
>  dhcp.c | 57 ++++++++++++++++++++++++++++++++++++++++-----------------
>  1 file changed, 40 insertions(+), 17 deletions(-)
> 
> diff --git a/dhcp.c b/dhcp.c
> index bb72b72..e5d89fc 100644
> --- a/dhcp.c
> +++ b/dhcp.c
> @@ -33,13 +33,24 @@
>  #include "log.h"
>  #include "dhcp.h"
>  
> +/**
> + * enum opt_state - DHCP option state
> + * @OPT_UNSET:		Option not configured
> + * @OPT_DEFAULT:	Option set from host config
> + */
> +enum opt_state {
> +	OPT_UNSET,
> +	OPT_DEFAULT,
> +};
> +
>  /**
>   * struct opt - DHCP option
>   * @sent:	Convenience flag, set while filling replies
> - * @slen:	Length of option defined for server, -1 if not going to be sent
> + * @slen:	Length of option defined for server
>   * @s:		Option payload from server
>   * @clen:	Length of option received from client, -1 if not received
>   * @c:		Option payload from client
> + * @state:	Option state (unset or default)
>   */
>  struct opt {
>  	int sent;
> @@ -47,6 +58,7 @@ struct opt {
>  	uint8_t s[255];
>  	int clen;
>  	uint8_t c[255];
> +	enum opt_state state;
>  };
>  
>  static struct opt opts[256];
> @@ -76,16 +88,19 @@ void dhcp_init(void)
>  	int i;
>  
>  	for (i = 0; i < ARRAY_SIZE(opts); i++)
> -		opts[i].slen = -1;
> -
> -	opts[1]  = (struct opt) { 0, 4, {     0 }, 0, { 0 }, };	/* Mask */
> -	opts[3]  = (struct opt) { 0, 4, {     0 }, 0, { 0 }, };	/* Router */
> -	opts[51] = (struct opt) { 0, 4, {  0xff,
> -					   0xff,
> -					   0xff,
> -					   0xff }, 0, { 0 }, };	/* Lease time */
> -	opts[53] = (struct opt) { 0, 1, {     0 }, 0, { 0 }, };	/* Type */
> -	opts[54] = (struct opt) { 0, 4, {     0 }, 0, { 0 }, };	/* Server ID */
> +		opts[i].state = OPT_UNSET;
> +
> +	/* Mask */
> +	opts[1]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
> +	/* Router */
> +	opts[3]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
> +	/* Lease time */
> +	opts[51] = (struct opt) { 0, 4, { 0xff, 0xff, 0xff, 0xff },
> +				 0, { 0 }, OPT_DEFAULT, };
> +	/* Type */
> +	opts[53] = (struct opt) { 0, 1, {  0 }, 0, { 0 }, OPT_DEFAULT, };
> +	/* Server ID */
> +	opts[54] = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
>  }
>  
>  /**
> @@ -183,13 +198,13 @@ static int fill(struct msg *m)
>  
>  	for (i = 0; i < opts[55].clen; i++) {
>  		o = opts[55].c[i];
> -		if (opts[o].slen != -1)
> +		if (opts[o].state != OPT_UNSET)
>  			if (fill_one(m->o, OPT_MAX, o, &offset))
>  				debug("DHCP: skipping option %i", o);
>  	}
>  
>  	for (o = 0; o < 255; o++) {
> -		if (opts[o].slen != -1 && !opts[o].sent)
> +		if (opts[o].state != OPT_UNSET && !opts[o].sent)
>  			if (fill_one(m->o, OPT_MAX, o, &offset))
>  				debug("DHCP: skipping option %i", o);
>  	}
> @@ -243,6 +258,7 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
>  	int i;
>  
>  	opts[119].slen = 0;
> +	opts[119].state = OPT_DEFAULT;
>  
>  	for (i = 0; i < 255; i++)
>  		max_len -= opts[i].slen;
> @@ -291,7 +307,7 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
>  	}
>  
>  	if (!opts[119].slen)
> -		opts[119].slen = -1;
> +		opts[119].state = OPT_UNSET;
>  }
>  
>  /**
> @@ -389,7 +405,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
>  		iov_drop_header(data, *olen);
>  	}
>  
> -	opts[80].slen = -1;
> +	opts[80].state = OPT_UNSET;
>  	if (opts[53].clen > 0 && opts[53].c[0] == DHCPDISCOVER) {
>  		if (opts[80].clen == -1) {
>  			info("DHCP: offer to discover");
> @@ -398,6 +414,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
>  			info("DHCP: ack to discover (Rapid Commit)");
>  			opts[53].s[0] = DHCPACK;
>  			opts[80].slen = 0;
> +			opts[80].state = OPT_DEFAULT;
>  		}
>  	} else if (opts[53].clen <= 0 || opts[53].c[0] == DHCPREQUEST) {
>  		info("%s: ack to request", /* DHCP needs a valid message type */
> @@ -421,6 +438,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
>  	    != (c->ip4.guest_gw.s_addr & mask.s_addr)) {
>  		/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
>  		opts[121].slen = 14;
> +		opts[121].state = OPT_DEFAULT;
>  		opts[121].s[0] = 32;
>  		memcpy(opts[121].s + 1,
>  		       &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
> @@ -430,6 +448,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
>  
>  	if (c->mtu) {
>  		opts[26].slen = 2;
> +		opts[26].state = OPT_DEFAULT;
>  		opts[26].s[0] = c->mtu / 256;
>  		opts[26].s[1] = c->mtu % 256;
>  	}
> @@ -441,12 +460,15 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
>  		((struct in_addr *)opts[6].s)[i] = c->ip4.dns[i];
>  		opts[6].slen += sizeof(uint32_t);
>  	}
> -	if (!opts[6].slen)
> -		opts[6].slen = -1;
> +	if (opts[6].slen)
> +		opts[6].state = OPT_DEFAULT;
> +	else
> +		opts[6].state = OPT_UNSET;
>  
>  	opt_len = strlen(c->hostname);
>  	if (opt_len > 0) {
>  		opts[12].slen = opt_len;
> +		opts[12].state = OPT_DEFAULT;
>  		memcpy(opts[12].s, &c->hostname, opt_len);
>  	}
>  
> @@ -463,6 +485,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
>  			encode_domain_name((char *)opts[81].s + 3, c->fqdn);
>  
>  			opts[81].slen = opt_len;
> +			opts[81].state = OPT_DEFAULT;
>  		} else {
>  			debug("DHCP: client FQDN option doesn't fit, skipping");
>  		}
> -- 
> 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] 17+ messages in thread

* Re: [PATCH v5 3/7] dhcp: Add option overload
  2026-07-17 17:56 ` [PATCH v5 3/7] dhcp: Add option overload Anshu Kumari
@ 2026-07-20  2:03   ` David Gibson
  0 siblings, 0 replies; 17+ messages in thread
From: David Gibson @ 2026-07-20  2:03 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy

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

On Fri, Jul 17, 2026 at 11:26:40PM +0530, Anshu Kumari wrote:
> When the options field is full, overflow remaining DHCP options into
> the sname and file fields per RFC 2132 option 52.
> 
> Per RFC 2132, Section 9.5, the boot file name is always placed in the
> 'file' header field.  When a boot file is set, the file field is
> reserved from overload and overflow uses only the sname field.
> 
> Link: https://bugs.passt.top/show_bug.cgi?id=192
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> ---
> v5:
>   - enhanced enum dhcp_overload to follow kernel-doc.
>   - Inline fill_overflow() into fill()
>   - Use state-based checks instead of slen
> 
> v4:
>   - Converted overload #defines to enum dhcp_overload.
>   - Fixed missing whitespace in comment before */.
>   - Boot file name always placed in 'file' header field per RFC 2132,
>     Section 9.5; file field reserved from overload when bootfile is
>     set; option 67 suppressed from options area.
> 
> v3:
>   - Added RFC 2132 Section 9.3 reference comment on overload
>     constants.
>   - Use ARRAY_SIZE(opts) instead of raw 255 in fill_overflow().
>   - Swapped overflow order: try sname (64 bytes) first, then file
>     (128 bytes) — better packing and keeps file field available for
>     boot file name.
>   - Removed '&' from &reply.file.
>   - Removed '+1' from memcpy — reply.file already zeroed.
>   - opt_set_dns_search() max_len: OPT_MAX - 3 instead of
>     sizeof(m->o).
> 
> v2:
>   - Added #define DHCP_OVERLOAD_FILE and #define DHCP_OVERLOAD_SNAME constants
>   - Added comment documenting space reservation: /* Reserve 3 bytes for option 52 */
>   - Fixed DNS search length: sizeof(m->o) only, not combined with file+sname
>   - Removed dhcp_boot references — reply.file copy now reads from opts[67]
>   - Used DHCP_OVERLOAD_FILE constant in reply.file guard
> 
> ---
>  dhcp.c | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
>  1 file changed, 79 insertions(+), 9 deletions(-)
> 
> diff --git a/dhcp.c b/dhcp.c
> index e5d89fc..39f7952 100644
> --- a/dhcp.c
> +++ b/dhcp.c
> @@ -176,13 +176,31 @@ static bool fill_one(uint8_t *buf, size_t size, int o, int *offset)
>  }
>  
>  /**
> - * fill() - Fill options in message
> - * @m:		Message to fill
> +* enum dhcp_overload - DHCP option overload values (RFC 2132, Section 9.3)

Nit: missing space before the *.

> + * @DHCP_OVERLOAD_NONE:		No overload
> + * @DHCP_OVERLOAD_FILE:		file field carries options
> + * @DHCP_OVERLOAD_SNAME:	sname field carries options
> + */
> +enum dhcp_overload {
> +	DHCP_OVERLOAD_NONE,
> +	DHCP_OVERLOAD_FILE,
> +	DHCP_OVERLOAD_SNAME,

Nit: I'd suggest putting specific value assignments on these, even
though they're technically redundant.  It serves to make it clearer
that the specific numerical values matters, since these go "over the
wire".

> +};
> +
> +/**
> + * fill() - Fill options in message, with overload into file/sname if needed
> + * @m:			Message to fill
> + * @overload:		Set to option 52 value (0 if none, 1/2/3 per RFC 2132)
> + * @has_bootfile:	Reserve file field for boot file name
>   *
>   * Return: current size of options field
>   */
> -static int fill(struct msg *m)
> +static int fill(struct msg *m, enum dhcp_overload *overload, bool has_bootfile)
>  {
> +	int sname_off = 0, file_off = 0;
> +	*overload = DHCP_OVERLOAD_NONE;
> +	/* Reserve 3 bytes for option 52 (overload) if needed */
> +	size_t size = OPT_MAX - 3;
>  	int i, o, offset = 0;
>  
>  	for (o = 0; o < 255; o++)
> @@ -199,14 +217,47 @@ static int fill(struct msg *m)
>  	for (i = 0; i < opts[55].clen; i++) {
>  		o = opts[55].c[i];
>  		if (opts[o].state != OPT_UNSET)
> -			if (fill_one(m->o, OPT_MAX, o, &offset))
> -				debug("DHCP: skipping option %i", o);
> +			fill_one(m->o, size, o, &offset);
>  	}
>  
>  	for (o = 0; o < 255; o++) {
>  		if (opts[o].state != OPT_UNSET && !opts[o].sent)
> -			if (fill_one(m->o, OPT_MAX, o, &offset))
> -				debug("DHCP: skipping option %i", o);
> +			fill_one(m->o, size, o, &offset);
> +	}
> +
> +	/* Overflow unsent options into sname, then file */
> +	for (o = 0; (size_t)o < ARRAY_SIZE(opts); o++) {
> +		if (opts[o].state == OPT_UNSET || opts[o].sent)
> +			continue;
> +		fill_one(m->sname, sizeof(m->sname) - 1, o, &sname_off);
> +	}
> +
> +	for (o = 0; (size_t)o < ARRAY_SIZE(opts); o++) {
> +		if (opts[o].state == OPT_UNSET || opts[o].sent)
> +			continue;
> +
> +		if (!has_bootfile &&
> +			fill_one(m->file, sizeof(m->file) - 1, o,
> +			         &file_off))
> +				debug("DHCP: skipping option %i"
> +				      " (overload full)", o);

The logic here will omit all the "skipping option" messages if
has_bootfile is true.  I think it might be cleaner to change
fill_one() to return void, and add a final pass generating the
"skipping" messages if !opts[o].sent.

> +	}
> +
> +	if (sname_off) {
> +		m->sname[sname_off] = 255;
> +		*overload |= DHCP_OVERLOAD_SNAME;
> +	}
> +
> +	if (file_off) {
> +		m->file[file_off] = 255;
> +		*overload |= DHCP_OVERLOAD_FILE;
> +	}
> +
> +
> +	if (*overload) {
> +		m->o[offset++] = 52;
> +		m->o[offset++] = 1;
> +		m->o[offset++] = *overload;
>  	}
>  
>  	m->o[offset++] = 255;
> @@ -320,6 +371,7 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
>  int dhcp(const struct ctx *c, struct iov_tail *data)
>  {
>  	char macstr[ETH_ADDRSTRLEN];
> +	enum dhcp_overload overload;
>  	size_t mlen, dlen, opt_len;
>  	struct in_addr mask, dst;
>  	struct ethhdr eh_storage;
> @@ -328,9 +380,12 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
>  	const struct ethhdr *eh;
>  	const struct iphdr *iph;
>  	const struct udphdr *uh;
> +	uint8_t bootfile[128];
>  	struct msg m_storage;
>  	struct msg const *m;
> +	bool has_bootfile;
>  	struct msg reply;
> +	int bootfile_len;
>  	unsigned int i;
>  
>  	eh = IOV_REMOVE_HEADER(data, eh_storage);
> @@ -492,9 +547,24 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
>  	}
>  
>  	if (!c->no_dhcp_dns_search)
> -		opt_set_dns_search(c, sizeof(m->o));
> +		 /* 3 bytes reserved for option 52 (code, length, value) */
> +		opt_set_dns_search(c, OPT_MAX - 3);
> +
> +	/* RFC 2132, Section 9.5: put boot file name in the 'file' header
> +	 * field.  Suppress option 67 from the options area and reserve
> +	 * the file field from overload.
> +	 */
> +	has_bootfile = opts[67].slen > 0 &&
> +		       (size_t)opts[67].slen < sizeof(reply.file);
> +	if (has_bootfile) {
> +		memcpy(bootfile, opts[67].s, opts[67].slen);
> +		bootfile_len = opts[67].slen;

Why copy into the 'bootfile' temporary..

> +	}
> +
> +	dlen = offsetof(struct msg, o) + fill(&reply, &overload, has_bootfile);
>  
> -	dlen = offsetof(struct msg, o) + fill(&reply);
> +	if (has_bootfile)
> +		memcpy(reply.file, bootfile, bootfile_len);
... then out again, rather than copying directly from opts[67].s into
reply.file?

>  
>  	if (m->flags & FLAG_BROADCAST)
>  		dst = in4addr_broadcast;
> -- 
> 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] 17+ messages in thread

* Re: [PATCH v5 4/7] dhcp: Add --dhcp-opt with option table and value parser
  2026-07-17 17:56 ` [PATCH v5 4/7] dhcp: Add --dhcp-opt with option table and value parser Anshu Kumari
@ 2026-07-20  3:58   ` David Gibson
  0 siblings, 0 replies; 17+ messages in thread
From: David Gibson @ 2026-07-20  3:58 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy

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

On Fri, Jul 17, 2026 at 11:26:41PM +0530, Anshu Kumari wrote:
> Introduce the --dhcp-opt flag that allows setting arbitrary DHCP
> options from command-line in the form [--dhcp-opt CODE,VALUE].
> 
> Add a type lookup table mapping option codes to RFC 2132 value types
> (IPv4, IPv4 list, integer, string) and dhcp_opt_parse() to convert
> CLI strings to binary wire format.  Parsed options are stored in
> struct opts[] and injected into DHCP replies.  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>
> ---
> v5:
>   - Rename enum values: INT8/INT16/INT32/SINT32 → UINT8/UINT16/UINT32/INT32
>   - Change int len to size_t len in dhcp_opt_parse(), remove int casts
>   - Make dhcp_opt_parse() static, add dhcp_set_opt() to store options 
>     directly in opts[] at startup instead of double-parsing via ctx->dhcp_opts
>   - Add OPT_USER to enum opt_state to protect user options from being
>     overwritten by built-in defaults
>   - Add dhcp_opt_to_str() for conf_print() display
>   - Remove dhcp_opts[] from struct ctx in passt.h
>   - Remove conf_dhcp_option() wrapper from conf.c
> 
> v4:
>   - Renamed custom_opts to dhcp_opts, 256 entries indexed by option
>     code, removed MAX_CUSTOM_DHCP_OPTS and count field.
>   - Changed str buffer from 256 to 255 bytes.
>   - Moved function to conf.c as static conf_dhcp_option(), renamed
>     from dhcp_add_option().
>   - Made dhcp_opt_parse() non-static, declared in dhcp.h
>   - Dropped val/len from ctx struct; conf_dhcp_option() validates
>     with temp buffer, dhcp() parses str directly into opts[] at
>     reply time.
>   - Replaced strtok_r() + 256-byte buffer with strcspn() +
>     INET_ADDRSTRLEN buffer.
>   - Added DHCP_OPT_SINT32 for option 2 (Time Offset), uses strtol()
>     per RFC 2132 Section 8.2.
>   - All errors in dhcp_opt_parse() return -1, removed die() calls;
>     caller handles error message consistently.
>   - Removed redundant !slen check in DHCP_OPT_STR case.
>   - Omitted explicit array size for dhcp_opt_types[], arraydded bounds
>     check before lookup.
>   - Added errno = 0 + errno check for strtoul() in case 34.
>   - Fixed usage text: "Set DHCP option CODE to VAL".
>   - Improved man page: added format description and examples
> 
> v3:
>   - Replaced DHCP_OPT_INTEGER with separate DHCP_OPT_INT8/INT16/INT32
>     enums, removed dhcp_opt_int_width[] array.
>   - Shared logic between DHCP_OPT_IPV4 and DHCP_OPT_IPV4_LIST — parse
>     both as list, error if >1 in single case.
>   - Added errno = 0 before strtoul() and check after.
>   - Fixed range check: 1ULL << (width * 8) for all widths including
>     width==4.
>   - strncpy → memcpy for DHCP_OPT_STR.
>   - Moved enum to dhcp.c since not used in other files.
>   - Removed options 55, 61 (client-only), 119 (DNS compression, use
>     --dhcp-search instead), 33 (IP pairs not supported).
>   - DHCP_OPT_PARSE_BUF 1024 → char tmp[256].
>   - Upgraded dhcp_add_option() to call dhcp_opt_parse() and populate
>     val[]/len.
>   - Aligned array entries for readability.
>   - Added tab after @DHCP_OPT_IPV4_LIST: in kerneldoc.
>   - Reject empty value strings before parsing
>   - Reject leading/trailing/consecutive commas in IP list values.
> 
> v2:
>   - Replaced struct lookup table + dhcp_opt_type_lookup() function with flat dhcp_opt_types[256] array indexed by code.
>   - Consolidated DHCP_OPT_UINT8/UINT16/UINT32 into single DHCP_OPT_INTEGER with dhcp_opt_int_width[256] table.
>   - Dropped DHCP_OPT_ROUTES / option 121 entirely.
>   - Added kerneldoc for enum dhcp_opt_type values.
>   - Removed curly braces from switch cases, declarations before switch.
>   - Added newlines before return statements.
>   - Changed IP list delimiter from space to comma (--dhcp-opt 6,1.1.1.1,8.8.8.8).
>   - Defined DHCP_OPT_PARSE_BUF constant for bare 1024.
>   - Added len and val[255] fields to struct here (moved from patch 1).
>   - Added kerneldoc for @custom_opts.len and @custom_opts.val.
>   - Wired dhcp_opt_parse() into case 32 (--dhcp-boot) to populate val/len.
> 
> ---
>  conf.c  |  25 ++++-
>  dhcp.c  | 338 +++++++++++++++++++++++++++++++++++++++++++++++++++-----
>  dhcp.h  |   2 +
>  passt.1 |  42 +++++++
>  4 files changed, 381 insertions(+), 26 deletions(-)
> 
> diff --git a/conf.c b/conf.c
> index 5b6cc2b..e7f3ea0 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -47,6 +47,7 @@
>  #include "lineread.h"
>  #include "isolation.h"
>  #include "log.h"
> +#include "dhcp.h"
>  #include "vhost_user.h"
>  #include "epoll_ctl.h"
>  #include "conf.h"
> @@ -615,7 +616,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"
> +		"  --dhcp-opt CODE,VAL	Set DHCP option CODE to VAL\n");
>  	if (strstr(name, "pasta"))
>  		FPRINTF(f, "    default: don't use any search list\n");
>  	else
> @@ -843,6 +845,9 @@ static void conf_print(const struct ctx *c)
>  			info("    router: %s",
>  			     inet_ntop(AF_INET, &c->ip4.guest_gw,
>  				       buf, sizeof(buf)));
> +			for (i = 1; i < 255; i++)
> +				if (dhcp_opt_to_str(i, buf, sizeof(buf)))
> +					info("    option %u: %s", i, buf);
>  		}
>  
>  		for (i = 0; i < ARRAY_SIZE(c->ip4.dns); i++) {
> @@ -1316,6 +1321,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 },
> +		{"dhcp-opt", required_argument,		NULL,		34 },
>  		{ 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:";
> @@ -1330,10 +1336,13 @@ void conf(struct ctx *c, int argc, char **argv)
>  	unsigned int ifi4 = 0, ifi6 = 0;
>  	bool opt_a_is_prefix = false;
>  	const char *logfile = NULL;
> +	unsigned long optcode;
>  	char *runas = NULL;
>  	size_t logsize = 0;
> +	const char *comma;
>  	uint8_t opt_n = 0;
>  	int name, ret;
> +	char *end;

It's probably worth putting a block in case 34 rather than adding
these new variables to the top of conf().

>  	uid_t uid;
>  	gid_t gid;
>  
> @@ -1557,6 +1566,20 @@ void conf(struct ctx *c, int argc, char **argv)
>  		case 32:
>  			c->chroot_fallback = true;
>  			break;
> +		case 34:
> +			comma = strchr(optarg, ',');
> +			if (!comma)
> +				die("--dhcp-opt requires CODE,VALUE format");
> +
> +			errno = 0;
> +			optcode = strtoul(optarg, &end, 0);
> +			if (end != comma || errno ||
> +			    optcode < 1 || optcode > 254)
> +				die("DHCP option code must be 1-254: %s",
> +				    optarg);
> +

This can now be done more neatly with the parse_unsigned() and
parse_literal() functions I've added.  Specifically:

	p = optarg;
	if (!parse_unsigned(&p, 0, &optcode) ||
	    !parse_literal(&p, ","))
		die("--dhcp-opt requires CODE,VALUE format");
	...

> +			dhcp_set_opt(optcode, comma + 1);
> +			break;
>  		case 'd':
>  			c->debug = 1;
>  			c->quiet = 0;
> diff --git a/dhcp.c b/dhcp.c
> index 39f7952..cc910ee 100644
> --- a/dhcp.c
> +++ b/dhcp.c
> @@ -23,6 +23,7 @@
>  #include <unistd.h>
>  #include <string.h>
>  #include <limits.h>
> +#include <errno.h>
>  
>  #include "util.h"
>  #include "ip.h"
> @@ -37,10 +38,12 @@
>   * enum opt_state - DHCP option state
>   * @OPT_UNSET:		Option not configured
>   * @OPT_DEFAULT:	Option set from host config
> + * @OPT_USER:		Option set via --dhcp-opt command line

Ok, now I see the point of enum opt_state.

>   */
>  enum opt_state {
>  	OPT_UNSET,
>  	OPT_DEFAULT,
> +	OPT_USER,
>  };
>  
>  /**
> @@ -50,7 +53,7 @@ enum opt_state {
>   * @s:		Option payload from server
>   * @clen:	Length of option received from client, -1 if not received
>   * @c:		Option payload from client
> - * @state:	Option state (unset or default)
> + * @state:	Option state (unset, default, or user)
>   */
>  struct opt {
>  	int sent;
> @@ -88,19 +91,20 @@ void dhcp_init(void)
>  	int i;
>  
>  	for (i = 0; i < ARRAY_SIZE(opts); i++)
> -		opts[i].state = OPT_UNSET;
> -
> -	/* Mask */
> -	opts[1]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
> -	/* Router */
> -	opts[3]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
> -	/* Lease time */
> -	opts[51] = (struct opt) { 0, 4, { 0xff, 0xff, 0xff, 0xff },
> +		if (opts[i].state != OPT_USER)
> +			opts[i].state = OPT_UNSET;
> +
> +	if (opts[1].state != OPT_USER)			/* Mask */
> +		opts[1]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
> +	if (opts[3].state != OPT_USER)			/* Router */
> +		opts[3]  = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
> +	if (opts[51].state != OPT_USER)			/* Lease time */
> +		opts[51] = (struct opt) { 0, 4, { 0xff, 0xff, 0xff, 0xff },
>  				 0, { 0 }, OPT_DEFAULT, };
> -	/* Type */
> -	opts[53] = (struct opt) { 0, 1, {  0 }, 0, { 0 }, OPT_DEFAULT, };
> -	/* Server ID */
> -	opts[54] = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
> +	if (opts[53].state != OPT_USER)			/* Type */
> +		opts[53] = (struct opt) { 0, 1, {  0 }, 0, { 0 }, OPT_DEFAULT, };
> +	if (opts[54].state != OPT_USER)			/* Server ID */
> +		opts[54] = (struct opt) { 0, 4, {  0 }, 0, { 0 }, OPT_DEFAULT, };
>  }
>  
>  /**
> @@ -145,6 +149,280 @@ struct msg {
>  	uint8_t o[OPT_MAX + 1 /* End option */ ];
>  } __attribute__((__packed__));
>  
> +/**
> + * enum dhcp_opt_type - DHCP option value types per RFC 2132
> + * @DHCP_OPT_NONE:	Unsupported or unknown option
> + * @DHCP_OPT_STR:	Variable-length string
> + * @DHCP_OPT_IPV4:	Single IPv4 address
> + * @DHCP_OPT_IPV4_LIST:	Multiple IPv4 addresses, comma-separated
> + * @DHCP_OPT_UINT8:	Unsigned 8-bit integer
> + * @DHCP_OPT_UINT16:	Unsigned 16-bit integer
> + * @DHCP_OPT_UINT32:	Unsigned 32-bit integer
> + * @DHCP_OPT_INT32:	Signed 32-bit integer
> + */
> +enum dhcp_opt_type {
> +	DHCP_OPT_NONE,
> +	DHCP_OPT_STR,
> +	DHCP_OPT_IPV4,
> +	DHCP_OPT_IPV4_LIST,
> +	DHCP_OPT_UINT8,
> +	DHCP_OPT_UINT16,
> +	DHCP_OPT_UINT32,
> +	DHCP_OPT_INT32,
> +};
> +
> +/**
> + * dhcp_opt_types - Maps option code to RFC 2132 value type, indexed by code
> + */
> +static const enum dhcp_opt_type dhcp_opt_types[] = {
> +	[1]   = DHCP_OPT_IPV4,		/* Subnet Mask */
> +	[2]   = DHCP_OPT_INT32,		/* Time Offset */
> +	[3]   = DHCP_OPT_IPV4_LIST,	/* Router */
> +	[4]   = DHCP_OPT_IPV4_LIST,	/* Time Server */
> +	[5]   = DHCP_OPT_IPV4_LIST,	/* Name Server */
> +	[6]   = DHCP_OPT_IPV4_LIST,	/* Domain Name Server */
> +	[7]   = DHCP_OPT_IPV4_LIST,	/* Log Server */
> +	[8]   = DHCP_OPT_IPV4_LIST,	/* Cookie Server */
> +	[9]   = DHCP_OPT_IPV4_LIST,	/* LPR Server */
> +	[10]  = DHCP_OPT_IPV4_LIST,	/* Impress Server */
> +	[11]  = DHCP_OPT_IPV4_LIST,	/* Resource Location Server */
> +	[12]  = DHCP_OPT_STR,		/* Host Name */
> +	[13]  = DHCP_OPT_UINT16,	/* Boot File Size */
> +	[15]  = DHCP_OPT_STR,		/* Domain Name */
> +	[16]  = DHCP_OPT_IPV4,		/* Swap Server */
> +	[17]  = DHCP_OPT_STR,		/* Root Path */
> +	[19]  = DHCP_OPT_UINT8,		/* IP Forwarding */
> +	[23]  = DHCP_OPT_UINT8,		/* Default IP TTL */
> +	[26]  = DHCP_OPT_UINT16,	/* Interface MTU */
> +	[28]  = DHCP_OPT_IPV4,		/* Broadcast Address */
> +	[37]  = DHCP_OPT_UINT8,		/* TCP Default TTL */
> +	[38]  = DHCP_OPT_UINT32,	/* TCP Keepalive Interval */
> +	[40]  = DHCP_OPT_STR,		/* NIS Domain Name */
> +	[41]  = DHCP_OPT_IPV4_LIST,	/* NIS Servers */
> +	[42]  = DHCP_OPT_IPV4_LIST,	/* NTP Servers */
> +	[44]  = DHCP_OPT_IPV4_LIST,	/* NetBIOS Name Server */
> +	[50]  = DHCP_OPT_IPV4,		/* Requested IP Address */
> +	[51]  = DHCP_OPT_UINT32,	/* IP Address Lease Time */
> +	[53]  = DHCP_OPT_UINT8,		/* DHCP Message Type */
> +	[54]  = DHCP_OPT_IPV4,		/* Server Identifier */
> +	[57]  = DHCP_OPT_UINT16,	/* Max DHCP Message Size */
> +	[58]  = DHCP_OPT_UINT32,	/* Renewal (T1) Time */
> +	[59]  = DHCP_OPT_UINT32,	/* Rebinding (T2) Time */
> +	[60]  = DHCP_OPT_STR,		/* Vendor Class Identifier */
> +	[66]  = DHCP_OPT_STR,		/* TFTP Server Name */
> +	[67]  = DHCP_OPT_STR,		/* Bootfile Name */
> +	[252] = DHCP_OPT_STR,		/* WPAD URL */
> +};
> +
> +/**
> + * dhcp_opt_parse() - Parse a DHCP option value
> + * @code:	DHCP 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 dhcp_opt_parse(uint8_t code, const char *str,
> +		   uint8_t *buf, size_t buf_len)
> +{
> +	enum dhcp_opt_type type;
> +	unsigned long val;
> +	unsigned int i;
> +	uint8_t width;
> +	size_t slen;
> +	size_t len;
> +	char *end;
> +
> +	if (code >= ARRAY_SIZE(dhcp_opt_types))
> +		return -1;
> +
> +	type = dhcp_opt_types[code];
> +
> +	if (!*str)
> +		return -1;
> +
> +	switch (type) {
> +	case DHCP_OPT_NONE:
> +		return -1;
> +	case DHCP_OPT_IPV4:
> +	case DHCP_OPT_IPV4_LIST:
> +		len = 0;
> +
> +		while (*str) {
> +			char ipbuf[INET_ADDRSTRLEN];
> +			size_t chunk;
> +
> +			chunk = strcspn(str, ",");
> +
> +			if (!chunk || chunk >= sizeof(ipbuf))
> +				return -1;
> +
> +			memcpy(ipbuf, str, chunk);
> +			ipbuf[chunk] = '\0';
> +
> +			if (len + sizeof(struct in_addr) > buf_len)
> +				return -1;
> +
> +			if (inet_pton(AF_INET, ipbuf, buf + len) != 1)
> +				return -1;

The new stuff in parse.c can make this easier too - I can walk you
through how to use it if you need.

> +
> +			len += sizeof(struct in_addr);
> +
> +			if (type == DHCP_OPT_IPV4) {
> +				if (str[chunk] == ',')
> +					return -1;
> +				break;
> +			}
> +
> +			str += chunk + (str[chunk] == ',');
> +		}
> +
> +		if (!len)
> +			return -1;
> +
> +		return len;
> +	case DHCP_OPT_UINT8:
> +	case DHCP_OPT_UINT16:
> +	case DHCP_OPT_UINT32:
> +	case DHCP_OPT_INT32:
> +		if (type == DHCP_OPT_UINT8)
> +			width = 1;
> +		else if (type == DHCP_OPT_UINT16)
> +			width = 2;
> +		else
> +			width = 4;
> +
> +		if (buf_len < width)
> +			return -1;
> +
> +		errno = 0;
> +		if (type == DHCP_OPT_INT32) {
> +			long sval;
> +
> +			sval = strtol(str, &end, 0);
> +			if (*end || errno ||
> +			    sval < INT32_MIN || sval > INT32_MAX)
> +				return -1;
> +			val = (uint32_t)sval;
> +		} else {
> +			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 DHCP_OPT_STR:
> +		slen = strlen(str);
> +
> +		if (slen >= buf_len)
> +			return -1;
> +
> +		memcpy(buf, str, slen);
> +
> +		return slen;
> +	}
> +
> +	return -1;
> +}
> +
> +/**
> + * dhcp_set_opt() - Parse and store a user-specified DHCP option
> + * @code:	DHCP option code
> + * @val_str:	Value string from command line
> + */
> +void dhcp_set_opt(uint8_t code, const char *val_str)
> +{
> +	int ret;
> +
> +	ret = dhcp_opt_parse(code, val_str, opts[code].s, sizeof(opts[code].s));
> +	if (ret < 0)
> +		die("Invalid value for DHCP option %u: %s", code, val_str);
> +
> +	opts[code].slen = ret;
> +	opts[code].state = OPT_USER;
> +}
> +
> +/**
> + * dhcp_opt_to_str() - Render a binary DHCP option value to a printable string
> + * @code:	DHCP option code
> + * @buf:	Output string buffer
> + * @buf_len:	Size of output buffer
> + *
> + * Return: pointer to @buf if option is user-set, NULL otherwise
> + */
> +const char *dhcp_opt_to_str(uint8_t code, char *buf, size_t buf_len)

Arguably, this isn't strictly necessary yet, since we could keep a
separate copy of the string values.  However, I think this will be
useful when/if we allow updating of these values with pesto, so I'm
happy enough to see it here.

> +{
> +	enum dhcp_opt_type type;
> +	int off = 0;
> +	unsigned int i;
> +
> +	if (opts[code].state != OPT_USER || opts[code].slen < 0)

Seems to me this could be useful even when in OPT_DEFAULT rather than
OPT_USER state.  Also, if I understood the earlier patch correctly,
slen < 0 should now be impossible, right?

> +		return NULL;
> +
> +	if (code >= ARRAY_SIZE(dhcp_opt_types))
> +		return NULL;
> +
> +	type = dhcp_opt_types[code];

Unlike the parse side an out of bounds code, or an undefined type
means we already have something bad in our opts[] table.  So, an
assert() would be appropriate, rather than just returning an error.

> +
> +	switch (type) {
> +	case DHCP_OPT_IPV4:
> +	case DHCP_OPT_IPV4_LIST:
> +		for (i = 0; i + 4 <= (unsigned int)opts[code].slen; i += 4) {

Prefer sizeof(struct in_addr) to a literal 4.

> +			if (off)
> +				off += snprintf(buf + off, buf_len - off, ",");
> +			inet_ntop(AF_INET, opts[code].s + i,
> +				  buf + off, buf_len - off);
> +			off = strlen(buf);
> +		}
> +		return buf;
> +	case DHCP_OPT_UINT8:
> +		if (snprintf(buf, buf_len, "%u",
> +			     opts[code].s[0]) >= (int)buf_len)
> +			return NULL;
> +		return buf;
> +	case DHCP_OPT_UINT16:
> +		if (snprintf(buf, buf_len, "%u",
> +			     ((unsigned)opts[code].s[0] << 8) |
> +			      opts[code].s[1]) >= (int)buf_len)
> +			return NULL;
> +		return buf;
> +	case DHCP_OPT_UINT32:

As with the parse side, it should be possible to consolidate more in
the various uint cases.

> +		if (snprintf(buf, buf_len, "%u",
> +			     ((unsigned)opts[code].s[0] << 24) |
> +			     ((unsigned)opts[code].s[1] << 16) |
> +			     ((unsigned)opts[code].s[2] << 8)  |
> +			      opts[code].s[3]) >= (int)buf_len)
> +			return NULL;
> +		return buf;
> +	case DHCP_OPT_INT32:
> +		if (snprintf(buf, buf_len, "%d",
> +			     (int)(((unsigned)opts[code].s[0] << 24) |
> +				   ((unsigned)opts[code].s[1] << 16) |
> +				   ((unsigned)opts[code].s[2] << 8)  |
> +				    opts[code].s[3])) >= (int)buf_len)
> +			return NULL;
> +		return buf;
> +	case DHCP_OPT_STR:
> +		if ((size_t)opts[code].slen < buf_len) {
> +			memcpy(buf, opts[code].s, opts[code].slen);
> +			buf[opts[code].slen] = '\0';
> +		} else {
> +			memcpy(buf, opts[code].s, buf_len - 1);
> +			buf[buf_len - 1] = '\0';
> +		}

This can be done more simply using some of printf()s fancier features:
	snprintf(buf, buf_len, "%.*s", opts[code].slen, opts[code].s);

"precision" on %s format specifiers gives the maximum number of
characters to format. "*" means the precision is given as a parameter,
rather than inline in the format string.

> +		return buf;
> +	default:
> +		return NULL;
> +	}
> +}
> +
>  /**
>   * fill_one() - Fill a single option into a buffer
>   * @buf:	Buffer to write option
> @@ -482,14 +760,19 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
>  	info("    from %s", eth_ntop(m->chaddr, macstr, sizeof(macstr)));
>  
>  	mask.s_addr = htonl(0xffffffff << (32 - c->ip4.prefix_len));
> -	memcpy(opts[1].s,  &mask,                sizeof(mask));
> -	memcpy(opts[3].s,  &c->ip4.guest_gw,     sizeof(c->ip4.guest_gw));
> -	memcpy(opts[54].s, &c->ip4.our_tap_addr, sizeof(c->ip4.our_tap_addr));
> +	if (opts[1].state != OPT_USER)
> +		memcpy(opts[1].s, &mask, sizeof(mask));
> +	if (opts[3].state != OPT_USER)
> +		memcpy(opts[3].s, &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
> +	if (opts[54].state != OPT_USER)
> +		memcpy(opts[54].s, &c->ip4.our_tap_addr,
> +		       sizeof(c->ip4.our_tap_addr));
>  
>  	/* If the gateway is not on the assigned subnet, send an option 121
>  	 * (Classless Static Routing) adding a dummy route to it.
>  	 */
> -	if ((c->ip4.addr.s_addr & mask.s_addr)
> +	if (opts[121].state != OPT_USER &&
> +	    (c->ip4.addr.s_addr & mask.s_addr)
>  	    != (c->ip4.guest_gw.s_addr & mask.s_addr)) {
>  		/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
>  		opts[121].slen = 14;
> @@ -501,27 +784,32 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
>  		       &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
>  	}
>  
> -	if (c->mtu) {
> +	if (opts[26].state != OPT_USER && c->mtu) {
>  		opts[26].slen = 2;
>  		opts[26].state = OPT_DEFAULT;
>  		opts[26].s[0] = c->mtu / 256;
>  		opts[26].s[1] = c->mtu % 256;
>  	}
>  
> -	for (i = 0, opts[6].slen = 0;
> -	     !c->no_dhcp_dns && i < ARRAY_SIZE(c->ip4.dns); i++) {
> -		if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns[i]))
> -			break;
> -		((struct in_addr *)opts[6].s)[i] = c->ip4.dns[i];
> -		opts[6].slen += sizeof(uint32_t);
> +	if (opts[6].state != OPT_USER) {
> +		for (i = 0, opts[6].slen = 0;

I'd suggest putting the opts[6].slen = 0 on the previous line, rather
than the potentially confusing comma operator.

> +		     !c->no_dhcp_dns && i < ARRAY_SIZE(c->ip4.dns); i++) {
> +			if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns[i]))
> +				break;
> +			((struct in_addr *)opts[6].s)[i] = c->ip4.dns[i];
> +			opts[6].slen += sizeof(uint32_t);
> +		}
> +		if (!opts[6].slen)
> +			opts[6].slen = -1;
>  	}
> +
>  	if (opts[6].slen)
>  		opts[6].state = OPT_DEFAULT;
>  	else
>  		opts[6].state = OPT_UNSET;
>  
>  	opt_len = strlen(c->hostname);
> -	if (opt_len > 0) {
> +	if (opts[12].state != OPT_USER && opt_len > 0) {
>  		opts[12].slen = opt_len;
>  		opts[12].state = OPT_DEFAULT;
>  		memcpy(opts[12].s, &c->hostname, opt_len);
> diff --git a/dhcp.h b/dhcp.h
> index cd50c99..80abda6 100644
> --- a/dhcp.h
> +++ b/dhcp.h
> @@ -8,5 +8,7 @@
>  
>  int dhcp(const struct ctx *c, struct iov_tail *data);
>  void dhcp_init(void);
> +void dhcp_set_opt(uint8_t code, const char *val_str);
> +const char *dhcp_opt_to_str(uint8_t code, char *buf, size_t buf_len);
>  
>  #endif /* DHCP_H */
> diff --git a/passt.1 b/passt.1
> index 995590a..215a289 100644
> --- a/passt.1
> +++ b/passt.1
> @@ -440,6 +440,48 @@ 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 \-\-dhcp-opt " " \fICODE\fR,\fIVALUE\fR
> +Set DHCP option \fICODE\fR (1\-254) to \fIVALUE\fR. The value format depends
> +on the option type and is determined automatically from the option code.
> +Multiple IPv4 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. Options set with
> +\fB\-\-dhcp-opt\fR override built-in values.
> +.PP
> +Examples:
> +.nf
> +  \-\-dhcp-opt 6,8.8.8.8,4.4.4.4
> +  \-\-dhcp-opt 12,myhostname
> +.fi
> +.PP
> +Only the following option codes are supported (unsupported codes cause an error):
> +.RS
> +.TP
> +.B IPv4 address options
> +1 (Subnet Mask), 16 (Swap Server), 28 (Broadcast Address), 50 (Requested IP),
> +54 (Server Identifier)
> +.TP
> +.B IPv4 address list options (comma-separated)
> +3 (Router), 4 (Time Server), 5 (Name Server), 6 (DNS), 7 (Log Server),
> +8 (Cookie Server), 9 (LPR Server), 10 (Impress Server),
> +11 (Resource Location Server), 41 (NIS Servers),
> +42 (NTP Servers), 44 (NetBIOS Name Server)
> +.TP
> +.B Integer options
> +2 (Time Offset, 32-bit), 13 (Boot File Size, 16-bit), 19 (IP Forwarding, 8-bit),
> +23 (Default IP TTL, 8-bit), 26 (Interface MTU, 16-bit),
> +37 (TCP Default TTL, 8-bit), 38 (TCP Keepalive Interval, 32-bit),
> +51 (IP Address Lease Time, 32-bit),
> +53 (DHCP Message Type, 8-bit), 57 (Max DHCP Message Size, 16-bit),
> +58 (Renewal Time, 32-bit), 59 (Rebinding Time, 32-bit)
> +.TP
> +.B String options
> +12 (Host Name), 15 (Domain Name), 17 (Root Path), 40 (NIS Domain Name),
> +60 (Vendor Class Identifier), 66 (TFTP Server Name),
> +67 (Bootfile Name), 252 (WPAD URL)
> +.RE
> +
>  .TP
>  .BR \-t ", " \-\-tcp-ports " " \fIspec
>  Configure TCP port forwarding to guest or namespace. \fIspec\fR can be either:
> -- 
> 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] 17+ messages in thread

* Re: [PATCH v5 5/7] dhcp: Add --dhcp-boot command-line option
  2026-07-17 17:56 ` [PATCH v5 5/7] dhcp: Add --dhcp-boot command-line option Anshu Kumari
@ 2026-07-20  4:06   ` David Gibson
  0 siblings, 0 replies; 17+ messages in thread
From: David Gibson @ 2026-07-20  4:06 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy

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

On Fri, Jul 17, 2026 at 11:26:42PM +0530, Anshu Kumari wrote:
> Add a convenience shorthand --dhcp-boot FILE that sets the boot file
> name (DHCP option 67) for network boot.  This is equivalent to
> --dhcp-opt 67,FILE.
> 
> Link: https://bugs.passt.top/show_bug.cgi?id=192
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>

Reviewed-by: David Gibson <david@gibson.dropbear.id.au>

> ---
> v5:
>   - replaced conf_dhcp_option() to dhcp_set_opt().
> 
> v4:
>   - Changed argument name from URL to FILE in usage and man page.
>   - Fixed UEFI HTTP boot wording in man page
> 
> v3:
>   - case 32 now calls dhcp_add_option(c, 67, optarg).
>   - Handles duplicate codes: --dhcp-boot and --dhcp-opt 67 coexist
>     correctly, last value wins.
> 
> v2:
>   - Removed separate dhcp_boot[PATH_MAX] field — --dhcp-boot foo now stores into custom_opts[] as code 67 (same as --dhcp-opt 67,foo)
> 
> ---
>  conf.c  | 5 +++++
>  passt.1 | 7 +++++++
>  2 files changed, 12 insertions(+)
> 
> diff --git a/conf.c b/conf.c
> index e7f3ea0..eccf1c0 100644
> --- a/conf.c
> +++ b/conf.c
> @@ -617,6 +617,7 @@ static void usage(const char *name, FILE *f, int status)
>  		"    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"
> +		"  --dhcp-boot FILE	Boot file name for network boot\n"
>  		"  --dhcp-opt CODE,VAL	Set DHCP option CODE to VAL\n");
>  	if (strstr(name, "pasta"))
>  		FPRINTF(f, "    default: don't use any search list\n");
> @@ -1321,6 +1322,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 },
> +		{"dhcp-boot", required_argument,	NULL,		33 },
>  		{"dhcp-opt", required_argument,		NULL,		34 },
>  		{ 0 },
>  	};
> @@ -1566,6 +1568,9 @@ void conf(struct ctx *c, int argc, char **argv)
>  		case 32:
>  			c->chroot_fallback = true;
>  			break;
> +		case 33:
> +			dhcp_set_opt(67, optarg);
> +			break;
>  		case 34:
>  			comma = strchr(optarg, ',');
>  			if (!comma)
> diff --git a/passt.1 b/passt.1
> index 215a289..bfc2111 100644
> --- a/passt.1
> +++ b/passt.1
> @@ -440,6 +440,13 @@ 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 \-\-dhcp-boot " " \fIfile
> +Convenience shorthand for \fB\-\-dhcp-opt\fR 67,\fIfile\fR.
> +Sets the boot file name (DHCP option 67) for network boot.
> +For UEFI HTTP boot, the vendor class identifier also needs to be set using
> +\fB\-\-dhcp-opt\fR 60,HTTPClient.
> +
>  .TP
>  .BR \-\-dhcp-opt " " \fICODE\fR,\fIVALUE\fR
>  Set DHCP option \fICODE\fR (1\-254) to \fIVALUE\fR. The value format depends
> -- 
> 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] 17+ messages in thread

* Re: [PATCH v5 6/7] dhcp: Add RFC 3396 option splitting for concatenation-requiring options
  2026-07-17 17:56 ` [PATCH v5 6/7] dhcp: Add RFC 3396 option splitting for concatenation-requiring options Anshu Kumari
@ 2026-07-20  4:21   ` David Gibson
  2026-07-20  9:14     ` Anshu Kumari
  0 siblings, 1 reply; 17+ messages in thread
From: David Gibson @ 2026-07-20  4:21 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy

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

On Fri, Jul 17, 2026 at 11:26:43PM +0530, Anshu Kumari wrote:
> Implement option splitting per RFC 3396 for options that may exceed
> 255 bytes.  A new DHCP_OPT_STR_CONCAT type marks concatenation-
> requiring options (currently option 81, Client FQDN per RFC 4702).
> 
> The opts[].s buffer is resized from 255 to 496 bytes to hold the
> maximum data that can be split across the options field, file field,
> and sname field.
> 
> When a concatenation-requiring option does not fit as a single option
> in any field, fill() splits it across fields in RFC 3396 order:
> options field first, then file, then sname.
> 
> Link: https://bugs.passt.top/show_bug.cgi?id=192
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> ---
> v5:
>   - New patch: implement option splitting per RFC 3396 for options exceeding 255 bytes
>   - Add DHCP_OPT_STR_CONCAT type, is_concat_opt(), fill_split() helpers
>   - Resize opts[].s from 255 to OPT_CONCAT_MAX (496) bytes
>   - Add /* fallthrough */ between DHCP_OPT_STR and DHCP_OPT_STR_CONCAT case
> 
> ---
>  dhcp.c | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
>  1 file changed, 106 insertions(+), 3 deletions(-)
> 
> diff --git a/dhcp.c b/dhcp.c
> index cc910ee..6bebb5f 100644
> --- a/dhcp.c
> +++ b/dhcp.c
> @@ -34,6 +34,11 @@
>  #include "log.h"
>  #include "dhcp.h"
>  
> +/* RFC 3396: maximum option data that can be split across options field,
> + * file field, and sname field (minus code+length overhead per portion).
> + */
> +#define OPT_CONCAT_MAX	496
> +
>  /**
>   * enum opt_state - DHCP option state
>   * @OPT_UNSET:		Option not configured
> @@ -58,7 +63,7 @@ enum opt_state {
>  struct opt {
>  	int sent;
>  	int slen;
> -	uint8_t s[255];
> +	uint8_t s[OPT_CONCAT_MAX];
>  	int clen;
>  	uint8_t c[255];
>  	enum opt_state state;
> @@ -159,6 +164,7 @@ struct msg {
>   * @DHCP_OPT_UINT16:	Unsigned 16-bit integer
>   * @DHCP_OPT_UINT32:	Unsigned 32-bit integer
>   * @DHCP_OPT_INT32:	Signed 32-bit integer
> + * @DHCP_OPT_STR_CONCAT:Concatenation-requiring string (RFC 3396)

It's not entirely clear to me that encoding this in the opt_type enum
makes sense.  Generally the dhcp_opt_type is saying how the option is
encoded as a string for the user.  This is saying how it's encoded
within the DHCP packet itself, which seems qualitatively different
information.

>   */
>  enum dhcp_opt_type {
>  	DHCP_OPT_NONE,
> @@ -169,6 +175,7 @@ enum dhcp_opt_type {
>  	DHCP_OPT_UINT16,
>  	DHCP_OPT_UINT32,
>  	DHCP_OPT_INT32,
> +	DHCP_OPT_STR_CONCAT,
>  };
>  
>  /**
> @@ -319,6 +326,10 @@ static int dhcp_opt_parse(uint8_t code, const char *str,
>  
>  		return width;
>  	case DHCP_OPT_STR:
> +		if (strlen(str) > 255)
> +			return -1;
> +		/* fallthrough */
> +	case DHCP_OPT_STR_CONCAT:
>  		slen = strlen(str);
>  
>  		if (slen >= buf_len)
> @@ -465,6 +476,53 @@ enum dhcp_overload {
>  	DHCP_OVERLOAD_SNAME,
>  };
>  
> +/**
> + * is_concat_opt() - Check if option requires RFC 3396 concatenation support
> + * @o:		Option number
> + *
> + * Return: true if option is a concatenation-requiring type
> + */
> +static bool is_concat_opt(int o)
> +{
> +	if ((size_t)o >= ARRAY_SIZE(dhcp_opt_types))
> +		return false;

IIUC, passing an out of bounds option code here would already be a bug
in our code, so an assert() might make more sense.

> +	return dhcp_opt_types[o] == DHCP_OPT_STR_CONCAT;
> +}
> +
> +/**
> + * fill_split() - Write a split portion of an option into a buffer
> + * @buf:	Buffer to write into
> + * @size:	Usable size of @buf
> + * @o:		Option number (code)
> + * @offset:	Current offset within @buf, updated on write
> + * @data:	Pointer to remaining option data to write
> + * @remaining:	Bytes of option data still to write
> + *
> + * Return: number of data bytes written (excluding code+length header)
> + */
> +static size_t fill_split(uint8_t *buf, size_t size, int o, int *offset,
> +			 const uint8_t *data, size_t remaining)
> +{
> +	size_t avail, chunk;
> +
> +	if (*offset + 2 >= (int)size)
> +		return 0;
> +
> +	avail = size - *offset - 2;
> +	chunk = remaining < avail ? remaining : avail;

You can use the existing MIN() macro here.

> +	if (!chunk)
> +		return 0;
> +
> +	buf[*offset] = o;
> +	buf[*offset + 1] = chunk;
> +	*offset += 2;
> +
> +	memcpy(buf + *offset, data, chunk);
> +	*offset += chunk;
> +
> +	return chunk;
> +}
> +
>  /**
>   * fill() - Fill options in message, with overload into file/sname if needed
>   * @m:			Message to fill
> @@ -513,14 +571,59 @@ static int fill(struct msg *m, enum dhcp_overload *overload, bool has_bootfile)
>  	for (o = 0; (size_t)o < ARRAY_SIZE(opts); o++) {
>  		if (opts[o].state == OPT_UNSET || opts[o].sent)
>  			continue;
> -
>  		if (!has_bootfile &&
>  			fill_one(m->file, sizeof(m->file) - 1, o,
> -			         &file_off))
> +				 &file_off))
> +			if (!is_concat_opt(o))
>  				debug("DHCP: skipping option %i"
>  				      " (overload full)", o);
>  	}
>  
> +	/* RFC 3396: split concatenation-requiring options that didn't fit
> +	 * as a single option.  Split order: options, file, sname.
> +	 */
> +	for (o = 0; (size_t)o < ARRAY_SIZE(opts); o++) {
> +		size_t file_cap, sname_cap, total, written;
> +
> +		if (opts[o].state == OPT_UNSET || opts[o].sent ||
> +		    !is_concat_opt(o))
> +			continue;
> +
> +		sname_cap = sizeof(m->sname) - 1 > (size_t)sname_off ?
> +			    sizeof(m->sname) - 1 - sname_off : 0;
> +
> +		if (has_bootfile || sizeof(m->file) - 1 <= (size_t)file_off)
> +			file_cap = 0;
> +		else
> +			file_cap = sizeof(m->file) - 1 - file_off;
> +
> +		total = (size > (size_t)offset ? size - offset : 0)
> +			+ file_cap + sname_cap;
> +
> +		if (total < (size_t)opts[o].slen) {
>

This doesn't account for the extra 2 bytes per chunk of option code
and length, right?

+			debug("DHCP: skipping option %i (no space to split)",
> +			      o);
> +			continue;
> +		}
> +
> +		written = 0;
> +		written += fill_split(m->o, size, o, &offset,
> +				      opts[o].s, opts[o].slen);
> +		if (written < (size_t)opts[o].slen && !has_bootfile)
> +			written += fill_split(m->file,
> +					      sizeof(m->file) - 1, o,
> +					      &file_off,
> +					      opts[o].s + written,
> +					      opts[o].slen - written);
> +		if (written < (size_t)opts[o].slen)
> +			fill_split(m->sname,
> +				   sizeof(m->sname) - 1, o,
> +				   &sname_off,
> +				   opts[o].s + written,
> +				   opts[o].slen - written);
> +		opts[o].sent = 1;

Since you didn't account for the per-chunk header, I think you need to
check for written < opts[o].slen and not set sent in that case.

> +	}
> +
>  	if (sname_off) {
>  		m->sname[sname_off] = 255;
>  		*overload |= DHCP_OVERLOAD_SNAME;
> -- 
> 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] 17+ messages in thread

* Re: [PATCH v5 7/7] dhcp: Handle FQDN option with RFC 3396 concatenation
  2026-07-17 17:56 ` [PATCH v5 7/7] dhcp: Handle FQDN option with RFC 3396 concatenation Anshu Kumari
@ 2026-07-20  4:23   ` David Gibson
  2026-07-20  7:38     ` Anshu Kumari
  0 siblings, 1 reply; 17+ messages in thread
From: David Gibson @ 2026-07-20  4:23 UTC (permalink / raw)
  To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy

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

On Fri, Jul 17, 2026 at 11:26:44PM +0530, Anshu Kumari wrote:
> Mark Client FQDN (option 81) as concatenation-requiring per
> RFC 4702, Section 2.  When the encoded FQDN exceeds 255 bytes,
> fill() now splits it across DHCP message fields using the RFC 3396
> splitting infrastructure instead of silently dropping it.
> 
> Link: https://bugs.passt.top/show_bug.cgi?id=192
> Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> ---
> v5:
>   - New patch: mark Client FQDN (option 81) as concatenation-requiring per RFC 4702
> ---
>  dhcp.c | 12 +++++++++---
>  1 file changed, 9 insertions(+), 3 deletions(-)
> 
> diff --git a/dhcp.c b/dhcp.c
> index 6bebb5f..a54ac58 100644
> --- a/dhcp.c
> +++ b/dhcp.c
> @@ -484,9 +484,15 @@ enum dhcp_overload {
>   */
>  static bool is_concat_opt(int o)
>  {
> -	if ((size_t)o >= ARRAY_SIZE(dhcp_opt_types))
> -		return false;
> -	return dhcp_opt_types[o] == DHCP_OPT_STR_CONCAT;
> +	if ((size_t)o < ARRAY_SIZE(dhcp_opt_types) &&
> +	    dhcp_opt_types[o] == DHCP_OPT_STR_CONCAT)
> +		return true;
> +
> +	/* RFC 4702, Section 2: Client FQDN option requires concatenation */
> +	if (o == 81)
> +		return true;

Why is this explicitly special cased, rather than setting the type in
the table?  Come to that, does this do anything, since we don't
appear to currently allow option 81 anyway.

> +
> +	return false;
>  }
>  
>  /**
> -- 
> 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] 17+ messages in thread

* Re: [PATCH v5 7/7] dhcp: Handle FQDN option with RFC 3396 concatenation
  2026-07-20  4:23   ` David Gibson
@ 2026-07-20  7:38     ` Anshu Kumari
  0 siblings, 0 replies; 17+ messages in thread
From: Anshu Kumari @ 2026-07-20  7:38 UTC (permalink / raw)
  To: David Gibson; +Cc: sbrivio, passt-dev, lvivier, jmaloy

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

On Mon, Jul 20, 2026 at 9:54 AM David Gibson <david@gibson.dropbear.id.au>
wrote:

> On Fri, Jul 17, 2026 at 11:26:44PM +0530, Anshu Kumari wrote:
> > Mark Client FQDN (option 81) as concatenation-requiring per
> > RFC 4702, Section 2.  When the encoded FQDN exceeds 255 bytes,
> > fill() now splits it across DHCP message fields using the RFC 3396
> > splitting infrastructure instead of silently dropping it.
> >
> > Link: https://bugs.passt.top/show_bug.cgi?id=192
> > Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> > ---
> > v5:
> >   - New patch: mark Client FQDN (option 81) as concatenation-requiring
> per RFC 4702
> > ---
> >  dhcp.c | 12 +++++++++---
> >  1 file changed, 9 insertions(+), 3 deletions(-)
> >
> > diff --git a/dhcp.c b/dhcp.c
> > index 6bebb5f..a54ac58 100644
> > --- a/dhcp.c
> > +++ b/dhcp.c
> > @@ -484,9 +484,15 @@ enum dhcp_overload {
> >   */
> >  static bool is_concat_opt(int o)
> >  {
> > -     if ((size_t)o >= ARRAY_SIZE(dhcp_opt_types))
> > -             return false;
> > -     return dhcp_opt_types[o] == DHCP_OPT_STR_CONCAT;
> > +     if ((size_t)o < ARRAY_SIZE(dhcp_opt_types) &&
> > +         dhcp_opt_types[o] == DHCP_OPT_STR_CONCAT)
> > +             return true;
> > +
> > +     /* RFC 4702, Section 2: Client FQDN option requires concatenation
> */
> > +     if (o == 81)
> > +             return true;
>
> Why is this explicitly special cased, rather than setting the type in
> the table?  Come to that, does this do anything, since we don't
> appear to currently allow option 81 anyway.
>
> As option 81 is internally supported inside passt, that's why I have not
included inside table
as parsing option 81 has a special format of 3 flag bytes + DNS-encoded
domain name.

Special condition is mentioned for option 81 in  is_concat_opt() function
because FQDN length can exceeds
255 bytes for a very long domain name. Without this condition, such FQDN
are dropped in current approach.
With this condition, they are split across DHCP fields per RFC 3396.

> +
> > +     return false;
> >  }
> >
> >  /**
> > --
> > 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
>


-- 
Anshu

[-- Attachment #2: Type: text/html, Size: 4125 bytes --]

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

* Re: [PATCH v5 6/7] dhcp: Add RFC 3396 option splitting for concatenation-requiring options
  2026-07-20  4:21   ` David Gibson
@ 2026-07-20  9:14     ` Anshu Kumari
  0 siblings, 0 replies; 17+ messages in thread
From: Anshu Kumari @ 2026-07-20  9:14 UTC (permalink / raw)
  To: David Gibson; +Cc: sbrivio, passt-dev, lvivier, jmaloy

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

On Mon, Jul 20, 2026 at 9:54 AM David Gibson <david@gibson.dropbear.id.au>
wrote:

> On Fri, Jul 17, 2026 at 11:26:43PM +0530, Anshu Kumari wrote:
> > Implement option splitting per RFC 3396 for options that may exceed
> > 255 bytes.  A new DHCP_OPT_STR_CONCAT type marks concatenation-
> > requiring options (currently option 81, Client FQDN per RFC 4702).
> >
> > The opts[].s buffer is resized from 255 to 496 bytes to hold the
> > maximum data that can be split across the options field, file field,
> > and sname field.
> >
> > When a concatenation-requiring option does not fit as a single option
> > in any field, fill() splits it across fields in RFC 3396 order:
> > options field first, then file, then sname.
> >
> > Link: https://bugs.passt.top/show_bug.cgi?id=192
> > Signed-off-by: Anshu Kumari <anskuma@redhat.com>
> > ---
> > v5:
> >   - New patch: implement option splitting per RFC 3396 for options
> exceeding 255 bytes
> >   - Add DHCP_OPT_STR_CONCAT type, is_concat_opt(), fill_split() helpers
> >   - Resize opts[].s from 255 to OPT_CONCAT_MAX (496) bytes
> >   - Add /* fallthrough */ between DHCP_OPT_STR and DHCP_OPT_STR_CONCAT
> case
> >
> > ---
> >  dhcp.c | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
> >  1 file changed, 106 insertions(+), 3 deletions(-)
> >
> > diff --git a/dhcp.c b/dhcp.c
> > index cc910ee..6bebb5f 100644
> > --- a/dhcp.c
> > +++ b/dhcp.c
> > @@ -34,6 +34,11 @@
> >  #include "log.h"
> >  #include "dhcp.h"
> >
> > +/* RFC 3396: maximum option data that can be split across options field,
> > + * file field, and sname field (minus code+length overhead per portion).
> > + */
> > +#define OPT_CONCAT_MAX       496
> > +
> >  /**
> >   * enum opt_state - DHCP option state
> >   * @OPT_UNSET:               Option not configured
> > @@ -58,7 +63,7 @@ enum opt_state {
> >  struct opt {
> >       int sent;
> >       int slen;
> > -     uint8_t s[255];
> > +     uint8_t s[OPT_CONCAT_MAX];
> >       int clen;
> >       uint8_t c[255];
> >       enum opt_state state;
> > @@ -159,6 +164,7 @@ struct msg {
> >   * @DHCP_OPT_UINT16: Unsigned 16-bit integer
> >   * @DHCP_OPT_UINT32: Unsigned 32-bit integer
> >   * @DHCP_OPT_INT32:  Signed 32-bit integer
> > + * @DHCP_OPT_STR_CONCAT:Concatenation-requiring string (RFC 3396)
>
> It's not entirely clear to me that encoding this in the opt_type enum
> makes sense.  Generally the dhcp_opt_type is saying how the option is
> encoded as a string for the user.  This is saying how it's encoded
> within the DHCP packet itself, which seems qualitatively different
> information.
>
> Since we don't limit on the string length that user can set via
--dhcp-opt, DHCP_OPT_STR rejects
anything over 255 bytes but with STR_CONCAT longer input can be parsed.

is_concat_opt() checks dhcp_opt_types[] for STR_CONCAT to decide which
options need splitting in fill().
I think these two concerns are coupled: an option that accepts long strings
from the CLI
is exactly the same option that needs splitting on the wire.

> >   */
> >  enum dhcp_opt_type {
> >       DHCP_OPT_NONE,
> > @@ -169,6 +175,7 @@ enum dhcp_opt_type {
> >       DHCP_OPT_UINT16,
> >       DHCP_OPT_UINT32,
> >       DHCP_OPT_INT32,
> > +     DHCP_OPT_STR_CONCAT,
> >  };
> >
> >  /**
> > @@ -319,6 +326,10 @@ static int dhcp_opt_parse(uint8_t code, const char
> *str,
> >
> >               return width;
> >       case DHCP_OPT_STR:
> > +             if (strlen(str) > 255)
> > +                     return -1;
> > +             /* fallthrough */
> > +     case DHCP_OPT_STR_CONCAT:
> >               slen = strlen(str);
> >
> >               if (slen >= buf_len)
> > @@ -465,6 +476,53 @@ enum dhcp_overload {
> >       DHCP_OVERLOAD_SNAME,
> >  };
> >
> > +/**
> > + * is_concat_opt() - Check if option requires RFC 3396 concatenation
> support
> > + * @o:               Option number
> > + *
> > + * Return: true if option is a concatenation-requiring type
> > + */
> > +static bool is_concat_opt(int o)
> > +{
> > +     if ((size_t)o >= ARRAY_SIZE(dhcp_opt_types))
> > +             return false;
>
> IIUC, passing an out of bounds option code here would already be a bug
> in our code, so an assert() might make more sense.
>
> > +     return dhcp_opt_types[o] == DHCP_OPT_STR_CONCAT;
> > +}
> > +
> > +/**
> > + * fill_split() - Write a split portion of an option into a buffer
> > + * @buf:     Buffer to write into
> > + * @size:    Usable size of @buf
> > + * @o:               Option number (code)
> > + * @offset:  Current offset within @buf, updated on write
> > + * @data:    Pointer to remaining option data to write
> > + * @remaining:       Bytes of option data still to write
> > + *
> > + * Return: number of data bytes written (excluding code+length header)
> > + */
> > +static size_t fill_split(uint8_t *buf, size_t size, int o, int *offset,
> > +                      const uint8_t *data, size_t remaining)
> > +{
> > +     size_t avail, chunk;
> > +
> > +     if (*offset + 2 >= (int)size)
> > +             return 0;
> > +
> > +     avail = size - *offset - 2;
> > +     chunk = remaining < avail ? remaining : avail;
>
> You can use the existing MIN() macro here.
>
> > +     if (!chunk)
> > +             return 0;
> > +
> > +     buf[*offset] = o;
> > +     buf[*offset + 1] = chunk;
> > +     *offset += 2;
> > +
> > +     memcpy(buf + *offset, data, chunk);
> > +     *offset += chunk;
> > +
> > +     return chunk;
> > +}
> > +
> >  /**
> >   * fill() - Fill options in message, with overload into file/sname if
> needed
> >   * @m:                       Message to fill
> > @@ -513,14 +571,59 @@ static int fill(struct msg *m, enum dhcp_overload
> *overload, bool has_bootfile)
> >       for (o = 0; (size_t)o < ARRAY_SIZE(opts); o++) {
> >               if (opts[o].state == OPT_UNSET || opts[o].sent)
> >                       continue;
> > -
> >               if (!has_bootfile &&
> >                       fill_one(m->file, sizeof(m->file) - 1, o,
> > -                              &file_off))
> > +                              &file_off))
> > +                     if (!is_concat_opt(o))
> >                               debug("DHCP: skipping option %i"
> >                                     " (overload full)", o);
> >       }
> >
> > +     /* RFC 3396: split concatenation-requiring options that didn't fit
> > +      * as a single option.  Split order: options, file, sname.
> > +      */
> > +     for (o = 0; (size_t)o < ARRAY_SIZE(opts); o++) {
> > +             size_t file_cap, sname_cap, total, written;
> > +
> > +             if (opts[o].state == OPT_UNSET || opts[o].sent ||
> > +                 !is_concat_opt(o))
> > +                     continue;
> > +
> > +             sname_cap = sizeof(m->sname) - 1 > (size_t)sname_off ?
> > +                         sizeof(m->sname) - 1 - sname_off : 0;
> > +
> > +             if (has_bootfile || sizeof(m->file) - 1 <=
> (size_t)file_off)
> > +                     file_cap = 0;
> > +             else
> > +                     file_cap = sizeof(m->file) - 1 - file_off;
> > +
> > +             total = (size > (size_t)offset ? size - offset : 0)
> > +                     + file_cap + sname_cap;
> > +
> > +             if (total < (size_t)opts[o].slen) {
> >
>
> This doesn't account for the extra 2 bytes per chunk of option code
> and length, right?
>

yes, I missed it by chance while calculating the total available bytes.

>
> +                       debug("DHCP: skipping option %i (no space to
> split)",
> > +                           o);
> > +                     continue;
> > +             }
> > +
> > +             written = 0;
> > +             written += fill_split(m->o, size, o, &offset,
> > +                                   opts[o].s, opts[o].slen);
> > +             if (written < (size_t)opts[o].slen && !has_bootfile)
> > +                     written += fill_split(m->file,
> > +                                           sizeof(m->file) - 1, o,
> > +                                           &file_off,
> > +                                           opts[o].s + written,
> > +                                           opts[o].slen - written);
> > +             if (written < (size_t)opts[o].slen)
> > +                     fill_split(m->sname,
> > +                                sizeof(m->sname) - 1, o,
> > +                                &sname_off,
> > +                                opts[o].s + written,
> > +                                opts[o].slen - written);
> > +             opts[o].sent = 1;
>
> Since you didn't account for the per-chunk header, I think you need to
> check for written < opts[o].slen and not set sent in that case.
>
> > +     }
> > +
> >       if (sname_off) {
> >               m->sname[sname_off] = 255;
> >               *overload |= DHCP_OVERLOAD_SNAME;
> > --
> > 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
>


-- 
Anshu

[-- Attachment #2: Type: text/html, Size: 12528 bytes --]

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

end of thread, other threads:[~2026-07-20  9:14 UTC | newest]

Thread overview: 17+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-17 17:56 [PATCH v5 0/7] Add --dhcp-boot and --dhcp-opt options Anshu Kumari
2026-07-17 17:56 ` [PATCH v5 1/7] dhcp: Refactor fill_one() to operate on a generic buffer Anshu Kumari
2026-07-20  1:18   ` David Gibson
2026-07-17 17:56 ` [PATCH v5 2/7] dhcp: Add option state management with enum opt_state Anshu Kumari
2026-07-20  1:36   ` David Gibson
2026-07-17 17:56 ` [PATCH v5 3/7] dhcp: Add option overload Anshu Kumari
2026-07-20  2:03   ` David Gibson
2026-07-17 17:56 ` [PATCH v5 4/7] dhcp: Add --dhcp-opt with option table and value parser Anshu Kumari
2026-07-20  3:58   ` David Gibson
2026-07-17 17:56 ` [PATCH v5 5/7] dhcp: Add --dhcp-boot command-line option Anshu Kumari
2026-07-20  4:06   ` David Gibson
2026-07-17 17:56 ` [PATCH v5 6/7] dhcp: Add RFC 3396 option splitting for concatenation-requiring options Anshu Kumari
2026-07-20  4:21   ` David Gibson
2026-07-20  9:14     ` Anshu Kumari
2026-07-17 17:56 ` [PATCH v5 7/7] dhcp: Handle FQDN option with RFC 3396 concatenation Anshu Kumari
2026-07-20  4:23   ` David Gibson
2026-07-20  7:38     ` Anshu Kumari

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