public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
From: Anshu Kumari <anskuma@redhat.com>
To: passt-dev@passt.top, anskuma@redhat.com, sbrivio@redhat.com
Cc: jmaloy@redhat.com, david@gibson.dropbear.id.au, lvivier@redhat.com
Subject: [PATCH v4 3/4] dhcp: Add --dhcp-opt with option table and value parser
Date: Wed, 17 Jun 2026 18:52:37 +0530	[thread overview]
Message-ID: <20260617132243.1499556-4-anskuma@redhat.com> (raw)
In-Reply-To: <20260617132243.1499556-1-anskuma@redhat.com>

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

Add a type lookup table mapping option codes to RFC 2132 value types
(IPv4, IPv4 list, integer, string) and dhcp_opt_parse() to convert
CLI strings to binary wire format.  Parsed options are stored in
struct ctx and injected into DHCP replies.  If the same option code
is given more than once, the last value wins.

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

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

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

diff --git a/conf.c b/conf.c
index cd05adf..836b297 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 CODE to VAL\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 = 1; i < 255; i++)
+				if (*c->dhcp_opts[i].str)
+					info("    option %u: %s", i,
+					     c->dhcp_opts[i].str);
 		}
 
 		for (i = 0; i < ARRAY_SIZE(c->ip4.dns); i++) {
@@ -1150,6 +1156,25 @@ static void conf_sock_listen(const struct ctx *c)
 		die_perror("Couldn't add configuration socket to epoll");
 }
 
+/**
+ * conf_dhcp_option() - Set value for a DHCP option in configuration
+ * @c:		Execution context
+ * @code:	DHCP option code
+ * @val_str:	Value string from command line
+ */
+static void conf_dhcp_option(struct ctx *c, uint8_t code, const char *val_str)
+{
+	uint8_t tmp[255];
+
+	if (dhcp_opt_parse(code, val_str, tmp, sizeof(tmp)) < 0)
+		die("Invalid value for DHCP option %u: %s", code, val_str);
+
+	if (snprintf_check(c->dhcp_opts[code].str,
+			   sizeof(c->dhcp_opts[0].str),
+			   "%s", val_str))
+		die("DHCP option value too long: %s", val_str);
+}
+
 /**
  * conf() - Process command-line arguments and set configuration
  * @c:		Execution context
@@ -1233,6 +1258,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,		34 },
 		{ 0 },
 	};
 	const char *optstring = "+dqfel:hs:c:F:I:p:P:m:a:n:M:g:i:o:D:S:H:461t:u:T:U:";
@@ -1248,10 +1274,13 @@ void conf(struct ctx *c, int argc, char **argv)
 	uint8_t prefix_len_from_opt = 0;
 	unsigned int ifi4 = 0, ifi6 = 0;
 	const char *logfile = NULL;
+	unsigned long optcode;
 	char *runas = NULL;
 	size_t logsize = 0;
+	const char *comma;
 	long fd_tap_opt;
 	int name, ret;
+	char *end;
 	uid_t uid;
 	gid_t gid;
 
@@ -1467,6 +1496,20 @@ 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 34:
+			comma = strchr(optarg, ',');
+			if (!comma)
+				die("--dhcp-opt requires CODE,VALUE format");
+
+			errno = 0;
+			optcode = strtoul(optarg, &end, 0);
+			if (end != comma || errno ||
+			    optcode < 1 || optcode > 254)
+				die("DHCP option code must be 1-254: %s",
+				    optarg);
+
+			conf_dhcp_option(c, optcode, comma + 1);
+			break;
 		case 'd':
 			c->debug = 1;
 			c->quiet = 0;
diff --git a/dhcp.c b/dhcp.c
index 78790d8..47bb524 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -23,6 +23,7 @@
 #include <unistd.h>
 #include <string.h>
 #include <limits.h>
+#include <errno.h>
 
 #include "util.h"
 #include "ip.h"
@@ -130,6 +131,189 @@ struct msg {
 	uint8_t o[OPT_MAX + 1 /* End option */ ];
 } __attribute__((__packed__));
 
