pxe: support pxe clients with custom vendor-class

From 606d638918edb0e0ec07fe27eb68d06fb5ebd981 Mon Sep 17 00:00:00 2001
From: Miao Wang <shankerwangmiao@gmail.com>
Date: Fri, 4 Dec 2020 09:59:37 +0800
Subject: [PATCH v2] pxe: support pxe clients with custom vendor-class

According to UEFI[1] and PXE[2] specs, PXE clients are required to have
`PXEClient` identfier in the vendor-class field of DHCP requests, and
PXE servers should also include that identifier in their responses.
However, the firmware of servers from a few vendors[3] are customized to
include a different identifier. This patch adds an option named
`dhcp-pxe-vendor` to provide a list of such identifiers. The identifier
used in responses sent from dnsmasq is identical to that in the coresponding
request.

[1]: https://uefi.org/sites/default/files/resources/UEFI%20Spec%202.8B%20May%202020.pdf
[2]: http://www.pix.net/software/pxeboot/archive/pxespec.pdf
[3]: For instance, TaiShan servers from Huawei, which are Arm64-based,
       send `HW-Client` in PXE requests up to now.

Signed-off-by: Miao Wang <shankerwangmiao@gmail.com>
diff --git a/src/dnsmasq.h b/src/dnsmasq.h
index 4220798..4d78c37 100644
--- a/src/dnsmasq.h
+++ b/src/dnsmasq.h
@@ -829,6 +829,7 @@
 #define DHOPT_RFC3925         2048
 #define DHOPT_TAGOK           4096
 #define DHOPT_ADDR6           8192
+#define DHOPT_VENDOR_PXE     16384
 
 struct dhcp_boot {
   char *file, *sname, *tftp_sname;
@@ -852,6 +853,8 @@
   struct pxe_service *next;
 };
 
+#define DHCP_PXE_DEF_VENDOR      "PXEClient"
+
 #define MATCH_VENDOR     1
 #define MATCH_USER       2
 #define MATCH_CIRCUIT    3
@@ -867,6 +870,11 @@
   struct dhcp_vendor *next;
 };
 
+struct dhcp_pxe_vendor {
+  char *data;
+  struct dhcp_pxe_vendor *next;
+};
+
 struct dhcp_mac {
   unsigned int mask;
   int hwaddr_len, hwaddr_type;
@@ -1040,6 +1048,7 @@
   struct dhcp_config *dhcp_conf;
   struct dhcp_opt *dhcp_opts, *dhcp_match, *dhcp_opts6, *dhcp_match6;
   struct dhcp_match_name *dhcp_name_match;
+  struct dhcp_pxe_vendor *dhcp_pxe_vendors;
   struct dhcp_vendor *dhcp_vendors;
   struct dhcp_mac *dhcp_macs;
   struct dhcp_boot *boot_config;
diff --git a/src/option.c b/src/option.c
index dbe5f90..316d9c8 100644
--- a/src/option.c
+++ b/src/option.c
@@ -167,6 +167,7 @@
 #define LOPT_IGNORE_CLID   358
 #define LOPT_SINGLE_PORT   359
 #define LOPT_SCRIPT_TIME   360
+#define LOPT_PXE_VENDOR    361
  
 #ifdef HAVE_GETOPT_LONG
 static const struct option opts[] =  
@@ -270,6 +271,7 @@
     { "dhcp-circuitid", 1, 0, LOPT_CIRCUIT },
     { "dhcp-remoteid", 1, 0, LOPT_REMOTE },
     { "dhcp-subscrid", 1, 0, LOPT_SUBSCR },
+    { "dhcp-pxe-vendor", 1, 0, LOPT_PXE_VENDOR },
     { "interface-name", 1, 0, LOPT_INTNAME },
     { "dhcp-hostsfile", 1, 0, LOPT_DHCP_HOST },
     { "dhcp-optsfile", 1, 0, LOPT_DHCP_OPTS },
@@ -383,6 +385,7 @@
   { LOPT_CIRCUIT, ARG_DUP, "set:<tag>,<circuit>", gettext_noop("Map RFC3046 circuit-id to tag."), NULL },
   { LOPT_REMOTE, ARG_DUP, "set:<tag>,<remote>", gettext_noop("Map RFC3046 remote-id to tag."), NULL },
   { LOPT_SUBSCR, ARG_DUP, "set:<tag>,<remote>", gettext_noop("Map RFC3993 subscriber-id to tag."), NULL },
+  { LOPT_PXE_VENDOR, ARG_DUP, "<vendor>[,...]", gettext_noop("Specify vendor class to match for PXE requests."), NULL },
   { 'J', ARG_DUP, "tag:<tag>...", gettext_noop("Don't do DHCP for hosts with tag set."), NULL },
   { LOPT_BROADCAST, ARG_DUP, "[=tag:<tag>...]", gettext_noop("Force broadcast replies for hosts with tag set."), NULL }, 
   { 'k', OPT_NO_FORK, NULL, gettext_noop("Do NOT fork into the background, do NOT run in debug mode."), NULL },
@@ -3672,8 +3675,8 @@
 	     new->val = opt_malloc(new->len);
 	     memcpy(new->val + 1, arg, new->len - 1);
 	     
-	     new->u.vendor_class = (unsigned char *)"PXEClient";
-	     new->flags = DHOPT_VENDOR;
+	     new->u.vendor_class = NULL;
+	     new->flags = DHOPT_VENDOR | DHOPT_VENDOR_PXE;
 	     
 	     if (comma && atoi_check(comma, &timeout))
 	       *(new->val) = timeout;
@@ -3935,6 +3938,19 @@
 	new->next = daemon->override_relays;
 	daemon->override_relays = new;
 	arg = comma;
+	}
+	  break;
+
+    case LOPT_PXE_VENDOR: /* --dhcp-pxe-vendor */
+      {
+        while (arg) {
+	  struct dhcp_pxe_vendor *new = opt_malloc(sizeof(struct dhcp_pxe_vendor));
+	  comma = split(arg);
+          new->data = opt_string_alloc(arg);
+	  new->next = daemon->dhcp_pxe_vendors;
+	  daemon->dhcp_pxe_vendors = new;
+	  arg = comma;
+	}
       }
       break;
 
