Print this page
7388 Support DHCP Client FQDN. Allow IAID/DUID for all v4.

@@ -18,15 +18,17 @@
  *
  * CDDL HEADER END
  */
 /*
  * Copyright (c) 1999, 2010, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, Chris Fraire <cfraire@me.com>.
  */
 
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/stat.h>
+#include <sys/utsname.h>
 #include <stdlib.h>
 #include <netinet/in.h>         /* struct in_addr */
 #include <netinet/dhcp.h>
 #include <signal.h>
 #include <sys/socket.h>

@@ -33,20 +35,27 @@
 #include <net/route.h>
 #include <net/if_arp.h>
 #include <string.h>
 #include <dhcpmsg.h>
 #include <ctype.h>
+#include <arpa/inet.h>
+#include <arpa/nameser.h>
+#include <resolv.h>
 #include <netdb.h>
 #include <fcntl.h>
 #include <stdio.h>
 #include <dhcp_hostconf.h>
+#include <limits.h>
+#include <strings.h>
+#include <libipadm.h>
 
 #include "states.h"
 #include "agent.h"
 #include "interface.h"
 #include "util.h"
 #include "packet.h"
+#include "defaults.h"
 
 /*
  * this file contains utility functions that have no real better home
  * of their own.  they can largely be broken into six categories:
  *

@@ -65,10 +74,16 @@
  *  o  routing table manipulation functions
  *
  *  o  true miscellany -- anything else
  */
 
