root/usr/src/lib/smbsrv/libmlsvc/common/mlsvc_domain.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) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2017 Nexenta Systems, Inc.  All rights reserved.
 * Copyright 2022 RackTop Systems, Inc.
 */

#include <syslog.h>
#include <synch.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <sys/errno.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/nameser.h>
#include <resolv.h>
#include <netdb.h>
#include <assert.h>

#include <smbsrv/libsmb.h>
#include <smbsrv/libsmbns.h>
#include <smbsrv/libmlsvc.h>

#include <smbsrv/smbinfo.h>
#include <lsalib.h>
#include <mlsvc.h>

/*
 * DC Locator
 */
#define SMB_DCLOCATOR_TIMEOUT   45      /* seconds */
#define SMB_IS_FQDN(domain)     (strchr(domain, '.') != NULL)

/* How long to pause after a failure to find any domain controllers. */
int smb_ddiscover_failure_pause = 5; /* sec. */

typedef struct smb_dclocator {
        smb_dcinfo_t    sdl_dci; /* .dc_name .dc_addr */
        char            sdl_domain[SMB_PI_MAX_DOMAIN];
        boolean_t       sdl_locate;
        boolean_t       sdl_bad_dc;
        boolean_t       sdl_cfg_chg;
        mutex_t         sdl_mtx;
        cond_t          sdl_cv;
        uint32_t        sdl_status;
} smb_dclocator_t;

static smb_dclocator_t smb_dclocator;
static pthread_t smb_dclocator_thr;

static void *smb_ddiscover_service(void *);
static uint32_t smb_ddiscover_qinfo(char *, char *, smb_domainex_t *);
static void smb_ddiscover_enum_trusted(char *, char *, smb_domainex_t *);
static uint32_t smb_ddiscover_use_config(char *, smb_domainex_t *);
static void smb_domainex_free(smb_domainex_t *);
static void smb_set_krb5_realm(char *);

/*
 * ===================================================================
 * API to initialize DC locator thread, trigger DC discovery, and
 * get the discovered DC and/or domain information.
 * ===================================================================
 */

/*
 * Initialization of the DC locator thread.
 * Returns 0 on success, an error number if thread creation fails.
 */
int
smb_dclocator_init(void)
{
        pthread_attr_t tattr;
        int rc;

        /*
         * We need the smb_ddiscover_service to run on startup,
         * so it will enter smb_ddiscover_main() and put the
         * SMB "domain cache" into "updating" state so clients
         * trying to logon will wait while we're finding a DC.
         */
        smb_dclocator.sdl_locate = B_TRUE;

        (void) pthread_attr_init(&tattr);
        (void) pthread_attr_setdetachstate(&tattr, PTHREAD_CREATE_DETACHED);
        rc = pthread_create(&smb_dclocator_thr, &tattr,
            smb_ddiscover_service, &smb_dclocator);
        (void) pthread_attr_destroy(&tattr);
        return (rc);
}

/*
 * This is the entry point for discovering a domain controller for the
 * specified domain.  Called during join domain, and then periodically
 * by smbd_dc_update (the "DC monitor" thread).
 *
 * The actual work of discovering a DC is handled by DC locator thread.
 * All we do here is signal the request and wait for a DC or a timeout.
 *
 * Input parameters:
 *  domain - domain to be discovered (can either be NetBIOS or DNS domain)
 *
 * Output parameter:
 *  dp - on success, dp will be filled with the discovered DC and domain
 *       information.
 *
 * Returns B_TRUE if the DC/domain info is available.
 */