@@ -5212,6 +5228,13 @@
       strcat(buff, daemon->authserver);
       daemon->hostmaster = opt_string_alloc(buff);
     }
+
+  if (!daemon->dhcp_pxe_vendors)
+    {
+      daemon->dhcp_pxe_vendors = opt_malloc(sizeof(struct dhcp_pxe_vendor));
+      daemon->dhcp_pxe_vendors->data = opt_string_alloc(DHCP_PXE_DEF_VENDOR);
+      daemon->dhcp_pxe_vendors->next = NULL;
+    }
   
   /* only one of these need be specified: the other defaults to the host-name */
   if (option_bool(OPT_LOCALMX) || daemon->mxnames || daemon->mxtarget)
diff --git a/src/rfc2131.c b/src/rfc2131.c
index fc54aab..d678068 100644
--- a/src/rfc2131.c
+++ b/src/rfc2131.c
@@ -30,7 +30,7 @@
 static unsigned int calc_time(struct dhcp_context *context, struct dhcp_config *config, unsigned char *opt);
 static void option_put(struct dhcp_packet *mess, unsigned char *end, int opt, int len, unsigned int val);
 static void option_put_string(struct dhcp_packet *mess, unsigned char *end, 
-			      int opt, char *string, int null_term);
+			      int opt, const char *string, int null_term);
 static struct in_addr option_addr(unsigned char *opt);
 static unsigned int option_uint(unsigned char *opt, int offset, int size);
 static void log_packet(char *type, void *addr, unsigned char *ext_mac, 
@@ -54,17 +54,19 @@
 		       int vendor_class_len,
 		       time_t now,
 		       unsigned int lease_time,
-		       unsigned short fuzz);
+		       unsigned short fuzz,
+		       const char *pxevendor);
 
 
 static void match_vendor_opts(unsigned char *opt, struct dhcp_opt *dopt); 
 static int do_encap_opts(struct dhcp_opt *opt, int encap, int flag, struct dhcp_packet *mess, unsigned char *end, int null_term);
-static void pxe_misc(struct dhcp_packet *mess, unsigned char *end, unsigned char *uuid);
+static void pxe_misc(struct dhcp_packet *mess, unsigned char *end, unsigned char *uuid, const char *pxevendor);
 static int prune_vendor_opts(struct dhcp_netid *netid);
 static struct dhcp_opt *pxe_opts(int pxe_arch, struct dhcp_netid *netid, struct in_addr local, time_t now);
 struct dhcp_boot *find_boot(struct dhcp_netid *netid);
 static int pxe_uefi_workaround(int pxe_arch, struct dhcp_netid *netid, struct dhcp_packet *mess, struct in_addr local, time_t now, int pxe);
 static void apply_delay(u32 xid, time_t recvtime, struct dhcp_netid *netid);
