root/usr/src/lib/smbsrv/libsmbns/common/smbns_ads.c
/*
 * 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-2024 RackTop Systems, Inc.
 */

#include <sys/param.h>
#include <ldap.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <netdb.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/nameser.h>
#include <resolv.h>
#include <sys/synch.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#include <sasl/sasl.h>
#include <note.h>
#include <errno.h>
#include <cryptoutil.h>
#include <ads/dsgetdc.h>

#include <smbsrv/libsmbns.h>
#include <smbns_dyndns.h>
#include <smbns_krb.h>

#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_UCPWD      "unicodePwd"
#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=<sharename>,<container RDNs>,<domain DN>
 *
 * 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);
}

/*
 * Set a machine account password using LDAP
 *
 * Compose the funky unicode password string per
 * [MS-ADTS] 3.1.1.3.1.5.1 unicodePwd
 * which includes literal double quotes.
 */
static int
smb_ads_update_acct_passwd(smb_ads_handle_t *ah, char *passwd, char *dn)
{
        LDAPMod *attrs[2];
        struct berval *bvp[2];
        struct berval bval;
        char *qpass_buf = NULL;         /* quoted password */
        int qpass_len = 0;
        smb_wchar_t *ucpwd_buf = NULL;  /* unicode pw */
        int ucpwd_len = 0;              /* length in bytes */
        int ret = 0;

        if (smb_ads_alloc_attr(attrs, sizeof (attrs) / sizeof (LDAPMod *)) != 0)
                return (LDAP_NO_MEMORY);

        qpass_len = asprintf(&qpass_buf, "\"%s\"", passwd);
        if (qpass_len < 0) {
                qpass_len = 0;
                ret = LDAP_NO_MEMORY;
                goto out;
        }

        /*
         * Allocate and fill Unicode passwd buffer.
         * Size to include a smb_wchar_t null termination.
         */
        ucpwd_len = smb_wcequiv_strlen(qpass_buf);
        ucpwd_buf = calloc(1, ucpwd_len + sizeof (smb_wchar_t));
        if (ucpwd_buf == NULL) {
                ret = LDAP_NO_MEMORY;
                goto out;
        }
        ret = smb_mbstowcs(ucpwd_buf, qpass_buf, ucpwd_len / 2);
        if (ret < 0) {
                ret = LDAP_ENCODING_ERROR;
                goto out;
        }

        bval.bv_val = (char *)ucpwd_buf;
        bval.bv_len = ucpwd_len;
        bvp[0] = &bval;
        bvp[1] = NULL;

        attrs[0]->mod_op = LDAP_MOD_REPLACE | LDAP_MOD_BVALUES;
        attrs[0]->mod_type = SMB_ADS_ATTR_UCPWD;
        attrs[0]->mod_bvalues = bvp;

        if ((ret = ldap_modify_s(ah->ld, dn, attrs)) != LDAP_SUCCESS) {
                syslog(LOG_NOTICE, "ldap_modify: %s", ldap_err2string(ret));
        }

out:
        freezero(ucpwd_buf, ucpwd_len);
        freezero(qpass_buf, qpass_len);
        smb_ads_free_attr(attrs);
        return (ret);
}

/*
 * 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);
}

/* Available as a work-around if smb_ads_update_acct_passwd fails */
boolean_t smbns_ads_try_kpasswd = 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_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. */
        x = smb_ads_update_acct_passwd(ah, machine_passwd, dn);
        if (x != 0 && smbns_ads_try_kpasswd)
                x = smb_krb5_setpwd(ctx, ah->domain, machine_passwd);
        if (x != 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." },
        { SMB_ADJOIN_ERR_ALREADY_JOINED,
            "Already joined to a domain. "
            "Run \"smbadm join -w workgroup\" first." },
        { 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);
}