+/**
+ * enum dhcp_opt_type - DHCP option value types per RFC 2132
+ * @DHCP_OPT_NONE:	Unsupported or unknown option
+ * @DHCP_OPT_STR:	Variable-length string
+ * @DHCP_OPT_IPV4:	Single IPv4 address
+ * @DHCP_OPT_IPV4_LIST:	Multiple IPv4 addresses, comma-separated
+ * @DHCP_OPT_INT8:	Unsigned 8-bit integer
+ * @DHCP_OPT_INT16:	Unsigned 16-bit integer
+ * @DHCP_OPT_INT32:	Unsigned 32-bit integer
+ * @DHCP_OPT_SINT32:	Signed 32-bit integer
+ */
+enum dhcp_opt_type {
+	DHCP_OPT_NONE,
+	DHCP_OPT_STR,
+	DHCP_OPT_IPV4,
+	DHCP_OPT_IPV4_LIST,
+	DHCP_OPT_INT8,
+	DHCP_OPT_INT16,
+	DHCP_OPT_INT32,
+	DHCP_OPT_SINT32,
+};
+
+/**
+ * dhcp_opt_types - Maps option code to RFC 2132 value type, indexed by code
+ */
+static const enum dhcp_opt_type dhcp_opt_types[] = {
+	[1]   = DHCP_OPT_IPV4,		/* Subnet Mask */
+	[2]   = DHCP_OPT_SINT32,	/* 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_INT16,		/* Boot File Size */
+	[15]  = DHCP_OPT_STR,		/* Domain Name */
+	[16]  = DHCP_OPT_IPV4,		/* Swap Server */
+	[17]  = DHCP_OPT_STR,		/* Root Path */
+	[19]  = DHCP_OPT_INT8,		/* IP Forwarding */
+	[23]  = DHCP_OPT_INT8,		/* Default IP TTL */
+	[26]  = DHCP_OPT_INT16,		/* Interface MTU */
+	[28]  = DHCP_OPT_IPV4,		/* Broadcast Address */
+	[37]  = DHCP_OPT_INT8,		/* TCP Default TTL */
+	[38]  = DHCP_OPT_INT32,		/* 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_INT32,		/* IP Address Lease Time */
+	[53]  = DHCP_OPT_INT8,		/* DHCP Message Type */
+	[54]  = DHCP_OPT_IPV4,		/* Server Identifier */
+	[57]  = DHCP_OPT_INT16,		/* Max DHCP Message Size */
+	[58]  = DHCP_OPT_INT32,		/* Renewal (T1) Time */
+	[59]  = DHCP_OPT_INT32,		/* Rebinding (T2) Time */
+	[60]  = DHCP_OPT_STR,		/* Vendor Class Identifier */
+	[66]  = DHCP_OPT_STR,		/* TFTP Server Name */
+	[67]  = DHCP_OPT_STR,		/* Bootfile Name */
+	[252] = DHCP_OPT_STR,		/* WPAD URL */
+};
+
+/**
+ * dhcp_opt_parse() - Parse a DHCP option value
+ * @code:	DHCP option code
+ * @str:	Value string from command line
+ * @buf:	Output buffer for binary value
+ * @buf_len:	Size of output buffer
+ *
+ * Return: number of bytes written to @buf, or -1 on error
+ */
+int dhcp_opt_parse(uint8_t code, const char *str,
+		   uint8_t *buf, size_t buf_len)
+{
+	enum dhcp_opt_type type;
+	unsigned long val;
+	unsigned int i;
+	uint8_t width;
+	size_t slen;
+	char *end;
+	int len;
+
+	if (code >= ARRAY_SIZE(dhcp_opt_types))
+		return -1;
+
+	type = dhcp_opt_types[code];
+
+	if (!*str)
+		return -1;
+
+	switch (type) {
+	case DHCP_OPT_NONE:
+		return -1;
+	case DHCP_OPT_IPV4:
+	case DHCP_OPT_IPV4_LIST:
+		len = 0;
+
+		while (*str) {
+			char ipbuf[INET_ADDRSTRLEN];
+			size_t chunk;
+
+			chunk = strcspn(str, ",");
+
+			if (!chunk || chunk >= sizeof(ipbuf))
+				return -1;
+
+			memcpy(ipbuf, str, chunk);
+			ipbuf[chunk] = '\0';
+
+			if (len + (int)sizeof(struct in_addr) > (int)buf_len)
+				return -1;
+
+			if (inet_pton(AF_INET, ipbuf, buf + len) != 1)
+				return -1;
+
+			len += sizeof(struct in_addr);
+
+			if (type == DHCP_OPT_IPV4) {
+				if (str[chunk] == ',')
+					return -1;
+				break;
+			}
+
+			str += chunk + (str[chunk] == ',');
+		}
+
+		if (!len)
+			return -1;
+
+		return len;
+	case DHCP_OPT_INT8:
+	case DHCP_OPT_INT16:
+	case DHCP_OPT_INT32:
+	case DHCP_OPT_SINT32:
+		if (type == DHCP_OPT_INT8)
+			width = 1;
+		else if (type == DHCP_OPT_INT16)
+			width = 2;
+		else
+			width = 4;
+
+		if (buf_len < width)
+			return -1;
+
+		errno = 0;
+		if (type == DHCP_OPT_SINT32) {
+			long sval;
+
+			sval = strtol(str, &end, 0);
+			if (*end || errno ||
+			    sval < INT32_MIN || sval > INT32_MAX)
+				return -1;
+			val = (uint32_t)sval;
+		} else {
+			val = strtoul(str, &end, 0);
+			if (*end || errno ||
+			    val >= (1ULL << (width * 8)))
+				return -1;
+		}
+
+		for (i = width; i > 0; i--) {
+			buf[i - 1] = val & 0xff;
+			val >>= 8;
+		}
+
+		return width;
+	case DHCP_OPT_STR:
+		slen = strlen(str);
+
+		if (slen >= buf_len)
+			return -1;
+
+		memcpy(buf, str, slen);
+
+		return slen;
+	}
+
+	return -1;
+}
+
 /**
  * fill_one() - Fill a single option into a buffer
  * @buf:	Buffer to write option
@@ -541,6 +725,13 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
 	if (!c->no_dhcp_dns_search)
 		opt_set_dns_search(c, OPT_MAX - 3);
 
+	for (i = 1; i < 255; i++) {
+		if (!c->dhcp_opts[i].str[0])
+			continue;
+		opts[i].slen = dhcp_opt_parse(i, c->dhcp_opts[i].str,
+					      opts[i].s, sizeof(opts[i].s));
+	}
+
 	/* RFC 2132, Section 9.5: put boot file name in the 'file' header
 	 * field.  Suppress option 67 from the options area and reserve
 	 * the file field from overload.
diff --git a/dhcp.h b/dhcp.h
index cd50c99..cc8d5dd 100644
--- a/dhcp.h
+++ b/dhcp.h
@@ -8,5 +8,7 @@
 
 int dhcp(const struct ctx *c, struct iov_tail *data);
 void dhcp_init(void);
+int dhcp_opt_parse(uint8_t code, const char *str,
+		   uint8_t *buf, size_t buf_len);
 
 #endif /* DHCP_H */
