[PATCH v2 0/6] Add --dhcp-boot and --dhcp-opt options
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. Anshu Kumari (6): conf: Add --dhcp-opt command-line option conf: Add --dhcp-boot command-line option dhcp: Add option type table and value parser dhcp: Refactor fill_one() to operate on a generic buffer dhcp: Add option overload doc: Add --dhcp-boot and --dhcp-opt to man page conf.c | 64 +++++++++++++- dhcp.c | 253 +++++++++++++++++++++++++++++++++++++++++++++++++++----- dhcp.h | 17 ++++ passt.1 | 41 +++++++++ passt.h | 16 ++++ 5 files changed, 370 insertions(+), 21 deletions(-) -- 2.54.0
Add an RFC 2132 type lookup table mapping DHCP option codes to their
expected value formats, and a dhcp_opt_parse() function that converts
CLI string values into their binary wire representation.
Wire dhcp_opt_parse() into the --dhcp-opt handler so that values are
validated and encoded at configuration time.
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari
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
A user can enter lots of options in command-line which may not fit in
existing buffer, So when the options field is full, overflow remaining
DHCP options into the file and sname fields per RFC 2132 option 52.
Also, when the file field is not used for overload, copy the boot
file URL there directly for legacy PXE clients.
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari
Document the new --dhcp-boot and --dhcp-opt command-line options in
the passt(1) man page, including supported option codes grouped by
value type and usage examples.
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari
On 5/26/26 14:31, Anshu Kumari wrote:
Add an RFC 2132 type lookup table mapping DHCP option codes to their expected value formats, and a dhcp_opt_parse() function that converts CLI string values into their binary wire representation.
Wire dhcp_opt_parse() into the --dhcp-opt handler so that values are validated and encoded at configuration time.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- 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 | 16 ++++++ dhcp.c | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dhcp.h | 17 +++++++ passt.h | 4 ++ 4 files changed, 185 insertions(+) diff --git a/conf.c b/conf.c index ae8ee26..3a5f45d 100644 --- a/conf.c +++ b/conf.c @@ -1266,6 +1266,7 @@ void conf(struct ctx *c, int argc, char **argv) char *end; uid_t uid; gid_t gid; + int len;
I think you don't need to introduce len for --dhcp-opt as you already use ret for --dhcp-boot
if (c->mode == MODE_PASTA) @@ -1482,7 +1483,14 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many DHCP options (max %d)", MAX_CUSTOM_DHCP_OPTS);
+ ret = dhcp_opt_parse(67, optarg, + c->custom_opts[c->custom_opts_count].val, + sizeof(c->custom_opts[0].val)); + if (ret < 0) + die("Invalid boot file value: %s", optarg); + c->custom_opts[c->custom_opts_count].code = 67; + c->custom_opts[c->custom_opts_count].len = ret; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", optarg)) @@ -1503,7 +1511,15 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many --dhcp-opt entries (max %d)", MAX_CUSTOM_DHCP_OPTS);
+ len = dhcp_opt_parse(optcode, comma + 1, + c->custom_opts[c->custom_opts_count].val, + sizeof(c->custom_opts[0].val));
you can use "ret" here too
+ if (len < 0) + die("Invalid value for DHCP option %lu: %s", + optcode, comma + 1); + c->custom_opts[c->custom_opts_count].code = optcode; + c->custom_opts[c->custom_opts_count].len = len; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", comma + 1)) diff --git a/dhcp.c b/dhcp.c index 1ff8cba..e1c95ad 100644 --- a/dhcp.c +++ b/dhcp.c @@ -33,6 +33,154 @@ #include "log.h" #include "dhcp.h"
+/** + * dhcp_opt_types - Maps option code to RFC 2132 value type, indexed by code + */ +static const enum dhcp_opt_type dhcp_opt_types[256] = { + [1] = DHCP_OPT_IPV4, /* Subnet Mask */ + [2] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* Boot File Size */ + [15] = DHCP_OPT_STR, /* Domain Name */ + [16] = DHCP_OPT_IPV4, /* Swap Server */ + [17] = DHCP_OPT_STR, /* Root Path */ + [19] = DHCP_OPT_INTEGER, /* IP Forwarding */ + [23] = DHCP_OPT_INTEGER, /* Default IP TTL */ + [26] = DHCP_OPT_INTEGER, /* Interface MTU */ + [28] = DHCP_OPT_IPV4, /* Broadcast Address */ + [33] = DHCP_OPT_IPV4_LIST, /* Static Routes */ + [37] = DHCP_OPT_INTEGER, /* TCP Default TTL */ + [38] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* IP Address Lease Time */ + [53] = DHCP_OPT_INTEGER, /* DHCP Message Type */ + [54] = DHCP_OPT_IPV4, /* Server Identifier */ + [55] = DHCP_OPT_STR, /* Parameter Request List */
This is a client option, I don't think we need to manage it.
+ [57] = DHCP_OPT_INTEGER, /* Max DHCP Message Size */ + [58] = DHCP_OPT_INTEGER, /* Renewal (T1) Time */ + [59] = DHCP_OPT_INTEGER, /* Rebinding (T2) Time */ + [60] = DHCP_OPT_STR, /* Vendor Class Identifier */ + [61] = DHCP_OPT_STR, /* Client Identifier */
This is also a client option.
+ [66] = DHCP_OPT_STR, /* TFTP Server Name */ + [67] = DHCP_OPT_STR, /* Bootfile Name */ + [119] = DHCP_OPT_STR, /* Domain Search List */
This is not really a string as this uses the message compression described in RFC 1035, 4.1.4. (See also 3. Example in rfc3397). I'm not sure we can encode this manually on the command line. And RFC 3396 defines how to encode search string for more than 255 characters length
+ [252] = DHCP_OPT_STR, /* WPAD URL */ +}; + +/** + * dhcp_opt_int_width - Integer width in bytes for INTEGER-typed options + */ +static const uint8_t dhcp_opt_int_width[256] = { + [2] = 4, /* Time Offset */ + [13] = 2, /* Boot File Size */ + [19] = 1, /* IP Forwarding */ + [23] = 1, /* Default IP TTL */ + [26] = 2, /* Interface MTU */ + [37] = 1, /* TCP Default TTL */ + [38] = 4, /* TCP Keepalive Interval */ + [51] = 4, /* IP Address Lease Time */ + [53] = 1, /* DHCP Message Type */ + [57] = 2, /* Max DHCP Message Size */ + [58] = 4, /* Renewal (T1) Time */ + [59] = 4, /* Rebinding (T2) Time */ +}; + +#define DHCP_OPT_PARSE_BUF 1024
Do we need 1024 bytes, all options are bounded to 256 bytes.
+ +/** + * dhcp_opt_parse() - Parse a DHCP option value + * @code: DHCP option code + * @str: DHCP Value string from command line + * @buf: Output buffer + * @buf_len: Size of output buffer + * + * Return: number of bytes written to @buf, or -1 on error + */ +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len) +{ + enum dhcp_opt_type type = dhcp_opt_types[code]; + char tmp[DHCP_OPT_PARSE_BUF]; + char *tok, *saveptr, *end; + struct in_addr addr; + unsigned long val; + unsigned int i; + uint8_t width; + size_t slen; + int len; + + switch (type) { + case DHCP_OPT_NONE: + die("Unsupported DHCP option: %u," + " see passt(1) for supported codes", code); + case DHCP_OPT_IPV4: + if (inet_pton(AF_INET, str, &addr) != 1) + return -1; + + if (buf_len < sizeof(addr)) + return -1; + + memcpy(buf, &addr, sizeof(addr)); + + return sizeof(addr); + case DHCP_OPT_IPV4_LIST: + len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, ",", &saveptr); tok; + tok = strtok_r(NULL, ",", &saveptr)) { + if (inet_pton(AF_INET, tok, &addr) != 1) + return -1; + + if (len + (int)sizeof(addr) > (int)buf_len) + return -1; + + memcpy(buf + len, &addr, sizeof(addr)); + len += sizeof(addr); + } + return len; + case DHCP_OPT_INTEGER: + width = dhcp_opt_int_width[code]; + val = strtoul(str, &end, 0); + + if (*end || buf_len < width) + return -1; + + if (width < 4 && val >= (1UL << (width * 8))) + return -1; + + for (i = 0; i < width; i++) + buf[i] = (val >> ((width - 1 - i) * 8)) & 0xff; + + return width; + case DHCP_OPT_STR: + slen = strlen(str); + + if (!slen || slen >= buf_len) + return -1; + + strncpy((char *)buf, str, buf_len); + + return slen; + } + + return -1; +} + /** * struct opt - DHCP option * @sent: Convenience flag, set while filling replies diff --git a/dhcp.h b/dhcp.h index cd50c99..3da571c 100644 --- a/dhcp.h +++ b/dhcp.h @@ -6,7 +6,24 @@ #ifndef DHCP_H #define DHCP_H
+/** + * 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_INTEGER: Unsigned integer (1, 2, or 4 bytes) + */ +enum dhcp_opt_type { + DHCP_OPT_NONE, + DHCP_OPT_STR, + DHCP_OPT_IPV4, + DHCP_OPT_IPV4_LIST, + DHCP_OPT_INTEGER, +}; + int dhcp(const struct ctx *c, struct iov_tail *data); void dhcp_init(void); +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len);
#endif /* DHCP_H */ diff --git a/passt.h b/passt.h index 3a0816f..751fee3 100644 --- a/passt.h +++ b/passt.h @@ -184,6 +184,8 @@ struct ip6_ctx { * @fqdn: Guest FQDN * @custom_opts: User-specified DHCP options from --dhcp-opt * @custom_opts.code: DHCP option code + * @custom_opts.len: Length of binary value in @val + * @custom_opts.val: Binary-encoded option value * @custom_opts.str: Original string value from command line * @custom_opts_count: Number of entries in @custom_opts * @ifi6: Template interface for IPv6, -1: none, 0: IPv6 disabled @@ -271,6 +273,8 @@ struct ctx {
struct { uint8_t code; + uint8_t len; + uint8_t val[255]; char str[256];
Why do we need to keep str[] once the value is encoded into val[]?
} custom_opts[MAX_CUSTOM_DHCP_OPTS]; int custom_opts_count;
Thanks, Laurent
On 5/26/26 14:31, 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
--- v2: - Renamed parameter cap → size. --- dhcp.c | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/dhcp.c b/dhcp.c index e1c95ad..e126063 100644 --- a/dhcp.c +++ b/dhcp.c @@ -279,28 +279,27 @@ 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)
Perhaps we can keep the comments?
+ if (*offset + 2 + slen > size) return true;
- m->o[*offset] = o; - m->o[*offset + 1] = slen; + buf[*offset] = o; + buf[*offset + 1] = slen;
- /* Move to option */
ditto
*offset += 2;
- memcpy(&m->o[*offset], opts[o].s, slen); + memcpy(&buf[*offset], opts[o].s, slen);
opts[o].sent = 1; *offset += slen; @@ -325,19 +324,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)) + 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); }
Reviewed-by: Laurent Vivier
On 5/26/26 14:31, Anshu Kumari wrote:
A user can enter lots of options in command-line which may not fit in existing buffer, So when the options field is full, overflow remaining DHCP options into the file and sname fields per RFC 2132 option 52.
Also, when the file field is not used for overload, copy the boot file URL there directly for legacy PXE clients.
This patch not only adds the overload, it also copies the options from the command line (custom_opts) to the actual opts.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- 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 | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/dhcp.c b/dhcp.c index e126063..a49a05a 100644 --- a/dhcp.c +++ b/dhcp.c @@ -306,14 +306,59 @@ static bool fill_one(uint8_t *buf, size_t size, int o, int *offset) return false; }
+#define DHCP_OVERLOAD_FILE 1 +#define DHCP_OVERLOAD_SNAME 2 + +/** + * fill_overflow() - Fill remaining options into file and sname fields + * @m: Message whose file/sname fields may be used for overflow + * + * Return: option 52 overload value: 0 if no overflow, + * DHCP_OVERLOAD_FILE for file, DHCP_OVERLOAD_SNAME for sname, + * or both OR'd together + */ +static int fill_overflow(struct msg *m) +{ + int file_off = 0, sname_off = 0, overload = 0; + int o; + + for (o = 0; o < 255; o++) { + if (opts[o].slen == -1 || opts[o].sent) + continue; + fill_one(m->file, sizeof(m->file) - 1, o, &file_off); + } + + for (o = 0; o < 255; o++) { + if (opts[o].slen == -1 || opts[o].sent) + continue; + if (fill_one(m->sname, sizeof(m->sname) - 1, o, &sname_off)) + debug("DHCP: skipping option %i (overload full)", o); + } + + if (file_off) { + m->file[file_off] = 255; + overload |= DHCP_OVERLOAD_FILE; + } + + if (sname_off) { + m->sname[sname_off] = 255; + overload |= DHCP_OVERLOAD_SNAME; + } + + return overload; +} + /** - * fill() - Fill options in message + * 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) * * Return: current size of options field */ -static int fill(struct msg *m) +static int fill(struct msg *m, int *overload) { + /* 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++) @@ -324,20 +369,25 @@ 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->o, OPT_MAX, 53, &offset)) - debug("DHCP: skipping option 53"); + fill_one(m->o, size, 53, &offset);
for (i = 0; i < opts[55].clen; i++) { o = opts[55].c[i]; if (opts[o].slen != -1) - 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].slen != -1 && !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); + } + + *overload = fill_overflow(m); + + if (*overload) { + m->o[offset++] = 52; + m->o[offset++] = 1; + m->o[offset++] = *overload; }
m->o[offset++] = 255; @@ -462,6 +512,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data) struct msg const *m; struct msg reply; unsigned int i; + int overload;
eh = IOV_REMOVE_HEADER(data, eh_storage); iph = IOV_PEEK_HEADER(data, iph_storage); @@ -613,7 +664,22 @@ int dhcp(const struct ctx *c, struct iov_tail *data) if (!c->no_dhcp_dns_search) opt_set_dns_search(c, sizeof(m->o));
opt_set_dns_search() manages option 119 but we have also it in custom_opts (incorrectly set as it doesn't manage compression). I think we overwrite it with incorrect values. Moreover I think this function is broken, because max_len is computed using opts[].slen, but opts[].slen is updated below (and max_len should be OPT_MAX - 3, not sizeof(m->o))
- dlen = offsetof(struct msg, o) + fill(&reply); + for (i = 0; i < (unsigned int)c->custom_opts_count; i++) { + uint8_t code = c->custom_opts[i].code; + + opts[code].slen = c->custom_opts[i].len; + memcpy(opts[code].s, c->custom_opts[i].val, + c->custom_opts[i].len); + } + + dlen = offsetof(struct msg, o) + fill(&reply, &overload); + + /* Copy boot file name into the file field for legacy PXE clients, + * unless the file field is already used for option overload. + */ + if (!(overload & DHCP_OVERLOAD_FILE) && + opts[67].slen > 0 && (size_t)opts[67].slen < sizeof(reply.file)) + memcpy(&reply.file, opts[67].s, opts[67].slen + 1);
No need for "&" in "&reply.size". the "+ 1" is wrong as reply.file has been cleared by a memset() (whereas opts[67].s[slen] can be not NUL).
if (m->flags & FLAG_BROADCAST) dst = in4addr_broadcast;
Thanks, Laurent
On Tue, May 26, 2026 at 06:05:04PM +0200, Laurent Vivier wrote:
On 5/26/26 14:31, Anshu Kumari wrote:
Add an RFC 2132 type lookup table mapping DHCP option codes to their expected value formats, and a dhcp_opt_parse() function that converts CLI string values into their binary wire representation.
Wire dhcp_opt_parse() into the --dhcp-opt handler so that values are validated and encoded at configuration time.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- 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 | 16 ++++++ dhcp.c | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dhcp.h | 17 +++++++ passt.h | 4 ++ 4 files changed, 185 insertions(+) diff --git a/conf.c b/conf.c index ae8ee26..3a5f45d 100644 --- a/conf.c +++ b/conf.c @@ -1266,6 +1266,7 @@ void conf(struct ctx *c, int argc, char **argv) char *end; uid_t uid; gid_t gid; + int len;
I think you don't need to introduce len for --dhcp-opt as you already use ret for --dhcp-boot
Also the inline code here in conf.c is becoming fairly bulky, and there's a little duplication between the --dhcp-boot and --dhcp-opt paths. I think it might be worth making an dhcp_add_option() helper allowing more of this logic, including this local to be moved to a function in dhcp.c. That will probably also make life easier for handling repeated options.
if (c->mode == MODE_PASTA) @@ -1482,7 +1483,14 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many DHCP options (max %d)", MAX_CUSTOM_DHCP_OPTS); + ret = dhcp_opt_parse(67, optarg, + c->custom_opts[c->custom_opts_count].val, + sizeof(c->custom_opts[0].val)); + if (ret < 0) + die("Invalid boot file value: %s", optarg); + c->custom_opts[c->custom_opts_count].code = 67; + c->custom_opts[c->custom_opts_count].len = ret; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", optarg)) @@ -1503,7 +1511,15 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many --dhcp-opt entries (max %d)", MAX_CUSTOM_DHCP_OPTS); + len = dhcp_opt_parse(optcode, comma + 1, + c->custom_opts[c->custom_opts_count].val, + sizeof(c->custom_opts[0].val));
you can use "ret" here too
+ if (len < 0) + die("Invalid value for DHCP option %lu: %s", + optcode, comma + 1); + c->custom_opts[c->custom_opts_count].code = optcode; + c->custom_opts[c->custom_opts_count].len = len; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", comma + 1)) diff --git a/dhcp.c b/dhcp.c index 1ff8cba..e1c95ad 100644 --- a/dhcp.c +++ b/dhcp.c @@ -33,6 +33,154 @@ #include "log.h" #include "dhcp.h" +/** + * dhcp_opt_types - Maps option code to RFC 2132 value type, indexed by code + */ +static const enum dhcp_opt_type dhcp_opt_types[256] = { + [1] = DHCP_OPT_IPV4, /* Subnet Mask */ + [2] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* Boot File Size */ + [15] = DHCP_OPT_STR, /* Domain Name */ + [16] = DHCP_OPT_IPV4, /* Swap Server */ + [17] = DHCP_OPT_STR, /* Root Path */ + [19] = DHCP_OPT_INTEGER, /* IP Forwarding */ + [23] = DHCP_OPT_INTEGER, /* Default IP TTL */ + [26] = DHCP_OPT_INTEGER, /* Interface MTU */ + [28] = DHCP_OPT_IPV4, /* Broadcast Address */ + [33] = DHCP_OPT_IPV4_LIST, /* Static Routes */ + [37] = DHCP_OPT_INTEGER, /* TCP Default TTL */ + [38] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* IP Address Lease Time */ + [53] = DHCP_OPT_INTEGER, /* DHCP Message Type */ + [54] = DHCP_OPT_IPV4, /* Server Identifier */ + [55] = DHCP_OPT_STR, /* Parameter Request List */
This is a client option, I don't think we need to manage it.
+ [57] = DHCP_OPT_INTEGER, /* Max DHCP Message Size */ + [58] = DHCP_OPT_INTEGER, /* Renewal (T1) Time */ + [59] = DHCP_OPT_INTEGER, /* Rebinding (T2) Time */ + [60] = DHCP_OPT_STR, /* Vendor Class Identifier */ + [61] = DHCP_OPT_STR, /* Client Identifier */
This is also a client option.
+ [66] = DHCP_OPT_STR, /* TFTP Server Name */ + [67] = DHCP_OPT_STR, /* Bootfile Name */ + [119] = DHCP_OPT_STR, /* Domain Search List */
This is not really a string as this uses the message compression described in RFC 1035, 4.1.4. (See also 3. Example in rfc3397). I'm not sure we can encode this manually on the command line. And RFC 3396 defines how to encode search string for more than 255 characters length
It's also already possible to set this via --dhcp-search. For now I think we want to exclude any options we already set ourselves (which includes 119). If we discover someone needs this in future, then we can figure out how the generic and specific ways of specifying it should interact.
+ [252] = DHCP_OPT_STR, /* WPAD URL */ +}; + +/** + * dhcp_opt_int_width - Integer width in bytes for INTEGER-typed options + */ +static const uint8_t dhcp_opt_int_width[256] = { + [2] = 4, /* Time Offset */ + [13] = 2, /* Boot File Size */ + [19] = 1, /* IP Forwarding */ + [23] = 1, /* Default IP TTL */ + [26] = 2, /* Interface MTU */ + [37] = 1, /* TCP Default TTL */ + [38] = 4, /* TCP Keepalive Interval */ + [51] = 4, /* IP Address Lease Time */ + [53] = 1, /* DHCP Message Type */ + [57] = 2, /* Max DHCP Message Size */ + [58] = 4, /* Renewal (T1) Time */ + [59] = 4, /* Rebinding (T2) Time */ +}; + +#define DHCP_OPT_PARSE_BUF 1024
Do we need 1024 bytes, all options are bounded to 256 bytes.
+ +/** + * dhcp_opt_parse() - Parse a DHCP option value + * @code: DHCP option code + * @str: DHCP Value string from command line + * @buf: Output buffer + * @buf_len: Size of output buffer + * + * Return: number of bytes written to @buf, or -1 on error + */ +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len) +{ + enum dhcp_opt_type type = dhcp_opt_types[code]; + char tmp[DHCP_OPT_PARSE_BUF]; + char *tok, *saveptr, *end; + struct in_addr addr; + unsigned long val; + unsigned int i; + uint8_t width; + size_t slen; + int len; + + switch (type) { + case DHCP_OPT_NONE: + die("Unsupported DHCP option: %u," + " see passt(1) for supported codes", code); + case DHCP_OPT_IPV4: + if (inet_pton(AF_INET, str, &addr) != 1) + return -1; + + if (buf_len < sizeof(addr)) + return -1; + + memcpy(buf, &addr, sizeof(addr)); + + return sizeof(addr); + case DHCP_OPT_IPV4_LIST: + len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, ",", &saveptr); tok; + tok = strtok_r(NULL, ",", &saveptr)) { + if (inet_pton(AF_INET, tok, &addr) != 1) + return -1; + + if (len + (int)sizeof(addr) > (int)buf_len) + return -1; + + memcpy(buf + len, &addr, sizeof(addr)); + len += sizeof(addr); + } + return len; + case DHCP_OPT_INTEGER: + width = dhcp_opt_int_width[code]; + val = strtoul(str, &end, 0); + + if (*end || buf_len < width) + return -1; + + if (width < 4 && val >= (1UL << (width * 8))) + return -1; + + for (i = 0; i < width; i++) + buf[i] = (val >> ((width - 1 - i) * 8)) & 0xff; + + return width; + case DHCP_OPT_STR: + slen = strlen(str); + + if (!slen || slen >= buf_len) + return -1; + + strncpy((char *)buf, str, buf_len); + + return slen; + } + + return -1; +} + /** * struct opt - DHCP option * @sent: Convenience flag, set while filling replies diff --git a/dhcp.h b/dhcp.h index cd50c99..3da571c 100644 --- a/dhcp.h +++ b/dhcp.h @@ -6,7 +6,24 @@ #ifndef DHCP_H #define DHCP_H +/** + * 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_INTEGER: Unsigned integer (1, 2, or 4 bytes) + */ +enum dhcp_opt_type { + DHCP_OPT_NONE, + DHCP_OPT_STR, + DHCP_OPT_IPV4, + DHCP_OPT_IPV4_LIST, + DHCP_OPT_INTEGER, +}; + int dhcp(const struct ctx *c, struct iov_tail *data); void dhcp_init(void); +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len); #endif /* DHCP_H */ diff --git a/passt.h b/passt.h index 3a0816f..751fee3 100644 --- a/passt.h +++ b/passt.h @@ -184,6 +184,8 @@ struct ip6_ctx { * @fqdn: Guest FQDN * @custom_opts: User-specified DHCP options from --dhcp-opt * @custom_opts.code: DHCP option code + * @custom_opts.len: Length of binary value in @val + * @custom_opts.val: Binary-encoded option value * @custom_opts.str: Original string value from command line * @custom_opts_count: Number of entries in @custom_opts * @ifi6: Template interface for IPv6, -1: none, 0: IPv6 disabled @@ -271,6 +273,8 @@ struct ctx { struct { uint8_t code; + uint8_t len; + uint8_t val[255]; char str[256];
Why do we need to keep str[] once the value is encoded into val[]?
} custom_opts[MAX_CUSTOM_DHCP_OPTS]; int custom_opts_count;
Thanks, Laurent
-- 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
On Tue, May 26, 2026 at 06:01:10PM +0530, Anshu Kumari wrote:
Add an RFC 2132 type lookup table mapping DHCP option codes to their expected value formats, and a dhcp_opt_parse() function that converts CLI string values into their binary wire representation.
Wire dhcp_opt_parse() into the --dhcp-opt handler so that values are validated and encoded at configuration time.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
Laurent's comments seconded, a few other ones here.
--- 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 | 16 ++++++ dhcp.c | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dhcp.h | 17 +++++++ passt.h | 4 ++ 4 files changed, 185 insertions(+)
diff --git a/conf.c b/conf.c index ae8ee26..3a5f45d 100644 --- a/conf.c +++ b/conf.c @@ -1266,6 +1266,7 @@ void conf(struct ctx *c, int argc, char **argv) char *end; uid_t uid; gid_t gid; + int len;
if (c->mode == MODE_PASTA) @@ -1482,7 +1483,14 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many DHCP options (max %d)", MAX_CUSTOM_DHCP_OPTS);
+ ret = dhcp_opt_parse(67, optarg, + c->custom_opts[c->custom_opts_count].val, + sizeof(c->custom_opts[0].val)); + if (ret < 0) + die("Invalid boot file value: %s", optarg); + c->custom_opts[c->custom_opts_count].code = 67; + c->custom_opts[c->custom_opts_count].len = ret; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", optarg)) @@ -1503,7 +1511,15 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many --dhcp-opt entries (max %d)", MAX_CUSTOM_DHCP_OPTS);
+ len = dhcp_opt_parse(optcode, comma + 1, + c->custom_opts[c->custom_opts_count].val, + sizeof(c->custom_opts[0].val)); + if (len < 0) + die("Invalid value for DHCP option %lu: %s", + optcode, comma + 1); + c->custom_opts[c->custom_opts_count].code = optcode; + c->custom_opts[c->custom_opts_count].len = len; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", comma + 1)) diff --git a/dhcp.c b/dhcp.c index 1ff8cba..e1c95ad 100644 --- a/dhcp.c +++ b/dhcp.c @@ -33,6 +33,154 @@ #include "log.h" #include "dhcp.h"
+/** + * dhcp_opt_types - Maps option code to RFC 2132 value type, indexed by code + */ +static const enum dhcp_opt_type dhcp_opt_types[256] = { + [1] = DHCP_OPT_IPV4, /* Subnet Mask */ + [2] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* Boot File Size */ + [15] = DHCP_OPT_STR, /* Domain Name */ + [16] = DHCP_OPT_IPV4, /* Swap Server */ + [17] = DHCP_OPT_STR, /* Root Path */ + [19] = DHCP_OPT_INTEGER, /* IP Forwarding */ + [23] = DHCP_OPT_INTEGER, /* Default IP TTL */ + [26] = DHCP_OPT_INTEGER, /* Interface MTU */ + [28] = DHCP_OPT_IPV4, /* Broadcast Address */ + [33] = DHCP_OPT_IPV4_LIST, /* Static Routes */ + [37] = DHCP_OPT_INTEGER, /* TCP Default TTL */ + [38] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* IP Address Lease Time */ + [53] = DHCP_OPT_INTEGER, /* DHCP Message Type */ + [54] = DHCP_OPT_IPV4, /* Server Identifier */ + [55] = DHCP_OPT_STR, /* Parameter Request List */ + [57] = DHCP_OPT_INTEGER, /* Max DHCP Message Size */ + [58] = DHCP_OPT_INTEGER, /* Renewal (T1) Time */ + [59] = DHCP_OPT_INTEGER, /* Rebinding (T2) Time */ + [60] = DHCP_OPT_STR, /* Vendor Class Identifier */ + [61] = DHCP_OPT_STR, /* Client Identifier */ + [66] = DHCP_OPT_STR, /* TFTP Server Name */ + [67] = DHCP_OPT_STR, /* Bootfile Name */ + [119] = DHCP_OPT_STR, /* Domain Search List */ + [252] = DHCP_OPT_STR, /* WPAD URL */ +}; + +/** + * dhcp_opt_int_width - Integer width in bytes for INTEGER-typed options + */ +static const uint8_t dhcp_opt_int_width[256] = {
I know I suggested this approach, but now seeing a whole array[256] it seems unpleasantly bulky. I'd suggest instead encoding the width into some bits of the DHCP_OPT_* constants.
+ [2] = 4, /* Time Offset */ + [13] = 2, /* Boot File Size */ + [19] = 1, /* IP Forwarding */ + [23] = 1, /* Default IP TTL */ + [26] = 2, /* Interface MTU */ + [37] = 1, /* TCP Default TTL */ + [38] = 4, /* TCP Keepalive Interval */ + [51] = 4, /* IP Address Lease Time */ + [53] = 1, /* DHCP Message Type */ + [57] = 2, /* Max DHCP Message Size */ + [58] = 4, /* Renewal (T1) Time */ + [59] = 4, /* Rebinding (T2) Time */ +}; + +#define DHCP_OPT_PARSE_BUF 1024 + +/** + * dhcp_opt_parse() - Parse a DHCP option value + * @code: DHCP option code + * @str: DHCP Value string from command line + * @buf: Output buffer + * @buf_len: Size of output buffer + * + * Return: number of bytes written to @buf, or -1 on error + */ +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len) +{ + enum dhcp_opt_type type = dhcp_opt_types[code]; + char tmp[DHCP_OPT_PARSE_BUF]; + char *tok, *saveptr, *end; + struct in_addr addr; + unsigned long val; + unsigned int i; + uint8_t width; + size_t slen; + int len; + + switch (type) { + case DHCP_OPT_NONE: + die("Unsupported DHCP option: %u," + " see passt(1) for supported codes", code); + case DHCP_OPT_IPV4: + if (inet_pton(AF_INET, str, &addr) != 1) + return -1; + + if (buf_len < sizeof(addr)) + return -1; + + memcpy(buf, &addr, sizeof(addr)); + + return sizeof(addr); + case DHCP_OPT_IPV4_LIST:
I think it should be possible to share more logic between the single IPv4 and IPv4 list options (parse them both as a list, but error if there's >1 in the single case).
+ len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, ",", &saveptr); tok; + tok = strtok_r(NULL, ",", &saveptr)) { + if (inet_pton(AF_INET, tok, &addr) != 1) + return -1; + + if (len + (int)sizeof(addr) > (int)buf_len) + return -1; + + memcpy(buf + len, &addr, sizeof(addr)); + len += sizeof(addr); + } + return len; + case DHCP_OPT_INTEGER: + width = dhcp_opt_int_width[code]; + val = strtoul(str, &end, 0);
Most strtoul() errors can be discerned by checking end, but not all. You also need to explicitly set errno to 0 beforehand and check it again afterwards (clunky, but that's the interface we have).
+ + if (*end || buf_len < width) + return -1; + + if (width < 4 && val >= (1UL << (width * 8)))
strtoul() will parse 64-bit values on most current platforms, so you need this range enforcement for width==4 as well.
+ return -1; + + for (i = 0; i < width; i++) + buf[i] = (val >> ((width - 1 - i) * 8)) & 0xff;
Might be easier to follow as: for (...) { buf[i] = val & 0xff; val >>= 8; }
+ + return width; + case DHCP_OPT_STR: + slen = strlen(str); + + if (!slen || slen >= buf_len) + return -1; + + strncpy((char *)buf, str, buf_len);
You've already know the length so you can make this a memcpy().
+ + return slen; + } + + return -1; +} + /** * struct opt - DHCP option * @sent: Convenience flag, set while filling replies diff --git a/dhcp.h b/dhcp.h index cd50c99..3da571c 100644 --- a/dhcp.h +++ b/dhcp.h @@ -6,7 +6,24 @@ #ifndef DHCP_H #define DHCP_H
+/** + * 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_INTEGER: Unsigned integer (1, 2, or 4 bytes) + */ +enum dhcp_opt_type { + DHCP_OPT_NONE, + DHCP_OPT_STR, + DHCP_OPT_IPV4, + DHCP_OPT_IPV4_LIST, + DHCP_OPT_INTEGER, +};
This can move to dhcp.c, since it's not used in any other files.
+ int dhcp(const struct ctx *c, struct iov_tail *data); void dhcp_init(void); +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len);
#endif /* DHCP_H */ diff --git a/passt.h b/passt.h index 3a0816f..751fee3 100644 --- a/passt.h +++ b/passt.h @@ -184,6 +184,8 @@ struct ip6_ctx { * @fqdn: Guest FQDN * @custom_opts: User-specified DHCP options from --dhcp-opt * @custom_opts.code: DHCP option code + * @custom_opts.len: Length of binary value in @val + * @custom_opts.val: Binary-encoded option value * @custom_opts.str: Original string value from command line * @custom_opts_count: Number of entries in @custom_opts * @ifi6: Template interface for IPv6, -1: none, 0: IPv6 disabled @@ -271,6 +273,8 @@ struct ctx {
struct { uint8_t code; + uint8_t len; + uint8_t val[255]; char str[256]; } custom_opts[MAX_CUSTOM_DHCP_OPTS]; int custom_opts_count; -- 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
On Tue, May 26, 2026 at 06:01:11PM +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
Reviewed-by: David Gibson
--- v2: - Renamed parameter cap → size. --- dhcp.c | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-)
diff --git a/dhcp.c b/dhcp.c index e1c95ad..e126063 100644 --- a/dhcp.c +++ b/dhcp.c @@ -279,28 +279,27 @@ 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
Pre-existing wart: It's very unusual for bool-valued functions to report failure with 'true' even though though it *is* usual for int-valued functions to report failure with negative =~ non-zero =~ "trueish" values.
*/ -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 + 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; @@ -325,19 +324,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)) + 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
On Tue, May 26, 2026 at 06:01:13PM +0530, Anshu Kumari wrote:
Document the new --dhcp-boot and --dhcp-opt command-line options in the passt(1) man page, including supported option codes grouped by value type and usage examples.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
Reviewed-by: David Gibson
--- v2: - Updated --dhcp-boot description. - Highlighted cross-referenced options with \fB...\fR. - Updated IP list format from "space-separated within quotes" to "comma-separated". - option 121 dropped. - Added option 55 to string options list. - Removed --dhcp-boot override reference from --dhcp-opt description. --- passt.1 | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+)
diff --git a/passt.1 b/passt.1 index 908fd4a..199172f 100644 --- a/passt.1 +++ b/passt.1 @@ -430,6 +430,47 @@ 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 " " \fIurl +Convenience shorthand for \fB\-\-dhcp-opt\fR 67,\fIurl\fR. +Sets the boot file name (DHCP option 67) for network boot. +For UEFI HTTP boot, also set the vendor class identifier using +\fB\-\-dhcp-opt\fR 60,HTTPClient. + +.TP +.BR \-\-dhcp-opt " " \fICODE\fR,\fIVALUE\fR +Set a DHCP option by numeric code. The value format is determined automatically +from the option code. Multiple IPv4 addresses are comma-separated. +This option can be specified multiple times. Options set with \fB\-\-dhcp-opt\fR +override built-in values. +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), 33 (Static Routes), 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), +55 (Parameter Request List), +60 (Vendor Class Identifier), 61 (Client Identifier), 66 (TFTP Server Name), +67 (Bootfile Name), 119 (Domain Search List), 252 (WPAD URL) +.RE + .TP .BR \-t ", " \-\-tcp-ports " " \fIspec Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of: -- 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
On Tue, May 26, 2026 at 06:01:12PM +0530, Anshu Kumari wrote:
A user can enter lots of options in command-line which may not fit in existing buffer, So when the options field is full, overflow remaining DHCP options into the file and sname fields per RFC 2132 option 52.
Also, when the file field is not used for overload, copy the boot file URL there directly for legacy PXE clients.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- 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 | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/dhcp.c b/dhcp.c index e126063..a49a05a 100644 --- a/dhcp.c +++ b/dhcp.c @@ -306,14 +306,59 @@ static bool fill_one(uint8_t *buf, size_t size, int o, int *offset) return false; }
+#define DHCP_OVERLOAD_FILE 1 +#define DHCP_OVERLOAD_SNAME 2
IIUC, these values are from the RFC - might be worth referencing the relevant section in a comment to make it clear they can't be changed arbitrarily.
+ +/** + * fill_overflow() - Fill remaining options into file and sname fields + * @m: Message whose file/sname fields may be used for overflow + * + * Return: option 52 overload value: 0 if no overflow, + * DHCP_OVERLOAD_FILE for file, DHCP_OVERLOAD_SNAME for sname, + * or both OR'd together + */ +static int fill_overflow(struct msg *m) +{ + int file_off = 0, sname_off = 0, overload = 0; + int o; + + for (o = 0; o < 255; o++) {
You could use ARRAY_size(opts) instead of raw 255, yes?
+ if (opts[o].slen == -1 || opts[o].sent) + continue; + fill_one(m->file, sizeof(m->file) - 1, o, &file_off); + } + + for (o = 0; o < 255; o++) { + if (opts[o].slen == -1 || opts[o].sent) + continue; + if (fill_one(m->sname, sizeof(m->sname) - 1, o, &sname_off)) + debug("DHCP: skipping option %i (overload full)", o); + } + + if (file_off) { + m->file[file_off] = 255; + overload |= DHCP_OVERLOAD_FILE; + } + + if (sname_off) { + m->sname[sname_off] = 255; + overload |= DHCP_OVERLOAD_SNAME; + } + + return overload; +} + /** - * fill() - Fill options in message + * 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) * * Return: current size of options field */ -static int fill(struct msg *m) +static int fill(struct msg *m, int *overload) { + /* 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++) @@ -324,20 +369,25 @@ 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->o, OPT_MAX, 53, &offset)) - debug("DHCP: skipping option 53"); + fill_one(m->o, size, 53, &offset);
for (i = 0; i < opts[55].clen; i++) { o = opts[55].c[i]; if (opts[o].slen != -1) - 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].slen != -1 && !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); + } + + *overload = fill_overflow(m); + + if (*overload) { + m->o[offset++] = 52; + m->o[offset++] = 1; + m->o[offset++] = *overload; }
m->o[offset++] = 255; @@ -462,6 +512,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data) struct msg const *m; struct msg reply; unsigned int i; + int overload;
eh = IOV_REMOVE_HEADER(data, eh_storage); iph = IOV_PEEK_HEADER(data, iph_storage); @@ -613,7 +664,22 @@ int dhcp(const struct ctx *c, struct iov_tail *data) if (!c->no_dhcp_dns_search) opt_set_dns_search(c, sizeof(m->o));
- dlen = offsetof(struct msg, o) + fill(&reply); + for (i = 0; i < (unsigned int)c->custom_opts_count; i++) { + uint8_t code = c->custom_opts[i].code; + + opts[code].slen = c->custom_opts[i].len; + memcpy(opts[code].s, c->custom_opts[i].val, + c->custom_opts[i].len); + } + + dlen = offsetof(struct msg, o) + fill(&reply, &overload); + + /* Copy boot file name into the file field for legacy PXE clients, + * unless the file field is already used for option overload.
Given we do this, would it make more sense to use the slen field in preference to the file field for overloads? The logic above appears to do it the other way around,
+ */ + if (!(overload & DHCP_OVERLOAD_FILE) && + opts[67].slen > 0 && (size_t)opts[67].slen < sizeof(reply.file)) + memcpy(&reply.file, opts[67].s, opts[67].slen + 1);
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
On Tue, May 26, 2026 at 9:35 PM Laurent Vivier
On 5/26/26 14:31, Anshu Kumari wrote:
Add an RFC 2132 type lookup table mapping DHCP option codes to their expected value formats, and a dhcp_opt_parse() function that converts CLI string values into their binary wire representation.
Wire dhcp_opt_parse() into the --dhcp-opt handler so that values are validated and encoded at configuration time.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- 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 | 16 ++++++ dhcp.c | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dhcp.h | 17 +++++++ passt.h | 4 ++ 4 files changed, 185 insertions(+) diff --git a/conf.c b/conf.c index ae8ee26..3a5f45d 100644 --- a/conf.c +++ b/conf.c @@ -1266,6 +1266,7 @@ void conf(struct ctx *c, int argc, char **argv) char *end; uid_t uid; gid_t gid; + int len;
I think you don't need to introduce len for --dhcp-opt as you already use ret for --dhcp-boot
if (c->mode == MODE_PASTA) @@ -1482,7 +1483,14 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many DHCP options (max %d)", MAX_CUSTOM_DHCP_OPTS);
+ ret = dhcp_opt_parse(67, optarg, +
c->custom_opts[c->custom_opts_count].val,
+ sizeof(c->custom_opts[0].val)); + if (ret < 0) + die("Invalid boot file value: %s", optarg); + c->custom_opts[c->custom_opts_count].code = 67; + c->custom_opts[c->custom_opts_count].len = ret; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", optarg)) @@ -1503,7 +1511,15 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many --dhcp-opt entries (max %d)", MAX_CUSTOM_DHCP_OPTS);
+ len = dhcp_opt_parse(optcode, comma + 1, + c->custom_opts[c->custom_opts_count].val, + sizeof(c->custom_opts[0].val));
you can use "ret" here too
+ if (len < 0) + die("Invalid value for DHCP option %lu: %s", + optcode, comma + 1); + c->custom_opts[c->custom_opts_count].code = optcode; + c->custom_opts[c->custom_opts_count].len = len; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", comma + 1)) diff --git a/dhcp.c b/dhcp.c index 1ff8cba..e1c95ad 100644 --- a/dhcp.c +++ b/dhcp.c @@ -33,6 +33,154 @@ #include "log.h" #include "dhcp.h"
+/** + * dhcp_opt_types - Maps option code to RFC 2132 value type, indexed by code + */ +static const enum dhcp_opt_type dhcp_opt_types[256] = { + [1] = DHCP_OPT_IPV4, /* Subnet Mask */ + [2] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* Boot File Size */ + [15] = DHCP_OPT_STR, /* Domain Name */ + [16] = DHCP_OPT_IPV4, /* Swap Server */ + [17] = DHCP_OPT_STR, /* Root Path */ + [19] = DHCP_OPT_INTEGER, /* IP Forwarding */ + [23] = DHCP_OPT_INTEGER, /* Default IP TTL */ + [26] = DHCP_OPT_INTEGER, /* Interface MTU */ + [28] = DHCP_OPT_IPV4, /* Broadcast Address */ + [33] = DHCP_OPT_IPV4_LIST, /* Static Routes */ + [37] = DHCP_OPT_INTEGER, /* TCP Default TTL */ + [38] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* IP Address Lease Time */ + [53] = DHCP_OPT_INTEGER, /* DHCP Message Type */ + [54] = DHCP_OPT_IPV4, /* Server Identifier */ + [55] = DHCP_OPT_STR, /* Parameter Request List */
This is a client option, I don't think we need to manage it.
+ [57] = DHCP_OPT_INTEGER, /* Max DHCP Message Size */ + [58] = DHCP_OPT_INTEGER, /* Renewal (T1) Time */ + [59] = DHCP_OPT_INTEGER, /* Rebinding (T2) Time */ + [60] = DHCP_OPT_STR, /* Vendor Class Identifier */ + [61] = DHCP_OPT_STR, /* Client Identifier */
This is also a client option.
+ [66] = DHCP_OPT_STR, /* TFTP Server Name */ + [67] = DHCP_OPT_STR, /* Bootfile Name */ + [119] = DHCP_OPT_STR, /* Domain Search List */
This is not really a string as this uses the message compression described in RFC 1035, 4.1.4. (See also 3. Example in rfc3397). I'm not sure we can encode this manually on the command line. And RFC 3396 defines how to encode search string for more than 255 characters length
+ [252] = DHCP_OPT_STR, /* WPAD URL */ +}; + +/** + * dhcp_opt_int_width - Integer width in bytes for INTEGER-typed options + */ +static const uint8_t dhcp_opt_int_width[256] = { + [2] = 4, /* Time Offset */ + [13] = 2, /* Boot File Size */ + [19] = 1, /* IP Forwarding */ + [23] = 1, /* Default IP TTL */ + [26] = 2, /* Interface MTU */ + [37] = 1, /* TCP Default TTL */ + [38] = 4, /* TCP Keepalive Interval */ + [51] = 4, /* IP Address Lease Time */ + [53] = 1, /* DHCP Message Type */ + [57] = 2, /* Max DHCP Message Size */ + [58] = 4, /* Renewal (T1) Time */ + [59] = 4, /* Rebinding (T2) Time */ +}; + +#define DHCP_OPT_PARSE_BUF 1024
Do we need 1024 bytes, all options are bounded to 256 bytes.
+ +/** + * dhcp_opt_parse() - Parse a DHCP option value + * @code: DHCP option code + * @str: DHCP Value string from command line + * @buf: Output buffer + * @buf_len: Size of output buffer + * + * Return: number of bytes written to @buf, or -1 on error + */ +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len) +{ + enum dhcp_opt_type type = dhcp_opt_types[code]; + char tmp[DHCP_OPT_PARSE_BUF]; + char *tok, *saveptr, *end; + struct in_addr addr; + unsigned long val; + unsigned int i; + uint8_t width; + size_t slen; + int len; + + switch (type) { + case DHCP_OPT_NONE: + die("Unsupported DHCP option: %u," + " see passt(1) for supported codes", code); + case DHCP_OPT_IPV4: + if (inet_pton(AF_INET, str, &addr) != 1) + return -1; + + if (buf_len < sizeof(addr)) + return -1; + + memcpy(buf, &addr, sizeof(addr)); + + return sizeof(addr); + case DHCP_OPT_IPV4_LIST: + len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, ",", &saveptr); tok; + tok = strtok_r(NULL, ",", &saveptr)) { + if (inet_pton(AF_INET, tok, &addr) != 1) + return -1; + + if (len + (int)sizeof(addr) > (int)buf_len) + return -1; + + memcpy(buf + len, &addr, sizeof(addr)); + len += sizeof(addr); + } + return len; + case DHCP_OPT_INTEGER: + width = dhcp_opt_int_width[code]; + val = strtoul(str, &end, 0); + + if (*end || buf_len < width) + return -1; + + if (width < 4 && val >= (1UL << (width * 8))) + return -1; + + for (i = 0; i < width; i++) + buf[i] = (val >> ((width - 1 - i) * 8)) & 0xff; + + return width; + case DHCP_OPT_STR: + slen = strlen(str); + + if (!slen || slen >= buf_len) + return -1; + + strncpy((char *)buf, str, buf_len); + + return slen; + } + + return -1; +} + /** * struct opt - DHCP option * @sent: Convenience flag, set while filling replies diff --git a/dhcp.h b/dhcp.h index cd50c99..3da571c 100644 --- a/dhcp.h +++ b/dhcp.h @@ -6,7 +6,24 @@ #ifndef DHCP_H #define DHCP_H
+/** + * 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_INTEGER: Unsigned integer (1, 2, or 4 bytes) + */ +enum dhcp_opt_type { + DHCP_OPT_NONE, + DHCP_OPT_STR, + DHCP_OPT_IPV4, + DHCP_OPT_IPV4_LIST, + DHCP_OPT_INTEGER, +}; + int dhcp(const struct ctx *c, struct iov_tail *data); void dhcp_init(void); +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len);
#endif /* DHCP_H */ diff --git a/passt.h b/passt.h index 3a0816f..751fee3 100644 --- a/passt.h +++ b/passt.h @@ -184,6 +184,8 @@ struct ip6_ctx { * @fqdn: Guest FQDN * @custom_opts: User-specified DHCP options from --dhcp-opt * @custom_opts.code: DHCP option code + * @custom_opts.len: Length of binary value in @val + * @custom_opts.val: Binary-encoded option value * @custom_opts.str: Original string value from command line * @custom_opts_count: Number of entries in @custom_opts * @ifi6: Template interface for IPv6, -1: none, 0: IPv6 disabled @@ -271,6 +273,8 @@ struct ctx {
struct { uint8_t code; + uint8_t len; + uint8_t val[255]; char str[256];
Why do we need to keep str[] once the value is encoded into val[]?
To print the option value as entered by user. anskuma@anskuma-thinkpadp1gen7:~/Documents/passt$ ./passt -f --dhcp-opt 60,HTTPClient --dhcp-opt 67,http://192.168.1.1:8080/alpine.iso --dhcp-opt 12,testhost --dhcp-opt 15,test.example.com --dhcp-opt 6,8.8.8.8,8.8.4.4 --dhcp-opt 26,1400 --dhcp-opt 42,192.168.1.1 --dhcp-opt 51,360 --dhcp-opt 6,1.1.1.1,9.9.9.9 UNIX domain socket bound at /tmp/passt_1.socket No IPv6 nameserver available for NDP/DHCPv6 Template interface: wlp9s0f0 (IPv4), wlp9s0f0 (IPv6) MAC: host: 9a:55:9a:55:9a:55 NAT to host 127.0.0.1: 192.168.1.1 DHCP: assign: 192.168.1.9 mask: 255.255.255.0 router: 192.168.1.1 *option 60: HTTPClient option 67: http://192.168.1.1:8080/alpine.iso http://192.168.1.1:8080/alpine.iso option 12: testhost option 15: test.example.com http://test.example.com option 6: 1.1.1.1,9.9.9.9 option 26: 1400 option 42: 192.168.1.1 option 51: 360*
} custom_opts[MAX_CUSTOM_DHCP_OPTS]; int custom_opts_count;
Thanks, Laurent
On Thu, May 28, 2026 at 11:09:06AM +0530, Anshu Kumari wrote:
On Tue, May 26, 2026 at 9:35 PM Laurent Vivier
wrote: On 5/26/26 14:31, Anshu Kumari wrote:
Add an RFC 2132 type lookup table mapping DHCP option codes to their expected value formats, and a dhcp_opt_parse() function that converts CLI string values into their binary wire representation.
Wire dhcp_opt_parse() into the --dhcp-opt handler so that values are validated and encoded at configuration time.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- 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 | 16 ++++++ dhcp.c | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dhcp.h | 17 +++++++ passt.h | 4 ++ 4 files changed, 185 insertions(+) diff --git a/conf.c b/conf.c index ae8ee26..3a5f45d 100644 --- a/conf.c +++ b/conf.c @@ -1266,6 +1266,7 @@ void conf(struct ctx *c, int argc, char **argv) char *end; uid_t uid; gid_t gid; + int len;
I think you don't need to introduce len for --dhcp-opt as you already use ret for --dhcp-boot
if (c->mode == MODE_PASTA) @@ -1482,7 +1483,14 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many DHCP options (max %d)", MAX_CUSTOM_DHCP_OPTS);
+ ret = dhcp_opt_parse(67, optarg, +
c->custom_opts[c->custom_opts_count].val,
+ sizeof(c->custom_opts[0].val)); + if (ret < 0) + die("Invalid boot file value: %s", optarg); + c->custom_opts[c->custom_opts_count].code = 67; + c->custom_opts[c->custom_opts_count].len = ret; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", optarg)) @@ -1503,7 +1511,15 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many --dhcp-opt entries (max %d)", MAX_CUSTOM_DHCP_OPTS);
+ len = dhcp_opt_parse(optcode, comma + 1, + c->custom_opts[c->custom_opts_count].val, + sizeof(c->custom_opts[0].val));
you can use "ret" here too
+ if (len < 0) + die("Invalid value for DHCP option %lu: %s", + optcode, comma + 1); + c->custom_opts[c->custom_opts_count].code = optcode; + c->custom_opts[c->custom_opts_count].len = len; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", comma + 1)) diff --git a/dhcp.c b/dhcp.c index 1ff8cba..e1c95ad 100644 --- a/dhcp.c +++ b/dhcp.c @@ -33,6 +33,154 @@ #include "log.h" #include "dhcp.h"
+/** + * dhcp_opt_types - Maps option code to RFC 2132 value type, indexed by code + */ +static const enum dhcp_opt_type dhcp_opt_types[256] = { + [1] = DHCP_OPT_IPV4, /* Subnet Mask */ + [2] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* Boot File Size */ + [15] = DHCP_OPT_STR, /* Domain Name */ + [16] = DHCP_OPT_IPV4, /* Swap Server */ + [17] = DHCP_OPT_STR, /* Root Path */ + [19] = DHCP_OPT_INTEGER, /* IP Forwarding */ + [23] = DHCP_OPT_INTEGER, /* Default IP TTL */ + [26] = DHCP_OPT_INTEGER, /* Interface MTU */ + [28] = DHCP_OPT_IPV4, /* Broadcast Address */ + [33] = DHCP_OPT_IPV4_LIST, /* Static Routes */ + [37] = DHCP_OPT_INTEGER, /* TCP Default TTL */ + [38] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* IP Address Lease Time */ + [53] = DHCP_OPT_INTEGER, /* DHCP Message Type */ + [54] = DHCP_OPT_IPV4, /* Server Identifier */ + [55] = DHCP_OPT_STR, /* Parameter Request List */
This is a client option, I don't think we need to manage it.
+ [57] = DHCP_OPT_INTEGER, /* Max DHCP Message Size */ + [58] = DHCP_OPT_INTEGER, /* Renewal (T1) Time */ + [59] = DHCP_OPT_INTEGER, /* Rebinding (T2) Time */ + [60] = DHCP_OPT_STR, /* Vendor Class Identifier */ + [61] = DHCP_OPT_STR, /* Client Identifier */
This is also a client option.
+ [66] = DHCP_OPT_STR, /* TFTP Server Name */ + [67] = DHCP_OPT_STR, /* Bootfile Name */ + [119] = DHCP_OPT_STR, /* Domain Search List */
This is not really a string as this uses the message compression described in RFC 1035, 4.1.4. (See also 3. Example in rfc3397). I'm not sure we can encode this manually on the command line. And RFC 3396 defines how to encode search string for more than 255 characters length
+ [252] = DHCP_OPT_STR, /* WPAD URL */ +}; + +/** + * dhcp_opt_int_width - Integer width in bytes for INTEGER-typed options + */ +static const uint8_t dhcp_opt_int_width[256] = { + [2] = 4, /* Time Offset */ + [13] = 2, /* Boot File Size */ + [19] = 1, /* IP Forwarding */ + [23] = 1, /* Default IP TTL */ + [26] = 2, /* Interface MTU */ + [37] = 1, /* TCP Default TTL */ + [38] = 4, /* TCP Keepalive Interval */ + [51] = 4, /* IP Address Lease Time */ + [53] = 1, /* DHCP Message Type */ + [57] = 2, /* Max DHCP Message Size */ + [58] = 4, /* Renewal (T1) Time */ + [59] = 4, /* Rebinding (T2) Time */ +}; + +#define DHCP_OPT_PARSE_BUF 1024
Do we need 1024 bytes, all options are bounded to 256 bytes.
+ +/** + * dhcp_opt_parse() - Parse a DHCP option value + * @code: DHCP option code + * @str: DHCP Value string from command line + * @buf: Output buffer + * @buf_len: Size of output buffer + * + * Return: number of bytes written to @buf, or -1 on error + */ +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len) +{ + enum dhcp_opt_type type = dhcp_opt_types[code]; + char tmp[DHCP_OPT_PARSE_BUF]; + char *tok, *saveptr, *end; + struct in_addr addr; + unsigned long val; + unsigned int i; + uint8_t width; + size_t slen; + int len; + + switch (type) { + case DHCP_OPT_NONE: + die("Unsupported DHCP option: %u," + " see passt(1) for supported codes", code); + case DHCP_OPT_IPV4: + if (inet_pton(AF_INET, str, &addr) != 1) + return -1; + + if (buf_len < sizeof(addr)) + return -1; + + memcpy(buf, &addr, sizeof(addr)); + + return sizeof(addr); + case DHCP_OPT_IPV4_LIST: + len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, ",", &saveptr); tok; + tok = strtok_r(NULL, ",", &saveptr)) { + if (inet_pton(AF_INET, tok, &addr) != 1) + return -1; + + if (len + (int)sizeof(addr) > (int)buf_len) + return -1; + + memcpy(buf + len, &addr, sizeof(addr)); + len += sizeof(addr); + } + return len; + case DHCP_OPT_INTEGER: + width = dhcp_opt_int_width[code]; + val = strtoul(str, &end, 0); + + if (*end || buf_len < width) + return -1; + + if (width < 4 && val >= (1UL << (width * 8))) + return -1; + + for (i = 0; i < width; i++) + buf[i] = (val >> ((width - 1 - i) * 8)) & 0xff; + + return width; + case DHCP_OPT_STR: + slen = strlen(str); + + if (!slen || slen >= buf_len) + return -1; + + strncpy((char *)buf, str, buf_len); + + return slen; + } + + return -1; +} + /** * struct opt - DHCP option * @sent: Convenience flag, set while filling replies diff --git a/dhcp.h b/dhcp.h index cd50c99..3da571c 100644 --- a/dhcp.h +++ b/dhcp.h @@ -6,7 +6,24 @@ #ifndef DHCP_H #define DHCP_H
+/** + * 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_INTEGER: Unsigned integer (1, 2, or 4 bytes) + */ +enum dhcp_opt_type { + DHCP_OPT_NONE, + DHCP_OPT_STR, + DHCP_OPT_IPV4, + DHCP_OPT_IPV4_LIST, + DHCP_OPT_INTEGER, +}; + int dhcp(const struct ctx *c, struct iov_tail *data); void dhcp_init(void); +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len);
#endif /* DHCP_H */ diff --git a/passt.h b/passt.h index 3a0816f..751fee3 100644 --- a/passt.h +++ b/passt.h @@ -184,6 +184,8 @@ struct ip6_ctx { * @fqdn: Guest FQDN * @custom_opts: User-specified DHCP options from --dhcp-opt * @custom_opts.code: DHCP option code + * @custom_opts.len: Length of binary value in @val + * @custom_opts.val: Binary-encoded option value * @custom_opts.str: Original string value from command line * @custom_opts_count: Number of entries in @custom_opts * @ifi6: Template interface for IPv6, -1: none, 0: IPv6 disabled @@ -271,6 +273,8 @@ struct ctx {
struct { uint8_t code; + uint8_t len; + uint8_t val[255]; char str[256];
Why do we need to keep str[] once the value is encoded into val[]?
To print the option value as entered by user.
Right, but I think Laurent's point is that it's unfortunate to keep str[] around indefinitely for something that's used once at startup.
anskuma@anskuma-thinkpadp1gen7:~/Documents/passt$ ./passt -f --dhcp-opt 60,HTTPClient --dhcp-opt 67,http://192.168.1.1:8080/alpine.iso --dhcp-opt 12,testhost --dhcp-opt 15,test.example.com --dhcp-opt 6,8.8.8.8,8.8.4.4 --dhcp-opt 26,1400 --dhcp-opt 42,192.168.1.1 --dhcp-opt 51,360 --dhcp-opt 6,1.1.1.1,9.9.9.9 UNIX domain socket bound at /tmp/passt_1.socket No IPv6 nameserver available for NDP/DHCPv6 Template interface: wlp9s0f0 (IPv4), wlp9s0f0 (IPv6) MAC: host: 9a:55:9a:55:9a:55 NAT to host 127.0.0.1: 192.168.1.1 DHCP: assign: 192.168.1.9 mask: 255.255.255.0 router: 192.168.1.1
*option 60: HTTPClient option 67: http://192.168.1.1:8080/alpine.iso http://192.168.1.1:8080/alpine.iso option 12: testhost option 15: test.example.com http://test.example.com option 6: 1.1.1.1,9.9.9.9 option 26: 1400 option 42: 192.168.1.1 option 51: 360*
} custom_opts[MAX_CUSTOM_DHCP_OPTS]; int custom_opts_count;
Thanks, Laurent
-- 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
On Wed, May 27, 2026 at 9:46 AM David Gibson
A user can enter lots of options in command-line which may not fit in existing buffer, So when the options field is full, overflow remaining DHCP options into the file and sname fields per RFC 2132 option 52.
Also, when the file field is not used for overload, copy the boot file URL there directly for legacy PXE clients.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- v2: - Added #define DHCP_OVERLOAD_FILE and #define DHCP_OVERLOAD_SNAME constants - Added comment documenting space reservation: /* Reserve 3 bytes for On Tue, May 26, 2026 at 06:01:12PM +0530, Anshu Kumari wrote: 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 | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 9 deletions(-)
diff --git a/dhcp.c b/dhcp.c index e126063..a49a05a 100644 --- a/dhcp.c +++ b/dhcp.c @@ -306,14 +306,59 @@ static bool fill_one(uint8_t *buf, size_t size, int o, int *offset) return false; }
+#define DHCP_OVERLOAD_FILE 1 +#define DHCP_OVERLOAD_SNAME 2
IIUC, these values are from the RFC - might be worth referencing the relevant section in a comment to make it clear they can't be changed arbitrarily.
+ +/** + * fill_overflow() - Fill remaining options into file and sname fields + * @m: Message whose file/sname fields may be used for overflow + * + * Return: option 52 overload value: 0 if no overflow, + * DHCP_OVERLOAD_FILE for file, DHCP_OVERLOAD_SNAME for sname, + * or both OR'd together + */ +static int fill_overflow(struct msg *m) +{ + int file_off = 0, sname_off = 0, overload = 0; + int o; + + for (o = 0; o < 255; o++) {
You could use ARRAY_size(opts) instead of raw 255, yes?
* * Return: current size of options field */ -static int fill(struct msg *m) +static int fill(struct msg *m, int *overload) { + /* 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++) @@ -324,20 +369,25 @@ 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->o, OPT_MAX, 53, &offset)) - debug("DHCP: skipping option 53"); + fill_one(m->o, size, 53, &offset);
for (i = 0; i < opts[55].clen; i++) { o = opts[55].c[i]; if (opts[o].slen != -1) - 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].slen != -1 && !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); + } + + *overload = fill_overflow(m); + + if (*overload) { + m->o[offset++] = 52; + m->o[offset++] = 1; + m->o[offset++] = *overload; }
m->o[offset++] = 255; @@ -462,6 +512,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data) struct msg const *m; struct msg reply; unsigned int i; + int overload;
eh = IOV_REMOVE_HEADER(data, eh_storage); iph = IOV_PEEK_HEADER(data, iph_storage); @@ -613,7 +664,22 @@ int dhcp(const struct ctx *c, struct iov_tail *data) if (!c->no_dhcp_dns_search) opt_set_dns_search(c, sizeof(m->o));
- dlen = offsetof(struct msg, o) + fill(&reply); + for (i = 0; i < (unsigned int)c->custom_opts_count; i++) { + uint8_t code = c->custom_opts[i].code; + + opts[code].slen = c->custom_opts[i].len; + memcpy(opts[code].s, c->custom_opts[i].val, + c->custom_opts[i].len); + } + + dlen = offsetof(struct msg, o) + fill(&reply, &overload); + + /* Copy boot file name into the file field for legacy PXE clients, + * unless the file field is already used for option overload.
+ if (opts[o].slen == -1 || opts[o].sent) + continue; + fill_one(m->file, sizeof(m->file) - 1, o, &file_off); + } + + for (o = 0; o < 255; o++) { + if (opts[o].slen == -1 || opts[o].sent) + continue; + if (fill_one(m->sname, sizeof(m->sname) - 1, o, &sname_off)) + debug("DHCP: skipping option %i (overload full)", o); + } + + if (file_off) { + m->file[file_off] = 255; + overload |= DHCP_OVERLOAD_FILE; + } + + if (sname_off) { + m->sname[sname_off] = 255; + overload |= DHCP_OVERLOAD_SNAME; + } + + return overload; +} + /** - * fill() - Fill options in message + * 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
Given we do this, would it make more sense to use the slen field in preference to the file field for overloads? The logic above appears to do it the other way around,
are you referring to the sname field here? I am not sure how to use the slen field instead of the file field.
+ */ + if (!(overload & DHCP_OVERLOAD_FILE) && + opts[67].slen > 0 && (size_t)opts[67].slen < sizeof(reply.file)) + memcpy(&reply.file, opts[67].s, opts[67].slen + 1);
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
On Tue, 26 May 2026 18:01:10 +0530
Anshu Kumari
Add an RFC 2132 type lookup table mapping DHCP option codes to their expected value formats, and a dhcp_opt_parse() function that converts CLI string values into their binary wire representation.
Wire dhcp_opt_parse() into the --dhcp-opt handler so that values are validated and encoded at configuration time.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- 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 | 16 ++++++ dhcp.c | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dhcp.h | 17 +++++++ passt.h | 4 ++ 4 files changed, 185 insertions(+) diff --git a/conf.c b/conf.c index ae8ee26..3a5f45d 100644 --- a/conf.c +++ b/conf.c @@ -1266,6 +1266,7 @@ void conf(struct ctx *c, int argc, char **argv) char *end; uid_t uid; gid_t gid; + int len;
if (c->mode == MODE_PASTA) @@ -1482,7 +1483,14 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many DHCP options (max %d)", MAX_CUSTOM_DHCP_OPTS);
+ ret = dhcp_opt_parse(67, optarg, + c->custom_opts[c->custom_opts_count].val, + sizeof(c->custom_opts[0].val)); + if (ret < 0) + die("Invalid boot file value: %s", optarg); + c->custom_opts[c->custom_opts_count].code = 67; + c->custom_opts[c->custom_opts_count].len = ret; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", optarg)) @@ -1503,7 +1511,15 @@ void conf(struct ctx *c, int argc, char **argv) die("Too many --dhcp-opt entries (max %d)", MAX_CUSTOM_DHCP_OPTS);
+ len = dhcp_opt_parse(optcode, comma + 1, + c->custom_opts[c->custom_opts_count].val, + sizeof(c->custom_opts[0].val)); + if (len < 0) + die("Invalid value for DHCP option %lu: %s", + optcode, comma + 1); + c->custom_opts[c->custom_opts_count].code = optcode; + c->custom_opts[c->custom_opts_count].len = len; if (snprintf_check(c->custom_opts[c->custom_opts_count].str, sizeof(c->custom_opts[0].str), "%s", comma + 1)) diff --git a/dhcp.c b/dhcp.c index 1ff8cba..e1c95ad 100644 --- a/dhcp.c +++ b/dhcp.c @@ -33,6 +33,154 @@ #include "log.h" #include "dhcp.h"
+/** + * dhcp_opt_types - Maps option code to RFC 2132 value type, indexed by code + */ +static const enum dhcp_opt_type dhcp_opt_types[256] = { + [1] = DHCP_OPT_IPV4, /* Subnet Mask */ + [2] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* Boot File Size */ + [15] = DHCP_OPT_STR, /* Domain Name */ + [16] = DHCP_OPT_IPV4, /* Swap Server */ + [17] = DHCP_OPT_STR, /* Root Path */ + [19] = DHCP_OPT_INTEGER, /* IP Forwarding */ + [23] = DHCP_OPT_INTEGER, /* Default IP TTL */ + [26] = DHCP_OPT_INTEGER, /* Interface MTU */ + [28] = DHCP_OPT_IPV4, /* Broadcast Address */ + [33] = DHCP_OPT_IPV4_LIST, /* Static Routes */ + [37] = DHCP_OPT_INTEGER, /* TCP Default TTL */ + [38] = DHCP_OPT_INTEGER, /* 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_INTEGER, /* IP Address Lease Time */ + [53] = DHCP_OPT_INTEGER, /* DHCP Message Type */ + [54] = DHCP_OPT_IPV4, /* Server Identifier */ + [55] = DHCP_OPT_STR, /* Parameter Request List */ + [57] = DHCP_OPT_INTEGER, /* Max DHCP Message Size */ + [58] = DHCP_OPT_INTEGER, /* Renewal (T1) Time */ + [59] = DHCP_OPT_INTEGER, /* Rebinding (T2) Time */ + [60] = DHCP_OPT_STR, /* Vendor Class Identifier */ + [61] = DHCP_OPT_STR, /* Client Identifier */ + [66] = DHCP_OPT_STR, /* TFTP Server Name */ + [67] = DHCP_OPT_STR, /* Bootfile Name */ + [119] = DHCP_OPT_STR, /* Domain Search List */
Nit: a bit easier on eyes if they are *all* aligned (with one extra space), that is: [9] = DHCP_OPT_IPV4_LIST, /* LPR Server */ [...] [67] = DHCP_OPT_STR, /* Bootfile Name */ [...] [119] = DHCP_OPT_STR, /* Domain Search List */ [...]
+ [252] = DHCP_OPT_STR, /* WPAD URL */ +}; + +/** + * dhcp_opt_int_width - Integer width in bytes for INTEGER-typed options + */ +static const uint8_t dhcp_opt_int_width[256] = { + [2] = 4, /* Time Offset */ + [13] = 2, /* Boot File Size */ + [19] = 1, /* IP Forwarding */ + [23] = 1, /* Default IP TTL */ + [26] = 2, /* Interface MTU */ + [37] = 1, /* TCP Default TTL */ + [38] = 4, /* TCP Keepalive Interval */ + [51] = 4, /* IP Address Lease Time */ + [53] = 1, /* DHCP Message Type */ + [57] = 2, /* Max DHCP Message Size */ + [58] = 4, /* Renewal (T1) Time */ + [59] = 4, /* Rebinding (T2) Time */ +}; + +#define DHCP_OPT_PARSE_BUF 1024 + +/** + * dhcp_opt_parse() - Parse a DHCP option value + * @code: DHCP option code + * @str: DHCP Value string from command line + * @buf: Output buffer + * @buf_len: Size of output buffer + * + * Return: number of bytes written to @buf, or -1 on error + */ +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len) +{ + enum dhcp_opt_type type = dhcp_opt_types[code]; + char tmp[DHCP_OPT_PARSE_BUF]; + char *tok, *saveptr, *end; + struct in_addr addr; + unsigned long val; + unsigned int i; + uint8_t width; + size_t slen; + int len; + + switch (type) { + case DHCP_OPT_NONE: + die("Unsupported DHCP option: %u," + " see passt(1) for supported codes", code); + case DHCP_OPT_IPV4: + if (inet_pton(AF_INET, str, &addr) != 1) + return -1; + + if (buf_len < sizeof(addr)) + return -1; + + memcpy(buf, &addr, sizeof(addr)); + + return sizeof(addr); + case DHCP_OPT_IPV4_LIST: + len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, ",", &saveptr); tok; + tok = strtok_r(NULL, ",", &saveptr)) { + if (inet_pton(AF_INET, tok, &addr) != 1) + return -1; + + if (len + (int)sizeof(addr) > (int)buf_len) + return -1; + + memcpy(buf + len, &addr, sizeof(addr)); + len += sizeof(addr); + } + return len; + case DHCP_OPT_INTEGER: + width = dhcp_opt_int_width[code]; + val = strtoul(str, &end, 0); + + if (*end || buf_len < width) + return -1; + + if (width < 4 && val >= (1UL << (width * 8))) + return -1; + + for (i = 0; i < width; i++) + buf[i] = (val >> ((width - 1 - i) * 8)) & 0xff; + + return width; + case DHCP_OPT_STR: + slen = strlen(str); + + if (!slen || slen >= buf_len) + return -1; + + strncpy((char *)buf, str, buf_len); + + return slen; + } + + return -1; +} + /** * struct opt - DHCP option * @sent: Convenience flag, set while filling replies diff --git a/dhcp.h b/dhcp.h index cd50c99..3da571c 100644 --- a/dhcp.h +++ b/dhcp.h @@ -6,7 +6,24 @@ #ifndef DHCP_H #define DHCP_H
+/** + * 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
Missing tab between @DHCP_OPT_IPV4_LIST and Multiple: * @DHCP_OPT_IPV4_LIST: Multiple IPv4 addresses, comma-separated
+ * @DHCP_OPT_INTEGER: Unsigned integer (1, 2, or 4 bytes) + */ +enum dhcp_opt_type { + DHCP_OPT_NONE, + DHCP_OPT_STR, + DHCP_OPT_IPV4, + DHCP_OPT_IPV4_LIST, + DHCP_OPT_INTEGER, +}; + int dhcp(const struct ctx *c, struct iov_tail *data); void dhcp_init(void); +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len);
#endif /* DHCP_H */ diff --git a/passt.h b/passt.h index 3a0816f..751fee3 100644 --- a/passt.h +++ b/passt.h @@ -184,6 +184,8 @@ struct ip6_ctx { * @fqdn: Guest FQDN * @custom_opts: User-specified DHCP options from --dhcp-opt * @custom_opts.code: DHCP option code + * @custom_opts.len: Length of binary value in @val + * @custom_opts.val: Binary-encoded option value * @custom_opts.str: Original string value from command line * @custom_opts_count: Number of entries in @custom_opts * @ifi6: Template interface for IPv6, -1: none, 0: IPv6 disabled @@ -271,6 +273,8 @@ struct ctx {
struct { uint8_t code; + uint8_t len; + uint8_t val[255]; char str[256]; } custom_opts[MAX_CUSTOM_DHCP_OPTS]; int custom_opts_count;
-- Stefano
On Wed, 27 May 2026 14:03:44 +1000
David Gibson
On Tue, May 26, 2026 at 06:01:12PM +0530, Anshu Kumari wrote:
A user can enter lots of options in command-line which may not fit in existing buffer, So when the options field is full, overflow remaining DHCP options into the file and sname fields per RFC 2132 option 52.
Also, when the file field is not used for overload, copy the boot file URL there directly for legacy PXE clients.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- 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 | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/dhcp.c b/dhcp.c index e126063..a49a05a 100644 --- a/dhcp.c +++ b/dhcp.c @@ -306,14 +306,59 @@ static bool fill_one(uint8_t *buf, size_t size, int o, int *offset) return false; }
+#define DHCP_OVERLOAD_FILE 1 +#define DHCP_OVERLOAD_SNAME 2
IIUC, these values are from the RFC - might be worth referencing the relevant section in a comment to make it clear they can't be changed arbitrarily.
+ +/** + * fill_overflow() - Fill remaining options into file and sname fields + * @m: Message whose file/sname fields may be used for overflow + * + * Return: option 52 overload value: 0 if no overflow, + * DHCP_OVERLOAD_FILE for file, DHCP_OVERLOAD_SNAME for sname, + * or both OR'd together + */ +static int fill_overflow(struct msg *m) +{ + int file_off = 0, sname_off = 0, overload = 0; + int o; + + for (o = 0; o < 255; o++) {
You could use ARRAY_size(opts) instead of raw 255, yes?
+ if (opts[o].slen == -1 || opts[o].sent) + continue; + fill_one(m->file, sizeof(m->file) - 1, o, &file_off); + } + + for (o = 0; o < 255; o++) { + if (opts[o].slen == -1 || opts[o].sent) + continue; + if (fill_one(m->sname, sizeof(m->sname) - 1, o, &sname_off)) + debug("DHCP: skipping option %i (overload full)", o); + } + + if (file_off) { + m->file[file_off] = 255; + overload |= DHCP_OVERLOAD_FILE; + } + + if (sname_off) { + m->sname[sname_off] = 255; + overload |= DHCP_OVERLOAD_SNAME; + } + + return overload; +} + /** - * fill() - Fill options in message + * 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) * * Return: current size of options field */ -static int fill(struct msg *m) +static int fill(struct msg *m, int *overload) { + /* 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++) @@ -324,20 +369,25 @@ 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->o, OPT_MAX, 53, &offset)) - debug("DHCP: skipping option 53"); + fill_one(m->o, size, 53, &offset);
for (i = 0; i < opts[55].clen; i++) { o = opts[55].c[i]; if (opts[o].slen != -1) - 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].slen != -1 && !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); + } + + *overload = fill_overflow(m); + + if (*overload) { + m->o[offset++] = 52; + m->o[offset++] = 1; + m->o[offset++] = *overload; }
m->o[offset++] = 255; @@ -462,6 +512,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data) struct msg const *m; struct msg reply; unsigned int i; + int overload;
eh = IOV_REMOVE_HEADER(data, eh_storage); iph = IOV_PEEK_HEADER(data, iph_storage); @@ -613,7 +664,22 @@ int dhcp(const struct ctx *c, struct iov_tail *data) if (!c->no_dhcp_dns_search) opt_set_dns_search(c, sizeof(m->o));
- dlen = offsetof(struct msg, o) + fill(&reply); + for (i = 0; i < (unsigned int)c->custom_opts_count; i++) { + uint8_t code = c->custom_opts[i].code; + + opts[code].slen = c->custom_opts[i].len; + memcpy(opts[code].s, c->custom_opts[i].val, + c->custom_opts[i].len); + } + + dlen = offsetof(struct msg, o) + fill(&reply, &overload); + + /* Copy boot file name into the file field for legacy PXE clients, + * unless the file field is already used for option overload.
Given we do this, would it make more sense to use the slen field in preference to the file field for overloads? The logic above appears to do it the other way around,
Assuming s/slen/sname/: I didn't really think this through, but there might be a more fundamental advantage in doing this, rather than just the boot file consideration: each single option we add needs to fit entirely in one of the two areas. If we try sname (smaller) first, we *should* have strictly better chances than the other way around. Example: we need to fit (hypothetical) options 20 (20 bytes), option 30 (30 bytes) and option 120 (120 bytes). If we try 'file' first: - option 20 fits, option 30 fits, nothing else, move to 'sname' - nothing else fits: two options encoded If we try 'sname' first: - option 20 fits, option 30 fits, nothing else, move to 'file' - option 120 fits: three options encoded There must be some relatively simple proof or theorem for this but right now I can't think of any name / reference.
+ */ + if (!(overload & DHCP_OVERLOAD_FILE) && + opts[67].slen > 0 && (size_t)opts[67].slen < sizeof(reply.file)) + memcpy(&reply.file, opts[67].s, opts[67].slen + 1);
if (m->flags & FLAG_BROADCAST) dst = in4addr_broadcast; -- 2.54.0
-- Stefano
On Thu, May 28, 2026 at 09:01:53PM +0200, Stefano Brivio wrote:
On Wed, 27 May 2026 14:03:44 +1000 David Gibson
wrote: On Tue, May 26, 2026 at 06:01:12PM +0530, Anshu Kumari wrote:
A user can enter lots of options in command-line which may not fit in existing buffer, So when the options field is full, overflow remaining DHCP options into the file and sname fields per RFC 2132 option 52.
Also, when the file field is not used for overload, copy the boot file URL there directly for legacy PXE clients.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- 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 | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/dhcp.c b/dhcp.c index e126063..a49a05a 100644 --- a/dhcp.c +++ b/dhcp.c @@ -306,14 +306,59 @@ static bool fill_one(uint8_t *buf, size_t size, int o, int *offset) return false; }
+#define DHCP_OVERLOAD_FILE 1 +#define DHCP_OVERLOAD_SNAME 2
IIUC, these values are from the RFC - might be worth referencing the relevant section in a comment to make it clear they can't be changed arbitrarily.
+ +/** + * fill_overflow() - Fill remaining options into file and sname fields + * @m: Message whose file/sname fields may be used for overflow + * + * Return: option 52 overload value: 0 if no overflow, + * DHCP_OVERLOAD_FILE for file, DHCP_OVERLOAD_SNAME for sname, + * or both OR'd together + */ +static int fill_overflow(struct msg *m) +{ + int file_off = 0, sname_off = 0, overload = 0; + int o; + + for (o = 0; o < 255; o++) {
You could use ARRAY_size(opts) instead of raw 255, yes?
+ if (opts[o].slen == -1 || opts[o].sent) + continue; + fill_one(m->file, sizeof(m->file) - 1, o, &file_off); + } + + for (o = 0; o < 255; o++) { + if (opts[o].slen == -1 || opts[o].sent) + continue; + if (fill_one(m->sname, sizeof(m->sname) - 1, o, &sname_off)) + debug("DHCP: skipping option %i (overload full)", o); + } + + if (file_off) { + m->file[file_off] = 255; + overload |= DHCP_OVERLOAD_FILE; + } + + if (sname_off) { + m->sname[sname_off] = 255; + overload |= DHCP_OVERLOAD_SNAME; + } + + return overload; +} + /** - * fill() - Fill options in message + * 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) * * Return: current size of options field */ -static int fill(struct msg *m) +static int fill(struct msg *m, int *overload) { + /* 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++) @@ -324,20 +369,25 @@ 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->o, OPT_MAX, 53, &offset)) - debug("DHCP: skipping option 53"); + fill_one(m->o, size, 53, &offset);
for (i = 0; i < opts[55].clen; i++) { o = opts[55].c[i]; if (opts[o].slen != -1) - 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].slen != -1 && !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); + } + + *overload = fill_overflow(m); + + if (*overload) { + m->o[offset++] = 52; + m->o[offset++] = 1; + m->o[offset++] = *overload; }
m->o[offset++] = 255; @@ -462,6 +512,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data) struct msg const *m; struct msg reply; unsigned int i; + int overload;
eh = IOV_REMOVE_HEADER(data, eh_storage); iph = IOV_PEEK_HEADER(data, iph_storage); @@ -613,7 +664,22 @@ int dhcp(const struct ctx *c, struct iov_tail *data) if (!c->no_dhcp_dns_search) opt_set_dns_search(c, sizeof(m->o));
- dlen = offsetof(struct msg, o) + fill(&reply); + for (i = 0; i < (unsigned int)c->custom_opts_count; i++) { + uint8_t code = c->custom_opts[i].code; + + opts[code].slen = c->custom_opts[i].len; + memcpy(opts[code].s, c->custom_opts[i].val, + c->custom_opts[i].len); + } + + dlen = offsetof(struct msg, o) + fill(&reply, &overload); + + /* Copy boot file name into the file field for legacy PXE clients, + * unless the file field is already used for option overload.
Given we do this, would it make more sense to use the slen field in preference to the file field for overloads? The logic above appears to do it the other way around,
Assuming s/slen/sname/: I didn't really think this through, but there might be a more fundamental advantage in doing this, rather than just the boot file consideration: each single option we add needs to fit entirely in one of the two areas.
If we try sname (smaller) first, we *should* have strictly better chances than the other way around.
Example: we need to fit (hypothetical) options 20 (20 bytes), option 30 (30 bytes) and option 120 (120 bytes). If we try 'file' first:
- option 20 fits, option 30 fits, nothing else, move to 'sname' - nothing else fits: two options encoded
If we try 'sname' first:
- option 20 fits, option 30 fits, nothing else, move to 'file' - option 120 fits: three options encoded
There must be some relatively simple proof or theorem for this but right now I can't think of any name / reference.
Alas, it's not strictly better. If we were packing the options in reverse order (120 byte, then 30 byte, then 20 byte), we get 3 encoded if we use file first, 1 encoded if we use sname first. To do strictly better we'd need to consider separately for each option which field we pack it into. That's probably more trouble than it's worth. Even that approach isn't theoretically optimal. This is (almost[1]) the multiple subset sum problem for m=2 [0]. That's closely related to the knapsack problem, and likewise NP-complete. It's probably a small enough example that it's tractable to solve, but that's *definitely* more trouble than it's worth. [0] https://en.wikipedia.org/wiki/Multiple_subset_sum [1] <huge tangent>. In looking at this I accidentally went down a rabbit hole of a bunch of related optimization problems: the knapsack problem, multiple subset sum problem, bin packing problem. Ours isn't _quite_ the same as any of them, because we have a slightly different goal: we just want to see if there's any way we can fit everything in our 2 bins, not necessarily exactly fill bins or minimise number of bins. I suspect, but I'm not sure, that this might allow it to be reduced to two instances of the (single) subset sum problem, each of which can be solved in O(#options * size of field we're packing)[2]. So maybe not actually NP-complete, still way more trouble than it's worth. [2] https://www.geeksforgeeks.org/dsa/subset-sum-problem-dp-25/ I still think "fill sname first" is the preferable option in practice. As well as leaving the file field free for the boot file, I suspect (but I'm not certain) it will result in better packing more often than not.
+ */ + if (!(overload & DHCP_OVERLOAD_FILE) && + opts[67].slen > 0 && (size_t)opts[67].slen < sizeof(reply.file)) + memcpy(&reply.file, opts[67].s, opts[67].slen + 1);
if (m->flags & FLAG_BROADCAST) dst = in4addr_broadcast; -- 2.54.0
-- Stefano
-- 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
On Fri, 29 May 2026 15:49:49 +1000
David Gibson
On Thu, May 28, 2026 at 09:01:53PM +0200, Stefano Brivio wrote:
On Wed, 27 May 2026 14:03:44 +1000 David Gibson
wrote: On Tue, May 26, 2026 at 06:01:12PM +0530, Anshu Kumari wrote:
A user can enter lots of options in command-line which may not fit in existing buffer, So when the options field is full, overflow remaining DHCP options into the file and sname fields per RFC 2132 option 52.
Also, when the file field is not used for overload, copy the boot file URL there directly for legacy PXE clients.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- 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 | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/dhcp.c b/dhcp.c index e126063..a49a05a 100644 --- a/dhcp.c +++ b/dhcp.c @@ -306,14 +306,59 @@ static bool fill_one(uint8_t *buf, size_t size, int o, int *offset) return false; }
+#define DHCP_OVERLOAD_FILE 1 +#define DHCP_OVERLOAD_SNAME 2
IIUC, these values are from the RFC - might be worth referencing the relevant section in a comment to make it clear they can't be changed arbitrarily.
+ +/** + * fill_overflow() - Fill remaining options into file and sname fields + * @m: Message whose file/sname fields may be used for overflow + * + * Return: option 52 overload value: 0 if no overflow, + * DHCP_OVERLOAD_FILE for file, DHCP_OVERLOAD_SNAME for sname, + * or both OR'd together + */ +static int fill_overflow(struct msg *m) +{ + int file_off = 0, sname_off = 0, overload = 0; + int o; + + for (o = 0; o < 255; o++) {
You could use ARRAY_size(opts) instead of raw 255, yes?
+ if (opts[o].slen == -1 || opts[o].sent) + continue; + fill_one(m->file, sizeof(m->file) - 1, o, &file_off); + } + + for (o = 0; o < 255; o++) { + if (opts[o].slen == -1 || opts[o].sent) + continue; + if (fill_one(m->sname, sizeof(m->sname) - 1, o, &sname_off)) + debug("DHCP: skipping option %i (overload full)", o); + } + + if (file_off) { + m->file[file_off] = 255; + overload |= DHCP_OVERLOAD_FILE; + } + + if (sname_off) { + m->sname[sname_off] = 255; + overload |= DHCP_OVERLOAD_SNAME; + } + + return overload; +} + /** - * fill() - Fill options in message + * 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) * * Return: current size of options field */ -static int fill(struct msg *m) +static int fill(struct msg *m, int *overload) { + /* 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++) @@ -324,20 +369,25 @@ 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->o, OPT_MAX, 53, &offset)) - debug("DHCP: skipping option 53"); + fill_one(m->o, size, 53, &offset);
for (i = 0; i < opts[55].clen; i++) { o = opts[55].c[i]; if (opts[o].slen != -1) - 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].slen != -1 && !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); + } + + *overload = fill_overflow(m); + + if (*overload) { + m->o[offset++] = 52; + m->o[offset++] = 1; + m->o[offset++] = *overload; }
m->o[offset++] = 255; @@ -462,6 +512,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data) struct msg const *m; struct msg reply; unsigned int i; + int overload;
eh = IOV_REMOVE_HEADER(data, eh_storage); iph = IOV_PEEK_HEADER(data, iph_storage); @@ -613,7 +664,22 @@ int dhcp(const struct ctx *c, struct iov_tail *data) if (!c->no_dhcp_dns_search) opt_set_dns_search(c, sizeof(m->o));
- dlen = offsetof(struct msg, o) + fill(&reply); + for (i = 0; i < (unsigned int)c->custom_opts_count; i++) { + uint8_t code = c->custom_opts[i].code; + + opts[code].slen = c->custom_opts[i].len; + memcpy(opts[code].s, c->custom_opts[i].val, + c->custom_opts[i].len); + } + + dlen = offsetof(struct msg, o) + fill(&reply, &overload); + + /* Copy boot file name into the file field for legacy PXE clients, + * unless the file field is already used for option overload.
Given we do this, would it make more sense to use the slen field in preference to the file field for overloads? The logic above appears to do it the other way around,
Assuming s/slen/sname/: I didn't really think this through, but there might be a more fundamental advantage in doing this, rather than just the boot file consideration: each single option we add needs to fit entirely in one of the two areas.
If we try sname (smaller) first, we *should* have strictly better chances than the other way around.
Example: we need to fit (hypothetical) options 20 (20 bytes), option 30 (30 bytes) and option 120 (120 bytes). If we try 'file' first:
- option 20 fits, option 30 fits, nothing else, move to 'sname' - nothing else fits: two options encoded
If we try 'sname' first:
- option 20 fits, option 30 fits, nothing else, move to 'file' - option 120 fits: three options encoded
There must be some relatively simple proof or theorem for this but right now I can't think of any name / reference.
Alas, it's not strictly better. If we were packing the options in reverse order (120 byte, then 30 byte, then 20 byte), we get 3 encoded if we use file first, 1 encoded if we use sname first.
Hmm, no, why? Note that Anshu's fill_overflow() loops on all (remaining) options for both fields, so in that case you would have: - sname first: fail to insert 120-byte option in sname, insert 30-byte option in sname, insert 20-byte option in sname, move to file, insert 120-byte option in file - file first: insert 120-byte option in file, fail to insert 30-byte option in file, fail to insert 20-byte option in file, move to sname, insert 30-byte option in sname, insert 20-byte option in sname
To do strictly better we'd need to consider separately for each option which field we pack it into. That's probably more trouble than it's worth.
It's already done in some sense. My hypothesis is that, by trying the smallest bin first, the greedy algorithm should already be optimal. I'm not quite sure but it intuitively makes sense for me.
Even that approach isn't theoretically optimal. This is (almost[1]) the multiple subset sum problem for m=2 [0].
Right! Thanks, that's the one I meant.
That's closely related to the knapsack problem, and likewise NP-complete. It's probably a small enough example that it's tractable to solve, but that's *definitely* more trouble than it's worth.
[0] https://en.wikipedia.org/wiki/Multiple_subset_sum
[1] <huge tangent>. In looking at this I accidentally went down a rabbit hole of a bunch of related optimization problems: the knapsack problem, multiple subset sum problem, bin packing problem. Ours isn't _quite_ the same as any of them, because we have a slightly different goal: we just want to see if there's any way we can fit everything in our 2 bins, not necessarily exactly fill bins or minimise number of bins.
To be picky, we would also like to minimise the number of bins (but that could be done in a trivial way when possible), because we might need to use 'file' as such.
I suspect, but I'm not sure, that this might allow it to be reduced to two instances of the (single) subset sum problem, each of which can be solved in O(#options * size of field we're packing)[2]. So maybe not actually NP-complete, still way more trouble than it's worth.
I'm under the impression that this could still qualify as the max-sum variant of the multiple subset sum problem (assuming it helps), because trying to fit all the options is also achieved by maximising the sum (even though it's a somewhat different goal).
[2] https://www.geeksforgeeks.org/dsa/subset-sum-problem-dp-25/
I still think "fill sname first" is the preferable option in practice. As well as leaving the file field free for the boot file, I suspect (but I'm not certain) it will result in better packing more often than not.
+ */ + if (!(overload & DHCP_OVERLOAD_FILE) && + opts[67].slen > 0 && (size_t)opts[67].slen < sizeof(reply.file)) + memcpy(&reply.file, opts[67].s, opts[67].slen + 1);
if (m->flags & FLAG_BROADCAST) dst = in4addr_broadcast;
-- Stefano
participants (4)
-
Anshu Kumari
-
David Gibson
-
Laurent Vivier
-
Stefano Brivio