Print this page
7388 Support DHCP Client FQDN. Allow IAID/DUID for all v4.
*** 18,32 ****
--- 18,34 ----
*
* 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,52 ****
--- 35,61 ----
#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,74 ****
--- 74,89 ----
* 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,502 ****
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
--- 479,488 ----
*** 554,564 ****
dhcpmsg(MSG_WARNING,
"iffile_to_hostname:"
" host name too long");
return (NULL);
}
! if (valid_hostname(p)) {
return (p);
} else {
dhcpmsg(MSG_WARNING,
"iffile_to_hostname:"
" host name not valid");
--- 540,550 ----
dhcpmsg(MSG_WARNING,
"iffile_to_hostname:"
" host name too long");
return (NULL);
}
! if (ipadm_is_valid_hostname(p)) {
return (p);
} else {
dhcpmsg(MSG_WARNING,
"iffile_to_hostname:"
" host name not valid");
*** 702,707 ****
--- 688,1105 ----
"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);
}