diff --git a/passt.1 b/passt.1
index 908fd4a..ccdcbb2 100644
--- a/passt.1
+++ b/passt.1
@@ -430,6 +430,48 @@ Send \fIname\fR as DHCP option 12 (hostname).
 FQDN to configure the client with.
 Send \fIname\fR as Client FQDN: DHCP option 81 and DHCPv6 option 39.
 
+.TP
+.BR \-\-dhcp-opt " " \fICODE\fR,\fIVALUE\fR
+Set DHCP option \fICODE\fR (1\-254) to \fIVALUE\fR. The value format depends
+on the option type and is determined automatically from the option code.
+Multiple IPv4 addresses are comma-separated.
+This option can be specified multiple times. If the same option code is
+given more than once, the last value wins. Options set with
+\fB\-\-dhcp-opt\fR override built-in values.
+.PP
+Examples:
+.nf
+  \-\-dhcp-opt 6,8.8.8.8,4.4.4.4
+  \-\-dhcp-opt 12,myhostname
+.fi
+.PP
+Only the following option codes are supported (unsupported codes cause an error):
+.RS
+.TP
+.B IPv4 address options
+1 (Subnet Mask), 16 (Swap Server), 28 (Broadcast Address), 50 (Requested IP),
+54 (Server Identifier)
+.TP
+.B IPv4 address list options (comma-separated)
+3 (Router), 4 (Time Server), 5 (Name Server), 6 (DNS), 7 (Log Server),
+8 (Cookie Server), 9 (LPR Server), 10 (Impress Server),
+11 (Resource Location Server), 41 (NIS Servers),
+42 (NTP Servers), 44 (NetBIOS Name Server)
+.TP
+.B Integer options
+2 (Time Offset, 32-bit), 13 (Boot File Size, 16-bit), 19 (IP Forwarding, 8-bit),
+23 (Default IP TTL, 8-bit), 26 (Interface MTU, 16-bit),
+37 (TCP Default TTL, 8-bit), 38 (TCP Keepalive Interval, 32-bit),
+51 (IP Address Lease Time, 32-bit),
+53 (DHCP Message Type, 8-bit), 57 (Max DHCP Message Size, 16-bit),
+58 (Renewal Time, 32-bit), 59 (Rebinding Time, 32-bit)
+.TP
+.B String options
+12 (Host Name), 15 (Domain Name), 17 (Root Path), 40 (NIS Domain Name),
+60 (Vendor Class Identifier), 66 (TFTP Server Name),
+67 (Bootfile Name), 252 (WPAD URL)
+.RE
+
 .TP
 .BR \-t ", " \-\-tcp-ports " " \fIspec
 Configure TCP port forwarding to guest or namespace. \fIspec\fR can be one of:
diff --git a/passt.h b/passt.h
index 3a07294..15e2d83 100644
--- a/passt.h
+++ b/passt.h
@@ -182,6 +182,8 @@ struct ip6_ctx {
  * @dns_search:		DNS search list
  * @hostname:		Guest hostname
  * @fqdn:		Guest FQDN
+ * @dhcp_opts:		User-specified DHCP options from --dhcp-opt
+ * @dhcp_opts.str:	String value from command line
  * @ifi6:		Template interface for IPv6, -1: none, 0: IPv6 disabled
  * @ip6:		IPv6 configuration
  * @pasta_ifn:		Name of namespace interface for pasta
@@ -264,6 +266,10 @@ struct ctx {
 	char hostname[PASST_MAXDNAME];
 	char fqdn[PASST_MAXDNAME];
 
+	struct {
+		char str[255];
+	} dhcp_opts[256];
+
 	int ifi6;
 	struct ip6_ctx ip6;
 
-- 
2.54.0


  parent reply	other threads:[~2026-06-17 13:23 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-17 13:22 [PATCH v4 0/4] Add --dhcp-boot and --dhcp-opt options Anshu Kumari
2026-06-17 13:22 ` [PATCH v4 1/4] dhcp: Refactor fill_one() to operate on a generic buffer Anshu Kumari
2026-06-17 13:22 ` [PATCH v4 2/4] dhcp: Add option overload Anshu Kumari
2026-06-17 13:22 ` Anshu Kumari [this message]
2026-06-17 13:22 ` [PATCH v4 4/4] conf: Add --dhcp-boot command-line option Anshu Kumari

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260617132243.1499556-4-anskuma@redhat.com \
    --to=anskuma@redhat.com \
    --cc=david@gibson.dropbear.id.au \
    --cc=jmaloy@redhat.com \
    --cc=lvivier@redhat.com \
    --cc=passt-dev@passt.top \
    --cc=sbrivio@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://passt.top/passt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).