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 configuration tracking with enum opt_conf dhcp: Add --dhcp-opt with option table and value parser dhcp: Add --dhcp-boot command-line option dhcp: Change fill_one() to return void dhcp: Add option overload dhcp: Add RFC 3396 option splitting for concatenation-requiring options conf.c | 32 ++- dhcp.c | 617 ++++++++++++++++++++++++++++++++++++++++++++++++++------ dhcp.h | 2 + passt.1 | 48 +++++ 4 files changed, 632 insertions(+), 67 deletions(-) -- 2.55.0
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
Overloading slen = -1 to mean "not set" conflates the option
length with its lifecycle state, and can't express additional
states such as "set by the user via command-line" which will
be needed for --dhcp-opt support.
Introduce enum opt_conf with OPT_UNSET (option not
configured) and OPT_DEFAULT (derived from host
configuration). Replace all slen = -1 / slen != -1 checks
with conf comparisons, and set conf = 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
Add a --dhcp-opt CODE,VALUE flag that sets any DHCP option by
numeric code with type-aware parsing per RFC 2132.
A type lookup table maps option codes to RFC 2132 value types
(IPv4, IPv4 list, integer, string). dhcp_opt_parse() converts
CLI strings to binary wire format; parsed options are stored in
opts[] and injected into DHCP replies. Options set via --dhcp-opt
(OPT_USER) take priority over host-derived defaults (OPT_DEFAULT).
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari
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.
Per RFC 2132, Section 9.5, the boot file name is placed in the
'file' header field of the DHCP reply.
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari
fill_one() previously returned true on overflow, but callers
used this to log a skip message per option — which becomes
noisy if the options field is full.
Instead, rely on opts[].sent (already set by fill_one()) to
detect unsent options: a final reporting loop walks all
options and logs a single skip message for each one that
couldn't be sent. This also makes the return value
unnecessary, so change fill_one() to return void.
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari
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
Implement option splitting per RFC 3396 for options that may exceed
255 bytes. A concat_req[] lookup table marks options requiring
concatenation (currently option 81, Client FQDN per RFC 4702).
The opts[].s buffer is resized from 255 to OPT_CONCAT_MAX 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() calls fill_split() to split it across
fields in RFC 3396 order: options field first, then file, then
sname. Each split chunk is capped at 255 bytes per the DHCP
option length field limit.
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari
On Thu, 1 Oct 2026 18:45:53 +0530
Anshu Kumari
This series adds support for custom DHCP options in passt, enabling network boot (PXE/UEFI HTTP Boot) and arbitrary DHCP option injection.
Note: this will need a trivial rebase on top of commit 588b545dae74 ("pasta: Add --no-pidns to keep spawned command in caller's PID namespace"), merged on September 16. Other than a bit of fuzz in nearby lines, option numbers conflict now (they should become 34 and 35 instead). -- Stefano
On Thu, 1 Oct 2026 18:45:55 +0530
Anshu Kumari
@@ -243,6 +256,7 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len) int i;
opts[119].slen = 0; + opts[119].conf = OPT_DEFAULT;
I think that at this point, instead of doing this...
for (i = 0; i < 255; i++) max_len -= opts[i].slen; @@ -291,7 +305,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].conf = OPT_UNSET;
...it would make more sense to simply set opts[119].conf here, that is: if (opts[119].slen) opts[119].conf = OPT_DEFAULT; -- Stefano
On Thu, 1 Oct 2026 18:45:56 +0530
Anshu Kumari
Add a --dhcp-opt CODE,VALUE flag that sets any DHCP option by numeric code with type-aware parsing per RFC 2132.
A type lookup table maps option codes to RFC 2132 value types (IPv4, IPv4 list, integer, string). dhcp_opt_parse() converts CLI strings to binary wire format; parsed options are stored in opts[] and injected into DHCP replies. Options set via --dhcp-opt (OPT_USER) take priority over host-derived defaults (OPT_DEFAULT).
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- v7: - Removed client-only options 50 (Requested IP) and 57 (Max Message Size) from dhcp_opt_types[] - Switched integer parsing from strtoul()/strtol() to parse_unsigned() for UINT8/UINT16/UINT32 types - Fixed alignment: use htons()/htonl() + memcpy() instead of direct pointer casts for UINT16/UINT32 encoding. - Used explicit OPT_DEFAULT check instead of != OPT_USER where the condition body does not set conf. - Removed unnecessary OPT_USER guard on option 121 - Checked inet_ntop() return value in dhcp_opt_to_str() v6: - dropped option 53. - Used parse_unsigned(), parse_literal(), parse_ipv4() from parse.c instead of manual strtoul/inet_pton. - Moved option-parsing variables into case 34 block scope.
This change comes from David's suggestion in: https://archives.passt.top/passt-dev/al2c8QVZUuQB69HV@zatzit/ ...but back then it was three variables. Now it's one:
[...]
@@ -1589,6 +1596,24 @@ void conf(struct ctx *c, int argc, char **argv) case 32: c->chroot_fallback = true; break; + case 34: { + unsigned long optcode;
...which I think could happily be declared at the top of the function along with max_mtu. In general, I think it would be good to avoid mixing up scoping logic in case switches (we almost always avoid extra blocks, except for a few cases here which I missed during review), because if we do the code becomes a bit more surprising (where do you look for variable declarations?). Other than that, I see the point of keeping variable scope limited. But here it's just one variable, so I think it could really be declared at the beginning of the function without much thinking.
[...]
+/** + * 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 set, NULL otherwise + */ +const char *dhcp_opt_to_str(uint8_t code, char *buf, size_t buf_len) +{ + enum dhcp_opt_type type; + unsigned int i; + int off = 0; + + if (opts[code].conf == OPT_UNSET) + return NULL; + + assert(code < ARRAY_SIZE(dhcp_opt_types)); + + type = dhcp_opt_types[code]; + + switch (type) { + case DHCP_OPT_IPV4: + case DHCP_OPT_IPV4_LIST: + for (i = 0; i + sizeof(struct in_addr) <= (unsigned int)opts[code].slen;
Given that opts[code].slen doesn't change in this loop, perhaps a temporary variable holding it would make this more readable.
+ i += sizeof(struct in_addr)) { + if (off) { + if (off + 1 >= (int)buf_len) + return NULL; + buf[off++] = ','; + } + if (!inet_ntop(AF_INET, opts[code].s + i, + buf + off, buf_len - off)) + return NULL; + off += strlen(buf + off); + } + return buf; + case DHCP_OPT_UINT8: + case DHCP_OPT_UINT16: + case DHCP_OPT_UINT32: {
Same here ('uval'?).
+ uint32_t val = 0; + + if (opts[code].slen == 1) { + val = opts[code].s[0]; + } else if (opts[code].slen == 2) { + uint16_t v16; + memcpy(&v16, opts[code].s, sizeof(v16)); + val = ntohs(v16); + } else if (opts[code].slen == 4) { + memcpy(&val, opts[code].s, sizeof(val)); + val = ntohl(val); + } + + if (snprintf(buf, buf_len, "%u", val) >= (int)buf_len) + return NULL; + return buf; + } + case DHCP_OPT_INT32: { + int32_t val;
And here. This one is especially surprising because there's another 'val' just above, and if you miss that curly bracket then you would think that the usual style / scoping applies, but it doesn't.
+ uint32_t v32; + + assert(opts[code].slen == 4); + memcpy(&v32, opts[code].s, sizeof(v32)); + val = (int32_t)ntohl(v32); + + if (snprintf(buf, buf_len, "%d", val) >= (int)buf_len) + return NULL; + return buf; + } + case DHCP_OPT_STR: + (void)snprintf(buf, buf_len, "%.*s", + opts[code].slen, opts[code].s); + return buf; + default: + assert(0); + } +} +
[...]
-- Stefano
On Thu, 1 Oct 2026 18:45:57 +0530
Anshu Kumari
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.
Per RFC 2132, Section 9.5, the boot file name is placed in the 'file' header field of the DHCP reply.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- v7: - moved ```has_bootfile``` declaration and initialization in this patch. v6: - no changes.
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 +++++ dhcp.c | 10 ++++++++++ passt.1 | 7 +++++++ 3 files changed, 22 insertions(+)
diff --git a/conf.c b/conf.c index c12a9335..37720bb5 100644 --- a/conf.c +++ b/conf.c @@ -634,6 +634,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"); @@ -1354,6 +1355,7 @@ void conf(struct ctx *c, int argc, char **argv) {"stats", required_argument, NULL, 31 }, {"conf-path", required_argument, NULL, 'c' }, {"chroot-fallback", no_argument, NULL, 32 }, + {"dhcp-boot", required_argument, NULL, 33 }, {"dhcp-opt", required_argument, NULL, 34 }, { 0 }, }; @@ -1596,6 +1598,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: { unsigned long optcode;
diff --git a/dhcp.c b/dhcp.c index dd3ab6c6..4dccae7b 100644 --- a/dhcp.c +++ b/dhcp.c @@ -600,6 +600,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data) const struct udphdr *uh; struct msg m_storage; struct msg const *m; + bool has_bootfile; struct msg reply; unsigned int i;
@@ -772,8 +773,17 @@ int dhcp(const struct ctx *c, struct iov_tail *data) if (!c->no_dhcp_dns_search) opt_set_dns_search(c, sizeof(m->o));
+ /* RFC 2132, Section 9.5: put boot file name in the 'file' header + * field. + */ + has_bootfile = opts[67].slen > 0 && + (size_t)opts[67].slen < sizeof(reply.file); + dlen = offsetof(struct msg, o) + fill(&reply);
+ if (has_bootfile) + memcpy(reply.file, opts[67].s, opts[67].slen); + if (m->flags & FLAG_BROADCAST) dst = in4addr_broadcast; else diff --git a/passt.1 b/passt.1 index 2bfe7d50..fdbe3d2c 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
Given that options in conf() need to be renumbered anyway because of the merge conflict, what about moving this after --dhcp-opt in the documentation (it refers to it and it's a special case of it, so it would look more natural to have it afterwards) and then make ordering consistent in conf() and usage message?
+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
-- Stefano
On Thu, 1 Oct 2026 18:45:58 +0530
Anshu Kumari
fill_one() previously returned true on overflow, but callers used this to log a skip message per option — which becomes noisy if the options field is full.
This looks generated by a language model that tried to come up with some kind of explanation, which sounds plausible, but entirely misses the point, as it often (usually?) happens. The number of messages (noise) is exactly the same, before and after this change, and also after the change that depends on this one. The rest of the description itself (not the motivation), though:
Instead, rely on opts[].sent (already set by fill_one()) to detect unsent options: a final reporting loop walks all options and logs a single skip message for each one that couldn't be sent. This also makes the return value unnecessary, so change fill_one() to return void.
...is plain wrong. That loop isn't implemented here. I guess the language model generated this before this was split off from 6/7? Or it simply had access to both as context? I think there's no need for such a long explanation either: this just does what you wrote in the title and, say, "make[s] fill_one() ignore failures because the next change will report all of them in a separate loop". By the way, I see Laurent's comment in: https://archives.passt.top/passt-dev/f6c80395-bb48-4697-88af-820def5e040f@re... and indeed, those are unrelated changes, but this change isn't really self-contained (it can't be) in the sense that it drops an important functionality, which is then restored by a subsequent patch (but if one just applies just this patch, the functionality is lost). This becomes obvious by looking at this change in isolation, of course. But it could also be obvious by mentioning this aspect in the commit message for 6/7. So I think it would actually be better to keep those together (Laurent could probably not see the dependency because it wasn't explained) and mention this explicitly in the commit message for 6/7: because of option overload, we now need a single loop reporting errors.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- v7: - (new patch) Split out from the option overload patch into its own commit --- dhcp.c | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-)
diff --git a/dhcp.c b/dhcp.c index 4dccae7b..6f69fd62 100644 --- a/dhcp.c +++ b/dhcp.c @@ -418,16 +418,14 @@ const char *dhcp_opt_to_str(uint8_t code, char *buf, size_t buf_len) * @size: Usable size of @buf (excluding end marker) * @o: Option number * @offset: Current offset within @buf, updated on insertion - * - * Return: false if @buf has space to write the option, true otherwise */ -static bool fill_one(uint8_t *buf, size_t size, int o, int *offset) +static void 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 > size) - return true; + return;
buf[*offset] = o; buf[*offset + 1] = slen; @@ -439,7 +437,6 @@ static bool fill_one(uint8_t *buf, size_t size, int o, int *offset)
opts[o].sent = 1; *offset += slen; - return false; }
/** @@ -459,24 +456,18 @@ static int fill(struct msg *m) * option 53 at the beginning of the list. * 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->o, OPT_MAX, 53, &offset)) - debug("DHCP: skipping option 53"); - } + if (opts[55].clen > 0 && !memchr(opts[55].c, 53, opts[55].clen)) + fill_one(m->o, OPT_MAX, 53, &offset);
for (i = 0; i < opts[55].clen; i++) { o = opts[55].c[i]; - if (opts[o].conf != OPT_UNSET) { - if (fill_one(m->o, OPT_MAX, o, &offset)) - debug("DHCP: skipping option %i", o); - } + if (opts[o].conf != OPT_UNSET) + fill_one(m->o, OPT_MAX, o, &offset); }
for (o = 0; o < 255; o++) { - if (opts[o].conf != OPT_UNSET && !opts[o].sent) { - if (fill_one(m->o, OPT_MAX, o, &offset)) - debug("DHCP: skipping option %i", o); - } + if (opts[o].conf != OPT_UNSET && !opts[o].sent) + fill_one(m->o, OPT_MAX, o, &offset); }
m->o[offset++] = 255;
...the change itself looks good to me. -- Stefano
On Thu, 1 Oct 2026 18:45:59 +0530
Anshu Kumari
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
--- v7: - Removed overload function parameter - Added foreach_opt() macro to iterate option table
Till v6 both "patch 5/7" and "patch 6/7" were one. --- dhcp.c | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 10 deletions(-)
diff --git a/dhcp.c b/dhcp.c index 6f69fd62..63ac8e3d 100644 --- a/dhcp.c +++ b/dhcp.c @@ -67,6 +67,8 @@ struct opt {
static struct opt opts[256];
+#define foreach_opt(o) for ((o) = 0; (size_t)(o) < ARRAY_SIZE(opts); (o)++) + #define DHCPDISCOVER 1 #define DHCPOFFER 2 #define DHCPREQUEST 3 @@ -440,13 +442,30 @@ static void 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 = 0, + DHCP_OVERLOAD_FILE = 1, + DHCP_OVERLOAD_SNAME = 2, +}; + +/** + * fill() - Fill options in message, with overload into file/sname if needed + * @m: Message to fill + * @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, bool has_bootfile) { + enum dhcp_overload overload = DHCP_OVERLOAD_NONE; + int sname_off = 0, file_off = 0; + /* 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++) @@ -457,17 +476,54 @@ 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)) - fill_one(m->o, OPT_MAX, 53, &offset); + fill_one(m->o, size, 53, &offset);
for (i = 0; i < opts[55].clen; i++) { o = opts[55].c[i]; if (opts[o].conf != OPT_UNSET) - fill_one(m->o, OPT_MAX, o, &offset); + fill_one(m->o, size, o, &offset); }
for (o = 0; o < 255; o++) { if (opts[o].conf != OPT_UNSET && !opts[o].sent) - fill_one(m->o, OPT_MAX, o, &offset); + fill_one(m->o, size, o, &offset); + } + + /* Overflow unsent options into sname, then file */ + foreach_opt(o) {
This comes from my suggestion on v6 but those were really subsequent steps (building a more specialised iterator on top of foreach_opt()), not alternatives. Look at flow_foreach() and flow_foreach_of_type() as examples. That is, here you could use: foreach_unsent_opt(o) fill_one(m->sname, sizeof(m->sname) - 1, o, &sname_off); with: #define foreach_unset_opt(o) \ foreach_opt((o)) \ /* NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) */ \ if (opts[(o)].conf != OPT_UNSET && !opts[(o)].sent) ...the option with the reverse condition and "continue; else" could also work, I'm not sure what's the most practical here. The direct option looks more... direct, to me. The two loops below could use this iterator, and perhaps the one above as well (I haven't tried). Note that in 7/7 you could probably switch to this other iterator as well, without the explicit need for the base foreach_opt(o) iterator, but I would suggest to keep them as two different macros anyway, it's clearer and more reusable.
+ if (opts[o].conf == OPT_UNSET || opts[o].sent) + continue; + fill_one(m->sname, sizeof(m->sname) - 1, o, &sname_off); + } + + if (!has_bootfile) { + foreach_opt(o) { + if (opts[o].conf == OPT_UNSET || opts[o].sent) + continue; + fill_one(m->file, sizeof(m->file) - 1, o, &file_off); + } + } + + /* Report any options that could not be sent */ + foreach_opt(o) { + if (opts[o].conf != OPT_UNSET && !opts[o].sent) + debug("DHCP: skipping option %i", 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; @@ -761,16 +817,18 @@ int dhcp(const struct ctx *c, struct iov_tail *data) } }
- if (!c->no_dhcp_dns_search) - opt_set_dns_search(c, sizeof(m->o)); + if (!c->no_dhcp_dns_search) { + /* 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. + * field. Reserve the file field from overload. */ has_bootfile = opts[67].slen > 0 && (size_t)opts[67].slen < sizeof(reply.file);
- dlen = offsetof(struct msg, o) + fill(&reply); + dlen = offsetof(struct msg, o) + fill(&reply, has_bootfile);
if (has_bootfile) memcpy(reply.file, opts[67].s, opts[67].slen);
-- Stefano
On Thu, 1 Oct 2026 18:46:00 +0530
Anshu Kumari
Implement option splitting per RFC 3396 for options that may exceed 255 bytes. A concat_req[] lookup table marks options requiring concatenation (currently option 81, Client FQDN per RFC 4702).
The opts[].s buffer is resized from 255 to OPT_CONCAT_MAX 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() calls fill_split() to split it across fields in RFC 3396 order: options field first, then file, then sname. Each split chunk is capped at 255 bytes per the DHCP option length field limit.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- v7: - Capped each split chunk at 255 bytes: chunk = MIN(MIN(remaining, avail), 255) to prevent overflow in the uint8_t length field - Replaced inline space calculation with a dry-run pass via fill_split_areas() - Moved OPT_MAX and OPT_CONCAT_MAX definitions above struct opt. - Reordered struct opt fields to place s[OPT_CONCAT_MAX] - Added dry_run parameter to fill_split() v6: - Merged v5 patches 6/7 and 7/7 into a single patch. - Replaced DHCP_OPT_STR_CONCAT enum value and is_concat_opt() helper with a concat_req[] boolean lookup table. - Used MIN() macro instead of ternary for chunk size. - Fixed space calculation to account for 2-byte code+length overhead per chunk.
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 (497) bytes - Add /* fallthrough */ between DHCP_OPT_STR and DHCP_OPT_STR_CONCAT case --- dhcp.c | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 142 insertions(+), 18 deletions(-)
diff --git a/dhcp.c b/dhcp.c index 63ac8e3d..990b650d 100644 --- a/dhcp.c +++ b/dhcp.c @@ -35,6 +35,19 @@ #include "dhcp.h" #include "parse.h"
+#define OPT_MIN 60 /* RFC 951 */ + +/* Total option size (excluding end option) is 576 (RFC 2131), minus + * offset of options (268), minus end option (1). + */ +#define OPT_MAX 307 + +/* RFC 3396: maximum option data that can be split across options field + * (OPT_MAX - 2), file field (64 - 2), and sname field (128 - 2), + * minus code+length overhead per portion.
That doesn't match the define below (which is the correct one, I think). If you additionally subtract at the end, from all that, code and length bytes for each section, you would end up with an extra -6 term, which isn't needed.
+ */ +#define OPT_CONCAT_MAX (OPT_MAX - 2 + 64 - 2 + 128 - 2) + /** * enum opt_conf - DHCP option configuration * @OPT_UNSET: Option not configured @@ -51,18 +64,18 @@ enum opt_conf { * struct opt - DHCP option * @sent: Convenience flag, set while filling replies * @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 * @conf: Option configuration (unset, default, or user) + * @s: Option payload from server */ struct opt { int sent; int slen; - uint8_t s[255]; int clen; uint8_t c[255]; enum opt_conf conf; + uint8_t s[OPT_CONCAT_MAX];
As I mentioned, clang-tidy (make clang-tidy) suggests rearranging this in a way that doesn't need padding.
};
static struct opt opts[256]; @@ -79,32 +92,23 @@ static struct opt opts[256]; #define DHCPINFORM 8 #define DHCPFORCERENEW 9
-#define OPT_MIN 60 /* RFC 951 */ - -/* Total option size (excluding end option) is 576 (RFC 2131), minus - * offset of options (268), minus end option (1). - */ -#define OPT_MAX 307 - /** * dhcp_init() - Initialise DHCP options */ void dhcp_init(void) { if (opts[1].conf != OPT_USER) /* Mask */ - opts[1] = (struct opt) { 0, 4, { 0 }, 0, { 0 }, OPT_DEFAULT }; + opts[1] = (struct opt) { 0, 4, 0, { 0 }, OPT_DEFAULT, { 0 } }; if (opts[3].conf != OPT_USER) /* Router */ - opts[3] = (struct opt) { 0, 4, { 0 }, 0, { 0 }, OPT_DEFAULT }; + opts[3] = (struct opt) { 0, 4, 0, { 0 }, OPT_DEFAULT, { 0 } }; if (opts[51].conf != OPT_USER) { /* Lease time */ - opts[51] = (struct opt) { 0, 4, { 0xff, 0xff, 0xff, 0xff }, - 0, { 0 }, OPT_DEFAULT }; + opts[51] = (struct opt) { 0, 4, 0, { 0 }, OPT_DEFAULT, + { 0xff, 0xff, 0xff, 0xff } }; } /* Type */ - opts[53] = (struct opt) { 0, 1, { 0 }, 0, { 0 }, OPT_DEFAULT }; - if (opts[54].conf != OPT_USER) { /* Server ID */ - opts[54] = (struct opt) { 0, 4, { 0 }, 0, { 0 }, - OPT_DEFAULT }; - } + opts[53] = (struct opt) { 0, 1, 0, { 0 }, OPT_DEFAULT, { 0 } }; + if (opts[54].conf != OPT_USER) /* Server ID */ + opts[54] = (struct opt) { 0, 4, 0, { 0 }, OPT_DEFAULT, { 0 } }; }
/** @@ -211,6 +215,11 @@ static const enum dhcp_opt_type dhcp_opt_types[] = { [252] = DHCP_OPT_STR, /* WPAD URL */ };
+/* Options requiring RFC 3396 concatenation, indexed by code */ +static const bool concat_req[256] = { + [81] = true, /* Client FQDN (RFC 4702, Section 2) */ +}; + /** * dhcp_opt_parse() - Parse a DHCP option value * @code: DHCP option code @@ -308,6 +317,9 @@ static int dhcp_opt_parse(uint8_t code, const char *str, case DHCP_OPT_STR: slen = strlen(str);
+ if (!concat_req[code] && slen > 255) + return -1; + if (slen > buf_len) return -1;
@@ -441,6 +453,45 @@ static void fill_one(uint8_t *buf, size_t size, int o, int *offset) *offset += slen; }
+/** + * 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 + * @dry_run: If true, calculate size without writing to @buf + * + * 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, + bool dry_run) +{ + size_t avail, chunk; + + if (*offset + 2 >= (int)size) + return 0; + + avail = size - *offset - 2; + chunk = MIN(MIN(remaining, avail), 255); + if (!chunk) + return 0; + + if (!dry_run) { + buf[*offset] = o; + buf[*offset + 1] = chunk; + } + *offset += 2; + + if (!dry_run) + memcpy(buf + *offset, data, chunk); + *offset += chunk; + + return chunk; +} + /** * enum dhcp_overload - DHCP option overload values (RFC 2132, Section 9.3) * @DHCP_OVERLOAD_NONE: No overload @@ -453,6 +504,43 @@ enum dhcp_overload { DHCP_OVERLOAD_SNAME = 2, };
+/** + * fill_split_areas() - Write a split option across options, file, and sname areas + * @m: Message to write into + * @opt_size: Usable size of the options area + * @o: Option number (code) + * @opt_off: Current offset within options area, updated on write + * @file_off: Current offset within file area, updated on write + * @sname_off: Current offset within sname area, updated on write + * @has_bootfile: If true, skip the file field + * @dry_run: If true, calculate size without writing + * + * Return: total data bytes written across all areas + */ +static size_t fill_split_areas(struct msg *m, size_t opt_size, int o, + int *opt_off, int *file_off, int *sname_off, + bool has_bootfile, bool dry_run) +{ + size_t written = 0;
No need to initialise this to 0 and then increment it unconditionally below, I think it's a bit confusing.
+ + written += fill_split(m->o, opt_size, o, opt_off, + opts[o].s, opts[o].slen, dry_run); + 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, dry_run); + } + if (written < (size_t)opts[o].slen) { + written += fill_split(m->sname, sizeof(m->sname) - 1, o, + sname_off, + opts[o].s + written, + opts[o].slen - written, dry_run); + } + + return written; +} + /** * fill() - Fill options in message, with overload into file/sname if needed * @m: Message to fill @@ -504,6 +592,42 @@ static int fill(struct msg *m, bool has_bootfile) } }
+ /* RFC 3396: split concatenation-requiring options that didn't fit + * as a single option. Split order: options, file, sname. + */ + foreach_opt(o) {
This could also use the foreach_unsent_opt() iterator I was suggesting for 6/7.
+ int dry_off, dry_file_off, dry_sname_off; + size_t total, written; + + if (opts[o].conf == OPT_UNSET || opts[o].sent || + !concat_req[o])
+ continue; + + /* Dry run: verify the option fits across all areas */ + dry_off = offset; + dry_file_off = file_off; + dry_sname_off = sname_off; + + total = fill_split_areas(m, size, o, + &dry_off, &dry_file_off, + &dry_sname_off, + has_bootfile, true); + + if (total < (size_t)opts[o].slen) { + debug("DHCP: skipping option %i (no space to split)",
Splitting itself doesn't need extra space, it's simply that we don't have space for the option (not even if split). Sorry, I missed this on v6.
+ o); + continue; + } + + /* Actual write */ + written = fill_split_areas(m, size, o, + &offset, &file_off, &sname_off, + has_bootfile, false); + + if (written >= (size_t)opts[o].slen) + opts[o].sent = 1; + } + /* Report any options that could not be sent */ foreach_opt(o) { if (opts[o].conf != OPT_UNSET && !opts[o].sent)
-- Stefano
participants (2)
-
Anshu Kumari
-
Stefano Brivio