+static int is_pxe_client(struct dhcp_packet *mess, size_t sz, const char **pxe_vendor);
 
 size_t dhcp_reply(struct dhcp_context *context, char *iface_name, int int_index,
 		  size_t sz, time_t now, int unicast_dest, int loopback,
@@ -76,6 +78,7 @@
   struct dhcp_mac *mac;
   struct dhcp_netid_list *id_list;
   int clid_len = 0, ignore = 0, do_classes = 0, rapid_commit = 0, selecting = 0, pxearch = -1;
+  const char *pxevendor = NULL;
   struct dhcp_packet *mess = (struct dhcp_packet *)daemon->dhcp_packet.iov_base;
   unsigned char *end = (unsigned char *)(mess + 1); 
   unsigned char *real_end = (unsigned char *)(mess + 1); 
@@ -647,7 +650,7 @@
 	      
 	      clear_packet(mess, end);
 	      do_options(context, mess, end, NULL, hostname, get_domain(mess->yiaddr), 
-			 netid, subnet_addr, 0, 0, -1, NULL, vendor_class_len, now, 0xffffffff, 0);
+			 netid, subnet_addr, 0, 0, -1, NULL, vendor_class_len, now, 0xffffffff, 0, NULL);
 	    }
 	}
       
@@ -835,9 +838,8 @@
     clid = NULL;
           
   /* Check if client is PXE client. */
-  if (daemon->enable_pxe && 
-      (opt = option_find(mess, sz, OPTION_VENDOR_ID, 9)) && 
-      strncmp(option_ptr(opt, 0), "PXEClient", 9) == 0)
+  if (daemon->enable_pxe &&
+      is_pxe_client(mess, sz, &pxevendor))
     {
       if ((opt = option_find(mess, sz, OPTION_PXE_UUID, 17)))
 	{
@@ -899,7 +901,7 @@
 	  
 	  option_put(mess, end, OPTION_MESSAGE_TYPE, 1, DHCPACK);
 	  option_put(mess, end, OPTION_SERVER_IDENTIFIER, INADDRSZ, htonl(context->local.s_addr));
-	  pxe_misc(mess, end, uuid);
+	  pxe_misc(mess, end, uuid, pxevendor);
 	  
 	  prune_vendor_opts(tagif_netid);
 	  opt71.val = save71;
@@ -979,7 +981,7 @@
 		  option_put(mess, end, OPTION_MESSAGE_TYPE, 1, 
 			     mess_type == DHCPDISCOVER ? DHCPOFFER : DHCPACK);
 		  option_put(mess, end, OPTION_SERVER_IDENTIFIER, INADDRSZ, htonl(tmp->local.s_addr));
-		  pxe_misc(mess, end, uuid);
+		  pxe_misc(mess, end, uuid, pxevendor);
 		  prune_vendor_opts(tagif_netid);
 		  if ((pxe && !workaround) || !redirect4011)
 		    do_encap_opts(pxe_opts(pxearch, tagif_netid, tmp->local, now), OPTION_VENDOR_CLASS_OPT, DHOPT_VENDOR_MATCH, mess, end, 0);
@@ -1150,7 +1152,7 @@
       option_put(mess, end, OPTION_LEASE_TIME, 4, time);
       /* T1 and T2 are required in DHCPOFFER by HP's wacky Jetdirect client. */
       do_options(context, mess, end, req_options, offer_hostname, get_domain(mess->yiaddr), 
-		 netid, subnet_addr, fqdn_flags, borken_opt, pxearch, uuid, vendor_class_len, now, time, fuzz);
+		 netid, subnet_addr, fqdn_flags, borken_opt, pxearch, uuid, vendor_class_len, now, time, fuzz, pxevendor);
       
       return dhcp_packet_size(mess, agent_id, real_end);
 	
@@ -1499,7 +1501,7 @@
 	  if (rapid_commit)
 	     option_put(mess, end, OPTION_RAPID_COMMIT, 0, 0);
 	   do_options(context, mess, end, req_options, hostname, get_domain(mess->yiaddr), 
-		     netid, subnet_addr, fqdn_flags, borken_opt, pxearch, uuid, vendor_class_len, now, time, fuzz);
+		     netid, subnet_addr, fqdn_flags, borken_opt, pxearch, uuid, vendor_class_len, now, time, fuzz, pxevendor);
 	}
 
       return dhcp_packet_size(mess, agent_id, real_end); 
@@ -1566,7 +1568,7 @@
 	}
 
       do_options(context, mess, end, req_options, hostname, get_domain(mess->ciaddr),
-		 netid, subnet_addr, fqdn_flags, borken_opt, pxearch, uuid, vendor_class_len, now, 0xffffffff, 0);
+		 netid, subnet_addr, fqdn_flags, borken_opt, pxearch, uuid, vendor_class_len, now, 0xffffffff, 0, pxevendor);
       
       *is_inform = 1; /* handle reply differently */
       return dhcp_packet_size(mess, agent_id, real_end); 