boolean_t
smb_locate_dc(char *domain, smb_domainex_t *dp)
{
        int rc;
        boolean_t rv;
        timestruc_t to;
        smb_domainex_t domain_info;

        if (domain == NULL || *domain == '\0') {
                syslog(LOG_DEBUG, "smb_locate_dc NULL dom");
                smb_set_krb5_realm(NULL);
                return (B_FALSE);
        }

        (void) mutex_lock(&smb_dclocator.sdl_mtx);

        if (strcmp(smb_dclocator.sdl_domain, domain)) {
                (void) strlcpy(smb_dclocator.sdl_domain, domain,
                    sizeof (smb_dclocator.sdl_domain));
                smb_dclocator.sdl_cfg_chg = B_TRUE;
                syslog(LOG_DEBUG, "smb_locate_dc new dom=%s", domain);
                smb_set_krb5_realm(domain);
        }

        if (!smb_dclocator.sdl_locate) {
                smb_dclocator.sdl_locate = B_TRUE;
                (void) cond_broadcast(&smb_dclocator.sdl_cv);
        }

        while (smb_dclocator.sdl_locate) {
                to.tv_sec = SMB_DCLOCATOR_TIMEOUT;
                to.tv_nsec = 0;
                rc = cond_reltimedwait(&smb_dclocator.sdl_cv,
                    &smb_dclocator.sdl_mtx, &to);

                if (rc == ETIME) {
                        syslog(LOG_NOTICE, "smb_locate_dc timeout");
                        rv = B_FALSE;
                        goto out;
                }
        }
        if (smb_dclocator.sdl_status != 0) {
                syslog(LOG_NOTICE, "smb_locate_dc status 0x%x",
                    smb_dclocator.sdl_status);
                rv = B_FALSE;
                goto out;
        }

        if (dp == NULL)
                dp = &domain_info;
        rv = smb_domain_getinfo(dp);

out:
        (void) mutex_unlock(&smb_dclocator.sdl_mtx);

        return (rv);
}

/*
 * Tell the domain discovery service to run again now,
 * and assume changed configuration (i.e. a new DC).
 * Like the first part of smb_locate_dc().
 *
 * Note: This is called from the service refresh handler
 * and the door handler to tell the ddiscover thread to
 * request the new DC from idmap.  Therefore, we must not
 * trigger a new idmap discovery run from here, or that
 * would start a ping-pong match.
 */
/* ARGSUSED */
void
smb_ddiscover_refresh()
{

        (void) mutex_lock(&smb_dclocator.sdl_mtx);

        if (smb_dclocator.sdl_cfg_chg == B_FALSE) {
                smb_dclocator.sdl_cfg_chg = B_TRUE;
                syslog(LOG_DEBUG, "smb_ddiscover_refresh set cfg changed");
        }
        if (!smb_dclocator.sdl_locate) {
                smb_dclocator.sdl_locate = B_TRUE;
                (void) cond_broadcast(&smb_dclocator.sdl_cv);
        }

        (void) mutex_unlock(&smb_dclocator.sdl_mtx);
}

/*
 * Called by our client-side threads after they fail to connect to
 * the DC given to them by smb_locate_dc().  This is often called
 * after some delay, because the connection timeout delays these
 * threads for a while, so it's quite common that the DC locator
 * service has already started looking for a new DC.  These late
 * notifications should not continually restart the DC locator.
 */
void
smb_ddiscover_bad_dc(char *bad_dc)
{

        assert(bad_dc[0] != '\0');

        (void) mutex_lock(&smb_dclocator.sdl_mtx);

        syslog(LOG_DEBUG, "smb_ddiscover_bad_dc, cur=%s, bad=%s",
            smb_dclocator.sdl_dci.dc_name, bad_dc);

        if (strcmp(smb_dclocator.sdl_dci.dc_name, bad_dc)) {
                /*
                 * The "bad" DC is no longer the current one.
                 * Probably a late "bad DC" report.
                 */
                goto out;
        }
        if (smb_dclocator.sdl_bad_dc) {
                /* Someone already marked the current DC as "bad". */
                syslog(LOG_DEBUG, "smb_ddiscover_bad_dc repeat");
                goto out;
        }

        /*
         * Mark the current DC as "bad" and let the DC Locator
         * run again if it's not already.
         */
        syslog(LOG_INFO, "smb_ddiscover, bad DC: %s", bad_dc);
        smb_dclocator.sdl_bad_dc = B_TRUE;
        smb_domain_bad_dc();

        /* In-line smb_ddiscover_kick */
        if (!smb_dclocator.sdl_locate) {
                smb_dclocator.sdl_locate = B_TRUE;
                (void) cond_broadcast(&smb_dclocator.sdl_cv);
        }

out:
        (void) mutex_unlock(&smb_dclocator.sdl_mtx);
}


/*
 * ==========================================================
 * DC discovery functions
 * ==========================================================
 */

/*
 * This is the domain and DC discovery service: it gets woken up whenever
 * there is need to locate a domain controller.
 *
 * Upon success, the SMB domain cache will be populated with the discovered
 * DC and domain info.
 */
