[PATCH 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. *** BLURB HERE *** 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 | 54 ++++++++- dhcp.c | 336 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- dhcp.h | 15 +++ passt.1 | 44 ++++++++ passt.h | 11 ++ 5 files changed, 438 insertions(+), 22 deletions(-) -- 2.54.0
Introduce the --dhcp-opt flag that allows setting arbitrary DHCP
options from command-line in the form of [Option CODE,VALUE].
This patch adds the option storage in struct ctx and CLI parsing;
the type-aware value parser and DHCP reply injection follow
in subsequent patches.
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari
Introduce the --dhcp-boot flag that sets the boot file URL for
network boot specially for ipxe. This patch adds the option
storage and CLI parsing.
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: 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
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 Mon, May 18, 2026 at 06:49:56PM +0530, Anshu Kumari wrote:
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.
*** BLURB HERE ***
Nit: remember to remove this boilerplate that git-publish inserts :) -- 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 Mon, May 18, 2026 at 06:49:57PM +0530, Anshu Kumari wrote:
Introduce the --dhcp-opt flag that allows setting arbitrary DHCP options from command-line in the form of [Option CODE,VALUE]. This patch adds the option storage in struct ctx and CLI parsing; the type-aware value parser and DHCP reply injection follow in subsequent patches.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- conf.c | 36 +++++++++++++++++++++++++++++++++++- passt.h | 10 ++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/conf.c b/conf.c index 029b9c7..2624e58 100644 --- a/conf.c +++ b/conf.c @@ -47,6 +47,7 @@ #include "lineread.h" #include "isolation.h" #include "log.h" +#include "dhcp.h" #include "vhost_user.h" #include "epoll_ctl.h" #include "conf.h" @@ -616,7 +617,8 @@ static void usage(const char *name, FILE *f, int status) " -S, --search LIST Space-separated list, search domains\n" " a single, empty option disables the DNS search list\n" " -H, --hostname NAME Hostname to configure client with\n" - " --fqdn NAME FQDN to configure client with\n"); + " --fqdn NAME FQDN to configure client with\n" + " --dhcp-opt CODE,VAL Set DHCP option by code\n"); if (strstr(name, "pasta")) FPRINTF(f, " default: don't use any search list\n"); else @@ -844,6 +846,10 @@ static void conf_print(const struct ctx *c) info(" router: %s", inet_ntop(AF_INET, &c->ip4.guest_gw, buf, sizeof(buf))); + for (i = 0; i < c->custom_opts_count; i++) + info(" option %u: %s", + c->custom_opts[i].code, + c->custom_opts[i].str); }
for (i = 0; i < ARRAY_SIZE(c->ip4.dns); i++) { @@ -1233,6 +1239,7 @@ void conf(struct ctx *c, int argc, char **argv) {"migrate-no-linger", no_argument, NULL, 30 }, {"stats", required_argument, NULL, 31 }, {"conf-path", required_argument, NULL, 'c' }, + {"dhcp-opt", required_argument, NULL, 33 }, { 0 }, }; const char *optstring = "+dqfel:hs:c:F:I:p:P:m:a:n:M:g:i:o:D:S:H:461t:u:T:U:"; @@ -1465,6 +1472,33 @@ void conf(struct ctx *c, int argc, char **argv) die("Can't display statistics if not running in foreground"); c->stats = strtol(optarg, NULL, 0); break; + case 33: { + unsigned long code; + const char *comma; + char *end; + + comma = strchr(optarg, ','); + if (!comma) + die("--dhcp-opt requires Option CODE,VALUE format"); + + code = strtoul(optarg, &end, 0); + if (end != comma || code < 1 || code > 254) + die("DHCP option code must be 1-254: %s", + optarg); + + if (c->custom_opts_count >= MAX_CUSTOM_DHCP_OPTS) + die("Too many --dhcp-opt entries (max %d)", + MAX_CUSTOM_DHCP_OPTS); + + c->custom_opts[c->custom_opts_count].code = code; + if (snprintf_check(c->custom_opts[c->custom_opts_count].str, + sizeof(c->custom_opts[0].str), + "%s", comma + 1)) + die("DHCP option value too long: %s", + comma + 1); + c->custom_opts_count++; + break; + } case 'd': c->debug = 1; c->quiet = 0; diff --git a/passt.h b/passt.h index 1726965..acb57dd 100644 --- a/passt.h +++ b/passt.h @@ -263,6 +263,16 @@ struct ctx { char hostname[PASST_MAXDNAME]; char fqdn[PASST_MAXDNAME];
+#define MAX_CUSTOM_DHCP_OPTS 32 + + struct { + uint8_t code; + uint8_t len; + uint8_t val[255];
The @len and @val fields are not used so far. I'm assuming they are later in the series, but in that case it's generally preferable to add those fields in the patches that use them. Possible more comments there.
+ char str[256]; + } custom_opts[MAX_CUSTOM_DHCP_OPTS]; + int custom_opts_count; + int ifi6; struct ip6_ctx ip6;
-- 2.54.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Mon, May 18, 2026 at 06:49:59PM +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
--- conf.c | 9 +++ dhcp.c | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dhcp.h | 15 ++++ 3 files changed, 251 insertions(+) diff --git a/conf.c b/conf.c index 61a393f..3ec10ac 100644 --- a/conf.c +++ b/conf.c @@ -1485,6 +1485,7 @@ void conf(struct ctx *c, int argc, char **argv) unsigned long code; const char *comma; char *end; + int len;
comma = strchr(optarg, ','); if (!comma) @@ -1499,7 +1500,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(code, 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", + code, comma + 1); + c->custom_opts[c->custom_opts_count].code = code; + c->custom_opts[c->custom_opts_count].len = len;
I don't love the fact that .val and .str permanently store redundant information. Initially I was going to suggest that you delay dhcp_opt_parse() until you're actually processing a DHCP message. However, that's also not great at that point it awkward to handle parse errors. Not sure how best to tackle this for now.
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..9220516 100644 --- a/dhcp.c +++ b/dhcp.c @@ -33,6 +33,233 @@ #include "log.h" #include "dhcp.h"
+/** + * struct dhcp_opt_type_entry - Maps option code to RFC 2132 value type + * @code: DHCP option code + * @type: Expected value format + */ +static const struct dhcp_opt_type_entry { + uint8_t code; + enum dhcp_opt_type type; +} dhcp_opt_types[] = { + { 1, DHCP_OPT_IPV4 }, /* Subnet Mask */ + { 2, DHCP_OPT_UINT32 }, /* Time Offset */ + { 3, DHCP_OPT_IPV4_LIST }, /* Router */ + { 4, DHCP_OPT_IPV4_LIST }, /* Time Server */ + { 5, DHCP_OPT_IPV4_LIST }, /* Name Server */ + + { 6, DHCP_OPT_IPV4_LIST }, /* Domain Name Server */ + { 7, DHCP_OPT_IPV4_LIST }, /* Log Server */ + { 8, DHCP_OPT_IPV4_LIST }, /* Cookie Server */ + { 9, DHCP_OPT_IPV4_LIST }, /* LPR Server */ + { 10, DHCP_OPT_IPV4_LIST }, /* Impress Server */ + + { 11, DHCP_OPT_IPV4_LIST }, /* Resource Location Server */ + { 12, DHCP_OPT_STR }, /* Host Name */ + { 13, DHCP_OPT_UINT16 }, /* Boot File Size */ + { 15, DHCP_OPT_STR }, /* Domain Name */ + { 16, DHCP_OPT_IPV4 }, /* Swap Server */ + + { 17, DHCP_OPT_STR }, /* Root Path */ + { 19, DHCP_OPT_UINT8 }, /* IP Forwarding */ + { 23, DHCP_OPT_UINT8 }, /* Default IP TTL */ + { 26, DHCP_OPT_UINT16 }, /* Interface MTU */ + { 28, DHCP_OPT_IPV4 }, /* Broadcast Address */ + + { 33, DHCP_OPT_IPV4_LIST }, /* Static Routes (dest+router pairs) */ + { 37, DHCP_OPT_UINT8 }, /* TCP Default TTL */ + { 38, DHCP_OPT_UINT32 }, /* TCP Keepalive Interval */ + { 40, DHCP_OPT_STR }, /* NIS Domain Name */ + { 41, DHCP_OPT_IPV4_LIST }, /* NIS Servers */ + + { 42, DHCP_OPT_IPV4_LIST }, /* NTP Servers */ + { 44, DHCP_OPT_IPV4_LIST }, /* NetBIOS Name Server */ + { 50, DHCP_OPT_IPV4 }, /* Requested IP Address */ + { 51, DHCP_OPT_UINT32 }, /* IP Address Lease Time */ + { 53, DHCP_OPT_UINT8 }, /* DHCP Message Type */ + + { 54, DHCP_OPT_IPV4 }, /* Server Identifier */ + { 57, DHCP_OPT_UINT16 }, /* Max DHCP Message Size */ + { 58, DHCP_OPT_UINT32 }, /* Renewal (T1) Time */ + { 59, DHCP_OPT_UINT32 }, /* Rebinding (T2) Time */ + { 60, DHCP_OPT_STR }, /* Vendor Class Identifier */ + + { 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 (RFC 3397) */ + { 121, DHCP_OPT_ROUTES }, /* Classless Static Routes */ + + { 252, DHCP_OPT_STR }, /* WPAD URL */
At least initially, I'd suggest we don't permit the user to set options which passt already manages internally (1, 3, 51, 53, 54, 55, and 199 from a quick scan).
+}; + +/** + * dhcp_opt_type_lookup() - Look up the value type for a DHCP option code + * @code: DHCP option code + * + * Return: type from table + */ +static enum dhcp_opt_type dhcp_opt_type_lookup(uint8_t code) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(dhcp_opt_types); i++) { + if (dhcp_opt_types[i].code == code) + return dhcp_opt_types[i].type; + } + + return DHCP_OPT_NONE; +} + +/** + * dhcp_opt_parse() - Parse a DHCP option value + * @code: DHCP option code (determines value type via lookup table) + * @str: Value string from command line + * @buf: Output buffer for binary value + * @buf_len: Size of output buffer + * + * Return: number of bytes written to @buf, or -1 on error + */ +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len) +{ + enum dhcp_opt_type type = dhcp_opt_type_lookup(code); + + switch (type) { + case DHCP_OPT_NONE: { + die("Unsupported DHCP option: %u," + " see passt(1) for supported codes", + code); + } + case DHCP_OPT_IPV4: { + struct in_addr addr; + + 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: { + char *tok, *saveptr; + char tmp[1024];
Bare 1024 constant isn't ideal.
+ int len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, " ", &saveptr); tok; + tok = strtok_r(NULL, " ", &saveptr)) {
Apparently strtok_r() is specified by POSIX, so it should be ok. Might be worth double checking musl has it, though. " " seems an odd choice of delimiter - it's pretty awkward to use on the command line. I guess "," is also problematic since it can be found within various of the of the option values.
+ struct in_addr addr; + + 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_UINT8: { + unsigned long val; + char *end; + + val = strtoul(str, &end, 0); + if (*end || val > 255 || buf_len < 1) + return -1; + buf[0] = val; + return 1; + } + case DHCP_OPT_UINT16: { + unsigned long val; + char *end; + + val = strtoul(str, &end, 0); + if (*end || val > 65535 || buf_len < 2) + return -1; + buf[0] = (val >> 8) & 0xff; + buf[1] = val & 0xff; + return 2; + } + case DHCP_OPT_UINT32: { + unsigned long val; + char *end; + + val = strtoul(str, &end, 0); + if (*end || buf_len < 4) + return -1; + buf[0] = (val >> 24) & 0xff; + buf[1] = (val >> 16) & 0xff; + buf[2] = (val >> 8) & 0xff; + buf[3] = val & 0xff; + return 4; + }
It might be possible to simplify the sections above using a single DHCP_OPT_INTEGER type, plus an extra field giving the integer width.
+ case DHCP_OPT_ROUTES: {
I'd consider excluding this one for now. For one thing it's rather a lot of code. But more importantly with the multi-address and route monitoring changes we're considering, there's a fairly high chance we might want to manage this one ourselves.
+ /* RFC 3442: "CIDR/mask,gateway" entries, space-separated + * Encodes as: mask-width + significant-octets + router + * e.g. "192.168.1.0/24,10.0.0.1 0.0.0.0/0,10.0.0.1" + */ + char *tok, *saveptr; + char tmp[1024]; + int len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, " ", &saveptr); tok; + tok = strtok_r(NULL, " ", &saveptr)) { + struct in_addr dest, gw; + char *slash, *comma; + unsigned long mask; + int sig_octets; + + slash = strchr(tok, '/'); + if (!slash) + return -1; + *slash = '\0'; + + if (inet_pton(AF_INET, tok, &dest) != 1) + return -1; + + comma = strchr(slash + 1, ','); + if (!comma) + return -1; + *comma = '\0'; + + mask = strtoul(slash + 1, NULL, 10); + if (mask > 32) + return -1; + + if (inet_pton(AF_INET, comma + 1, &gw) != 1) + return -1; + + sig_octets = (mask + 7) / 8; + + if (len + 1 + sig_octets + 4 > (int)buf_len) + return -1; + + buf[len++] = mask; + memcpy(buf + len, &dest, sig_octets); + len += sig_octets; + memcpy(buf + len, &gw, 4); + len += 4; + } + return len; + } + case DHCP_OPT_STR: { + size_t len = strlen(str); + + if (!len || len >= buf_len) + return -1; + strncpy((char *)buf, str, buf_len); + return len; + } + } + + return -1; +} + /** * struct opt - DHCP option * @sent: Convenience flag, set while filling replies diff --git a/dhcp.h b/dhcp.h index cd50c99..01b2290 100644 --- a/dhcp.h +++ b/dhcp.h @@ -6,7 +6,22 @@ #ifndef DHCP_H #define DHCP_H
+/** + * enum dhcp_opt_type - DHCP option value types per RFC 2132 + */ +enum dhcp_opt_type { + DHCP_OPT_NONE, + DHCP_OPT_STR, + DHCP_OPT_IPV4, + DHCP_OPT_IPV4_LIST, + DHCP_OPT_UINT8, + DHCP_OPT_UINT16, + DHCP_OPT_UINT32, + DHCP_OPT_ROUTES, +}; + 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 */ -- 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 Mon, May 18, 2026 at 06:49:58PM +0530, Anshu Kumari wrote:
Introduce the --dhcp-boot flag that sets the boot file URL for network boot specially for ipxe. This patch adds the option storage and CLI parsing.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- conf.c | 9 +++++++++ passt.h | 1 + 2 files changed, 10 insertions(+) diff --git a/conf.c b/conf.c index 2624e58..61a393f 100644 --- a/conf.c +++ b/conf.c @@ -618,6 +618,7 @@ static void usage(const char *name, FILE *f, int status) " a single, empty option disables the DNS search list\n" " -H, --hostname NAME Hostname to configure client with\n" " --fqdn NAME FQDN to configure client with\n" + " --dhcp-boot URL Boot file URL for network boot\n" " --dhcp-opt CODE,VAL Set DHCP option by code\n"); if (strstr(name, "pasta")) FPRINTF(f, " default: don't use any search list\n"); @@ -846,6 +847,8 @@ static void conf_print(const struct ctx *c) info(" router: %s", inet_ntop(AF_INET, &c->ip4.guest_gw, buf, sizeof(buf))); + if (c->dhcp_boot[0]) + info(" boot file: %s", c->dhcp_boot); for (i = 0; i < c->custom_opts_count; i++) info(" option %u: %s", c->custom_opts[i].code, @@ -1239,6 +1242,7 @@ void conf(struct ctx *c, int argc, char **argv) {"migrate-no-linger", no_argument, NULL, 30 }, {"stats", required_argument, NULL, 31 }, {"conf-path", required_argument, NULL, 'c' }, + {"dhcp-boot", required_argument, NULL, 32 }, {"dhcp-opt", required_argument, NULL, 33 }, { 0 }, }; @@ -1472,6 +1476,11 @@ void conf(struct ctx *c, int argc, char **argv) die("Can't display statistics if not running in foreground"); c->stats = strtol(optarg, NULL, 0); break; + case 32: + if (snprintf_check(c->dhcp_boot, sizeof(c->dhcp_boot), + "%s", optarg)) + die("Boot file name too long: %s", optarg); + break;
IIUC, --dhcp-boot foo is equivalent to --dhcp-opt 67,foo Is that right? If so, it would be preferable to make that more explicit in the code, using a single way of storing the options and a common "Add dhcp option helper" that's called from both the --dhcp-boot and --dhcp-opt handling.
case 33: { unsigned long code; const char *comma; diff --git a/passt.h b/passt.h index acb57dd..2354a0f 100644 --- a/passt.h +++ b/passt.h @@ -262,6 +262,7 @@ struct ctx {
char hostname[PASST_MAXDNAME]; char fqdn[PASST_MAXDNAME]; + char dhcp_boot[PATH_MAX];
#define MAX_CUSTOM_DHCP_OPTS 32
-- 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 Mon, May 18, 2026 at 06:50:01PM +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
--- dhcp.c | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/dhcp.c b/dhcp.c index a966c34..fde5d57 100644 --- a/dhcp.c +++ b/dhcp.c @@ -386,13 +386,53 @@ static bool fill_one(uint8_t *buf, size_t cap, int o, int *offset) }
/** - * fill() - Fill options in message + * 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, 1 for file, + * 2 for sname, 3 for both + */ +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 |= 1;
Some #defined constants for the overload bits would probably be a good idea.
+ } + + if (sname_off) { + m->sname[sname_off] = 255; + overload |= 2; + } + + return overload; +} + +/** + * 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) { + size_t cap = OPT_MAX - 3; int i, o, offset = 0;
for (o = 0; o < 255; o++) @@ -403,20 +443,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, cap, 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, cap, 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, cap, o, &offset); + } + + *overload = fill_overflow(m); + + if (*overload) { + m->o[offset++] = 52; + m->o[offset++] = 1; + m->o[offset++] = *overload;
If we reach this path then we've near-filled the normal option area. What guarantees we'll have space for option 52 itself?
}
m->o[offset++] = 255; @@ -541,6 +586,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); @@ -690,9 +736,31 @@ 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(c, sizeof(m->o) + sizeof(m->file) + + sizeof(m->sname));
Does passing the combined length here actually make sense? IIUC each single option still needs to fit within one of the buffer areas.
+ + if (c->dhcp_boot[0]) { + size_t boot_len = strlen(c->dhcp_boot); + + if (boot_len <= sizeof(opts[67].s)) { + opts[67].slen = boot_len; + memcpy(opts[67].s, c->dhcp_boot, boot_len); + } + } + + 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);
- dlen = offsetof(struct msg, o) + fill(&reply); + if (!(overload & 1) && + c->dhcp_boot[0] && strlen(c->dhcp_boot) < sizeof(reply.file)) + memcpy(&reply.file, c->dhcp_boot, strlen(c->dhcp_boot) + 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 Mon, May 18, 2026 at 06:50:00PM +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
--- dhcp.c | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-)
diff --git a/dhcp.c b/dhcp.c index 9220516..a966c34 100644 --- a/dhcp.c +++ b/dhcp.c @@ -358,28 +358,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 + * @cap: Usable capacity of @buf (excluding end marker)
Nit: I'd suggest "size" or "len". "cap" more commonly means "capability" in the codebase, rather than "capacity".
* @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 cap, 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 > cap) 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; @@ -404,19 +403,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, 19 May 2026 15:30:41 +1000
David Gibson
On Mon, May 18, 2026 at 06:49:56PM +0530, Anshu Kumari wrote:
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.
*** BLURB HERE ***
Nit: remember to remove this boilerplate that git-publish inserts :)
No, no, that's from git format-patch, I swear (as somebody who doesn't use git-publish). Another detail, while at it: for some reason git send-email picked the "chain-reply" style, that is, if you look at this in a threaded email client, patch #1 is a reply to the cover letter (fine, expected) but patch #2 is a reply to patch #1 (not expected), and so on. That's the --chain-reply-to option for git send-email. You should disable that (for example in gitconfig in case you have it on there) and just use --cover-letter. -- Stefano
On Mon, 18 May 2026 18:49:57 +0530
Anshu Kumari
Introduce the --dhcp-opt flag that allows setting arbitrary DHCP options from command-line in the form of [Option CODE,VALUE]. This patch adds the option storage in struct ctx and CLI parsing; the type-aware value parser and DHCP reply injection follow in subsequent patches.
Link: https://bugs.passt.top/show_bug.cgi?id=192 Signed-off-by: Anshu Kumari
--- conf.c | 36 +++++++++++++++++++++++++++++++++++- passt.h | 10 ++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/conf.c b/conf.c index 029b9c7..2624e58 100644 --- a/conf.c +++ b/conf.c @@ -47,6 +47,7 @@ #include "lineread.h" #include "isolation.h" #include "log.h" +#include "dhcp.h" #include "vhost_user.h" #include "epoll_ctl.h" #include "conf.h" @@ -616,7 +617,8 @@ static void usage(const char *name, FILE *f, int status) " -S, --search LIST Space-separated list, search domains\n" " a single, empty option disables the DNS search list\n" " -H, --hostname NAME Hostname to configure client with\n" - " --fqdn NAME FQDN to configure client with\n"); + " --fqdn NAME FQDN to configure client with\n" + " --dhcp-opt CODE,VAL Set DHCP option by code\n"); if (strstr(name, "pasta")) FPRINTF(f, " default: don't use any search list\n"); else @@ -844,6 +846,10 @@ static void conf_print(const struct ctx *c) info(" router: %s", inet_ntop(AF_INET, &c->ip4.guest_gw, buf, sizeof(buf))); + for (i = 0; i < c->custom_opts_count; i++) + info(" option %u: %s", + c->custom_opts[i].code, + c->custom_opts[i].str); }
for (i = 0; i < ARRAY_SIZE(c->ip4.dns); i++) { @@ -1233,6 +1239,7 @@ void conf(struct ctx *c, int argc, char **argv) {"migrate-no-linger", no_argument, NULL, 30 }, {"stats", required_argument, NULL, 31 }, {"conf-path", required_argument, NULL, 'c' }, + {"dhcp-opt", required_argument, NULL, 33 }, { 0 }, }; const char *optstring = "+dqfel:hs:c:F:I:p:P:m:a:n:M:g:i:o:D:S:H:461t:u:T:U:"; @@ -1465,6 +1472,33 @@ void conf(struct ctx *c, int argc, char **argv) die("Can't display statistics if not running in foreground"); c->stats = strtol(optarg, NULL, 0); break; + case 33: { + unsigned long code; + const char *comma; + char *end; + + comma = strchr(optarg, ','); + if (!comma) + die("--dhcp-opt requires Option CODE,VALUE format"); + + code = strtoul(optarg, &end, 0); + if (end != comma || code < 1 || code > 254) + die("DHCP option code must be 1-254: %s", + optarg); + + if (c->custom_opts_count >= MAX_CUSTOM_DHCP_OPTS) + die("Too many --dhcp-opt entries (max %d)", + MAX_CUSTOM_DHCP_OPTS); + + c->custom_opts[c->custom_opts_count].code = code; + if (snprintf_check(c->custom_opts[c->custom_opts_count].str, + sizeof(c->custom_opts[0].str), + "%s", comma + 1)) + die("DHCP option value too long: %s", + comma + 1); + c->custom_opts_count++; + break; + } case 'd': c->debug = 1; c->quiet = 0; diff --git a/passt.h b/passt.h index 1726965..acb57dd 100644 --- a/passt.h +++ b/passt.h @@ -263,6 +263,16 @@ struct ctx { char hostname[PASST_MAXDNAME]; char fqdn[PASST_MAXDNAME];
+#define MAX_CUSTOM_DHCP_OPTS 32 + + struct { + uint8_t code; + uint8_t len; + uint8_t val[255]; + char str[256]; + } custom_opts[MAX_CUSTOM_DHCP_OPTS]; + int custom_opts_count;
Assuming you actually need those fields here (but the same reasoning would apply to another struct as well): we document structs and fields (and enums, and functions) using the so-called kerneldoc style: https://docs.kernel.org/doc-guide/kernel-doc.html Just look at the beginning of struct ctx: you'll find those comments. They need to be updated when you add / change / remove fields.
+ int ifi6; struct ip6_ctx ip6;
-- Stefano
On Mon, 18 May 2026 18:49:59 +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
--- conf.c | 9 +++ dhcp.c | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dhcp.h | 15 ++++ 3 files changed, 251 insertions(+) diff --git a/conf.c b/conf.c index 61a393f..3ec10ac 100644 --- a/conf.c +++ b/conf.c @@ -1485,6 +1485,7 @@ void conf(struct ctx *c, int argc, char **argv) unsigned long code; const char *comma; char *end; + int len;
comma = strchr(optarg, ','); if (!comma) @@ -1499,7 +1500,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(code, 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", + code, comma + 1); + c->custom_opts[c->custom_opts_count].code = code; + 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..9220516 100644 --- a/dhcp.c +++ b/dhcp.c @@ -33,6 +33,233 @@ #include "log.h" #include "dhcp.h"
+/** + * struct dhcp_opt_type_entry - Maps option code to RFC 2132 value type + * @code: DHCP option code + * @type: Expected value format + */ +static const struct dhcp_opt_type_entry { + uint8_t code; + enum dhcp_opt_type type; +} dhcp_opt_types[] = { + { 1, DHCP_OPT_IPV4 }, /* Subnet Mask */ + { 2, DHCP_OPT_UINT32 }, /* Time Offset */ + { 3, DHCP_OPT_IPV4_LIST }, /* Router */ + { 4, DHCP_OPT_IPV4_LIST }, /* Time Server */ + { 5, DHCP_OPT_IPV4_LIST }, /* Name Server */ + + { 6, DHCP_OPT_IPV4_LIST }, /* Domain Name Server */ + { 7, DHCP_OPT_IPV4_LIST }, /* Log Server */ + { 8, DHCP_OPT_IPV4_LIST }, /* Cookie Server */ + { 9, DHCP_OPT_IPV4_LIST }, /* LPR Server */ + { 10, DHCP_OPT_IPV4_LIST }, /* Impress Server */ + + { 11, DHCP_OPT_IPV4_LIST }, /* Resource Location Server */ + { 12, DHCP_OPT_STR }, /* Host Name */ + { 13, DHCP_OPT_UINT16 }, /* Boot File Size */ + { 15, DHCP_OPT_STR }, /* Domain Name */ + { 16, DHCP_OPT_IPV4 }, /* Swap Server */ + + { 17, DHCP_OPT_STR }, /* Root Path */ + { 19, DHCP_OPT_UINT8 }, /* IP Forwarding */ + { 23, DHCP_OPT_UINT8 }, /* Default IP TTL */ + { 26, DHCP_OPT_UINT16 }, /* Interface MTU */ + { 28, DHCP_OPT_IPV4 }, /* Broadcast Address */ + + { 33, DHCP_OPT_IPV4_LIST }, /* Static Routes (dest+router pairs) */ + { 37, DHCP_OPT_UINT8 }, /* TCP Default TTL */ + { 38, DHCP_OPT_UINT32 }, /* TCP Keepalive Interval */ + { 40, DHCP_OPT_STR }, /* NIS Domain Name */ + { 41, DHCP_OPT_IPV4_LIST }, /* NIS Servers */ + + { 42, DHCP_OPT_IPV4_LIST }, /* NTP Servers */ + { 44, DHCP_OPT_IPV4_LIST }, /* NetBIOS Name Server */ + { 50, DHCP_OPT_IPV4 }, /* Requested IP Address */ + { 51, DHCP_OPT_UINT32 }, /* IP Address Lease Time */ + { 53, DHCP_OPT_UINT8 }, /* DHCP Message Type */ + + { 54, DHCP_OPT_IPV4 }, /* Server Identifier */ + { 57, DHCP_OPT_UINT16 }, /* Max DHCP Message Size */ + { 58, DHCP_OPT_UINT32 }, /* Renewal (T1) Time */ + { 59, DHCP_OPT_UINT32 }, /* Rebinding (T2) Time */ + { 60, DHCP_OPT_STR }, /* Vendor Class Identifier */ + + { 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 (RFC 3397) */ + { 121, DHCP_OPT_ROUTES }, /* Classless Static Routes */ + + { 252, DHCP_OPT_STR }, /* WPAD URL */ +}; + +/** + * dhcp_opt_type_lookup() - Look up the value type for a DHCP option code + * @code: DHCP option code + * + * Return: type from table + */ +static enum dhcp_opt_type dhcp_opt_type_lookup(uint8_t code) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(dhcp_opt_types); i++) { + if (dhcp_opt_types[i].code == code) + return dhcp_opt_types[i].type; + } + + return DHCP_OPT_NONE; +}
Not a complete review, but here's something that occurred to me while browsing this quickly (I plan to finish reviewing at some point, but meanwhile feel free to post a v2 if you have enough changes). You're defining 41 options here, and it might look like a waste to just make an array with 255 elements to store those, so you added an index (not the natural index of the array). And a lookup function. And one call to that lookup function (instead of direct addressing). Now, if you try this: --- $ CFLAGS="-g" && make && pahole passt | grep -A10 dhcp_opt_type_entry struct dhcp_opt_type_entry { uint8_t code; /* 0 1 */ /* XXX 3 bytes hole, try to pack */ enum dhcp_opt_type type; /* 4 4 */ /* size: 8, cachelines: 1, members: 2 */ /* sum members: 5, holes: 1, sum holes: 3 */ /* last cacheline: 8 bytes */ }; --- you'll see that a table with an index takes double the size for each element, it's 8 bytes instead of 4. With 41 elements, we are at 41 * 8 = 328 bytes instead of 255 * 4 = 1020. Still a win so far. Then, rebuild with -g -O0, and: $ nm -td -Sr -P passt | grep dhcp_opt_type_lookup dhcp_opt_type_lookup t 58041 85 (there are some examples of 'nm' usage in test/memory by the way). Fine, that function is 85 bytes, we're at 413 bytes. But you can probably expect (have a look with objdump -Ddslrx passt for example) that having to call that function takes a few more instructions compared to just a reference to an array, I guess you might have 10 or 20 bytes more in total. The easiest way (and most meaningful way) to check would be to compare the binary sizes of the two versions (I didn't do it). For passt, code or data doesn't really matter, it's all allocated statically, so you would see it in the size. But note that a static memory area (maybe at least a page size? I don't remember) that's not initialised (completely zeroed), which would be the case for all the options you don't define, doesn't take space. The kernel would only allocate memory for us only as we write it. Again, checking with nm: $ nm -td -Sr --size-sort -P passt | head -2 tcp_migrate_snd_queue b 43143680 67108864 tcp_migrate_rcv_queue b 110252544 67108864 $ ls -lh passt -rwxr-xr-x 1 sbrivio sbrivio 897K May 20 02:17 passt ...that's definitely less than those 128 MB we allocate statically for the tcp_migrate_* buffers. That being said, I expect that the version with a separate index field and lookup function and calls to the lookup function actually takes more memory than the "dumb" one. And readability / keeping lines of code to a minimum is also relevant here. For example, you could have:
+/** + * dhcp_opt_parse() - Parse a DHCP option value + * @code: DHCP option code (determines value type via lookup table) + * @str: Value string from command line + * @buf: Output buffer for binary value + * @buf_len: Size of output buffer + * + * Return: number of bytes written to @buf, or -1 on error + */ +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len) +{ + enum dhcp_opt_type type = dhcp_opt_type_lookup(code); +
enum dhcp_opt_type type = dhcp_opt_types[code]; instead, and it's immediately clear where those definitions come from, without having to read the lookup function first.
+ switch (type) { + case DHCP_OPT_NONE: { + die("Unsupported DHCP option: %u," + " see passt(1) for supported codes", + code); + } + case DHCP_OPT_IPV4: { + struct in_addr addr; + + 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: { + char *tok, *saveptr; + char tmp[1024]; + int len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, " ", &saveptr); tok; + tok = strtok_r(NULL, " ", &saveptr)) { + struct in_addr addr; + + 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_UINT8: { + unsigned long val; + char *end; + + val = strtoul(str, &end, 0); + if (*end || val > 255 || buf_len < 1) + return -1; + buf[0] = val; + return 1; + } + case DHCP_OPT_UINT16: { + unsigned long val; + char *end; + + val = strtoul(str, &end, 0); + if (*end || val > 65535 || buf_len < 2) + return -1; + buf[0] = (val >> 8) & 0xff; + buf[1] = val & 0xff; + return 2; + } + case DHCP_OPT_UINT32: { + unsigned long val; + char *end; + + val = strtoul(str, &end, 0); + if (*end || buf_len < 4) + return -1; + buf[0] = (val >> 24) & 0xff; + buf[1] = (val >> 16) & 0xff; + buf[2] = (val >> 8) & 0xff; + buf[3] = val & 0xff; + return 4; + } + case DHCP_OPT_ROUTES: { + /* RFC 3442: "CIDR/mask,gateway" entries, space-separated + * Encodes as: mask-width + significant-octets + router + * e.g. "192.168.1.0/24,10.0.0.1 0.0.0.0/0,10.0.0.1" + */ + char *tok, *saveptr; + char tmp[1024]; + int len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, " ", &saveptr); tok; + tok = strtok_r(NULL, " ", &saveptr)) { + struct in_addr dest, gw; + char *slash, *comma; + unsigned long mask; + int sig_octets; + + slash = strchr(tok, '/'); + if (!slash) + return -1; + *slash = '\0'; + + if (inet_pton(AF_INET, tok, &dest) != 1) + return -1; + + comma = strchr(slash + 1, ','); + if (!comma) + return -1; + *comma = '\0'; + + mask = strtoul(slash + 1, NULL, 10); + if (mask > 32) + return -1; + + if (inet_pton(AF_INET, comma + 1, &gw) != 1) + return -1; + + sig_octets = (mask + 7) / 8; + + if (len + 1 + sig_octets + 4 > (int)buf_len) + return -1; + + buf[len++] = mask; + memcpy(buf + len, &dest, sig_octets); + len += sig_octets; + memcpy(buf + len, &gw, 4); + len += 4; + } + return len; + } + case DHCP_OPT_STR: { + size_t len = strlen(str); + + if (!len || len >= buf_len) + return -1; + strncpy((char *)buf, str, buf_len); + return len; + } + } + + return -1; +} + /** * struct opt - DHCP option * @sent: Convenience flag, set while filling replies diff --git a/dhcp.h b/dhcp.h index cd50c99..01b2290 100644 --- a/dhcp.h +++ b/dhcp.h @@ -6,7 +6,22 @@ #ifndef DHCP_H #define DHCP_H
+/** + * enum dhcp_opt_type - DHCP option value types per RFC 2132
Nit: this should be documented as well. We haven't been very consistent with that, but (at least) enum tcp_iov_parts in tcp_internal.h is properly documented in the kerneldoc style.
+ */ +enum dhcp_opt_type { + DHCP_OPT_NONE, + DHCP_OPT_STR, + DHCP_OPT_IPV4, + DHCP_OPT_IPV4_LIST, + DHCP_OPT_UINT8, + DHCP_OPT_UINT16, + DHCP_OPT_UINT32, + DHCP_OPT_ROUTES, +}; + 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 */
-- Stefano
On Wed, May 20, 2026 at 02:38:04AM +0200, Stefano Brivio wrote:
On Tue, 19 May 2026 15:30:41 +1000 David Gibson
wrote: On Mon, May 18, 2026 at 06:49:56PM +0530, Anshu Kumari wrote:
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.
*** BLURB HERE ***
Nit: remember to remove this boilerplate that git-publish inserts :)
No, no, that's from git format-patch, I swear (as somebody who doesn't use git-publish).
Oh, my mistake. Same thought applies, though.
Another detail, while at it: for some reason git send-email picked the "chain-reply" style, that is, if you look at this in a threaded email client, patch #1 is a reply to the cover letter (fine, expected) but patch #2 is a reply to patch #1 (not expected), and so on.
That's the --chain-reply-to option for git send-email. You should disable that (for example in gitconfig in case you have it on there) and just use --cover-letter.
-- 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 Mon, 18 May 2026 18:49:59 +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
--- conf.c | 9 +++ dhcp.c | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dhcp.h | 15 ++++ 3 files changed, 251 insertions(+) diff --git a/conf.c b/conf.c index 61a393f..3ec10ac 100644 --- a/conf.c +++ b/conf.c @@ -1485,6 +1485,7 @@ void conf(struct ctx *c, int argc, char **argv) unsigned long code; const char *comma; char *end; + int len;
comma = strchr(optarg, ','); if (!comma) @@ -1499,7 +1500,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(code, 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", + code, comma + 1); + c->custom_opts[c->custom_opts_count].code = code; + 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..9220516 100644 --- a/dhcp.c +++ b/dhcp.c @@ -33,6 +33,233 @@ #include "log.h" #include "dhcp.h"
+/** + * struct dhcp_opt_type_entry - Maps option code to RFC 2132 value type + * @code: DHCP option code + * @type: Expected value format + */ +static const struct dhcp_opt_type_entry { + uint8_t code; + enum dhcp_opt_type type; +} dhcp_opt_types[] = { + { 1, DHCP_OPT_IPV4 }, /* Subnet Mask */ + { 2, DHCP_OPT_UINT32 }, /* Time Offset */ + { 3, DHCP_OPT_IPV4_LIST }, /* Router */ + { 4, DHCP_OPT_IPV4_LIST }, /* Time Server */ + { 5, DHCP_OPT_IPV4_LIST }, /* Name Server */ + + { 6, DHCP_OPT_IPV4_LIST }, /* Domain Name Server */ + { 7, DHCP_OPT_IPV4_LIST }, /* Log Server */ + { 8, DHCP_OPT_IPV4_LIST }, /* Cookie Server */ + { 9, DHCP_OPT_IPV4_LIST }, /* LPR Server */ + { 10, DHCP_OPT_IPV4_LIST }, /* Impress Server */ + + { 11, DHCP_OPT_IPV4_LIST }, /* Resource Location Server */ + { 12, DHCP_OPT_STR }, /* Host Name */ + { 13, DHCP_OPT_UINT16 }, /* Boot File Size */ + { 15, DHCP_OPT_STR }, /* Domain Name */ + { 16, DHCP_OPT_IPV4 }, /* Swap Server */ + + { 17, DHCP_OPT_STR }, /* Root Path */ + { 19, DHCP_OPT_UINT8 }, /* IP Forwarding */ + { 23, DHCP_OPT_UINT8 }, /* Default IP TTL */ + { 26, DHCP_OPT_UINT16 }, /* Interface MTU */ + { 28, DHCP_OPT_IPV4 }, /* Broadcast Address */ + + { 33, DHCP_OPT_IPV4_LIST }, /* Static Routes (dest+router pairs) */ + { 37, DHCP_OPT_UINT8 }, /* TCP Default TTL */ + { 38, DHCP_OPT_UINT32 }, /* TCP Keepalive Interval */ + { 40, DHCP_OPT_STR }, /* NIS Domain Name */ + { 41, DHCP_OPT_IPV4_LIST }, /* NIS Servers */ + + { 42, DHCP_OPT_IPV4_LIST }, /* NTP Servers */ + { 44, DHCP_OPT_IPV4_LIST }, /* NetBIOS Name Server */ + { 50, DHCP_OPT_IPV4 }, /* Requested IP Address */ + { 51, DHCP_OPT_UINT32 }, /* IP Address Lease Time */ + { 53, DHCP_OPT_UINT8 }, /* DHCP Message Type */ + + { 54, DHCP_OPT_IPV4 }, /* Server Identifier */ + { 57, DHCP_OPT_UINT16 }, /* Max DHCP Message Size */ + { 58, DHCP_OPT_UINT32 }, /* Renewal (T1) Time */ + { 59, DHCP_OPT_UINT32 }, /* Rebinding (T2) Time */ + { 60, DHCP_OPT_STR }, /* Vendor Class Identifier */ + + { 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 (RFC 3397) */ + { 121, DHCP_OPT_ROUTES }, /* Classless Static Routes */ + + { 252, DHCP_OPT_STR }, /* WPAD URL */ +}; + +/** + * dhcp_opt_type_lookup() - Look up the value type for a DHCP option code + * @code: DHCP option code + * + * Return: type from table + */ +static enum dhcp_opt_type dhcp_opt_type_lookup(uint8_t code) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(dhcp_opt_types); i++) { + if (dhcp_opt_types[i].code == code) + return dhcp_opt_types[i].type; + } + + return DHCP_OPT_NONE; +} + +/** + * dhcp_opt_parse() - Parse a DHCP option value + * @code: DHCP option code (determines value type via lookup table) + * @str: Value string from command line + * @buf: Output buffer for binary value + * @buf_len: Size of output buffer + * + * Return: number of bytes written to @buf, or -1 on error + */ +int dhcp_opt_parse(uint8_t code, const char *str, uint8_t *buf, size_t buf_len) +{ + enum dhcp_opt_type type = dhcp_opt_type_lookup(code); + + switch (type) { + case DHCP_OPT_NONE: {
We don't need to add a block here, and...
+ die("Unsupported DHCP option: %u," + " see passt(1) for supported codes", + code); + } + case DHCP_OPT_IPV4: { + struct in_addr addr;
here I think it would be preferable to stick to what we do (almost) everywhere else and just have the variable declarations before the switch, and drop all those curly brackets. I think it's more readable. See also for example that: } } at the end.
+ + if (inet_pton(AF_INET, str, &addr) != 1) + return -1; + if (buf_len < sizeof(addr)) + return -1; + memcpy(buf, &addr, sizeof(addr));
Not strictly enforced, but we usually add an extra newline before return statements, to make those a bit more separated / visible.
+ return sizeof(addr); + } + case DHCP_OPT_IPV4_LIST: { + char *tok, *saveptr; + char tmp[1024]; + int len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, " ", &saveptr); tok; + tok = strtok_r(NULL, " ", &saveptr)) { + struct in_addr addr; + + 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_UINT8: { + unsigned long val; + char *end; + + val = strtoul(str, &end, 0); + if (*end || val > 255 || buf_len < 1) + return -1; + buf[0] = val; + return 1; + } + case DHCP_OPT_UINT16: { + unsigned long val; + char *end; + + val = strtoul(str, &end, 0); + if (*end || val > 65535 || buf_len < 2) + return -1; + buf[0] = (val >> 8) & 0xff; + buf[1] = val & 0xff; + return 2; + } + case DHCP_OPT_UINT32: { + unsigned long val; + char *end; + + val = strtoul(str, &end, 0); + if (*end || buf_len < 4) + return -1; + buf[0] = (val >> 24) & 0xff; + buf[1] = (val >> 16) & 0xff; + buf[2] = (val >> 8) & 0xff; + buf[3] = val & 0xff; + return 4; + } + case DHCP_OPT_ROUTES: { + /* RFC 3442: "CIDR/mask,gateway" entries, space-separated + * Encodes as: mask-width + significant-octets + router + * e.g. "192.168.1.0/24,10.0.0.1 0.0.0.0/0,10.0.0.1"
I don't think the example is particularly fitting or explaining the kind of madness RFC 3442 gifted us with. I would rather pick something like 192.168.2.0/28 as subnet and 192.168.2.1 (RFC 3442 doesn't seem to care about using appropriate IP addresses reserved for documentation), and there our "destination descriptor" would be: 28.192.168.2 hint: 'sipcalc' / 'ipcalc' are pretty useful to visualise this stuff: $ sipcalc 192.168.2.0/28 -[ipv4 : 192.168.2.0/28] - 0 [CIDR] Host address - 192.168.2.0 Host address (decimal) - 3232236032 Host address (hex) - C0A80200 Network address - 192.168.2.0 Network mask - 255.255.255.240 Network mask (bits) - 28 Network mask (hex) - FFFFFFF0 Broadcast address - 192.168.2.15 Cisco wildcard - 0.0.0.15 Addresses in network - 16 Network range - 192.168.2.0 - 192.168.2.15 Usable range - 192.168.2.1 - 192.168.2.14 - $ ipcalc 192.168.2.0/28 Address: 192.168.2.0 11000000.10101000.00000010.0000 0000 Netmask: 255.255.255.240 = 28 11111111.11111111.11111111.1111 0000 Wildcard: 0.0.0.15 00000000.00000000.00000000.0000 1111 => Network: 192.168.2.0/28 11000000.10101000.00000010.0000 0000 HostMin: 192.168.2.1 11000000.10101000.00000010.0000 0001 HostMax: 192.168.2.14 11000000.10101000.00000010.0000 1110 Broadcast: 192.168.2.15 11000000.10101000.00000010.0000 1111 Hosts/Net: 14 Class C, Private Internet
+ */ + char *tok, *saveptr; + char tmp[1024]; + int len = 0; + + if (snprintf_check(tmp, sizeof(tmp), "%s", str)) + return -1; + + for (tok = strtok_r(tmp, " ", &saveptr); tok; + tok = strtok_r(NULL, " ", &saveptr)) { + struct in_addr dest, gw; + char *slash, *comma; + unsigned long mask; + int sig_octets; + + slash = strchr(tok, '/'); + if (!slash) + return -1; + *slash = '\0'; + + if (inet_pton(AF_INET, tok, &dest) != 1) + return -1; + + comma = strchr(slash + 1, ','); + if (!comma) + return -1; + *comma = '\0';
It looks relatively sane until here (as sane as RFC 3442 permits, of course), but then, I think the calculation of the "destination descriptor", below, would deserve its own function.
+ mask = strtoul(slash + 1, NULL, 10); + if (mask > 32) + return -1; + + if (inet_pton(AF_INET, comma + 1, &gw) != 1) + return -1;
This part could happily live here instead.
+ sig_octets = (mask + 7) / 8;
And this should be ROUND_UP() (see common.h).
+ + if (len + 1 + sig_octets + 4 > (int)buf_len) + return -1; + + buf[len++] = mask; + memcpy(buf + len, &dest, sig_octets); + len += sig_octets; + memcpy(buf + len, &gw, 4); + len += 4; + } + return len; + } + case DHCP_OPT_STR: { + size_t len = strlen(str); + + if (!len || len >= buf_len) + return -1; + strncpy((char *)buf, str, buf_len); + return len; + } + } + + return -1; +} + /** * struct opt - DHCP option * @sent: Convenience flag, set while filling replies diff --git a/dhcp.h b/dhcp.h index cd50c99..01b2290 100644 --- a/dhcp.h +++ b/dhcp.h @@ -6,7 +6,22 @@ #ifndef DHCP_H #define DHCP_H
+/** + * enum dhcp_opt_type - DHCP option value types per RFC 2132 + */ +enum dhcp_opt_type { + DHCP_OPT_NONE, + DHCP_OPT_STR, + DHCP_OPT_IPV4, + DHCP_OPT_IPV4_LIST, + DHCP_OPT_UINT8, + DHCP_OPT_UINT16, + DHCP_OPT_UINT32, + DHCP_OPT_ROUTES, +}; + 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 */
-- Stefano
On Mon, 18 May 2026 18:50:02 +0530
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
--- passt.1 | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/passt.1 b/passt.1 index 908fd4a..c39e5ec 100644 --- a/passt.1 +++ b/passt.1 @@ -430,6 +430,50 @@ 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 +Boot file URL for network boot. +Populates the boot file field in DHCP replies. For UEFI HTTP boot, +also set the vendor class identifier using \-\-dhcp-opt 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 IP addresses are space-separated within quotes. +This option can be specified multiple times. Options set with \-\-dhcp-opt +override built-in values and \-\-dhcp-boot settings.
When we refer to other options, we highlight them, say: \fB--dhcp-boot\fR For further examples, look for "See option" or "Implies" in this file.
+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 +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), +60 (Vendor Class Identifier), 61 (Client Identifier), 66 (TFTP Server Name), +67 (Bootfile Name), 119 (Domain Search List), 252 (WPAD URL) +.TP +.B Classless static route options (RFC 3442 encoding) +121 (Classless Static Routes). +Format: "CIDR/mask,gateway" entries, space-separated. +Example: \-\-dhcp-opt 121,"10.0.1.0/24,10.0.0.1 0.0.0.0/0,10.0.0.1" +.RE + .TP .BR \-t ", " \-\-tcp-ports " " \fIspec Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of:
Except for pending comments from David and myself, the whole series looks good to me! I suppose that addressing those comments especially around 2/6 and 3/6 might take a few iterations, though. -- Stefano
participants (3)
-
Anshu Kumari
-
David Gibson
-
Stefano Brivio