/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright 2010 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SMB_ADS_AF_UNKNOWN(x) (((x)->ipaddr.a_family != AF_INET) && \ ((x)->ipaddr.a_family != AF_INET6)) #define SMB_ADS_MAXBUFLEN 100 #define SMB_ADS_DN_MAX 300 #define SMB_ADS_MAXMSGLEN 512 #define SMB_ADS_COMPUTERS_CN "Computers" #define SMB_ADS_COMPUTER_NUM_ATTR 8 #define SMB_ADS_SHARE_NUM_ATTR 3 #define SMB_ADS_SITE_MAX MAXHOSTNAMELEN /* * [MS-DISO] A machine password is an ASCII string of randomly chosen * characters. Each character's ASCII code is between 32 and 122 inclusive. */ #define SMB_ADS_PWD_CHAR_NUM 91 #define SMB_ADS_PWD_CHAR_START 32 #define SMB_ADS_MSDCS_SRV_DC_RR "_ldap._tcp.dc._msdcs" #define SMB_ADS_MSDCS_SRV_SITE_RR "_ldap._tcp.%s._sites.dc._msdcs" /* * domainControllerFunctionality * * This rootDSE attribute indicates the functional level of the DC. */ #define SMB_ADS_ATTR_DCLEVEL "domainControllerFunctionality" #define SMB_ADS_DCLEVEL_W2K 0 #define SMB_ADS_DCLEVEL_W2K3 2 #define SMB_ADS_DCLEVEL_W2K8 3 #define SMB_ADS_DCLEVEL_W2K8_R2 4 /* * msDs-supportedEncryptionTypes (Windows Server 2008 only) * * This attribute defines the encryption types supported by the system. * Encryption Types: * - DES cbc mode with CRC-32 * - DES cbc mode with RSA-MD5 * - ArcFour with HMAC/md5 * - AES-128 * - AES-256 */ #define SMB_ADS_ATTR_ENCTYPES "msDs-supportedEncryptionTypes" #define SMB_ADS_ENC_DES_CRC 1 #define SMB_ADS_ENC_DES_MD5 2 #define SMB_ADS_ENC_RC4 4 #define SMB_ADS_ENC_AES128 8 #define SMB_ADS_ENC_AES256 16 #define SMB_ADS_ATTR_SAMACCT "sAMAccountName" #define SMB_ADS_ATTR_UPN "userPrincipalName" #define SMB_ADS_ATTR_SPN "servicePrincipalName" #define SMB_ADS_ATTR_CTL "userAccountControl" #define SMB_ADS_ATTR_DNSHOST "dNSHostName" #define SMB_ADS_ATTR_KVNO "msDS-KeyVersionNumber" #define SMB_ADS_ATTR_DN "distinguishedName" /* * UserAccountControl flags: manipulate user account properties. * * The hexadecimal value of the following property flags are based on MSDN * article # 305144. */ #define SMB_ADS_USER_ACCT_CTL_SCRIPT 0x00000001 #define SMB_ADS_USER_ACCT_CTL_ACCOUNTDISABLE 0x00000002 #define SMB_ADS_USER_ACCT_CTL_HOMEDIR_REQUIRED 0x00000008 #define SMB_ADS_USER_ACCT_CTL_LOCKOUT 0x00000010 #define SMB_ADS_USER_ACCT_CTL_PASSWD_NOTREQD 0x00000020 #define SMB_ADS_USER_ACCT_CTL_PASSWD_CANT_CHANGE 0x00000040 #define SMB_ADS_USER_ACCT_CTL_ENCRYPTED_TEXT_PWD_ALLOWED 0x00000080 #define SMB_ADS_USER_ACCT_CTL_TMP_DUP_ACCT 0x00000100 #define SMB_ADS_USER_ACCT_CTL_NORMAL_ACCT 0x00000200 #define SMB_ADS_USER_ACCT_CTL_INTERDOMAIN_TRUST_ACCT 0x00000800 #define SMB_ADS_USER_ACCT_CTL_WKSTATION_TRUST_ACCT 0x00001000 #define SMB_ADS_USER_ACCT_CTL_SRV_TRUST_ACCT 0x00002000 #define SMB_ADS_USER_ACCT_CTL_DONT_EXPIRE_PASSWD 0x00010000 #define SMB_ADS_USER_ACCT_CTL_MNS_LOGON_ACCT 0x00020000 #define SMB_ADS_USER_ACCT_CTL_SMARTCARD_REQUIRED 0x00040000 #define SMB_ADS_USER_ACCT_CTL_TRUSTED_FOR_DELEGATION 0x00080000 #define SMB_ADS_USER_ACCT_CTL_NOT_DELEGATED 0x00100000 #define SMB_ADS_USER_ACCT_CTL_USE_DES_KEY_ONLY 0x00200000 #define SMB_ADS_USER_ACCT_CTL_DONT_REQ_PREAUTH 0x00400000 #define SMB_ADS_USER_ACCT_CTL_PASSWD_EXPIRED 0x00800000 #define SMB_ADS_USER_ACCT_CTL_TRUSTED_TO_AUTH_FOR_DELEGATION 0x01000000 /* * Length of "dc=" prefix. */ #define SMB_ADS_DN_PREFIX_LEN 3 static char *smb_ads_computer_objcls[] = { "top", "person", "organizationalPerson", "user", "computer", NULL }; static char *smb_ads_share_objcls[] = { "top", "leaf", "connectionPoint", "volume", NULL }; /* Cached ADS server to communicate with */ static smb_ads_host_info_t *smb_ads_cached_host_info = NULL; static mutex_t smb_ads_cached_host_mtx; /* * SMB ADS config cache is maintained to facilitate the detection of * changes in configuration that is relevant to AD selection. */ typedef struct smb_ads_config { char c_site[SMB_ADS_SITE_MAX]; smb_inaddr_t c_pdc; mutex_t c_mtx; } smb_ads_config_t; static smb_ads_config_t smb_ads_cfg; /* attribute/value pair */ typedef struct smb_ads_avpair { char *avp_attr; char *avp_val; } smb_ads_avpair_t; /* query status */ typedef enum smb_ads_qstat { SMB_ADS_STAT_ERR = -2, SMB_ADS_STAT_DUP, SMB_ADS_STAT_NOT_FOUND, SMB_ADS_STAT_FOUND } smb_ads_qstat_t; typedef struct smb_ads_host_list { int ah_cnt; smb_ads_host_info_t *ah_list; } smb_ads_host_list_t; static smb_ads_handle_t *smb_ads_open_main(char *, char *, char *); static int smb_ads_add_computer(smb_ads_handle_t *, int, char *); static int smb_ads_modify_computer(smb_ads_handle_t *, int, char *); static int smb_ads_computer_op(smb_ads_handle_t *, int, int, char *); static smb_ads_qstat_t smb_ads_lookup_computer_n_attr(smb_ads_handle_t *, smb_ads_avpair_t *, int, char *); static int smb_ads_update_computer_cntrl_attr(smb_ads_handle_t *, int, char *); static krb5_kvno smb_ads_lookup_computer_attr_kvno(smb_ads_handle_t *, char *); static int smb_ads_gen_machine_passwd(char *, size_t); static void smb_ads_free_cached_host(void); static int smb_ads_get_spnset(char *, char **); static void smb_ads_free_spnset(char **); static int smb_ads_alloc_attr(LDAPMod **, int); static void smb_ads_free_attr(LDAPMod **); static int smb_ads_get_dc_level(smb_ads_handle_t *); static smb_ads_host_info_t *smb_ads_select_dc(smb_ads_host_list_t *); static smb_ads_qstat_t smb_ads_find_computer(smb_ads_handle_t *, char *); static smb_ads_qstat_t smb_ads_getattr(LDAP *, LDAPMessage *, smb_ads_avpair_t *); static smb_ads_qstat_t smb_ads_get_qstat(smb_ads_handle_t *, LDAPMessage *, smb_ads_avpair_t *); static boolean_t smb_ads_match_pdc(smb_ads_host_info_t *); static boolean_t smb_ads_is_sought_host(smb_ads_host_info_t *, char *); static boolean_t smb_ads_is_same_domain(char *, char *); static boolean_t smb_ads_is_pdc_configured(void); static smb_ads_host_info_t *smb_ads_dup_host_info(smb_ads_host_info_t *); static char *smb_ads_get_sharedn(const char *, const char *, const char *); /* * smb_ads_init * * Initializes the ADS config cache. */ void smb_ads_init(void) { (void) mutex_lock(&smb_ads_cfg.c_mtx); (void) smb_config_getstr(SMB_CI_ADS_SITE, smb_ads_cfg.c_site, SMB_ADS_SITE_MAX); (void) smb_config_getip(SMB_CI_DOMAIN_SRV, &smb_ads_cfg.c_pdc); (void) mutex_unlock(&smb_ads_cfg.c_mtx); } void smb_ads_fini(void) { smb_ads_free_cached_host(); } /* * smb_ads_refresh * * This function will be called when smb/server SMF service is refreshed. * Clearing the smb_ads_cached_host_info would allow the next DC * discovery process to pick up an AD based on the new AD configuration. */ void smb_ads_refresh(void) { char new_site[SMB_ADS_SITE_MAX]; smb_inaddr_t new_pdc; boolean_t purge = B_FALSE; (void) smb_config_getstr(SMB_CI_ADS_SITE, new_site, SMB_ADS_SITE_MAX); (void) smb_config_getip(SMB_CI_DOMAIN_SRV, &new_pdc); (void) mutex_lock(&smb_ads_cfg.c_mtx); if (smb_strcasecmp(smb_ads_cfg.c_site, new_site, 0)) { (void) strlcpy(smb_ads_cfg.c_site, new_site, SMB_ADS_SITE_MAX); purge = B_TRUE; } smb_ads_cfg.c_pdc = new_pdc; (void) mutex_unlock(&smb_ads_cfg.c_mtx); (void) mutex_lock(&smb_ads_cached_host_mtx); if (smb_ads_cached_host_info && smb_ads_is_pdc_configured() && !smb_ads_match_pdc(smb_ads_cached_host_info)) purge = B_TRUE; (void) mutex_unlock(&smb_ads_cached_host_mtx); if (purge) smb_ads_free_cached_host(); } static boolean_t smb_ads_is_pdc_configured(void) { boolean_t configured; (void) mutex_lock(&smb_ads_cfg.c_mtx); configured = !smb_inet_iszero(&smb_ads_cfg.c_pdc); (void) mutex_unlock(&smb_ads_cfg.c_mtx); return (configured); } /* * smb_ads_build_unc_name * * Construct the UNC name of the share object in the format of * \\hostname.domain\shareUNC * * Returns 0 on success, -1 on error. */ int smb_ads_build_unc_name(char *unc_name, int maxlen, const char *hostname, const char *shareUNC) { char my_domain[MAXHOSTNAMELEN]; if (smb_getfqdomainname(my_domain, sizeof (my_domain)) != 0) return (-1); (void) snprintf(unc_name, maxlen, "\\\\%s.%s\\%s", hostname, my_domain, shareUNC); return (0); } /* * smb_ads_ldap_ping * * This is used to bind to an ADS server to see * if it is still alive. * * Returns: * -1: error * 0: successful */ /*ARGSUSED*/ static int smb_ads_ldap_ping(smb_ads_host_info_t *ads_host) { int ldversion = LDAP_VERSION3, status, timeoutms = 5 * 1000; LDAP *ld = NULL; ld = ldap_init(ads_host->name, ads_host->port); if (ld == NULL) return (-1); ldversion = LDAP_VERSION3; (void) ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ldversion); /* setup TCP/IP connect timeout */ (void) ldap_set_option(ld, LDAP_X_OPT_CONNECT_TIMEOUT, &timeoutms); status = ldap_bind_s(ld, "", NULL, LDAP_AUTH_SIMPLE); if (status != LDAP_SUCCESS) { (void) ldap_unbind(ld); return (-1); } (void) ldap_unbind(ld); return (0); } /* * The cached ADS host is no longer valid if one of the following criteria * is satisfied: * * 1) not in the specified domain * 2) not the sought host (if specified) * 3) not reachable * * The caller is responsible for acquiring the smb_ads_cached_host_mtx lock * prior to calling this function. * * Return B_TRUE if the cache host is still valid. Otherwise, return B_FALSE. */ static boolean_t smb_ads_validate_cache_host(char *domain, char *srv) { if (!smb_ads_cached_host_info) return (B_FALSE); if (!smb_ads_is_same_domain(smb_ads_cached_host_info->name, domain)) return (B_FALSE); if (smb_ads_ldap_ping(smb_ads_cached_host_info) == 0) { if (!srv) return (B_TRUE); if (smb_ads_is_sought_host(smb_ads_cached_host_info, srv)) return (B_TRUE); } return (B_FALSE); } /* * smb_ads_is_sought_host * * Returns true, if the sought host name matches the input host (host) name. * The sought host is expected to be in Fully Qualified Domain Name (FQDN) * format. */ static boolean_t smb_ads_is_sought_host(smb_ads_host_info_t *host, char *sought_host_name) { if ((host == NULL) || (sought_host_name == NULL)) return (B_FALSE); if (smb_strcasecmp(host->name, sought_host_name, 0)) return (B_FALSE); return (B_TRUE); } /* * smb_ads_match_hosts_same_domain * * Returns true, if the cached ADS host is in the same domain as the * current (given) domain. */ static boolean_t smb_ads_is_same_domain(char *cached_host_name, char *current_domain) { char *cached_host_domain; if ((cached_host_name == NULL) || (current_domain == NULL)) return (B_FALSE); cached_host_domain = strchr(cached_host_name, '.'); if (cached_host_domain == NULL) return (B_FALSE); ++cached_host_domain; if (smb_strcasecmp(cached_host_domain, current_domain, 0)) return (B_FALSE); return (B_TRUE); } /* * smb_ads_skip_ques_sec * Skips the question section. */ static int smb_ads_skip_ques_sec(int qcnt, uchar_t **ptr, uchar_t *eom) { int i, len; for (i = 0; i < qcnt; i++) { if ((len = dn_skipname(*ptr, eom)) < 0) return (-1); *ptr += len + QFIXEDSZ; } return (0); } /* * smb_ads_decode_host_ans_sec * Decodes ADS hosts, priority, weight and port number from the answer * section based on the current buffer pointer. */ static int smb_ads_decode_host_ans_sec(int ans_cnt, uchar_t **ptr, uchar_t *eom, uchar_t *buf, smb_ads_host_info_t *ads_host_list) { int i, len; smb_ads_host_info_t *ads_host; for (i = 0; i < ans_cnt; i++) { ads_host = &ads_host_list[i]; if ((len = dn_skipname(*ptr, eom)) < 0) return (-1); *ptr += len; /* skip type, class, ttl */ *ptr += 8; /* data size */ *ptr += 2; /* Get priority, weight */ /* LINTED: E_CONSTANT_CONDITION */ NS_GET16(ads_host->priority, *ptr); /* LINTED: E_CONSTANT_CONDITION */ NS_GET16(ads_host->weight, *ptr); /* port */ /* LINTED: E_CONSTANT_CONDITION */ NS_GET16(ads_host->port, *ptr); /* domain name */ len = dn_expand(buf, eom, *ptr, ads_host->name, MAXHOSTNAMELEN); if (len < 0) return (-1); *ptr += len; } return (0); } /* * smb_ads_skip_auth_sec * Skips the authority section. */ static int smb_ads_skip_auth_sec(int ns_cnt, uchar_t **ptr, uchar_t *eom) { int i, len; uint16_t size; for (i = 0; i < ns_cnt; i++) { if ((len = dn_skipname(*ptr, eom)) < 0) return (-1); *ptr += len; /* skip type, class, ttl */ *ptr += 8; /* get len of data */ /* LINTED: E_CONSTANT_CONDITION */ NS_GET16(size, *ptr); if ((*ptr + size) > eom) return (-1); *ptr += size; } return (0); } /* * smb_ads_decode_host_ip * * Decodes ADS hosts and IP Addresses from the additional section based * on the current buffer pointer. */ static int smb_ads_decode_host_ip(int addit_cnt, int ans_cnt, uchar_t **ptr, uchar_t *eom, uchar_t *buf, smb_ads_host_info_t *ads_host_list) { int i, j, len; smb_inaddr_t ipaddr; char hostname[MAXHOSTNAMELEN]; char *name; uint16_t size = 0; for (i = 0; i < addit_cnt; i++) { /* domain name */ len = dn_expand(buf, eom, *ptr, hostname, MAXHOSTNAMELEN); if (len < 0) return (-1); *ptr += len; /* skip type, class, TTL, data len */ *ptr += 8; /* LINTED: E_CONSTANT_CONDITION */ NS_GET16(size, *ptr); if (size == INADDRSZ) { /* LINTED: E_CONSTANT_CONDITION */ NS_GET32(ipaddr.a_ipv4, *ptr); ipaddr.a_ipv4 = htonl(ipaddr.a_ipv4); ipaddr.a_family = AF_INET; } else if (size == IN6ADDRSZ) { #ifdef BIG_ENDIAN bcopy(*ptr, &ipaddr.a_ipv6, IN6ADDRSZ); #else for (i = 0; i < IN6ADDRSZ; i++) (uint8_t *)(ipaddr.a_ipv6) [IN6ADDRSZ-1-i] = *(*ptr+i); #endif ipaddr.a_family = AF_INET6; } /* * find the host in the list of DC records from * the answer section, that matches the host in the * additional section, and set its IP address. */ for (j = 0; j < ans_cnt; j++) { if ((name = ads_host_list[j].name) == NULL) continue; if (smb_strcasecmp(name, hostname, 0) == 0) { ads_host_list[j].ipaddr = ipaddr; } } } return (0); } /* * smb_ads_dup_host_info * * Duplicates the passed smb_ads_host_info_t structure. * Caller must free memory allocated by this method. * * Returns a reference to the duplicated smb_ads_host_info_t structure. * Returns NULL on error. */ static smb_ads_host_info_t * smb_ads_dup_host_info(smb_ads_host_info_t *ads_host) { smb_ads_host_info_t *dup_host; if (ads_host == NULL) return (NULL); dup_host = malloc(sizeof (smb_ads_host_info_t)); if (dup_host != NULL) bcopy(ads_host, dup_host, sizeof (smb_ads_host_info_t)); return (dup_host); } /* * smb_ads_hlist_alloc */ static smb_ads_host_list_t * smb_ads_hlist_alloc(int count) { int size; smb_ads_host_list_t *hlist; if (count == 0) return (NULL); size = sizeof (smb_ads_host_info_t) * count; hlist = (smb_ads_host_list_t *)malloc(sizeof (smb_ads_host_list_t)); if (hlist == NULL) return (NULL); hlist->ah_cnt = count; hlist->ah_list = (smb_ads_host_info_t *)malloc(size); if (hlist->ah_list == NULL) { free(hlist); return (NULL); } bzero(hlist->ah_list, size); return (hlist); } /* * smb_ads_hlist_free */ static void smb_ads_hlist_free(smb_ads_host_list_t *host_list) { if (host_list == NULL) return; free(host_list->ah_list); free(host_list); } /* * smb_ads_query_dns_server * * This routine sends a DNS service location (SRV) query message to the * DNS server via TCP to query it for a list of ADS server(s). Once a reply * is received, the reply message is parsed to get the hostname. If there are IP * addresses populated in the additional section then the additional section * is parsed to obtain the IP addresses. * * The service location of _ldap._tcp.dc.msdcs. is used to * guarantee that Microsoft domain controllers are returned. Microsoft domain * controllers are also ADS servers. * * The ADS hostnames are stored in the answer section of the DNS reply message. * The IP addresses are stored in the additional section. * * The DNS reply message may be in compress formed. The compression is done * on repeating domain name label in the message. i.e hostname. * * Upon successful completion, host list of ADS server(s) is returned. */ static smb_ads_host_list_t * smb_ads_query_dns_server(char *domain, char *msdcs_svc_name) { smb_ads_host_list_t *hlist = NULL; int len, qcnt, ans_cnt, ns_cnt, addit_cnt; uchar_t *ptr, *eom; struct __res_state res_state; union { HEADER hdr; uchar_t buf[NS_MAXMSG]; } msg; bzero(&res_state, sizeof (struct __res_state)); if (res_ninit(&res_state) < 0) return (NULL); /* use TCP */ res_state.options |= RES_USEVC; len = res_nquerydomain(&res_state, msdcs_svc_name, domain, C_IN, T_SRV, msg.buf, sizeof (msg.buf)); if (len < 0) { syslog(LOG_NOTICE, "DNS query for %s failed: %s", msdcs_svc_name, hstrerror(res_state.res_h_errno)); res_ndestroy(&res_state); return (NULL); } if (len > sizeof (msg.buf)) { syslog(LOG_NOTICE, "DNS query for %s failed: too big", msdcs_svc_name); res_ndestroy(&res_state); return (NULL); } /* parse the reply, skip header and question sections */ ptr = msg.buf + sizeof (msg.hdr); eom = msg.buf + len; /* check truncated message bit */ if (msg.hdr.tc) syslog(LOG_NOTICE, "DNS query for %s failed: truncated", msdcs_svc_name); qcnt = ntohs(msg.hdr.qdcount); ans_cnt = ntohs(msg.hdr.ancount); ns_cnt = ntohs(msg.hdr.nscount); addit_cnt = ntohs(msg.hdr.arcount); if (smb_ads_skip_ques_sec(qcnt, &ptr, eom) != 0) { res_ndestroy(&res_state); return (NULL); } hlist = smb_ads_hlist_alloc(ans_cnt); if (hlist == NULL) { res_ndestroy(&res_state); return (NULL); } /* walk through the answer section */ if (smb_ads_decode_host_ans_sec(ans_cnt, &ptr, eom, msg.buf, hlist->ah_list) != 0) { smb_ads_hlist_free(hlist); res_ndestroy(&res_state); return (NULL); } /* check authority section */ if (ns_cnt > 0) { if (smb_ads_skip_auth_sec(ns_cnt, &ptr, eom) != 0) { smb_ads_hlist_free(hlist); res_ndestroy(&res_state); return (NULL); } } /* * Check additional section to get IP address of ADS host. */ if (addit_cnt > 0) { if (smb_ads_decode_host_ip(addit_cnt, ans_cnt, &ptr, eom, msg.buf, hlist->ah_list) != 0) { smb_ads_hlist_free(hlist); res_ndestroy(&res_state); return (NULL); } } res_ndestroy(&res_state); return (hlist); } /* * smb_ads_get_site_service * * Gets the msdcs SRV RR for the specified site. */ static void smb_ads_get_site_service(char *site_service, size_t len) { (void) mutex_lock(&smb_ads_cfg.c_mtx); if (*smb_ads_cfg.c_site == '\0') *site_service = '\0'; else (void) snprintf(site_service, len, SMB_ADS_MSDCS_SRV_SITE_RR, smb_ads_cfg.c_site); (void) mutex_unlock(&smb_ads_cfg.c_mtx); } /* * smb_ads_getipnodebyname * * This method gets the IP address by doing a host name lookup. */ static int smb_ads_getipnodebyname(smb_ads_host_info_t *hentry) { struct hostent *h; int error; switch (hentry->ipaddr.a_family) { case AF_INET6: h = getipnodebyname(hentry->name, hentry->ipaddr.a_family, AI_DEFAULT, &error); if (h == NULL || h->h_length != IPV6_ADDR_LEN) return (-1); break; case AF_INET: h = getipnodebyname(hentry->name, hentry->ipaddr.a_family, 0, &error); if (h == NULL || h->h_length != INADDRSZ) return (-1); break; default: return (-1); } bcopy(*(h->h_addr_list), &hentry->ipaddr.a_ip, h->h_length); freehostent(h); return (0); } /* * Checks the IP address to see if it is zero. If so, then do a host * lookup by hostname to get the IP address based on the IP family. * * If the family is unknown then do a lookup by hostame based on the * setting of the SMB_CI_IPV6_ENABLE property. */ static int smb_ads_set_ipaddr(smb_ads_host_info_t *hentry) { if (smb_inet_iszero(&hentry->ipaddr)) { if (smb_ads_getipnodebyname(hentry) < 0) return (-1); } else if (SMB_ADS_AF_UNKNOWN(hentry)) { hentry->ipaddr.a_family = smb_config_getbool(SMB_CI_IPV6_ENABLE) ? AF_INET6 : AF_INET; if (smb_ads_getipnodebyname(hentry) < 0) { hentry->ipaddr.a_family = 0; return (-1); } } return (0); } /* * smb_ads_find_host * * Finds an ADS host in a given domain. * * If the cached host is valid, it will be used. Otherwise, a DC will * be selected based on the following criteria: * * 1) pdc (aka preferred DC) configuration * 2) AD site configuration - the scope of the DNS lookup will be * restricted to the specified site. * 3) DC on the same subnet * 4) DC with the lowest priority/highest weight * * The above items are listed in decreasing preference order. The selected * DC must be online. * * If this function is called during domain join, the specified kpasswd server * takes precedence over preferred DC, AD site, and so on. * * Parameters: * domain: fully-qualified domain name. * kpasswd_srv: fully-quailifed hostname of the kpasswd server. * * Returns: * A copy of the cached host info is returned. The caller is responsible * for deallocating the memory returned by this function. */ /*ARGSUSED*/ smb_ads_host_info_t * smb_ads_find_host(char *domain, char *kpasswd_srv) { int i; char site_service[MAXHOSTNAMELEN]; smb_ads_host_list_t *hlist, *hlist2; smb_ads_host_info_t *hlistp = NULL, *host = NULL; smb_ads_host_info_t *found_kpasswd_srv = NULL; smb_ads_host_info_t *found_pdc = NULL; if ((kpasswd_srv) && (*kpasswd_srv == '\0')) kpasswd_srv = NULL; (void) mutex_lock(&smb_ads_cached_host_mtx); if (smb_ads_validate_cache_host(domain, kpasswd_srv)) { host = smb_ads_dup_host_info(smb_ads_cached_host_info); (void) mutex_unlock(&smb_ads_cached_host_mtx); return (host); } (void) mutex_unlock(&smb_ads_cached_host_mtx); smb_ads_free_cached_host(); /* * First look for ADS hosts in ADS site if configured. Then try * without ADS site info. */ hlist = NULL; smb_ads_get_site_service(site_service, MAXHOSTNAMELEN); /* * If we're given an AD, the DNS SRV RR lookup should not be restricted * to the specified site since there is no guarantee that the specified * AD is in the specified site. */ if (*site_service != '\0' && !kpasswd_srv && !smb_ads_is_pdc_configured()) hlist = smb_ads_query_dns_server(domain, site_service); if (!hlist) hlist = smb_ads_query_dns_server(domain, SMB_ADS_MSDCS_SRV_DC_RR); if ((hlist == NULL) || (hlist->ah_list == NULL) || (hlist->ah_cnt == 0)) return (NULL); for (i = 0, hlistp = hlist->ah_list; i < hlist->ah_cnt; i++) { if (smb_ads_set_ipaddr(&hlistp[i]) < 0) continue; if (smb_ads_is_sought_host(&hlistp[i], kpasswd_srv)) found_kpasswd_srv = &hlistp[i]; if (smb_ads_match_pdc(&hlistp[i])) found_pdc = &hlistp[i]; } if (found_kpasswd_srv && smb_ads_ldap_ping(found_kpasswd_srv) == 0) { host = found_kpasswd_srv; goto update_cache; } if (found_pdc && smb_ads_ldap_ping(found_pdc) == 0) { host = found_pdc; goto update_cache; } /* * If the specified DC (kpasswd_srv or pdc) is not found, fallback * to find a DC in the specified AD site. */ if (*site_service != '\0' && (kpasswd_srv || smb_ads_is_pdc_configured())) { hlist2 = smb_ads_query_dns_server(domain, site_service); if (hlist2 && hlist2->ah_list && hlist2->ah_cnt != 0) { smb_ads_hlist_free(hlist); hlist = hlist2; hlistp = hlist->ah_list; for (i = 0; i < hlist->ah_cnt; i++) (void) smb_ads_set_ipaddr(&hlistp[i]); } } /* Select DC from DC list */ host = smb_ads_select_dc(hlist); update_cache: if (host) { (void) mutex_lock(&smb_ads_cached_host_mtx); if (!smb_ads_cached_host_info) smb_ads_cached_host_info = smb_ads_dup_host_info(host); host = smb_ads_dup_host_info(smb_ads_cached_host_info); (void) mutex_unlock(&smb_ads_cached_host_mtx); } smb_ads_hlist_free(hlist); return (host); } /* * Return the number of dots in a string. */ static int smb_ads_count_dots(const char *s) { int ndots = 0; while (*s) { if (*s++ == '.') ndots++; } return (ndots); } /* * Convert a domain name in dot notation to distinguished name format, * for example: sun.com -> dc=sun,dc=com. * * Returns a pointer to an allocated buffer containing the distinguished * name. */ static char * smb_ads_convert_domain(const char *domain_name) { const char *s; char *dn_name; char buf[2]; int ndots; int len; if (domain_name == NULL || *domain_name == 0) return (NULL); ndots = smb_ads_count_dots(domain_name); ++ndots; len = strlen(domain_name) + (ndots * SMB_ADS_DN_PREFIX_LEN) + 1; if ((dn_name = malloc(len)) == NULL) return (NULL); bzero(dn_name, len); (void) strlcpy(dn_name, "dc=", len); buf[1] = '\0'; s = domain_name; while (*s) { if (*s == '.') { (void) strlcat(dn_name, ",dc=", len); } else { buf[0] = *s; (void) strlcat(dn_name, buf, len); } ++s; } return (dn_name); } /* * smb_ads_free_cached_host * * Free the memory use by the global smb_ads_cached_host_info & set it to NULL. */ static void smb_ads_free_cached_host(void) { (void) mutex_lock(&smb_ads_cached_host_mtx); if (smb_ads_cached_host_info) { free(smb_ads_cached_host_info); smb_ads_cached_host_info = NULL; } (void) mutex_unlock(&smb_ads_cached_host_mtx); } /* * smb_ads_open * Open a LDAP connection to an ADS server if the system is in domain mode. * Acquire both Kerberos TGT and LDAP service tickets for the host principal. * * This function should only be called after the system is successfully joined * to a domain. */ smb_ads_handle_t * smb_ads_open(void) { char domain[MAXHOSTNAMELEN]; if (smb_config_get_secmode() != SMB_SECMODE_DOMAIN) return (NULL); if (smb_getfqdomainname(domain, MAXHOSTNAMELEN) != 0) return (NULL); return (smb_ads_open_main(domain, NULL, NULL)); } static int smb_ads_saslcallback(LDAP *ld, unsigned flags, void *defaults, void *prompts) { NOTE(ARGUNUSED(ld, defaults)); sasl_interact_t *interact; if (prompts == NULL || flags != LDAP_SASL_INTERACTIVE) return (LDAP_PARAM_ERROR); /* There should be no extra arguemnts for SASL/GSSAPI authentication */ for (interact = prompts; interact->id != SASL_CB_LIST_END; interact++) { interact->result = NULL; interact->len = 0; } return (LDAP_SUCCESS); } /* * smb_ads_open_main * Open a LDAP connection to an ADS server. * If ADS is enabled and the administrative username, password, and * ADS domain are defined then query DNS to find an ADS server if this is the * very first call to this routine. After an ADS server is found then this * server will be used everytime this routine is called until the system is * rebooted or the ADS server becomes unavailable then an ADS server will * be queried again. After the connection is made then an ADS handle * is created to be returned. * * After the LDAP connection, the LDAP version will be set to 3 using * ldap_set_option(). * * The LDAP connection is bound before the ADS handle is returned. * Parameters: * domain - fully-qualified domain name * user - the user account for whom the Kerberos TGT ticket and ADS * service tickets are acquired. * password - password of the specified user * * Returns: * NULL : can't connect to ADS server or other errors * smb_ads_handle_t* : handle to ADS server */ static smb_ads_handle_t * smb_ads_open_main(char *domain, char *user, char *password) { smb_ads_handle_t *ah; LDAP *ld; int version = 3; smb_ads_host_info_t *ads_host = NULL; int rc; if (user != NULL) { if (smb_kinit(user, password) == 0) return (NULL); user = NULL; password = NULL; } ads_host = smb_ads_find_host(domain, NULL); if (ads_host == NULL) return (NULL); ah = (smb_ads_handle_t *)malloc(sizeof (smb_ads_handle_t)); if (ah == NULL) { free(ads_host); return (NULL); } (void) memset(ah, 0, sizeof (smb_ads_handle_t)); if ((ld = ldap_init(ads_host->name, ads_host->port)) == NULL) { smb_ads_free_cached_host(); free(ah); free(ads_host); return (NULL); } if (ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &version) != LDAP_SUCCESS) { smb_ads_free_cached_host(); free(ah); free(ads_host); (void) ldap_unbind(ld); return (NULL); } (void) ldap_set_option(ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); ah->ld = ld; ah->domain = strdup(domain); if (ah->domain == NULL) { smb_ads_close(ah); free(ads_host); return (NULL); } /* * ah->domain is often used for generating service principal name. * Convert it to lower case for RFC 4120 section 6.2.1 conformance. */ (void) smb_strlwr(ah->domain); ah->domain_dn = smb_ads_convert_domain(domain); if (ah->domain_dn == NULL) { smb_ads_close(ah); free(ads_host); return (NULL); } ah->hostname = strdup(ads_host->name); if (ah->hostname == NULL) { smb_ads_close(ah); free(ads_host); return (NULL); } (void) mutex_lock(&smb_ads_cfg.c_mtx); if (*smb_ads_cfg.c_site != '\0') { if ((ah->site = strdup(smb_ads_cfg.c_site)) == NULL) { smb_ads_close(ah); (void) mutex_unlock(&smb_ads_cfg.c_mtx); free(ads_host); return (NULL); } } else { ah->site = NULL; } (void) mutex_unlock(&smb_ads_cfg.c_mtx); rc = ldap_sasl_interactive_bind_s(ah->ld, "", "GSSAPI", NULL, NULL, LDAP_SASL_INTERACTIVE, &smb_ads_saslcallback, NULL); if (rc != LDAP_SUCCESS) { syslog(LOG_ERR, "ldal_sasl_interactive_bind_s failed (%s)", ldap_err2string(rc)); smb_ads_close(ah); free(ads_host); return (NULL); } free(ads_host); return (ah); } /* * smb_ads_close * Close connection to ADS server and free memory allocated for ADS handle. * LDAP unbind is called here. * Parameters: * ah: handle to ADS server * Returns: * void */ void smb_ads_close(smb_ads_handle_t *ah) { if (ah == NULL) return; /* close and free connection resources */ if (ah->ld) (void) ldap_unbind(ah->ld); free(ah->domain); free(ah->domain_dn); free(ah->hostname); free(ah->site); free(ah); } /* * smb_ads_alloc_attr * * Since the attrs is a null-terminated array, all elements * in the array (except the last one) will point to allocated * memory. */ static int smb_ads_alloc_attr(LDAPMod *attrs[], int num) { int i; bzero(attrs, num * sizeof (LDAPMod *)); for (i = 0; i < (num - 1); i++) { attrs[i] = (LDAPMod *)malloc(sizeof (LDAPMod)); if (attrs[i] == NULL) { smb_ads_free_attr(attrs); return (-1); } } return (0); } /* * smb_ads_free_attr * Free memory allocated when publishing a share. * Parameters: * attrs: an array of LDAPMod pointers * Returns: * None */ static void smb_ads_free_attr(LDAPMod *attrs[]) { int i; for (i = 0; attrs[i]; i++) { free(attrs[i]); } } /* * smb_ads_get_spnset * * Derives the core set of SPNs based on the FQHN. * The spn_set is a null-terminated array of char pointers. * * Returns 0 upon success. Otherwise, returns -1. */ static int smb_ads_get_spnset(char *fqhost, char **spn_set) { int i; bzero(spn_set, (SMBKRB5_SPN_IDX_MAX + 1) * sizeof (char *)); for (i = 0; i < SMBKRB5_SPN_IDX_MAX; i++) { if ((spn_set[i] = smb_krb5_get_spn(i, fqhost)) == NULL) { smb_ads_free_spnset(spn_set); return (-1); } } return (0); } /* * smb_ads_free_spnset * * Free the memory allocated for the set of SPNs. */ static void smb_ads_free_spnset(char **spn_set) { int i; for (i = 0; spn_set[i]; i++) free(spn_set[i]); } /* * Returns share DN in an allocated buffer. The format of the DN is * cn=,, * * If the domain DN is not included in the container parameter, * then it will be appended to create the share DN. * * The caller must free the allocated buffer. */ static char * smb_ads_get_sharedn(const char *sharename, const char *container, const char *domain_dn) { char *share_dn; int rc, offset, container_len, domain_len; boolean_t append_domain = B_TRUE; container_len = strlen(container); domain_len = strlen(domain_dn); if (container_len >= domain_len) { /* offset to last domain_len characters */ offset = container_len - domain_len; if (smb_strcasecmp(container + offset, domain_dn, domain_len) == 0) append_domain = B_FALSE; } if (append_domain) rc = asprintf(&share_dn, "cn=%s,%s,%s", sharename, container, domain_dn); else rc = asprintf(&share_dn, "cn=%s,%s", sharename, container); return ((rc == -1) ? NULL : share_dn); } /* * smb_ads_add_share * Call by smb_ads_publish_share to create share object in ADS. * This routine specifies the attributes of an ADS LDAP share object. The first * attribute and values define the type of ADS object, the share object. The * second attribute and value define the UNC of the share data for the share * object. The LDAP synchronous add command is used to add the object into ADS. * The container location to add the object needs to specified. * Parameters: * ah : handle to ADS server * adsShareName: name of share object to be created in ADS * shareUNC : share name on NetForce * adsContainer: location in ADS to create share object * * Returns: * -1 : error * 0 : success */ int smb_ads_add_share(smb_ads_handle_t *ah, const char *adsShareName, const char *unc_name, const char *adsContainer) { LDAPMod *attrs[SMB_ADS_SHARE_NUM_ATTR]; int j = 0; char *share_dn; int ret; char *unc_names[] = {(char *)unc_name, NULL}; if ((share_dn = smb_ads_get_sharedn(adsShareName, adsContainer, ah->domain_dn)) == NULL) return (-1); if (smb_ads_alloc_attr(attrs, SMB_ADS_SHARE_NUM_ATTR) != 0) { free(share_dn); return (-1); } attrs[j]->mod_op = LDAP_MOD_ADD; attrs[j]->mod_type = "objectClass"; attrs[j]->mod_values = smb_ads_share_objcls; attrs[++j]->mod_op = LDAP_MOD_ADD; attrs[j]->mod_type = "uNCName"; attrs[j]->mod_values = unc_names; if ((ret = ldap_add_s(ah->ld, share_dn, attrs)) != LDAP_SUCCESS) { if (ret == LDAP_NO_SUCH_OBJECT) { syslog(LOG_ERR, "Failed to publish share %s in" \ " AD. Container does not exist: %s.\n", adsShareName, share_dn); } else { syslog(LOG_ERR, "Failed to publish share %s in" \ " AD: %s (%s).\n", adsShareName, share_dn, ldap_err2string(ret)); } smb_ads_free_attr(attrs); free(share_dn); return (ret); } free(share_dn); smb_ads_free_attr(attrs); return (0); } /* * smb_ads_del_share * Call by smb_ads_remove_share to remove share object from ADS. The container * location to remove the object needs to specified. The LDAP synchronous * delete command is used. * Parameters: * ah : handle to ADS server * adsShareName: name of share object in ADS to be removed * adsContainer: location of share object in ADS * Returns: * -1 : error * 0 : success */ static int smb_ads_del_share(smb_ads_handle_t *ah, const char *adsShareName, const char *adsContainer) { char *share_dn; int ret; if ((share_dn = smb_ads_get_sharedn(adsShareName, adsContainer, ah->domain_dn)) == NULL) return (-1); if ((ret = ldap_delete_s(ah->ld, share_dn)) != LDAP_SUCCESS) { smb_tracef("ldap_delete: %s", ldap_err2string(ret)); free(share_dn); return (-1); } free(share_dn); return (0); } /* * smb_ads_escape_search_filter_chars * * This routine will escape the special characters found in a string * that will later be passed to the ldap search filter. * * RFC 1960 - A String Representation of LDAP Search Filters * 3. String Search Filter Definition * If a value must contain one of the characters '*' OR '(' OR ')', * these characters * should be escaped by preceding them with the backslash '\' character. * * RFC 2252 - LDAP Attribute Syntax Definitions * a backslash quoting mechanism is used to escape * the following separator symbol character (such as "'", "$" or "#") if * it should occur in that string. */ static int smb_ads_escape_search_filter_chars(const char *src, char *dst) { int avail = SMB_ADS_MAXBUFLEN - 1; /* reserve a space for NULL char */ if (src == NULL || dst == NULL) return (-1); while (*src) { if (!avail) { *dst = 0; return (-1); } switch (*src) { case '\\': case '\'': case '$': case '#': case '*': case '(': case ')': *dst++ = '\\'; avail--; /* fall through */ default: *dst++ = *src++; avail--; } } *dst = 0; return (0); } /* * smb_ads_lookup_share * The search filter is set to search for a specific share name in the * specified ADS container. The LDSAP synchronous search command is used. * Parameters: * ah : handle to ADS server * adsShareName: name of share object in ADS to be searched * adsContainer: location of share object in ADS * Returns: * -1 : error * 0 : not found * 1 : found */ int smb_ads_lookup_share(smb_ads_handle_t *ah, const char *adsShareName, const char *adsContainer, char *unc_name) { char *attrs[4], filter[SMB_ADS_MAXBUFLEN]; char *share_dn; int ret; LDAPMessage *res; char tmpbuf[SMB_ADS_MAXBUFLEN]; if (adsShareName == NULL || adsContainer == NULL) return (-1); if ((share_dn = smb_ads_get_sharedn(adsShareName, adsContainer, ah->domain_dn)) == NULL) return (-1); res = NULL; attrs[0] = "cn"; attrs[1] = "objectClass"; attrs[2] = "uNCName"; attrs[3] = NULL; if (smb_ads_escape_search_filter_chars(unc_name, tmpbuf) != 0) { free(share_dn); return (-1); } (void) snprintf(filter, sizeof (filter), "(&(objectClass=volume)(uNCName=%s))", tmpbuf); if ((ret = ldap_search_s(ah->ld, share_dn, LDAP_SCOPE_BASE, filter, attrs, 0, &res)) != LDAP_SUCCESS) { if (ret != LDAP_NO_SUCH_OBJECT) smb_tracef("%s: ldap_search: %s", share_dn, ldap_err2string(ret)); (void) ldap_msgfree(res); free(share_dn); return (0); } (void) free(share_dn); /* no match is found */ if (ldap_count_entries(ah->ld, res) == 0) { (void) ldap_msgfree(res); return (0); } /* free the search results */ (void) ldap_msgfree(res); return (1); } /* * smb_ads_publish_share * Publish share into ADS. If a share name already exist in ADS in the same * container then the existing share object is removed before adding the new * share object. * Parameters: * ah : handle return from smb_ads_open * adsShareName: name of share to be added to ADS directory * shareUNC : name of share on client, can be NULL to use the same name * as adsShareName * adsContainer: location for share to be added in ADS directory, ie * ou=share_folder * uncType : use UNC_HOSTNAME to use hostname for UNC, use UNC_HOSTADDR * to use host ip addr for UNC. * Returns: * -1 : error * 0 : success */ int smb_ads_publish_share(smb_ads_handle_t *ah, const char *adsShareName, const char *shareUNC, const char *adsContainer, const char *hostname) { int ret; char unc_name[SMB_ADS_MAXBUFLEN]; if (adsShareName == NULL || adsContainer == NULL) return (-1); if (shareUNC == 0 || *shareUNC == 0) shareUNC = adsShareName; if (smb_ads_build_unc_name(unc_name, sizeof (unc_name), hostname, shareUNC) < 0) return (-1); ret = smb_ads_lookup_share(ah, adsShareName, adsContainer, unc_name); switch (ret) { case 1: (void) smb_ads_del_share(ah, adsShareName, adsContainer); ret = smb_ads_add_share(ah, adsShareName, unc_name, adsContainer); break; case 0: ret = smb_ads_add_share(ah, adsShareName, unc_name, adsContainer); if (ret == LDAP_ALREADY_EXISTS) ret = -1; break; case -1: default: /* return with error code */ ret = -1; } return (ret); } /* * smb_ads_remove_share * Remove share from ADS. A search is done first before explicitly removing * the share. * Parameters: * ah : handle return from smb_ads_open * adsShareName: name of share to be removed from ADS directory * adsContainer: location for share to be removed from ADS directory, ie * ou=share_folder * Returns: * -1 : error * 0 : success */ int smb_ads_remove_share(smb_ads_handle_t *ah, const char *adsShareName, const char *shareUNC, const char *adsContainer, const char *hostname) { int ret; char unc_name[SMB_ADS_MAXBUFLEN]; if (adsShareName == NULL || adsContainer == NULL) return (-1); if (shareUNC == 0 || *shareUNC == 0) shareUNC = adsShareName; if (smb_ads_build_unc_name(unc_name, sizeof (unc_name), hostname, shareUNC) < 0) return (-1); ret = smb_ads_lookup_share(ah, adsShareName, adsContainer, unc_name); if (ret == 0) return (0); if (ret == -1) return (-1); return (smb_ads_del_share(ah, adsShareName, adsContainer)); } /* * smb_ads_get_default_comp_container_dn * * Build the distinguished name for the default computer conatiner (i.e. the * pre-defined Computers container). */ static void smb_ads_get_default_comp_container_dn(smb_ads_handle_t *ah, char *buf, size_t buflen) { (void) snprintf(buf, buflen, "cn=%s,%s", SMB_ADS_COMPUTERS_CN, ah->domain_dn); } /* * smb_ads_get_default_comp_dn * * Build the distinguished name for this system. */ static void smb_ads_get_default_comp_dn(smb_ads_handle_t *ah, char *buf, size_t buflen) { char nbname[NETBIOS_NAME_SZ]; char container_dn[SMB_ADS_DN_MAX]; (void) smb_getnetbiosname(nbname, sizeof (nbname)); smb_ads_get_default_comp_container_dn(ah, container_dn, SMB_ADS_DN_MAX); (void) snprintf(buf, buflen, "cn=%s,%s", nbname, container_dn); } /* * smb_ads_add_computer * * Returns 0 upon success. Otherwise, returns -1. */ static int smb_ads_add_computer(smb_ads_handle_t *ah, int dclevel, char *dn) { return (smb_ads_computer_op(ah, LDAP_MOD_ADD, dclevel, dn)); } /* * smb_ads_modify_computer * * Returns 0 upon success. Otherwise, returns -1. */ static int smb_ads_modify_computer(smb_ads_handle_t *ah, int dclevel, char *dn) { return (smb_ads_computer_op(ah, LDAP_MOD_REPLACE, dclevel, dn)); } /* * smb_ads_get_dc_level * * Returns the functional level of the DC upon success. * Otherwise, -1 is returned. */ static int smb_ads_get_dc_level(smb_ads_handle_t *ah) { LDAPMessage *res, *entry; char *attr[2]; char **vals; int rc = -1; res = NULL; attr[0] = SMB_ADS_ATTR_DCLEVEL; attr[1] = NULL; if (ldap_search_s(ah->ld, "", LDAP_SCOPE_BASE, NULL, attr, 0, &res) != LDAP_SUCCESS) { (void) ldap_msgfree(res); return (-1); } /* no match for the specified attribute is found */ if (ldap_count_entries(ah->ld, res) == 0) { (void) ldap_msgfree(res); return (-1); } entry = ldap_first_entry(ah->ld, res); if (entry) { if ((vals = ldap_get_values(ah->ld, entry, SMB_ADS_ATTR_DCLEVEL)) == NULL) { /* * Observed the values aren't populated * by the Windows 2000 server. */ (void) ldap_msgfree(res); return (SMB_ADS_DCLEVEL_W2K); } if (vals[0] != NULL) rc = atoi(vals[0]); ldap_value_free(vals); } (void) ldap_msgfree(res); return (rc); } /* * The fully-qualified hostname returned by this function is often used for * constructing service principal name. Return the fully-qualified hostname * in lower case for RFC 4120 section 6.2.1 conformance. */ static int smb_ads_getfqhostname(smb_ads_handle_t *ah, char *fqhost, int len) { if (smb_gethostname(fqhost, len, SMB_CASE_LOWER) != 0) return (-1); (void) snprintf(fqhost, len, "%s.%s", fqhost, ah->domain); return (0); } static int smb_ads_computer_op(smb_ads_handle_t *ah, int op, int dclevel, char *dn) { LDAPMod *attrs[SMB_ADS_COMPUTER_NUM_ATTR]; char *sam_val[2], *usr_val[2]; char *spn_set[SMBKRB5_SPN_IDX_MAX + 1], *ctl_val[2], *fqh_val[2]; char *encrypt_val[2]; int j = -1; int ret, usrctl_flags = 0; char sam_acct[SMB_SAMACCT_MAXLEN]; char fqhost[MAXHOSTNAMELEN]; char *user_principal; char usrctl_buf[16]; char encrypt_buf[16]; int max; if (smb_getsamaccount(sam_acct, sizeof (sam_acct)) != 0) return (-1); if (smb_ads_getfqhostname(ah, fqhost, MAXHOSTNAMELEN)) return (-1); if (smb_ads_get_spnset(fqhost, spn_set) != 0) return (-1); user_principal = smb_krb5_get_upn(spn_set[SMBKRB5_SPN_IDX_HOST], ah->domain); if (user_principal == NULL) { smb_ads_free_spnset(spn_set); return (-1); } max = (SMB_ADS_COMPUTER_NUM_ATTR - ((op != LDAP_MOD_ADD) ? 1 : 0)) - (dclevel >= SMB_ADS_DCLEVEL_W2K8 ? 0 : 1); if (smb_ads_alloc_attr(attrs, max) != 0) { free(user_principal); smb_ads_free_spnset(spn_set); return (-1); } /* objectClass attribute is not modifiable. */ if (op == LDAP_MOD_ADD) { attrs[++j]->mod_op = op; attrs[j]->mod_type = "objectClass"; attrs[j]->mod_values = smb_ads_computer_objcls; } attrs[++j]->mod_op = op; attrs[j]->mod_type = SMB_ADS_ATTR_SAMACCT; sam_val[0] = sam_acct; sam_val[1] = 0; attrs[j]->mod_values = sam_val; attrs[++j]->mod_op = op; attrs[j]->mod_type = SMB_ADS_ATTR_UPN; usr_val[0] = user_principal; usr_val[1] = 0; attrs[j]->mod_values = usr_val; attrs[++j]->mod_op = op; attrs[j]->mod_type = SMB_ADS_ATTR_SPN; attrs[j]->mod_values = spn_set; attrs[++j]->mod_op = op; attrs[j]->mod_type = SMB_ADS_ATTR_CTL; usrctl_flags |= (SMB_ADS_USER_ACCT_CTL_WKSTATION_TRUST_ACCT | SMB_ADS_USER_ACCT_CTL_PASSWD_NOTREQD | SMB_ADS_USER_ACCT_CTL_ACCOUNTDISABLE); (void) snprintf(usrctl_buf, sizeof (usrctl_buf), "%d", usrctl_flags); ctl_val[0] = usrctl_buf; ctl_val[1] = 0; attrs[j]->mod_values = ctl_val; attrs[++j]->mod_op = op; attrs[j]->mod_type = SMB_ADS_ATTR_DNSHOST; fqh_val[0] = fqhost; fqh_val[1] = 0; attrs[j]->mod_values = fqh_val; /* enctypes support starting in Windows Server 2008 */ if (dclevel > SMB_ADS_DCLEVEL_W2K3) { attrs[++j]->mod_op = op; attrs[j]->mod_type = SMB_ADS_ATTR_ENCTYPES; (void) snprintf(encrypt_buf, sizeof (encrypt_buf), "%d", SMB_ADS_ENC_AES256 + SMB_ADS_ENC_AES128 + SMB_ADS_ENC_RC4 + SMB_ADS_ENC_DES_MD5 + SMB_ADS_ENC_DES_CRC); encrypt_val[0] = encrypt_buf; encrypt_val[1] = 0; attrs[j]->mod_values = encrypt_val; } switch (op) { case LDAP_MOD_ADD: if ((ret = ldap_add_s(ah->ld, dn, attrs)) != LDAP_SUCCESS) { syslog(LOG_NOTICE, "ldap_add: %s", ldap_err2string(ret)); ret = -1; } break; case LDAP_MOD_REPLACE: if ((ret = ldap_modify_s(ah->ld, dn, attrs)) != LDAP_SUCCESS) { syslog(LOG_NOTICE, "ldap_modify: %s", ldap_err2string(ret)); ret = -1; } break; default: ret = -1; } smb_ads_free_attr(attrs); free(user_principal); smb_ads_free_spnset(spn_set); return (ret); } /* * Delete an ADS computer account. */ static void smb_ads_del_computer(smb_ads_handle_t *ah, char *dn) { int rc; if ((rc = ldap_delete_s(ah->ld, dn)) != LDAP_SUCCESS) smb_tracef("ldap_delete: %s", ldap_err2string(rc)); } /* * Gets the value of the given attribute. */ static smb_ads_qstat_t smb_ads_getattr(LDAP *ld, LDAPMessage *entry, smb_ads_avpair_t *avpair) { char **vals; smb_ads_qstat_t rc = SMB_ADS_STAT_FOUND; assert(avpair); avpair->avp_val = NULL; vals = ldap_get_values(ld, entry, avpair->avp_attr); if (!vals) return (SMB_ADS_STAT_NOT_FOUND); if (!vals[0]) { ldap_value_free(vals); return (SMB_ADS_STAT_NOT_FOUND); } avpair->avp_val = strdup(vals[0]); if (!avpair->avp_val) rc = SMB_ADS_STAT_ERR; ldap_value_free(vals); return (rc); } /* * Process query's result. */ static smb_ads_qstat_t smb_ads_get_qstat(smb_ads_handle_t *ah, LDAPMessage *res, smb_ads_avpair_t *avpair) { char fqhost[MAXHOSTNAMELEN]; smb_ads_avpair_t dnshost_avp; smb_ads_qstat_t rc = SMB_ADS_STAT_FOUND; LDAPMessage *entry; if (smb_ads_getfqhostname(ah, fqhost, MAXHOSTNAMELEN)) return (SMB_ADS_STAT_ERR); if (ldap_count_entries(ah->ld, res) == 0) return (SMB_ADS_STAT_NOT_FOUND); if ((entry = ldap_first_entry(ah->ld, res)) == NULL) return (SMB_ADS_STAT_ERR); dnshost_avp.avp_attr = SMB_ADS_ATTR_DNSHOST; rc = smb_ads_getattr(ah->ld, entry, &dnshost_avp); switch (rc) { case SMB_ADS_STAT_FOUND: /* * Returns SMB_ADS_STAT_DUP to avoid overwriting * the computer account of another system whose * NetBIOS name collides with that of the current * system. */ if (strcasecmp(dnshost_avp.avp_val, fqhost)) rc = SMB_ADS_STAT_DUP; free(dnshost_avp.avp_val); break; case SMB_ADS_STAT_NOT_FOUND: /* * Pre-created computer account doesn't have * the dNSHostname attribute. It's been observed * that the dNSHostname attribute is only set after * a successful domain join. * Returns SMB_ADS_STAT_FOUND as the account is * pre-created for the current system. */ rc = SMB_ADS_STAT_FOUND; break; default: break; } if (rc != SMB_ADS_STAT_FOUND) return (rc); if (avpair) rc = smb_ads_getattr(ah->ld, entry, avpair); return (rc); } /* * smb_ads_lookup_computer_n_attr * * If avpair is NULL, checks the status of the specified computer account. * Otherwise, looks up the value of the specified computer account's attribute. * If found, the value field of the avpair will be allocated and set. The * caller should free the allocated buffer. * * Return: * SMB_ADS_STAT_FOUND - if both the computer and the specified attribute is * found. * SMB_ADS_STAT_NOT_FOUND - if either the computer or the specified attribute * is not found. * SMB_ADS_STAT_DUP - if the computer account is already used by other systems * in the AD. This could happen if the hostname of multiple * systems resolved to the same NetBIOS name. * SMB_ADS_STAT_ERR - any failure. */ static smb_ads_qstat_t smb_ads_lookup_computer_n_attr(smb_ads_handle_t *ah, smb_ads_avpair_t *avpair, int scope, char *dn) { char *attrs[3], filter[SMB_ADS_MAXBUFLEN]; LDAPMessage *res; char sam_acct[SMB_SAMACCT_MAXLEN], sam_acct2[SMB_SAMACCT_MAXLEN]; smb_ads_qstat_t rc; if (smb_getsamaccount(sam_acct, sizeof (sam_acct)) != 0) return (SMB_ADS_STAT_ERR); res = NULL; attrs[0] = SMB_ADS_ATTR_DNSHOST; attrs[1] = NULL; attrs[2] = NULL; if (avpair) { if (!avpair->avp_attr) return (SMB_ADS_STAT_ERR); attrs[1] = avpair->avp_attr; } if (smb_ads_escape_search_filter_chars(sam_acct, sam_acct2) != 0) return (SMB_ADS_STAT_ERR); (void) snprintf(filter, sizeof (filter), "(&(objectClass=computer)(%s=%s))", SMB_ADS_ATTR_SAMACCT, sam_acct2); if (ldap_search_s(ah->ld, dn, scope, filter, attrs, 0, &res) != LDAP_SUCCESS) { (void) ldap_msgfree(res); return (SMB_ADS_STAT_NOT_FOUND); } rc = smb_ads_get_qstat(ah, res, avpair); /* free the search results */ (void) ldap_msgfree(res); return (rc); } /* * smb_ads_find_computer * * Starts by searching for the system's AD computer object in the default * container (i.e. cn=Computers). If not found, searches the entire directory. * If found, 'dn' will be set to the distinguished name of the system's AD * computer object. */ static smb_ads_qstat_t smb_ads_find_computer(smb_ads_handle_t *ah, char *dn) { smb_ads_qstat_t stat; smb_ads_avpair_t avpair; avpair.avp_attr = SMB_ADS_ATTR_DN; smb_ads_get_default_comp_container_dn(ah, dn, SMB_ADS_DN_MAX); stat = smb_ads_lookup_computer_n_attr(ah, &avpair, LDAP_SCOPE_ONELEVEL, dn); if (stat == SMB_ADS_STAT_NOT_FOUND) { (void) strlcpy(dn, ah->domain_dn, SMB_ADS_DN_MAX); stat = smb_ads_lookup_computer_n_attr(ah, &avpair, LDAP_SCOPE_SUBTREE, dn); } if (stat == SMB_ADS_STAT_FOUND) { (void) strlcpy(dn, avpair.avp_val, SMB_ADS_DN_MAX); free(avpair.avp_val); } return (stat); } /* * smb_ads_update_computer_cntrl_attr * * Modify the user account control attribute of an existing computer * object on AD. * * Returns LDAP error code. */ static int smb_ads_update_computer_cntrl_attr(smb_ads_handle_t *ah, int flags, char *dn) { LDAPMod *attrs[2]; char *ctl_val[2]; int ret = 0; char usrctl_buf[16]; if (smb_ads_alloc_attr(attrs, sizeof (attrs) / sizeof (LDAPMod *)) != 0) return (LDAP_NO_MEMORY); attrs[0]->mod_op = LDAP_MOD_REPLACE; attrs[0]->mod_type = SMB_ADS_ATTR_CTL; (void) snprintf(usrctl_buf, sizeof (usrctl_buf), "%d", flags); ctl_val[0] = usrctl_buf; ctl_val[1] = 0; attrs[0]->mod_values = ctl_val; if ((ret = ldap_modify_s(ah->ld, dn, attrs)) != LDAP_SUCCESS) { syslog(LOG_NOTICE, "ldap_modify: %s", ldap_err2string(ret)); } smb_ads_free_attr(attrs); return (ret); } /* * smb_ads_lookup_computer_attr_kvno * * Lookup the value of the Kerberos version number attribute of the computer * account. */ static krb5_kvno smb_ads_lookup_computer_attr_kvno(smb_ads_handle_t *ah, char *dn) { smb_ads_avpair_t avpair; int kvno = 1; avpair.avp_attr = SMB_ADS_ATTR_KVNO; if (smb_ads_lookup_computer_n_attr(ah, &avpair, LDAP_SCOPE_BASE, dn) == SMB_ADS_STAT_FOUND) { kvno = atoi(avpair.avp_val); free(avpair.avp_val); } return (kvno); } static int smb_ads_gen_machine_passwd(char *machine_passwd, size_t bufsz) { int i; size_t pwdlen; uint8_t *random_bytes; errno = 0; if (machine_passwd == NULL || bufsz == 0) { errno = EINVAL; return (-1); } pwdlen = bufsz - 1; random_bytes = calloc(1, pwdlen); if (random_bytes == NULL) return (-1); if (pkcs11_get_random(random_bytes, pwdlen) != 0) { free(random_bytes); return (-1); } for (i = 0; i < pwdlen; i++) machine_passwd[i] = (random_bytes[i] % SMB_ADS_PWD_CHAR_NUM) + SMB_ADS_PWD_CHAR_START; machine_passwd[pwdlen] = 0; bzero(random_bytes, pwdlen); free(random_bytes); return (0); } /* * smb_ads_join * * Besides the NT-4 style domain join (using MS-RPC), CIFS server also * provides the domain join using Kerberos Authentication, Keberos * Change & Set password, and LDAP protocols. Basically, AD join * operation would require the following tickets to be acquired for the * the user account that is provided for the domain join. * * 1) a Keberos TGT ticket, * 2) a ldap service ticket, and * 3) kadmin/changpw service ticket * * The ADS client first sends a ldap search request to find out whether * or not the workstation trust account already exists in the Active Directory. * The existing computer object for this workstation will be removed and * a new one will be added. The machine account password is randomly * generated and set for the newly created computer object using KPASSWD * protocol (See RFC 3244). Once the password is set, our ADS client * finalizes the machine account by modifying the user acount control * attribute of the computer object. Kerberos keys derived from the machine * account password will be stored locally in /etc/krb5/krb5.keytab file. * That would be needed while acquiring Kerberos TGT ticket for the host * principal after the domain join operation. */ smb_adjoin_status_t smb_ads_join(char *domain, char *user, char *usr_passwd, char *machine_passwd, size_t len) { smb_ads_handle_t *ah = NULL; krb5_context ctx = NULL; krb5_principal krb5princs[SMBKRB5_SPN_IDX_MAX]; krb5_kvno kvno; boolean_t des_only, delete = B_TRUE; smb_adjoin_status_t rc = SMB_ADJOIN_SUCCESS; boolean_t new_acct; int dclevel, num, usrctl_flags = 0; smb_ads_qstat_t qstat; char dn[SMB_ADS_DN_MAX]; char *tmpfile; /* * Call library functions that can be used to get * the list of encryption algorithms available on the system. * (similar to what 'encrypt -l' CLI does). For now, * unless someone has modified the configuration of the * cryptographic framework (very unlikely), the following is the * list of algorithms available on any system running Nevada * by default. */ krb5_enctype w2k8enctypes[] = {ENCTYPE_AES256_CTS_HMAC_SHA1_96, ENCTYPE_AES128_CTS_HMAC_SHA1_96, ENCTYPE_ARCFOUR_HMAC, ENCTYPE_DES_CBC_CRC, ENCTYPE_DES_CBC_MD5, }; krb5_enctype pre_w2k8enctypes[] = {ENCTYPE_ARCFOUR_HMAC, ENCTYPE_DES_CBC_CRC, ENCTYPE_DES_CBC_MD5, }; krb5_enctype *encptr; if ((ah = smb_ads_open_main(domain, user, usr_passwd)) == NULL) { smb_ccache_remove(SMB_CCACHE_PATH); return (SMB_ADJOIN_ERR_GET_HANDLE); } if (smb_ads_gen_machine_passwd(machine_passwd, len) != 0) { syslog(LOG_NOTICE, "machine password generation: %m"); smb_ads_close(ah); smb_ccache_remove(SMB_CCACHE_PATH); return (SMB_ADJOIN_ERR_GEN_PWD); } if ((dclevel = smb_ads_get_dc_level(ah)) == -1) { smb_ads_close(ah); smb_ccache_remove(SMB_CCACHE_PATH); return (SMB_ADJOIN_ERR_GET_DCLEVEL); } qstat = smb_ads_find_computer(ah, dn); switch (qstat) { case SMB_ADS_STAT_FOUND: new_acct = B_FALSE; if (smb_ads_modify_computer(ah, dclevel, dn) != 0) { smb_ads_close(ah); smb_ccache_remove(SMB_CCACHE_PATH); return (SMB_ADJOIN_ERR_MOD_TRUST_ACCT); } break; case SMB_ADS_STAT_NOT_FOUND: new_acct = B_TRUE; smb_ads_get_default_comp_dn(ah, dn, SMB_ADS_DN_MAX); if (smb_ads_add_computer(ah, dclevel, dn) != 0) { smb_ads_close(ah); smb_ccache_remove(SMB_CCACHE_PATH); return (SMB_ADJOIN_ERR_ADD_TRUST_ACCT); } break; default: if (qstat == SMB_ADS_STAT_DUP) rc = SMB_ADJOIN_ERR_DUP_TRUST_ACCT; else rc = SMB_ADJOIN_ERR_TRUST_ACCT; smb_ads_close(ah); smb_ccache_remove(SMB_CCACHE_PATH); return (rc); } des_only = B_FALSE; if (smb_krb5_ctx_init(&ctx) != 0) { rc = SMB_ADJOIN_ERR_INIT_KRB_CTX; goto adjoin_cleanup; } if (smb_krb5_get_principals(ah->domain, ctx, krb5princs) != 0) { rc = SMB_ADJOIN_ERR_GET_SPNS; goto adjoin_cleanup; } if (smb_krb5_setpwd(ctx, krb5princs[SMBKRB5_SPN_IDX_HOST], machine_passwd) != 0) { rc = SMB_ADJOIN_ERR_KSETPWD; goto adjoin_cleanup; } kvno = smb_ads_lookup_computer_attr_kvno(ah, dn); /* * Only members of Domain Admins and Enterprise Admins can set * the TRUSTED_FOR_DELEGATION userAccountControl flag. */ if (smb_ads_update_computer_cntrl_attr(ah, SMB_ADS_USER_ACCT_CTL_WKSTATION_TRUST_ACCT | SMB_ADS_USER_ACCT_CTL_TRUSTED_FOR_DELEGATION, dn) == LDAP_INSUFFICIENT_ACCESS) { usrctl_flags |= (SMB_ADS_USER_ACCT_CTL_WKSTATION_TRUST_ACCT | SMB_ADS_USER_ACCT_CTL_DONT_EXPIRE_PASSWD); syslog(LOG_NOTICE, "Unable to set the " "TRUSTED_FOR_DELEGATION userAccountControl flag on " "the machine account in Active Directory. Please refer " "to the Troubleshooting guide for more information."); } else { usrctl_flags |= (SMB_ADS_USER_ACCT_CTL_WKSTATION_TRUST_ACCT | SMB_ADS_USER_ACCT_CTL_TRUSTED_FOR_DELEGATION | SMB_ADS_USER_ACCT_CTL_DONT_EXPIRE_PASSWD); } if (des_only) usrctl_flags |= SMB_ADS_USER_ACCT_CTL_USE_DES_KEY_ONLY; if (smb_ads_update_computer_cntrl_attr(ah, usrctl_flags, dn) != 0) { rc = SMB_ADJOIN_ERR_UPDATE_CNTRL_ATTR; goto adjoin_cleanup; } if (dclevel >= SMB_ADS_DCLEVEL_W2K8) { num = sizeof (w2k8enctypes) / sizeof (krb5_enctype); encptr = w2k8enctypes; } else { num = sizeof (pre_w2k8enctypes) / sizeof (krb5_enctype); encptr = pre_w2k8enctypes; } tmpfile = mktemp(SMBNS_KRB5_KEYTAB_TMP); if (tmpfile == NULL) tmpfile = SMBNS_KRB5_KEYTAB_TMP; if (smb_krb5_add_keytab_entries(ctx, krb5princs, tmpfile, kvno, machine_passwd, encptr, num) != 0) { rc = SMB_ADJOIN_ERR_WRITE_KEYTAB; goto adjoin_cleanup; } delete = B_FALSE; adjoin_cleanup: if (new_acct && delete) smb_ads_del_computer(ah, dn); if (rc != SMB_ADJOIN_ERR_INIT_KRB_CTX) { if (rc != SMB_ADJOIN_ERR_GET_SPNS) smb_krb5_free_principals(ctx, krb5princs, SMBKRB5_SPN_IDX_MAX); smb_krb5_ctx_fini(ctx); } /* commit keytab file */ if (rc == SMB_ADJOIN_SUCCESS) { if (rename(tmpfile, SMBNS_KRB5_KEYTAB) != 0) { (void) unlink(tmpfile); rc = SMB_ADJOIN_ERR_COMMIT_KEYTAB; } else { /* Set IDMAP config */ if (smb_config_set_idmap_domain(ah->domain) != 0) { rc = SMB_ADJOIN_ERR_IDMAP_SET_DOMAIN; } else { /* Refresh IDMAP service */ if (smb_config_refresh_idmap() != 0) rc = SMB_ADJOIN_ERR_IDMAP_REFRESH; } } } else { (void) unlink(tmpfile); } smb_ads_close(ah); smb_ccache_remove(SMB_CCACHE_PATH); return (rc); } /* * smb_ads_join_errmsg * * Display error message for the specific adjoin error code. */ void smb_ads_join_errmsg(smb_adjoin_status_t status) { int i; struct xlate_table { smb_adjoin_status_t status; char *msg; } adjoin_table[] = { { SMB_ADJOIN_ERR_GET_HANDLE, "Failed to connect to an " "Active Directory server." }, { SMB_ADJOIN_ERR_GEN_PWD, "Failed to generate machine " "password." }, { SMB_ADJOIN_ERR_GET_DCLEVEL, "Unknown functional level of " "the domain controller. The rootDSE attribute named " "\"domainControllerFunctionality\" is missing from the " "Active Directory." }, { SMB_ADJOIN_ERR_ADD_TRUST_ACCT, "Failed to create the " "workstation trust account." }, { SMB_ADJOIN_ERR_MOD_TRUST_ACCT, "Failed to modify the " "workstation trust account." }, { SMB_ADJOIN_ERR_DUP_TRUST_ACCT, "Failed to create the " "workstation trust account because its name is already " "in use." }, { SMB_ADJOIN_ERR_TRUST_ACCT, "Error in querying the " "workstation trust account" }, { SMB_ADJOIN_ERR_INIT_KRB_CTX, "Failed to initialize Kerberos " "context." }, { SMB_ADJOIN_ERR_GET_SPNS, "Failed to get Kerberos " "principals." }, { SMB_ADJOIN_ERR_KSETPWD, "Failed to set machine password." }, { SMB_ADJOIN_ERR_UPDATE_CNTRL_ATTR, "Failed to modify " "userAccountControl attribute of the workstation trust " "account." }, { SMB_ADJOIN_ERR_WRITE_KEYTAB, "Error in writing to local " "keytab file (i.e /etc/krb5/krb5.keytab)." }, { SMB_ADJOIN_ERR_IDMAP_SET_DOMAIN, "Failed to update idmap " "configuration." }, { SMB_ADJOIN_ERR_IDMAP_REFRESH, "Failed to refresh idmap " "service." }, { SMB_ADJOIN_ERR_COMMIT_KEYTAB, "Failed to commit changes to " "local keytab file (i.e. /etc/krb5/krb5.keytab)." } }; for (i = 0; i < sizeof (adjoin_table) / sizeof (adjoin_table[0]); i++) { if (adjoin_table[i].status == status) syslog(LOG_NOTICE, "%s", adjoin_table[i].msg); } } /* * smb_ads_match_pdc * * Returns B_TRUE if the given host's IP address matches the preferred DC's * IP address. Otherwise, returns B_FALSE. */ static boolean_t smb_ads_match_pdc(smb_ads_host_info_t *host) { boolean_t match = B_FALSE; if (!host) return (match); (void) mutex_lock(&smb_ads_cfg.c_mtx); if (smb_inet_equal(&host->ipaddr, &smb_ads_cfg.c_pdc)) match = B_TRUE; (void) mutex_unlock(&smb_ads_cfg.c_mtx); return (match); } /* * smb_ads_select_dcfromsubnet * * This method walks the list of DCs and returns the first DC record that * responds to ldap ping and is in the same subnet as the host. * * Returns a pointer to the found DC record. * Returns NULL, on error or if no DC record is found. */ static smb_ads_host_info_t * smb_ads_select_dcfromsubnet(smb_ads_host_list_t *hlist) { smb_ads_host_info_t *hentry; smb_nic_t *lnic; smb_niciter_t ni; size_t cnt; int i; if (smb_nic_getfirst(&ni) != SMB_NIC_SUCCESS) return (NULL); do { lnic = &ni.ni_nic; cnt = hlist->ah_cnt; for (i = 0; i < cnt; i++) { hentry = &hlist->ah_list[i]; if ((hentry->ipaddr.a_family == AF_INET) && (lnic->nic_ip.a_family == AF_INET)) { if ((hentry->ipaddr.a_ipv4 & lnic->nic_mask) == (lnic->nic_ip.a_ipv4 & lnic->nic_mask)) if (smb_ads_ldap_ping(hentry) == 0) return (hentry); } } } while (smb_nic_getnext(&ni) == SMB_NIC_SUCCESS); return (NULL); } /* * smb_ads_select_dcfromlist * * This method walks the list of DCs and returns the first DC that * responds to ldap ping. * * Returns a pointer to the found DC record. * Returns NULL if no DC record is found. */ static smb_ads_host_info_t * smb_ads_select_dcfromlist(smb_ads_host_list_t *hlist) { smb_ads_host_info_t *hentry; size_t cnt; int i; cnt = hlist->ah_cnt; for (i = 0; i < cnt; i++) { hentry = &hlist->ah_list[i]; if (smb_ads_ldap_ping(hentry) == 0) return (hentry); } return (NULL); } /* * smb_ads_dc_compare * * Comparision function for sorting host entries (SRV records of DC) via qsort. * RFC 2052/2782 are taken as reference, while implementing this algorithm. * * Domain Controllers(DCs) with lowest priority in their SRV DNS records * are selected first. If they have equal priorities, then DC with highest * weight in its SRV DNS record is selected. If the priority and weight are * both equal, then the DC at the top of the list is selected. */ static int smb_ads_dc_compare(const void *p, const void *q) { smb_ads_host_info_t *h1 = (smb_ads_host_info_t *)p; smb_ads_host_info_t *h2 = (smb_ads_host_info_t *)q; if (h1->priority < h2->priority) return (-1); if (h1->priority > h2->priority) return (1); /* Priorities are equal */ if (h1->weight < h2->weight) return (1); if (h1->weight > h2->weight) return (-1); return (0); } /* * smb_ads_select_dc * * The list of ADS hosts returned by ADS lookup, is sorted by lowest priority * and highest weight. On this sorted list, following additional rules are * applied, to select a DC. * * - If there is a DC in the same subnet, then return the DC, * if it responds to ldap ping. * - Else, return first DC that responds to ldap ping. * * A reference to the host entry from input host list is returned. * * Returns NULL on error. */ static smb_ads_host_info_t * smb_ads_select_dc(smb_ads_host_list_t *hlist) { smb_ads_host_info_t *hentry = NULL; if (hlist->ah_cnt == 0) return (NULL); if (hlist->ah_cnt == 1) { hentry = hlist->ah_list; if (smb_ads_ldap_ping(hentry) == 0) return (hentry); } /* Sort the list by priority and weight */ qsort(hlist->ah_list, hlist->ah_cnt, sizeof (smb_ads_host_info_t), smb_ads_dc_compare); if ((hentry = smb_ads_select_dcfromsubnet(hlist)) != NULL) return (hentry); if ((hentry = smb_ads_select_dcfromlist(hlist)) != NULL) return (hentry); return (NULL); } /* * smb_ads_lookup_msdcs * * If server argument is set, try to locate the specified DC. * If it is set to empty string, locate any DCs in the specified domain. * Returns the discovered DC via buf. * * fqdn - fully-qualified domain name * server - fully-qualifed hostname of a DC * buf - the hostname of the discovered DC */ boolean_t smb_ads_lookup_msdcs(char *fqdn, char *server, char *buf, uint32_t buflen) { smb_ads_host_info_t *hinfo = NULL; char *p; char *sought_host; char ipstr[INET6_ADDRSTRLEN]; if (!fqdn || !buf) return (B_FALSE); ipstr[0] = '\0'; *buf = '\0'; sought_host = (*server == 0 ? NULL : server); if ((hinfo = smb_ads_find_host(fqdn, sought_host)) == NULL) return (B_FALSE); (void) smb_inet_ntop(&hinfo->ipaddr, ipstr, SMB_IPSTRLEN(hinfo->ipaddr.a_family)); smb_tracef("msdcsLookupADS: %s [%s]", hinfo->name, ipstr); (void) strlcpy(buf, hinfo->name, buflen); /* * Remove the domain extension */ if ((p = strchr(buf, '.')) != 0) *p = '\0'; free(hinfo); return (B_TRUE); }