* [PATCH 4/6] dhcp: Refactor fill_one() to operate on a generic buffer
2026-05-18 13:19 ` [PATCH 3/6] dhcp: Add option type table and value parser Anshu Kumari
@ 2026-05-18 13:20 ` Anshu Kumari
2026-05-18 13:20 ` [PATCH 5/6] dhcp: Add option overload Anshu Kumari
2026-05-19 6:02 ` [PATCH 4/6] dhcp: Refactor fill_one() to operate on a generic buffer David Gibson
2026-05-19 5:59 ` [PATCH 3/6] dhcp: Add option type table and value parser David Gibson
` (2 subsequent siblings)
3 siblings, 2 replies; 19+ messages in thread
From: Anshu Kumari @ 2026-05-18 13:20 UTC (permalink / raw)
To: anskuma, sbrivio, passt-dev; +Cc: lvivier, jmaloy, david
Change fill_one() to accept a buffer pointer and capacity instead of
a struct msg pointer. This is a pure refactor with no behavior change,
preparing for option overload support where fill_one() will also write
into the file and sname fields.
Link: https://bugs.passt.top/show_bug.cgi?id=192
Signed-off-by: Anshu Kumari <anskuma@redhat.com>
---
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)
* @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
^ permalink raw reply [flat|nested] 19+ messages in thread* [PATCH 5/6] dhcp: Add option overload
2026-05-18 13:20 ` [PATCH 4/6] dhcp: Refactor fill_one() to operate on a generic buffer Anshu Kumari
@ 2026-05-18 13:20 ` Anshu Kumari
2026-05-18 13:20 ` [PATCH 6/6] doc: Add --dhcp-boot and --dhcp-opt to man page Anshu Kumari
2026-05-19 6:11 ` [PATCH 5/6] dhcp: Add option overload David Gibson
2026-05-19 6:02 ` [PATCH 4/6] dhcp: Refactor fill_one() to operate on a generic buffer David Gibson
1 sibling, 2 replies; 19+ messages in thread
From: Anshu Kumari @ 2026-05-18 13:20 UTC (permalink / raw)
To: anskuma, sbrivio, passt-dev; +Cc: lvivier, jmaloy, david
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 <anskuma@redhat.com>
---
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;
+ }
+
+ 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;
}
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));
+
+ 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
^ permalink raw reply [flat|nested] 19+ messages in thread* [PATCH 6/6] doc: Add --dhcp-boot and --dhcp-opt to man page
2026-05-18 13:20 ` [PATCH 5/6] dhcp: Add option overload Anshu Kumari
@ 2026-05-18 13:20 ` Anshu Kumari
2026-05-20 21:23 ` Stefano Brivio
2026-05-19 6:11 ` [PATCH 5/6] dhcp: Add option overload David Gibson
1 sibling, 1 reply; 19+ messages in thread
From: Anshu Kumari @ 2026-05-18 13:20 UTC (permalink / raw)
To: anskuma, sbrivio, passt-dev; +Cc: lvivier, jmaloy, david
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 <anskuma@redhat.com>
---
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.
+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:
--
2.54.0
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH 6/6] doc: Add --dhcp-boot and --dhcp-opt to man page
2026-05-18 13:20 ` [PATCH 6/6] doc: Add --dhcp-boot and --dhcp-opt to man page Anshu Kumari
@ 2026-05-20 21:23 ` Stefano Brivio
0 siblings, 0 replies; 19+ messages in thread
From: Stefano Brivio @ 2026-05-20 21:23 UTC (permalink / raw)
To: Anshu Kumari; +Cc: passt-dev, lvivier, jmaloy, david
On Mon, 18 May 2026 18:50:02 +0530
Anshu Kumari <anskuma@redhat.com> 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 <anskuma@redhat.com>
> ---
> 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
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH 5/6] dhcp: Add option overload
2026-05-18 13:20 ` [PATCH 5/6] dhcp: Add option overload Anshu Kumari
2026-05-18 13:20 ` [PATCH 6/6] doc: Add --dhcp-boot and --dhcp-opt to man page Anshu Kumari
@ 2026-05-19 6:11 ` David Gibson
1 sibling, 0 replies; 19+ messages in thread
From: David Gibson @ 2026-05-19 6:11 UTC (permalink / raw)
To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy
[-- Attachment #1: Type: text/plain, Size: 5275 bytes --]
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 <anskuma@redhat.com>
> ---
> 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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH 4/6] dhcp: Refactor fill_one() to operate on a generic buffer
2026-05-18 13:20 ` [PATCH 4/6] dhcp: Refactor fill_one() to operate on a generic buffer Anshu Kumari
2026-05-18 13:20 ` [PATCH 5/6] dhcp: Add option overload Anshu Kumari
@ 2026-05-19 6:02 ` David Gibson
1 sibling, 0 replies; 19+ messages in thread
From: David Gibson @ 2026-05-19 6:02 UTC (permalink / raw)
To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy
[-- Attachment #1: Type: text/plain, Size: 3191 bytes --]
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 <anskuma@redhat.com>
Reviewed-by: David Gibson <david@gibson.dropbear.id.au>
Though one cosmetic flaw noted below.
> ---
> 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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH 3/6] dhcp: Add option type table and value parser
2026-05-18 13:19 ` [PATCH 3/6] dhcp: Add option type table and value parser Anshu Kumari
2026-05-18 13:20 ` [PATCH 4/6] dhcp: Refactor fill_one() to operate on a generic buffer Anshu Kumari
@ 2026-05-19 5:59 ` David Gibson
2026-05-20 0:39 ` Stefano Brivio
2026-05-20 21:23 ` Stefano Brivio
3 siblings, 0 replies; 19+ messages in thread
From: David Gibson @ 2026-05-19 5:59 UTC (permalink / raw)
To: Anshu Kumari; +Cc: sbrivio, passt-dev, lvivier, jmaloy
[-- Attachment #1: Type: text/plain, Size: 10791 bytes --]
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 <anskuma@redhat.com>
> ---
> 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
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply [flat|nested] 19+ messages in thread* Re: [PATCH 3/6] dhcp: Add option type table and value parser
2026-05-18 13:19 ` [PATCH 3/6] dhcp: Add option type table and value parser Anshu Kumari
2026-05-18 13:20 ` [PATCH 4/6] dhcp: Refactor fill_one() to operate on a generic buffer Anshu Kumari
2026-05-19 5:59 ` [PATCH 3/6] dhcp: Add option type table and value parser David Gibson
@ 2026-05-20 0:39 ` Stefano Brivio
2026-05-20 21:23 ` Stefano Brivio
3 siblings, 0 replies; 19+ messages in thread
From: Stefano Brivio @ 2026-05-20 0:39 UTC (permalink / raw)
To: Anshu Kumari; +Cc: passt-dev, lvivier, jmaloy, david
On Mon, 18 May 2026 18:49:59 +0530
Anshu Kumari <anskuma@redhat.com> 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 <anskuma@redhat.com>
> ---
> 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
^ permalink raw reply [flat|nested] 19+ messages in thread* Re: [PATCH 3/6] dhcp: Add option type table and value parser
2026-05-18 13:19 ` [PATCH 3/6] dhcp: Add option type table and value parser Anshu Kumari
` (2 preceding siblings ...)
2026-05-20 0:39 ` Stefano Brivio
@ 2026-05-20 21:23 ` Stefano Brivio
3 siblings, 0 replies; 19+ messages in thread
From: Stefano Brivio @ 2026-05-20 21:23 UTC (permalink / raw)
To: Anshu Kumari; +Cc: passt-dev, lvivier, jmaloy, david
On Mon, 18 May 2026 18:49:59 +0530
Anshu Kumari <anskuma@redhat.com> 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 <anskuma@redhat.com>
> ---
> 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
^ permalink raw reply [flat|nested] 19+ messages in thread