+#define ETCNODENAME             "/etc/nodename"
+#define ETCDEFAULTDOMAIN        "/etc/defaultdomain"
+
+static  boolean_t       is_fqdn(const char *);
+static int                      dhcp_assemble_fqdn(dhcp_smach_t *dsmp);
+
 /*
  * pkt_type_to_string(): stringifies a packet type
  *
  *   input: uchar_t: a DHCP packet type value, RFC 2131 or 3315
  *          boolean_t: B_TRUE if IPv6

@@ -464,39 +479,10 @@
 
         return (bind(fd, (struct sockaddr *)&sin6, sizeof (sin6)) == 0);
 }
 
 /*
- * valid_hostname(): check whether a string is a valid hostname
- *
- *   input: const char *: the string to verify as a hostname
- *  output: boolean_t: B_TRUE if the string is a valid hostname
- *
- * Note that we accept both host names beginning with a digit and
- * those containing hyphens.  Neither is strictly legal according
- * to the RFCs, but both are in common practice, so we endeavour
- * to not break what customers are using.
- */
-
-static boolean_t
-valid_hostname(const char *hostname)
-{
-        unsigned int i;
-
-        for (i = 0; hostname[i] != '\0'; i++) {
-
-                if (isalpha(hostname[i]) || isdigit(hostname[i]) ||
-                    (((hostname[i] == '-') || (hostname[i] == '.')) && (i > 0)))
-                        continue;
-
-                return (B_FALSE);
-        }
-
-        return (i > 0);
-}
-
-/*
  * iffile_to_hostname(): return the hostname contained on a line of the form
  *
  * [ ^I]*inet[ ^I]+hostname[\n]*\0
  *
  * in the file located at the specified path

@@ -554,11 +540,11 @@
                                         dhcpmsg(MSG_WARNING,
                                             "iffile_to_hostname:"
                                             " host name too long");
                                         return (NULL);
                                 }
-                                if (valid_hostname(p)) {
+                                if (ipadm_is_valid_hostname(p)) {
                                         return (p);
                                 } else {
                                         dhcpmsg(MSG_WARNING,
                                             "iffile_to_hostname:"
                                             " host name not valid");

@@ -702,6 +688,418 @@
                     "system; not saving lease", hcfile);
         } else {
                 dhcpmsg(MSG_ERR, "cannot write %s (reboot will "
                     "not use cached configuration)", hcfile);
         }
+}
+
+/*
+ * Try to get a string from the first line of a file, up to but not
+ * including any space (0x20) or newline.
+ */
+
+static int
+dhcp_get_oneline(const char *filename, char *buf, size_t buflen)
+{
+        char    value[SYS_NMLN], *c;
+        int             fd, i;
+
+        if ((fd = open(filename, O_RDONLY)) <= 0) {
+                dhcpmsg(MSG_DEBUG, "dhcp_get_oneline: could not open %s",
+                    filename);
+                *buf = '\0';
+        } else {
+                if ((i = read(fd, value, SYS_NMLN - 1)) <= 0) {
+                        dhcpmsg(MSG_WARNING, "dhcp_get_oneline: no line in %s",
+                            filename);
+                        *buf = '\0';
+                } else {
+                        value[i] = '\0';
+                        if ((c = strchr(value, '\n')) != NULL)
+                                *c = '\0';
+                        if ((c = strchr(value, ' ')) != NULL)
+                                *c = '\0';
+
+                        if (strlcpy(buf, value, buflen) >= buflen) {
+                                dhcpmsg(MSG_WARNING, "dhcp_get_oneline: too long value, %s",
+                                    value);
+                                *buf = '\0';
+                        }
+                }
+                (void) close(fd);
+        }
+
+        return (*buf != '\0' ? 0 : -1);
+}
+
+/*
+ * Try to get the hostname from the /etc/nodename file. uname(2) cannot
+ * be used, because that is initialized after DHCP has solicited, in order
+ * to allow for the possibility that utsname.nodename can be set from
+ * DHCP Hostname. Here, though, we want to send a value specified
+ * advance of DHCP, so read /etc/nodename directly.
+ */
+
+static int
+dhcp_get_nodename(char *buf, size_t buflen)
+{
+        return dhcp_get_oneline(ETCNODENAME, buf, buflen);
+}
+
+/*
+ * Try to get the value from the /etc/defaultdomain file. libnsl's
+ * domainname() cannot be used, because that is initialized after DHCP has
+ * solicited. Here, though, we want to send a value specified in advance
+ * of DHCP, so read /etc/defaultdomain directly.
+ */
+
+static int
+dhcp_get_defaultdomain(char *buf, size_t buflen)
+{
+        return dhcp_get_oneline(ETCDEFAULTDOMAIN, buf, buflen);
+}
+
+/*
+ * dhcp_add_hostname_opt(): Set CD_HOSTNAME option if REQUEST_HOSTNAME is
+ *                       affirmative and if 1) dsm_msg_reqhost is available; or
+ *                       2) hostname is read from an extant /etc/hostname.<ifname>
+ *                       file; or 3) interface is primary and nodename(4) is defined.
+ *
+ *   input: dhcp_pkt_t *: pointer to DHCP message being constructed;
+ *          dhcp_smach_t *: pointer to interface DHCP state machine;
+ */
+
+int
+dhcp_add_hostname_opt(dhcp_pkt_t *dpkt, dhcp_smach_t *dsmp)
+{
+        const char      *reqhost;
+        char    nodename[MAXNAMELEN];
+
+        if (!df_get_bool(dsmp->dsm_name, dsmp->dsm_isv6, DF_REQUEST_HOSTNAME))
+                return (0);
+
+        dhcpmsg(MSG_DEBUG, "dhcp_add_hostname_opt: DF_REQUEST_HOSTNAME");
+
+        if (dsmp->dsm_msg_reqhost != NULL
+            && ipadm_is_valid_hostname(dsmp->dsm_msg_reqhost)) {
+                reqhost = dsmp->dsm_msg_reqhost;
+        } else {
+                char            hostfile[PATH_MAX + 1];
+
+                (void) snprintf(hostfile, sizeof (hostfile),
+                    "/etc/hostname.%s", dsmp->dsm_name);
+                reqhost = iffile_to_hostname(hostfile);
+        }
+
+        if (reqhost == NULL && (dsmp->dsm_dflags & DHCP_IF_PRIMARY) &&
+            dhcp_get_nodename(nodename, sizeof (nodename)) == 0) {
+                reqhost = nodename;
+        }
+
+        if (reqhost != NULL) {
+                free(dsmp->dsm_reqhost);
+                if ((dsmp->dsm_reqhost = strdup(reqhost)) == NULL)
+                        dhcpmsg(MSG_WARNING, "dhcp_add_hostname_opt: cannot allocate "
+                            "memory for host name option");
+        }
+
+        if (dsmp->dsm_reqhost != NULL) {
+                dhcpmsg(MSG_DEBUG, "dhcp_add_hostname_opt: host %s for %s",
+                    dsmp->dsm_reqhost, dsmp->dsm_name);
+                (void) add_pkt_opt(dpkt, CD_HOSTNAME, dsmp->dsm_reqhost,
+                    strlen(dsmp->dsm_reqhost));
+                return 1;
+        }
+        else {
+                dhcpmsg(MSG_DEBUG, "dhcp_add_hostname_opt: no hostname for %s",
+                    dsmp->dsm_name);
+        }
+
+        return (0);
+}
+
+/*
+ * dhcp_add_fqdn_opt(): Set CD_CLIENTFQDN option if dsm_reqfqdn is not NULL
+ *                       or if dhcp_assemble_fqdn() initializes it. If dsm_reqfqdn
+ *                       cannot be set, no option will be added.
+ *
+ *   input: dhcp_pkt_t *: pointer to DHCP message being constructed;
+ *          dhcp_smach_t *: pointer to interface DHCP state machine;
+ *  output: 0 if a CLIENT_FQDN was added; non-zero if otherwise;
+ */
+
+int
+dhcp_add_fqdn_opt(dhcp_pkt_t *dpkt, dhcp_smach_t *dsmp)
+{
+        /*
+         * RFC 4702 section 2:
+         *
+         * The format of the Client FQDN option is:
+         *
+         *  Code   Len    Flags  RCODE1 RCODE2   Domain Name
+         * +------+------+------+------+------+------+--
+         * |  81  |   n  |      |      |      |       ...
+         * +------+------+------+------+------+------+--
+         *
+         * Code and Len are distinct, and the remainder is in a single buffer,
+         * opt81, for Flags + (unused) RCODE1 and RCODE2 (all octets) and a
+         * potentially maximum-length domain name.
+         *
+         * The format of the Flags field is:
+         *
+         *  0 1 2 3 4 5 6 7
+         * +-+-+-+-+-+-+-+-+
+         * |  MBZ  |N|E|O|S|
+         * +-+-+-+-+-+-+-+-+
+         *
+         * where MBZ is ignored and NEOS are:
+         *
+         * S = 1 to request that "the server SHOULD perform the A RR (FQDN-to-
+         * address) DNS updates;
+         *
+         * O = 0, for a server-only response bit;
+         *
+         * E = 1 to indicate the domain name is in "canonical wire format,
+         * without compression (i.e., ns_name_pton2) ....  This encoding SHOULD
+         * be used by clients ....";
+         *
+         * N = 0 to request that "the server SHALL perform DNS updates [of the
+         * PTR RR]." (1 would request SHALL NOT update).
+         *
+         * The format of the DHCPv6 Client FQDN option is shown below:
+         *
+         *     0                   1                   2                   3
+         *     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+         *    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         *    |          OPTION_FQDN          |         option-len            |
+         *    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         *    |   flags       |                                               |
+         *    +-+-+-+-+-+-+-+-+                                               |
+         *    .                                                               .
+         *    .                          domain-name                          .
+         *    .                                                               .
+         *    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         *
+         *      option-code      OPTION_CLIENT_FQDN (39)
+         *
+         *      option-len       1 + length of domain name
+         *
+         *      flags            flag bits used between client and server to
+         *                       negotiate who performs which updates
+         *
+         *      domain-name      the partial or fully qualified domain name
+         *                       (with length option-len - 1)
+         *
+         *    The DHCPv6 format of the Flags field is:
+         *
+         *         0 1 2 3 4 5 6 7
+         *        +-+-+-+-+-+-+-+-+
+         *        |  MBZ    |N|O|S|
+         *        +-+-+-+-+-+-+-+-+
+         */
+
+        const uint8_t   S_BITPOS = 7 - 7;
+        const uint8_t   E_BITPOS = 7 - 5;
+        const size_t    OPT_FQDN_METALEN = 3, OPT_V6_FQDN_METALEN = 1;
+        uint_t          fqdncode;
+        u_char          enc_fqdnbuf[MAXNAMELEN];
+        uint8_t         fqdnopt[MAXNAMELEN + OPT_FQDN_METALEN];
+        size_t          len, metalen;
+
+        if (dsmp->dsm_reqfqdn == NULL && dhcp_assemble_fqdn(dsmp) != 0)
+                return (-1);
+
+        /* encode the FQDN in canonical wire format */
+
+        if (ns_name_pton2(dsmp->dsm_reqfqdn, enc_fqdnbuf, sizeof (enc_fqdnbuf),
+            &len) < 0) {
+                dhcpmsg(MSG_WARNING, "dhcp_add_fqdn_opt: error encoding domain"
+                    " name %s", dsmp->dsm_reqfqdn);
+                return (-1);
+        }
+
+        dhcpmsg(MSG_DEBUG, "dhcp_add_fqdn_opt: interface FQDN is %s"
+            " for %s", dsmp->dsm_reqfqdn, dsmp->dsm_name);
+
+        bzero(fqdnopt, sizeof (fqdnopt));
+        if (dsmp->dsm_isv6) {
+                fqdncode = DHCPV6_OPT_CLIENT_FQDN;
+                metalen = OPT_V6_FQDN_METALEN;
+                *fqdnopt = (uint8_t)(1 << S_BITPOS);
+        } else {
+                fqdncode = CD_CLIENTFQDN;
+                metalen = OPT_FQDN_METALEN;
+                *fqdnopt = (uint8_t)((1 << S_BITPOS) | (1 << E_BITPOS));
+        }
+        (void) memcpy(fqdnopt + metalen, enc_fqdnbuf, len);
+        (void) add_pkt_opt(dpkt, fqdncode, fqdnopt, metalen + len);
+
+        return (0);
+}
+
+/*
+ * dhcp_assemble_fqdn(): Set dsm_reqfqdn if REQUEST_FQDN is set and
+ *                       either a host name was sent in the IPC message (e.g., from
+ *                       ipadm(1M) -h,--reqhost) or the interface is primary and a
+ *                       nodename(4) is defined. If the host name is not already fully
+ *                       qualified per is_fqdn(), then a value from
+ *                       dhcp_get_defaultdomain() or from resolv.conf(4)--if defined--
+ *                       is used to construct an FQDN.
+ *
+ *                       If no FQDN can be determined, dsm_reqfqdn will be NULL.
+ *
+ *   input: dhcp_smach_t *: pointer to interface DHCP state machine;
+ *  output: 0 if dsm_reqfqdn was assigned; non-zero if otherwise;
+ */
+
+static int
+dhcp_assemble_fqdn(dhcp_smach_t *dsmp)
+{
+        char            fqdnbuf[MAXNAMELEN], nodename[MAXNAMELEN], *reqhost;
+        size_t          pos, len;
+
+        if (dsmp->dsm_reqfqdn != NULL) {
+                free(dsmp->dsm_reqfqdn);
+                dsmp->dsm_reqfqdn = NULL;
+        }
+
+        if (!df_get_bool(dsmp->dsm_name, dsmp->dsm_isv6, DF_REQUEST_FQDN))
+                return (-1);
+
+        dhcpmsg(MSG_DEBUG, "dhcp_assemble_fqdn: DF_REQUEST_FQDN");
+
+        bzero(fqdnbuf, sizeof (fqdnbuf));
+
+        reqhost = dsmp->dsm_msg_reqhost;
+        if (ipadm_is_nil_hostname(reqhost) &&
+            (dsmp->dsm_dflags & DHCP_IF_PRIMARY) &&
+            dhcp_get_nodename(nodename, sizeof (nodename)) == 0) {
+                reqhost = nodename;
+        }
+
+        if (ipadm_is_nil_hostname(reqhost)) {
+                dhcpmsg(MSG_DEBUG,
+                    "dhcp_assemble_fqdn: no interface reqhost for %s",
+                    dsmp->dsm_name);
+                return (-1);
+        }
+
+        if ((pos = strlcpy(fqdnbuf, reqhost, sizeof (fqdnbuf))) >=
+            sizeof (fqdnbuf)) {
+                dhcpmsg(MSG_WARNING, "dhcp_assemble_fqdn: too long reqhost %s"
+                    " for %s", reqhost, dsmp->dsm_name);
+                return (-1);
+        }
+
+        if (!is_fqdn(reqhost)) {
+                char            dnamebuf[MAXHOSTNAMELEN];
+                size_t          needdots;
+                int                     lasterrno;
+                char            *domainname = NULL;
+
+                /*
+                 * determine FQDN domain name, if possible
+                 */
+
+                if (dhcp_get_defaultdomain(dnamebuf, sizeof (dnamebuf)) == 0 &&
+                    !ipadm_is_nil_hostname(dnamebuf)) {
+                        domainname = dnamebuf;
+                } else {
+                        /*
+                         * fall back to resolv's "default domain (deprecated)"
+                         */
+
+                        struct __res_state      res_state;
+                        bzero(&res_state, sizeof (struct __res_state));
+
+                        /* initialize resolver or warn */
+                        if ((lasterrno = res_ninit(&res_state)) != 0) {
+                                dhcpmsg(MSG_WARNING, "dhcp_assemble_fqdn: error %d"
+                                    " initializing resolver", lasterrno);
+                        }
+                        else {
+                                if (*res_state.defdname != '\0') {
+                                        (void) strlcpy(dnamebuf, res_state.defdname,
+                                            sizeof (dnamebuf));
+                                        domainname = dnamebuf;
+                                }
+                                res_ndestroy(&res_state);
+                        }
+                }
+
+                if (ipadm_is_nil_hostname(domainname)) {
+                        dhcpmsg(MSG_DEBUG, "dhcp_assemble_fqdn: no domain name for %s",
+                            dsmp->dsm_name);
+                        return (-1);
+                }
+
+                /*
+                 * Finish constructing FQDN. Account for space needed to hold a
+                 * separator '.' and a terminating '.'.
+                 */
+                len = strlen(domainname);
+                needdots = 1 + (domainname[len - 1] != '.');
+
+                if (pos + len + needdots >= sizeof (fqdnbuf)) {
+                        dhcpmsg(MSG_WARNING, "dhcp_assemble_fqdn: too long FQDN %s.%s"
+                            " for %s", fqdnbuf, domainname, dsmp->dsm_name);
+                        return (-1);
+                }
+
+                /* add separator and then domain name */
+                fqdnbuf[pos++] = '.';
+                (void) strlcpy(fqdnbuf + pos, domainname, sizeof (fqdnbuf) - pos);
+                pos += len;
+
+                /* ensure the final character is '.' */
+                if (needdots > 1)
+                        fqdnbuf[pos++] = '.'; /* following is already zeroed */
+        }
+
+        if (!ipadm_is_valid_hostname(fqdnbuf)) {
+                dhcpmsg(MSG_WARNING, "dhcp_assemble_fqdn: invalid FQDN %s for %s",
+                    fqdnbuf, dsmp->dsm_name);
+                return (-1);
+        }
+
+        free(dsmp->dsm_reqfqdn);
+        if ((dsmp->dsm_reqfqdn = strdup(fqdnbuf)) == NULL) {
+                dhcpmsg(MSG_WARNING, "dhcp_assemble_fqdn: cannot allocate memory");
+                return (-1);
+        }
+        return (0);
+}
+
+/*
+ * is_fqdn() : Determine if the `hostname' can be considered as a Fully
+ *                      Qualified Domain Name by being "rooted" (i.e., ending in '.')
+ *                      or by containing at least three DNS labels (e.g.,
+ *                      srv.example.com).
+ *
+ *   input: const char *: the hostname to inspect;
+ *  output: boolean_t: B_TRUE if `hostname' is not NULL satisfies the
+ *                      criteria above; otherwise, B_FALSE;
+ */
+
+boolean_t
+is_fqdn(const char *hostname)
+{
+        const char *c;
+        size_t i;
+
+        if (hostname == NULL)
+                return (B_FALSE);
+
+        i = strlen(hostname);
+        if (i > 0 && hostname[i - 1] == '.')
+                return (B_TRUE);
+
+        c = hostname;
+        i = 0;
+        while ((c = strchr(c, '.')) != NULL) {
+                ++i;
+                ++c;
+        }
+
+        /* at least two separators is inferred to be fully-qualified */
+        return (i >= 2);
 }