@@ -1948,7 +1950,7 @@
 }
 
 static void option_put_string(struct dhcp_packet *mess, unsigned char *end, int opt, 
-			      char *string, int null_term)
+			      const char *string, int null_term)
 {
   unsigned char *p;
   size_t len = strlen(string);
@@ -2026,15 +2028,32 @@
       dopt->flags &= ~DHOPT_VENDOR_MATCH;
       if (opt && (dopt->flags & DHOPT_VENDOR))
 	{
-	  int i, len = 0;
-	  if (dopt->u.vendor_class)
-	    len = strlen((char *)dopt->u.vendor_class);
-	  for (i = 0; i <= (option_len(opt) - len); i++)
-	    if (len == 0 || memcmp(dopt->u.vendor_class, option_ptr(opt, i), len) == 0)
-	      {
-		dopt->flags |= DHOPT_VENDOR_MATCH;
-		break;
-	      }
+	  const struct dhcp_pxe_vendor *pv;
+	  struct dhcp_pxe_vendor dummy_vendor = {
+	    .data = (char *)dopt->u.vendor_class,
+	    .next = NULL,
+	  };
+	  if (dopt->flags & DHOPT_VENDOR_PXE)
+	    pv = daemon->dhcp_pxe_vendors;
+	  else
+	    pv = &dummy_vendor;
+	  for (; pv; pv = pv->next)
+	    {
+	      int i, len = 0, matched = 0;
+	      if (pv->data)
+	        len = strlen(pv->data);
+	      for (i = 0; i <= (option_len(opt) - len); i++)
+	        if (len == 0 || memcmp(pv->data, option_ptr(opt, i), len) == 0)
+	          {
+		    matched = 1;
+	            break;
+	          }
+	      if (matched)
+		{
+	          dopt->flags |= DHOPT_VENDOR_MATCH;
+		  break;
+		}
+	    }
 	}
     }
 }
@@ -2087,11 +2106,13 @@
   return ret;
 }
 
-static void pxe_misc(struct dhcp_packet *mess, unsigned char *end, unsigned char *uuid)
+static void pxe_misc(struct dhcp_packet *mess, unsigned char *end, unsigned char *uuid, const char *pxevendor)
 {
   unsigned char *p;
 
-  option_put_string(mess, end, OPTION_VENDOR_ID, "PXEClient", 0);
+  if (!pxevendor)
+    pxevendor="PXEClient";
+  option_put_string(mess, end, OPTION_VENDOR_ID, pxevendor, 0);
   if (uuid && (p = free_space(mess, end, OPTION_PXE_UUID, 17)))
     memcpy(p, uuid, 17);
 }
@@ -2308,6 +2329,29 @@
   return boot;
 }
 
+static int is_pxe_client(struct dhcp_packet *mess, size_t sz, const char **pxe_vendor)
+{
+  const unsigned char *opt = NULL;
+  ssize_t conf_len = 0;
+  const struct dhcp_pxe_vendor *conf = daemon->dhcp_pxe_vendors;
+  opt = option_find(mess, sz, OPTION_VENDOR_ID, 0);
+  if (!opt) 
+    return 0;
+  for (; conf; conf = conf->next)
+    {
+      conf_len = strlen(conf->data);
+      if (option_len(opt) < conf_len)
+        continue;
+      if (strncmp(option_ptr(opt, 0), conf->data, conf_len) == 0)
+        {
+          if (pxe_vendor)
+            *pxe_vendor = conf->data;
+          return 1;
+        }
+    }
+  return 0;
+}
+
 static void do_options(struct dhcp_context *context,
 		       struct dhcp_packet *mess,
 		       unsigned char *end, 
@@ -2322,7 +2366,8 @@
 		       int vendor_class_len,
 		       time_t now,
 		       unsigned int lease_time,
-		       unsigned short fuzz)
+		       unsigned short fuzz,
+		       const char *pxevendor)
 {
   struct dhcp_opt *opt, *config_opts = daemon->dhcp_opts;
   struct dhcp_boot *boot;
@@ -2696,7 +2741,7 @@
   
   if (context && pxe_arch != -1)
     {
-      pxe_misc(mess, end, uuid);
+      pxe_misc(mess, end, uuid, pxevendor);
       if (!pxe_uefi_workaround(pxe_arch, tagif, mess, context->local, now, 0))
 	config_opts = pxe_opts(pxe_arch, tagif, context->local, now);
     }