/*ARGSUSED*/
static void *
smb_ddiscover_service(void *arg)
{
        smb_domainex_t dxi;
        smb_dclocator_t *sdl = arg;
        uint32_t status;
        boolean_t bad_dc;
        boolean_t cfg_chg;

        for (;;) {
                /*
                 * Wait to be signaled for work by one of:
                 * smb_locate_dc(), smb_ddiscover_refresh(),
                 * smb_ddiscover_bad_dc()
                 */
                syslog(LOG_DEBUG, "smb_ddiscover_service waiting");

                (void) mutex_lock(&sdl->sdl_mtx);
                while (!sdl->sdl_locate)
                        (void) cond_wait(&sdl->sdl_cv,
                            &sdl->sdl_mtx);

        find_again:
                if (!smb_config_getbool(SMB_CI_DOMAIN_MEMB)) {
                        sdl->sdl_status = NT_STATUS_INVALID_SERVER_STATE;
                        syslog(LOG_DEBUG, "smb_ddiscover_service: "
                            "not a domain member");
                        goto wait_again;
                }
                if (sdl->sdl_domain[0] == '\0') {
                        sdl->sdl_status = NT_STATUS_INVALID_SERVER_STATE;
                        syslog(LOG_DEBUG, "smb_ddiscover_service: "
                            "null domain name");
                        goto wait_again;
                }

                /*
                 * Want to know if these change below.
                 * Note: mutex held here
                 */
                bad_dc = sdl->sdl_bad_dc;
                sdl->sdl_bad_dc = B_FALSE;
                if (bad_dc) {
                        /*
                         * Need to clear the current DC name or
                         * ddiscover_bad_dc will keep setting bad_dc
                         */
                        sdl->sdl_dci.dc_name[0] = '\0';
                }
                cfg_chg = sdl->sdl_cfg_chg;
                sdl->sdl_cfg_chg = B_FALSE;

                (void) mutex_unlock(&sdl->sdl_mtx);

                syslog(LOG_DEBUG, "smb_ddiscover_service running "
                    "cfg_chg=%d bad_dc=%d", (int)cfg_chg, (int)bad_dc);

                /*
                 * Clear the cached DC now so that we'll ask idmap again.
                 * If our current DC gave us errors, force rediscovery.
                 */
                smb_ads_refresh(bad_dc);

                /*
                 * Search for the DC, save the result.
                 */
                bzero(&dxi, sizeof (dxi));
                status = smb_ddiscover_main(sdl->sdl_domain, &dxi);
                if (status == 0)
                        smb_domain_save();

                (void) mutex_lock(&sdl->sdl_mtx);

                sdl->sdl_status = status;
                if (status == 0) {
                        sdl->sdl_dci = dxi.d_dci;
                } else {
                        syslog(LOG_DEBUG, "smb_ddiscover_service "
                            "retry after STATUS_%s",
                            xlate_nt_status(status));
                        (void) sleep(smb_ddiscover_failure_pause);
                        goto find_again;
                }

                /*
                 * Run again if either of cfg_chg or bad_dc
                 * was turned on during smb_ddiscover_main().
                 * Note: mutex held here.
                 */
                if (sdl->sdl_bad_dc) {
                        syslog(LOG_DEBUG, "smb_ddiscover_service "
                            "restart because bad_dc was set");
                        goto find_again;
                }
                if (sdl->sdl_cfg_chg) {
                        syslog(LOG_DEBUG, "smb_ddiscover_service "
                            "restart because cfg_chg was set");
                        goto find_again;
                }

        wait_again:
                sdl->sdl_locate = B_FALSE;
                sdl->sdl_bad_dc = B_FALSE;
                sdl->sdl_cfg_chg = B_FALSE;
                (void) cond_broadcast(&sdl->sdl_cv);
                (void) mutex_unlock(&sdl->sdl_mtx);
        }

        /*NOTREACHED*/
        return (NULL);
}

/*
 * Discovers a domain controller for the specified domain via DNS.
 * After the domain controller is discovered successfully primary and
 * trusted domain infromation will be queried using RPC queries.
 *
 * Caller should zero out *dxi before calling, and after a
 * successful return should call:  smb_domain_save()
 */
