/* * 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 (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved. * Copyright 2022 Tintri by DDN, Inc. All rights reserved. * Copyright 2021 RackTop Systems, Inc. */ #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 #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 #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 static 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, }; static krb5_enctype pre_w2k8enctypes[] = { ENCTYPE_ARCFOUR_HMAC, ENCTYPE_DES_CBC_CRC, ENCTYPE_DES_CBC_MD5, }; #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]; 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 int smb_ads_open_main(smb_ads_handle_t **, 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 void smb_ads_free_cached_host(void); 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_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_is_same_domain(char *, char *); 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 *); static krb5_enctype *smb_ads_get_enctypes(int, int *); /* * 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) mutex_unlock(&smb_ads_cfg.c_mtx); /* Force -lads to load, for dtrace. */ DsFreeDcInfo(NULL); } 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. * (See smbd_join.c) * * 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(boolean_t force_rediscovery) { char new_site[SMB_ADS_SITE_MAX]; (void) smb_config_getstr(SMB_CI_ADS_SITE, new_site, SMB_ADS_SITE_MAX); (void) mutex_lock(&smb_ads_cfg.c_mtx); (void) strlcpy(smb_ads_cfg.c_site, new_site, SMB_ADS_SITE_MAX); (void) mutex_unlock(&smb_ads_cfg.c_mtx); smb_ads_free_cached_host(); if (force_rediscovery) { (void) _DsForceRediscovery(NULL, 0); } } /* * 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); } /* * 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) { 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); 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_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_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. * * 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) { smb_ads_host_info_t *host = NULL; DOMAIN_CONTROLLER_INFO *dci = NULL; struct sockaddr_storage *ss; uint32_t flags = DS_DS_FLAG; uint32_t status; int tries; (void) mutex_lock(&smb_ads_cached_host_mtx); if (smb_ads_validate_cache_host(domain)) { 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(); /* * The _real_ DC Locator is over in idmapd. * Door call over there to get it. */ tries = 15; again: status = _DsGetDcName( NULL, /* ComputerName */ domain, NULL, /* DomainGuid */ NULL, /* SiteName */ flags, &dci); switch (status) { case 0: break; /* * We can see these errors when joining a domain, if we race * asking idmap for the DC before it knows the new domain. */ case NT_STATUS_NO_SUCH_DOMAIN: /* Specified domain unknown */ case NT_STATUS_INVALID_SERVER_STATE: /* not in domain mode. */ if (--tries > 0) { (void) sleep(1); goto again; } /* FALLTHROUGH */ case NT_STATUS_DOMAIN_CONTROLLER_NOT_FOUND: case NT_STATUS_CANT_WAIT: /* timeout over in idmap */ default: return (NULL); } host = calloc(1, sizeof (*host)); if (host == NULL) goto out; (void) strlcpy(host->name, dci->DomainControllerName, MAXHOSTNAMELEN); ss = (void *)dci->_sockaddr; switch (ss->ss_family) { case AF_INET: { struct sockaddr_in *sin = (void *)ss; host->port = ntohs(sin->sin_port); host->ipaddr.a_family = AF_INET; (void) memcpy(&host->ipaddr.a_ipv4, &sin->sin_addr, sizeof (in_addr_t)); break; } case AF_INET6: { struct sockaddr_in6 *sin6 = (void *)ss; host->port = ntohs(sin6->sin6_port); host->ipaddr.a_family = AF_INET6; (void) memcpy(&host->ipaddr.a_ipv6, &sin6->sin6_addr, sizeof (in6_addr_t)); break; } default: syslog(LOG_ERR, "no addr for DC %s", dci->DomainControllerName); free(host); host = NULL; goto out; } host->flags = dci->Flags; (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); (void) mutex_unlock(&smb_ads_cached_host_mtx); out: DsFreeDcInfo(dci); 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]; smb_ads_handle_t *h; smb_ads_status_t err; if (smb_config_get_secmode() != SMB_SECMODE_DOMAIN) return (NULL); if (smb_getfqdomainname(domain, MAXHOSTNAMELEN) != 0) return (NULL); err = smb_ads_open_main(&h, domain, NULL, NULL); if (err != 0) { smb_ads_log_errmsg(err); return (NULL); } return (h); } 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 int smb_ads_open_main(smb_ads_handle_t **hp, 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 err, rc; *hp = NULL; if (user != NULL) { err = smb_kinit(domain, user, password); if (err != 0) { syslog(LOG_ERR, "smbns: kinit failed"); return (err); } user = NULL; password = NULL; } ads_host = smb_ads_find_host(domain); if (ads_host == NULL) return (SMB_ADS_CANT_LOCATE_DC); ah = (smb_ads_handle_t *)malloc(sizeof (smb_ads_handle_t)); if (ah == NULL) { free(ads_host); return (ENOMEM); } (void) memset(ah, 0, sizeof (smb_ads_handle_t)); if ((ld = ldap_init(ads_host->name, ads_host->port)) == NULL) { syslog(LOG_ERR, "smbns: ldap_init failed"); smb_ads_free_cached_host(); free(ah); free(ads_host); return (SMB_ADS_LDAP_INIT); } 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 (SMB_ADS_LDAP_SETOPT); } (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 (SMB_ADS_LDAP_SETOPT); } /* * 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 (SMB_ADS_LDAP_SET_DOM); } ah->hostname = strdup(ads_host->name); if (ah->hostname == NULL) { smb_ads_close(ah); free(ads_host); return (ENOMEM); } (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 (ENOMEM); } } else { ah->site = NULL; } (void) mutex_unlock(&smb_ads_cfg.c_mtx); syslog(LOG_DEBUG, "smbns: smb_ads_open_main"); syslog(LOG_DEBUG, "smbns: domain: %s", ah->domain); syslog(LOG_DEBUG, "smbns: domain_dn: %s", ah->domain_dn); syslog(LOG_DEBUG, "smbns: ip_addr: %s", ah->ip_addr); syslog(LOG_DEBUG, "smbns: hostname: %s", ah->hostname); syslog(LOG_DEBUG, "smbns: site: %s", (ah->site != NULL) ? ah->site : ""); 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, "smbns: ldap_sasl_..._bind_s failed (%s)", ldap_err2string(rc)); smb_ads_close(ah); free(ads_host); return (SMB_ADS_LDAP_SASL_BIND); } syslog(LOG_DEBUG, "smbns: ldap_sasl_..._bind_s success"); free(ads_host); *hp = ah; return (SMB_ADS_SUCCESS); } /* * 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]); } } /* * 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_new_comp_dn * * Build the distinguished name for a new machine account * prepend: cn=SamAccountName, cn=Computers, ...domain_dn... */ static void smb_ads_get_new_comp_dn(smb_ads_handle_t *ah, char *buf, size_t buflen, char *container) { char nbname[NETBIOS_NAME_SZ]; if (container == NULL) container = "cn=" SMB_ADS_COMPUTERS_CN; (void) smb_getnetbiosname(nbname, sizeof (nbname)); (void) snprintf(buf, buflen, "cn=%s,%s,%s", nbname, container, ah->domain_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; res = NULL; attr[0] = SMB_ADS_ATTR_DCLEVEL; attr[1] = NULL; rc = ldap_search_s(ah->ld, "", LDAP_SCOPE_BASE, NULL, attr, 0, &res); if (rc != LDAP_SUCCESS) { syslog(LOG_ERR, "smb_ads_get_dc_level: " "LDAP search, error %s", ldap_err2string(rc)); (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); } rc = -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. */ syslog(LOG_DEBUG, "smb_ads_get_dc_level: " "LDAP values missing, assume W2K"); (void) ldap_msgfree(res); return (SMB_ADS_DCLEVEL_W2K); } if (vals[0] != NULL) { rc = atoi(vals[0]); syslog(LOG_DEBUG, "smb_ads_get_dc_level: " "LDAP value %d", rc); } 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) strlcat(fqhost, ".", len); (void) strlcat(fqhost, ah->domain, len); 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]; char *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 usrctl_buf[16]; char encrypt_buf[16]; int max; smb_krb5_pn_set_t spn, upn; syslog(LOG_DEBUG, "smb_ads_computer_op, op=%s dn=%s", (op == LDAP_MOD_ADD) ? "add" : "replace", dn); if (smb_getsamaccount(sam_acct, sizeof (sam_acct)) != 0) return (-1); if (smb_ads_getfqhostname(ah, fqhost, MAXHOSTNAMELEN)) return (-1); /* The SPN attribute is multi-valued and must be 1 or greater */ if (smb_krb5_get_pn_set(&spn, SMB_PN_SPN_ATTR, ah->domain) == 0) return (-1); /* The UPN attribute is single-valued and cannot be zero */ if (smb_krb5_get_pn_set(&upn, SMB_PN_UPN_ATTR, ah->domain) != 1) { smb_krb5_free_pn_set(&spn); smb_krb5_free_pn_set(&upn); 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) { smb_krb5_free_pn_set(&spn); smb_krb5_free_pn_set(&upn); 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; attrs[j]->mod_values = upn.s_pns; attrs[++j]->mod_op = op; attrs[j]->mod_type = SMB_ADS_ATTR_SPN; attrs[j]->mod_values = spn.s_pns; 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); smb_krb5_free_pn_set(&spn); smb_krb5_free_pn_set(&upn); 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; syslog(LOG_DEBUG, "smbns: ads_getattr (%s)", avpair->avp_attr); vals = ldap_get_values(ld, entry, avpair->avp_attr); if (!vals) { syslog(LOG_DEBUG, "smbns: ads_getattr err: no vals"); return (SMB_ADS_STAT_NOT_FOUND); } if (!vals[0]) { syslog(LOG_DEBUG, "smbns: ads_getattr err: no vals[0]"); ldap_value_free(vals); return (SMB_ADS_STAT_NOT_FOUND); } avpair->avp_val = strdup(vals[0]); if (!avpair->avp_val) { syslog(LOG_DEBUG, "smbns: ads_getattr err: no mem"); rc = SMB_ADS_STAT_ERR; } else { syslog(LOG_DEBUG, "smbns: ads_getattr (%s) OK, val=%s", avpair->avp_attr, avpair->avp_val); } ldap_value_free(vals); return (rc); } /* * Process query's result, making sure we have what we need. * * There's some non-obvious logic here for checking the returned * DNS name for the machine account, trying to avoid modifying * someone else's machine account. When we search for a machine * account we always ask for the DNS name. For a pre-created * machine account, the DNS name will be not set, and that's OK. * If we see a DNS name and it doesn't match our DNS name, we'll * assume the account belongs to someone else and return "DUP". * * Only do the DNS name check for our initial search for the * machine account, which has avpair->avp_attr = SMB_ADS_ATTR_DN */ static smb_ads_qstat_t smb_ads_get_qstat(smb_ads_handle_t *ah, LDAPMessage *res, smb_ads_avpair_t *avpair) { smb_ads_qstat_t rc = SMB_ADS_STAT_FOUND; LDAPMessage *entry; if (ldap_count_entries(ah->ld, res) == 0) { syslog(LOG_DEBUG, "smbns: find_computer, " "ldap_count_entries zero"); return (SMB_ADS_STAT_NOT_FOUND); } if ((entry = ldap_first_entry(ah->ld, res)) == NULL) { syslog(LOG_DEBUG, "smbns: find_computer, " "ldap_first_entry error"); return (SMB_ADS_STAT_ERR); } /* Have an LDAP entry (found something) */ syslog(LOG_DEBUG, "smbns: find_computer, have LDAP resp."); if (avpair != NULL && strcmp(avpair->avp_attr, SMB_ADS_ATTR_DN) == 0) { char fqhost[MAXHOSTNAMELEN]; smb_ads_avpair_t dnshost_avp; syslog(LOG_DEBUG, "smbns: find_computer, check DNS name"); if (smb_ads_getfqhostname(ah, fqhost, MAXHOSTNAMELEN)) return (SMB_ADS_STAT_ERR); dnshost_avp.avp_attr = SMB_ADS_ATTR_DNSHOST; dnshost_avp.avp_val = NULL; rc = smb_ads_getattr(ah->ld, entry, &dnshost_avp); /* * Status from finding the DNS name value */ switch (rc) { case SMB_ADS_STAT_FOUND: /* * Found a DNS name. If it doesn't match ours, * 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)) { syslog(LOG_DEBUG, "smbns: find_computer, " "duplicate name (%s)", dnshost_avp.avp_val); rc = SMB_ADS_STAT_DUP; } free(dnshost_avp.avp_val); break; case SMB_ADS_STAT_NOT_FOUND: /* * No dNSHostname attribute, so probably a * pre-created computer account. Use it. * * Returns SMB_ADS_STAT_FOUND for the status * of finding the machine account. */ rc = SMB_ADS_STAT_FOUND; break; default: break; } if (rc != SMB_ADS_STAT_FOUND) return (rc); } if (avpair) { syslog(LOG_DEBUG, "smbns: find_computer, check %s", avpair->avp_attr); 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. Caller avpair requests are: * smb_ads_find_computer() asks for SMB_ADS_ATTR_DN * smb_ads_lookup_computer_attr_kvno() SMB_ADS_ATTR_KVNO * * 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]; char tmpbuf[SMB_ADS_MAXBUFLEN]; smb_ads_qstat_t rc; int err; 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, tmpbuf) != 0) return (SMB_ADS_STAT_ERR); (void) snprintf(filter, sizeof (filter), "(&(objectClass=computer)(%s=%s))", SMB_ADS_ATTR_SAMACCT, tmpbuf); syslog(LOG_DEBUG, "smbns: lookup_computer, " "dn=%s, scope=%d", dn, scope); syslog(LOG_DEBUG, "smbns: lookup_computer, " "filter=%s", filter); syslog(LOG_DEBUG, "smbns: lookup_computer, " "attrs[0]=%s", attrs[0]); syslog(LOG_DEBUG, "smbns: lookup_computer, " "attrs[1]=%s", attrs[1] ? attrs[1] : ""); err = ldap_search_s(ah->ld, dn, scope, filter, attrs, 0, &res); if (err != LDAP_SUCCESS) { syslog(LOG_DEBUG, "smbns: lookup_computer, " "LDAP search failed, dn=(%s), scope=%d, err=%s", dn, scope, ldap_err2string(err)); (void) ldap_msgfree(res); return (SMB_ADS_STAT_NOT_FOUND); } syslog(LOG_DEBUG, "smbns: find_computer, ldap_search OK"); rc = smb_ads_get_qstat(ah, res, avpair); if (rc == SMB_ADS_STAT_FOUND) { syslog(LOG_DEBUG, "smbns: find_computer, attr %s = %s", avpair->avp_attr, avpair->avp_val); } else { syslog(LOG_DEBUG, "smbns: find_computer, " "get query status, error %d", rc); } /* free the search results */ (void) ldap_msgfree(res); return (rc); } /* * smb_ads_find_computer * * Searches the directory for the machine account (SamAccountName) * 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; avpair.avp_val = NULL; (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; avpair.avp_val = NULL; 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); } /* * 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_ads_status_t smb_ads_join(char *domain, char *container, char *user, char *usr_passwd, char *machine_passwd) { smb_ads_handle_t *ah = NULL; krb5_context ctx = NULL; krb5_principal *krb5princs = NULL; krb5_kvno kvno; boolean_t delete = B_TRUE; smb_ads_status_t rc; boolean_t new_acct; int dclevel, num, usrctl_flags = 0; smb_ads_qstat_t qstat; char dn[SMB_ADS_DN_MAX]; char tmpfile[] = SMBNS_KRB5_KEYTAB_TMP; int cnt, x; smb_krb5_pn_set_t spns; krb5_enctype *encptr; rc = smb_ads_open_main(&ah, domain, user, usr_passwd); if (rc != 0) { const char *s = smb_ads_strerror(rc); syslog(LOG_ERR, "smb_ads_join: open_main, error %s", s); smb_ccache_remove(SMB_CCACHE_PATH); return (rc); } 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; syslog(LOG_INFO, "smb_ads_join: machine account found." " Updating: %s", dn); 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_new_comp_dn(ah, dn, SMB_ADS_DN_MAX, container); syslog(LOG_INFO, "smb_ads_join: machine account not found." " Creating: %s", dn); 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: syslog(LOG_INFO, "smb_ads_find_computer, rc=%d", qstat); 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); } if (smb_krb5_ctx_init(&ctx) != 0) { rc = SMB_ADJOIN_ERR_INIT_KRB_CTX; goto adjoin_cleanup; } if (smb_krb5_get_pn_set(&spns, SMB_PN_KEYTAB_ENTRY, ah->domain) == 0) { rc = SMB_ADJOIN_ERR_GET_SPNS; goto adjoin_cleanup; } if (smb_krb5_get_kprincs(ctx, spns.s_pns, spns.s_cnt, &krb5princs) != 0) { smb_krb5_free_pn_set(&spns); rc = SMB_ADJOIN_ERR_GET_SPNS; goto adjoin_cleanup; } cnt = spns.s_cnt; smb_krb5_free_pn_set(&spns); /* New machine_passwd was filled in by our caller. */ if (smb_krb5_setpwd(ctx, ah->domain, 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. * Try to set this, but don't fail the join if we can't. * Look into just removing this... */ 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); set_ctl_again: x = smb_ads_update_computer_cntrl_attr(ah, usrctl_flags, dn); if (x != LDAP_SUCCESS && (usrctl_flags & SMB_ADS_USER_ACCT_CTL_TRUSTED_FOR_DELEGATION) != 0) { syslog(LOG_NOTICE, "Unable to set the " "TRUSTED_FOR_DELEGATION userAccountControl flag on the " "machine account in Active Directory. It may be necessary " "to set that via Active Directory administration."); usrctl_flags &= ~SMB_ADS_USER_ACCT_CTL_TRUSTED_FOR_DELEGATION; goto set_ctl_again; } if (x != LDAP_SUCCESS) { rc = SMB_ADJOIN_ERR_UPDATE_CNTRL_ATTR; goto adjoin_cleanup; } if (mktemp(tmpfile) == NULL) { rc = SMB_ADJOIN_ERR_WRITE_KEYTAB; goto adjoin_cleanup; } encptr = smb_ads_get_enctypes(dclevel, &num); if (smb_krb5_kt_populate(ctx, ah->domain, krb5princs, cnt, tmpfile, kvno, machine_passwd, encptr, num) != 0) { rc = SMB_ADJOIN_ERR_WRITE_KEYTAB; goto adjoin_cleanup; } delete = B_FALSE; rc = SMB_ADS_SUCCESS; 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_kprincs(ctx, krb5princs, cnt); smb_krb5_ctx_fini(ctx); } /* commit keytab file */ if (rc == SMB_ADS_SUCCESS) { if (rename(tmpfile, SMBNS_KRB5_KEYTAB) != 0) { (void) unlink(tmpfile); rc = SMB_ADJOIN_ERR_COMMIT_KEYTAB; } } else { (void) unlink(tmpfile); } smb_ads_close(ah); smb_ccache_remove(SMB_CCACHE_PATH); return (rc); } struct xlate_table { int err; const char * const msg; }; static const struct xlate_table adjoin_table[] = { { SMB_ADS_SUCCESS, "Success" }, { SMB_ADS_KRB5_INIT_CTX, "Failed creating a Kerberos context." }, { SMB_ADS_KRB5_CC_DEFAULT, "Failed to resolve default credential cache." }, { SMB_ADS_KRB5_PARSE_PRINCIPAL, "Failed parsing the user principal name." }, { SMB_ADS_KRB5_GET_INIT_CREDS_OTHER, "Failed getting initial credentials. (See svc. log)" }, { SMB_ADS_KRB5_GET_INIT_CREDS_PW, "Failed getting initial credentials. (Wrong password?)" }, { SMB_ADS_KRB5_GET_INIT_CREDS_SKEW, "Failed getting initial credentials. (Clock skew too great)" }, { SMB_ADS_KRB5_CC_INITIALIZE, "Failed initializing the credential cache." }, { SMB_ADS_KRB5_CC_STORE_CRED, "Failed to update the credential cache." }, { SMB_ADS_CANT_LOCATE_DC, "Failed to locate a domain controller." }, { SMB_ADS_LDAP_INIT, "Failed to create an LDAP handle." }, { SMB_ADS_LDAP_SETOPT, "Failed to set an LDAP option." }, { SMB_ADS_LDAP_SET_DOM, "Failed to set the LDAP handle DN." }, { SMB_ADS_LDAP_SASL_BIND, "Failed to bind the LDAP handle. " "Usually indicates an authentication problem." }, { 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)." }, { SMB_ADJOIN_ERR_AUTH_NETLOGON, "Failed to authenticate using the new computer account." }, { SMB_ADJOIN_ERR_STORE_PROPS, "Failed to store computer account information locally." }, { 0, NULL } }; /* * smb_ads_strerror * * Lookup an error message for the specific adjoin error code. */ const char * smb_ads_strerror(int err) { const struct xlate_table *xt; if (err > 0 && err < SMB_ADS_ERRNO_GAP) return (strerror(err)); for (xt = adjoin_table; xt->msg; xt++) if (xt->err == err) return (xt->msg); return ("Unknown error code."); } void smb_ads_log_errmsg(smb_ads_status_t err) { const char *s = smb_ads_strerror(err); syslog(LOG_NOTICE, "%s", s); } /* * 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 * dci - the name and address of the found DC */ uint32_t smb_ads_lookup_msdcs(char *fqdn, smb_dcinfo_t *dci) { smb_ads_host_info_t *hinfo = NULL; char ipstr[INET6_ADDRSTRLEN]; if (!fqdn || !dci) return (NT_STATUS_INTERNAL_ERROR); ipstr[0] = '\0'; if ((hinfo = smb_ads_find_host(fqdn)) == NULL) return (NT_STATUS_DOMAIN_CONTROLLER_NOT_FOUND); (void) smb_inet_ntop(&hinfo->ipaddr, ipstr, SMB_IPSTRLEN(hinfo->ipaddr.a_family)); smb_tracef("msdcsLookupADS: %s [%s]", hinfo->name, ipstr); (void) strlcpy(dci->dc_name, hinfo->name, sizeof (dci->dc_name)); dci->dc_addr = hinfo->ipaddr; dci->dc_flags = hinfo->flags; free(hinfo); return (NT_STATUS_SUCCESS); } static krb5_enctype * smb_ads_get_enctypes(int dclevel, int *num) { krb5_enctype *encptr; 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; } return (encptr); }