uint32_t
smb_ddiscover_main(char *domain, smb_domainex_t *dxi)
{
        uint32_t status;

        if (domain[0] == '\0') {
                syslog(LOG_DEBUG, "smb_ddiscover_main NULL domain");
                return (NT_STATUS_INTERNAL_ERROR);
        }

        status = smb_ads_lookup_msdcs(domain, &dxi->d_dci);
        if (status != 0) {
                syslog(LOG_DEBUG, "smb_ddiscover_main can't find DC (%s)",
                    xlate_nt_status(status));
                goto out;
        }

        status = smb_ddiscover_qinfo(domain, dxi->d_dci.dc_name, dxi);
        if (status != 0) {
                syslog(LOG_DEBUG,
                    "smb_ddiscover_main can't get domain info (%s)",
                    xlate_nt_status(status));
                goto out;
        }

        if (smb_domain_start_update() != SMB_DOMAIN_SUCCESS) {
                syslog(LOG_DEBUG, "smb_ddiscover_main can't get lock");
                status = NT_STATUS_INTERNAL_ERROR;
        } else {
                smb_domain_update(dxi);
                smb_domain_end_update();
        }

out:
        /* Don't need the trusted domain list anymore. */
        smb_domainex_free(dxi);

        return (status);
}

/*
 * Obtain primary and trusted domain information using LSA queries.
 *
 * domain - either NetBIOS or fully-qualified domain name
 */
static uint32_t
smb_ddiscover_qinfo(char *domain, char *server, smb_domainex_t *dxi)
{
        uint32_t ret, tmp;

        /* If we must return failure, use this first one. */
        ret = lsa_query_dns_domain_info(server, domain, &dxi->d_primary);
        if (ret == NT_STATUS_SUCCESS)
                goto success;
        tmp = smb_ddiscover_use_config(domain, dxi);
        if (tmp == NT_STATUS_SUCCESS)
                goto success;
        tmp = lsa_query_primary_domain_info(server, domain, &dxi->d_primary);
        if (tmp == NT_STATUS_SUCCESS)
                goto success;

        /* All of the above failed. */
        return (ret);

success:
        smb_ddiscover_enum_trusted(domain, server, dxi);
        return (NT_STATUS_SUCCESS);
}

/*
 * Obtain trusted domains information using LSA queries.
 *
 * domain - either NetBIOS or fully-qualified domain name.
 */
static void
smb_ddiscover_enum_trusted(char *domain, char *server, smb_domainex_t *dxi)
{
        smb_trusted_domains_t *list;
        uint32_t status;

        list = &dxi->d_trusted;
        status = lsa_enum_trusted_domains_ex(server, domain, list);
        if (status != NT_STATUS_SUCCESS)
                (void) lsa_enum_trusted_domains(server, domain, list);
}

/*
 * If the domain to be discovered matches the current domain (i.e the
 * value of either domain or fqdn configuration), then get the primary
 * domain information from SMF.
 */
static uint32_t
smb_ddiscover_use_config(char *domain, smb_domainex_t *dxi)
{
        boolean_t use;
        smb_domain_t *dinfo;

        dinfo = &dxi->d_primary;
        bzero(dinfo, sizeof (smb_domain_t));

        if (smb_config_get_secmode() != SMB_SECMODE_DOMAIN)
                return (NT_STATUS_UNSUCCESSFUL);

        smb_config_getdomaininfo(dinfo->di_nbname, dinfo->di_fqname,
            NULL, NULL, NULL);

        if (SMB_IS_FQDN(domain))
                use = (smb_strcasecmp(dinfo->di_fqname, domain, 0) == 0);
        else
                use = (smb_strcasecmp(dinfo->di_nbname, domain, 0) == 0);

        if (use)
                smb_config_getdomaininfo(NULL, NULL, dinfo->di_sid,
                    dinfo->di_u.di_dns.ddi_forest,
                    dinfo->di_u.di_dns.ddi_guid);

        return ((use) ? NT_STATUS_SUCCESS : NT_STATUS_UNSUCCESSFUL);
}

static void
smb_domainex_free(smb_domainex_t *dxi)
{
        free(dxi->d_trusted.td_domains);
        dxi->d_trusted.td_domains = NULL;
}

static void
smb_set_krb5_realm(char *domain)
{
        static char realm[MAXHOSTNAMELEN];

        if (domain == NULL || domain[0] == '\0') {
                (void) unsetenv("KRB5_DEFAULT_REALM");
                return;
        }

        /* In case krb5.conf is not configured, set the default realm. */
        (void) strlcpy(realm, domain, sizeof (realm));
        (void) smb_strupr(realm);

        (void) setenv("KRB5_DEFAULT_REALM", realm, 1);
}