/*
 * 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 2019 Nexenta Systems, Inc.  All rights reserved.
 */

/*
 * Active Directory Auto-Discovery.
 *
 * This [project private] API allows the caller to provide whatever
 * details it knows a priori (i.e., provided via configuration so as to
 * override auto-discovery) and in any order.  Then the caller can ask
 * for any of the auto-discoverable parameters in any order.
 *
 * But there is an actual order in which discovery must be done.  Given
 * the discovery mechanism implemented here, that order is:
 *
 *  - the domain name joined must be discovered first
 *  - then the domain controllers
 *  - then the forest name and site name
 *  - then the global catalog servers, and site-specific domain
 *    controllers and global catalog servers.
 *
 * The API does not require it be called in the same order because there
 * may be other discovery mechanisms in the future, and exposing
 * ordering requirements of the current mechanism now can create trouble
 * down the line.  Also, this makes the API easier to use now, which
 * means less work to do some day when we make this a public API.
 *
 * Domain discovery is done by res_nsearch() of the DNS SRV RR name for
 * domain controllers.  As long as the joined domain appears in the DNS
 * resolver's search list then we'll find it.
 *
 * Domain controller discovery is a matter of formatting the DNS SRV RR
 * FQDN for domain controllers and doing a lookup for them.  Knowledge
 * of the domain name is not fundamentally required, but we separate the
 * two processes, which in practice can lead to one more DNS lookup than
 * is strictly required.
 *
 * Forest and site name discovery require an LDAP search of the AD
 * "configuration partition" at a domain controller for the joined
 * domain.  Forest and site name discovery depend on knowing the joined
 * domain name and domain controllers for that domain.
 *
 * Global catalog server discovery requires knowledge of the forest
 * name in order to format the DNS SRV RR FQDN to lookup.  Site-specific
 * domain controller discovery depends on knowing the site name (and,
 * therefore, joined domain, ...).  Site-specific global catalog server
 * discovery depends on knowledge of the forest and site names, which
 * depend on...
 *
 * All the work of discovering particular items is done by functions
 * named validate_<item>().  Each such function calls validate_<item>()
 * for any items that it depends on.
 *
 * This API is not thread-safe.
 */


#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <net/if.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/sockio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <resolv.h>
#include <netdb.h>
#include <ctype.h>
#include <errno.h>
#include <ldap.h>
#include <note.h>
#include <sasl/sasl.h>
#include <sys/u8_textprep.h>
#include <syslog.h>
#include <uuid/uuid.h>
#include <ads/dsgetdc.h>
#include "adutils_impl.h"
#include "addisc_impl.h"

/*
 * These set some sanity policies for discovery.  After a discovery
 * cycle, we will consider the results (successful or unsuccessful)
 * to be valid for at least MINIMUM_TTL seconds, and for at most
 * MAXIMUM_TTL seconds.  Note that the caller is free to request
 * discovery cycles sooner than MINIMUM_TTL if it has reason to believe
 * that the situation has changed.
 */
#define	MINIMUM_TTL	(5 * 60)
#define	MAXIMUM_TTL	(20 * 60)


#define	DNS_MAX_NAME	NS_MAXDNAME

#define	GC_PORT		3268

/* SRV RR names for various queries */
#define	LDAP_SRV_HEAD		"_ldap._tcp."
#define	SITE_SRV_MIDDLE		"%s._sites."
#define	GC_SRV_TAIL		"gc._msdcs"
#define	DC_SRV_TAIL		"dc._msdcs"
#define	ALL_GC_SRV_TAIL		"_gc._tcp"
#define	PDC_SRV			 "_ldap._tcp.pdc._msdcs.%s"

/* A RR name for all GCs -- last resort this works */
#define	GC_ALL_A_NAME_FSTR "gc._msdcs.%s."


/*
 * We try res_ninit() whenever we don't have one.  res_ninit() fails if
 * idmapd is running before the network is up!
 */
#define	DO_RES_NINIT(ctx)				\
	if (!(ctx)->res_ninitted)			\
		(void) do_res_ninit(ctx)

#define	DO_GETNAMEINFO(b, l, s)				\
	if (ad_disc_getnameinfo(b, l, s) != 0)		\
		(void) strlcpy(b, "?", l)

#define	DEBUG1STATUS(ctx, ...) do { \
	if (DBG(DISC, 1)) \
		logger(LOG_DEBUG, __VA_ARGS__); \
	if (ctx->status_fp) { \
		(void) fprintf(ctx->status_fp, __VA_ARGS__); \
		(void) fprintf(ctx->status_fp, "\n"); \
	} \
	_NOTE(CONSTCOND) \
} while (0)

#define	is_fixed(item)					\
	((item)->state == AD_STATE_FIXED)

#define	is_changed(item, num, param)			\
	((item)->param_version[num] != (param)->version)

void * uuid_dup(void *);

static ad_item_t *validate_SiteName(ad_disc_t ctx);
static ad_item_t *validate_PreferredDC(ad_disc_t ctx);

/*
 * Function definitions
 */


static int
do_res_ninit(ad_disc_t ctx)
{
	int rc;

	rc = res_ninit(&ctx->res_state);
	if (rc != 0)
		return (rc);
	ctx->res_ninitted = 1;
	/*
	 * The SRV records returnd by AD can be larger than 512 bytes,
	 * so we'd like to use TCP for those searches.  Unfortunately,
	 * the TCP connect timeout seen by the resolver is very long
	 * (more than a couple minutes) and we can't wait that long.
	 * Don't do use TCP until we can override the timeout.
	 *
	 * Note that some queries will try TCP anyway.
	 */
#if 0
	ctx->res_state.options |= RES_USEVC;
#endif
	return (0);
}

/*
 * Private getnameinfo(3socket) variant tailored to our needs.
 */
int
ad_disc_getnameinfo(char *obuf, int olen, struct sockaddr_storage *ss)
{
	struct sockaddr *sa;
	int eai, slen;

	sa = (void *)ss;
	switch (sa->sa_family) {
	case AF_INET:
		slen = sizeof (struct sockaddr_in);
		break;
	case AF_INET6:
		slen = sizeof (struct sockaddr_in6);
		break;
	default:
		return (EAI_FAMILY);
	}

	eai = getnameinfo(sa, slen, obuf, olen, NULL, 0, NI_NUMERICHOST);

	return (eai);
}

static void
update_version(ad_item_t *item, int  num, ad_item_t *param)
{
	item->param_version[num] = param->version;
}



static boolean_t
is_valid(ad_item_t *item)
{
	if (item->value != NULL) {
		if (item->state == AD_STATE_FIXED)
			return (B_TRUE);
		if (item->state == AD_STATE_AUTO &&
		    (item->expires == 0 || item->expires > time(NULL)))
			return (B_TRUE);
	}
	return (B_FALSE);
}


static void
update_item(ad_item_t *item, void *value, enum ad_item_state state,
		uint32_t ttl)
{
	if (item->value != NULL && value != NULL) {
		if ((item->type == AD_STRING &&
		    strcmp(item->value, value) != 0) ||
		    (item->type == AD_UUID &&
		    ad_disc_compare_uuid(item->value, value) != 0)||
		    (item->type == AD_DIRECTORY &&
		    ad_disc_compare_ds(item->value, value) != 0)||
		    (item->type == AD_DOMAINS_IN_FOREST &&
		    ad_disc_compare_domainsinforest(item->value, value) != 0) ||
		    (item->type == AD_TRUSTED_DOMAINS &&
		    ad_disc_compare_trusteddomains(item->value, value) != 0))
			item->version++;
	} else if (item->value != value)
		item->version++;

	if (item->value != NULL)
		free(item->value);

	item->value = value;
	item->state = state;

	if (ttl == 0)
		item->expires = 0;
	else
		item->expires = time(NULL) + ttl;
}

/* Compare UUIDs */
int
ad_disc_compare_uuid(uuid_t *u1, uuid_t *u2)
{
	int rc;

	rc = memcmp(u1, u2, UUID_LEN);
	return (rc);
}

void *
uuid_dup(void *src)
{
	void *dst;
	dst = malloc(UUID_LEN);
	if (dst != NULL)
		(void) memcpy(dst, src, UUID_LEN);
	return (dst);
}

/* Compare DS lists */
int
ad_disc_compare_ds(ad_disc_ds_t *ds1, ad_disc_ds_t *ds2)
{
	int		i, j;
	int		num_ds1;
	int		num_ds2;
	boolean_t	match;

	for (i = 0; ds1[i].host[0] != '\0'; i++)
		continue;
	num_ds1 = i;
	for (j = 0; ds2[j].host[0] != '\0'; j++)
		continue;
	num_ds2 = j;
	if (num_ds1 != num_ds2)
		return (1);

	for (i = 0; i < num_ds1; i++) {
		match = B_FALSE;
		for (j = 0; j < num_ds2; j++) {
			if (strcmp(ds1[i].host, ds2[j].host) == 0 &&
			    ds1[i].port == ds2[j].port) {
				match = B_TRUE;
				break;
			}
		}
		if (!match)
			return (1);
	}
	return (0);
}


/* Copy a list of DSs */
static ad_disc_ds_t *
ds_dup(const ad_disc_ds_t *srv)
{
	int	i;
	int	size;
	ad_disc_ds_t *new = NULL;

	for (i = 0; srv[i].host[0] != '\0'; i++)
		continue;

	size = (i + 1) * sizeof (ad_disc_ds_t);
	new = malloc(size);
	if (new != NULL)
		(void) memcpy(new, srv, size);
	return (new);
}


int
ad_disc_compare_trusteddomains(ad_disc_trusteddomains_t *td1,
			ad_disc_trusteddomains_t *td2)
{
	int		i, j;
	int		num_td1;
	int		num_td2;
	boolean_t	match;

	for (i = 0; td1[i].domain[0] != '\0'; i++)
		continue;
	num_td1 = i;

	for (j = 0; td2[j].domain[0] != '\0'; j++)
		continue;
	num_td2 = j;

	if (num_td1 != num_td2)
		return (1);

	for (i = 0; i < num_td1; i++) {
		match = B_FALSE;
		for (j = 0; j < num_td2; j++) {
			if (domain_eq(td1[i].domain, td2[j].domain)) {
				match = B_TRUE;
				break;
			}
		}
		if (!match)
			return (1);
	}
	return (0);
}



/* Copy a list of Trusted Domains */
static ad_disc_trusteddomains_t *
td_dup(const ad_disc_trusteddomains_t *td)
{
	int	i;
	int	size;
	ad_disc_trusteddomains_t *new = NULL;

	for (i = 0; td[i].domain[0] != '\0'; i++)
		continue;

	size = (i + 1) * sizeof (ad_disc_trusteddomains_t);
	new = malloc(size);
	if (new != NULL)
		(void) memcpy(new, td, size);
	return (new);
}



int
ad_disc_compare_domainsinforest(ad_disc_domainsinforest_t *df1,
			ad_disc_domainsinforest_t *df2)
{
	int		i, j;
	int		num_df1;
	int		num_df2;
	boolean_t	match;

	for (i = 0; df1[i].domain[0] != '\0'; i++)
		continue;
	num_df1 = i;

	for (j = 0; df2[j].domain[0] != '\0'; j++)
		continue;
	num_df2 = j;

	if (num_df1 != num_df2)
		return (1);

	for (i = 0; i < num_df1; i++) {
		match = B_FALSE;
		for (j = 0; j < num_df2; j++) {
			if (domain_eq(df1[i].domain, df2[j].domain) &&
			    strcmp(df1[i].sid, df2[j].sid) == 0) {
				match = B_TRUE;
				break;
			}
		}
		if (!match)
			return (1);
	}
	return (0);
}



/* Copy a list of Trusted Domains */
static ad_disc_domainsinforest_t *
df_dup(const ad_disc_domainsinforest_t *df)
{
	int	i;
	int	size;
	ad_disc_domainsinforest_t *new = NULL;

	for (i = 0; df[i].domain[0] != '\0'; i++)
		continue;

	size = (i + 1) * sizeof (ad_disc_domainsinforest_t);
	new = malloc(size);
	if (new != NULL)
		(void) memcpy(new, df, size);
	return (new);
}





/*
 * Returns an array of IPv4 address/prefix length
 * The last subnet is NULL
 */
static ad_subnet_t *
find_subnets()
{
	int		sock, n, i;
	struct lifconf	lifc;
	struct lifreq	lifr, *lifrp;
	struct lifnum	lifn;
	uint32_t	prefix_len;
	char		*s;
	ad_subnet_t	*results;

	lifrp = &lifr;

	if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
		logger(LOG_ERR, "Failed to open IPv4 socket for "
		    "listing network interfaces (%s)", strerror(errno));
		return (NULL);
	}

	lifn.lifn_family = AF_INET;
	lifn.lifn_flags = 0;
	if (ioctl(sock, SIOCGLIFNUM, (char *)&lifn) < 0) {
		logger(LOG_ERR,
		    "Failed to find the number of network interfaces (%s)",
		    strerror(errno));
		(void) close(sock);
		return (NULL);
	}

	if (lifn.lifn_count < 1) {
		logger(LOG_ERR, "No IPv4 network interfaces found");
		(void) close(sock);
		return (NULL);
	}

	lifc.lifc_family = AF_INET;
	lifc.lifc_flags = 0;
	lifc.lifc_len = lifn.lifn_count * sizeof (struct lifreq);
	lifc.lifc_buf = malloc(lifc.lifc_len);

	if (lifc.lifc_buf == NULL) {
		logger(LOG_ERR, "Out of memory");
		(void) close(sock);
		return (NULL);
	}

	if (ioctl(sock, SIOCGLIFCONF, (char *)&lifc) < 0) {
		logger(LOG_ERR, "Failed to list network interfaces (%s)",
		    strerror(errno));
		free(lifc.lifc_buf);
		(void) close(sock);
		return (NULL);
	}

	n = lifc.lifc_len / (int)sizeof (struct lifreq);

	if ((results = calloc(n + 1, sizeof (ad_subnet_t))) == NULL) {
		free(lifc.lifc_buf);
		(void) close(sock);
		return (NULL);
	}

	for (i = 0, lifrp = lifc.lifc_req; i < n; i++, lifrp++) {
		if (ioctl(sock, SIOCGLIFFLAGS, lifrp) < 0)
			continue;

		if ((lifrp->lifr_flags & IFF_UP) == 0)
			continue;

		if (ioctl(sock, SIOCGLIFSUBNET, lifrp) < 0)
			continue;

		prefix_len = lifrp->lifr_addrlen;

		s = inet_ntoa(((struct sockaddr_in *)
		    &lifrp->lifr_addr)->sin_addr);

		(void) snprintf(results[i].subnet, sizeof (ad_subnet_t),
		    "%s/%d", s, prefix_len);
	}

	free(lifc.lifc_buf);
	(void) close(sock);

	return (results);
}

static int
cmpsubnets(ad_subnet_t *subnets1, ad_subnet_t *subnets2)
{
	int num_subnets1;
	int num_subnets2;
	boolean_t matched;
	int i, j;

	for (i = 0; subnets1[i].subnet[0] != '\0'; i++)
		continue;
	num_subnets1 = i;

	for (i = 0; subnets2[i].subnet[0] != '\0'; i++)
		continue;
	num_subnets2 = i;

	if (num_subnets1 != num_subnets2)
		return (1);

	for (i = 0;  i < num_subnets1; i++) {
		matched = B_FALSE;
		for (j = 0; j < num_subnets2; j++) {
			if (strcmp(subnets1[i].subnet,
			    subnets2[j].subnet) == 0) {
				matched = B_TRUE;
				break;
			}
		}
		if (!matched)
			return (1);
	}
	return (0);
}




/* Convert a DN's DC components into a DNS domainname */
char *
DN_to_DNS(const char *dn_name)
{
	char	dns[DNS_MAX_NAME];
	char	*dns_name;
	int	i, j;
	int	num = 0;

	j = 0;
	i = 0;

	if (dn_name == NULL)
		return (NULL);
	/*
	 * Find all DC=<value> and form DNS name of the
	 * form <value1>.<value2>...
	 */
	while (dn_name[i] != '\0') {
		if (strncasecmp(&dn_name[i], "DC=", 3) == 0) {
			i += 3;
			if (dn_name[i] != '\0' && num > 0)
				dns[j++] = '.';
			while (dn_name[i] != '\0' &&
			    dn_name[i] != ',' && dn_name[i] != '+')
				dns[j++] = dn_name[i++];
			num++;
		} else {
			/* Skip attr=value as it is not DC= */
			while (dn_name[i] != '\0' &&
			    dn_name[i] != ',' && dn_name[i] != '+')
				i++;
		}
		/* Skip over separator ','  or '+' */
		if (dn_name[i] != '\0') i++;
	}
	dns[j] = '\0';
	dns_name = malloc(j + 1);
	if (dns_name != NULL)
		(void) strlcpy(dns_name, dns, j + 1);
	return (dns_name);
}


/*
 * A utility function to bind to a Directory server
 */

static
LDAP *
ldap_lookup_init(ad_disc_ds_t *ds)
{
	int	i;
	int	rc, ldversion;
	int	zero = 0;
	int	timeoutms = 5 * 1000;
	char	*saslmech = "GSSAPI";
	uint32_t saslflags = LDAP_SASL_INTERACTIVE;
	LDAP	*ld = NULL;

	for (i = 0; ds[i].host[0] != '\0'; i++) {
		if (DBG(LDAP, 2)) {
			logger(LOG_DEBUG, "adutils: ldap_lookup_init, host %s",
			    ds[i].host);
		}

		ld = ldap_init(ds[i].host, ds[i].port);
		if (ld == NULL) {
			if (DBG(LDAP, 1)) {
				logger(LOG_DEBUG,
				    "Couldn't connect to AD DC %s:%d (%s)",
				    ds[i].host, ds[i].port,
				    strerror(errno));
			}
			continue;
		}

		ldversion = LDAP_VERSION3;
		(void) ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION,
		    &ldversion);
		(void) ldap_set_option(ld, LDAP_OPT_REFERRALS,
		    LDAP_OPT_OFF);
		(void) ldap_set_option(ld, LDAP_OPT_TIMELIMIT, &zero);
		(void) ldap_set_option(ld, LDAP_OPT_SIZELIMIT, &zero);
		/* setup TCP/IP connect timeout */
		(void) ldap_set_option(ld, LDAP_X_OPT_CONNECT_TIMEOUT,
		    &timeoutms);
		(void) ldap_set_option(ld, LDAP_OPT_RESTART,
		    LDAP_OPT_ON);

		rc = adutils_set_thread_functions(ld);
		if (rc != LDAP_SUCCESS) {
			/* Error has already been logged */
			(void) ldap_unbind(ld);
			ld = NULL;
			continue;
		}

		rc = ldap_sasl_interactive_bind_s(ld, "" /* binddn */,
		    saslmech, NULL, NULL, saslflags, &saslcallback,
		    NULL /* defaults */);
		if (rc == LDAP_SUCCESS)
			break;

		if (DBG(LDAP, 0)) {
			logger(LOG_INFO, "LDAP: %s:%d: %s",
			    ds[i].host, ds[i].port, ldap_err2string(rc));
			ldap_perror(ld, ds[i].host);
		}
		(void) ldap_unbind(ld);
		ld = NULL;
	}
	return (ld);
}



/*
 * Lookup the trusted domains in the global catalog.
 *
 * Returns:
 *	array of trusted domains which is terminated by
 *		an empty trusted domain.
 *	NULL an error occured
 */
ad_disc_trusteddomains_t *
ldap_lookup_trusted_domains(LDAP **ld, ad_disc_ds_t *globalCatalog,
			char *base_dn)
{
	int		scope = LDAP_SCOPE_SUBTREE;
	char		*attrs[3];
	int		rc;
	LDAPMessage	*results = NULL;
	LDAPMessage	*entry;
	char		*filter;
	char		**partner = NULL;
	char		**direction = NULL;
	int		num = 0;
	ad_disc_trusteddomains_t *trusted_domains = NULL;

	if (DBG(DISC, 1))
		logger(LOG_DEBUG, "Looking for trusted domains...");

	if (*ld == NULL)
		*ld = ldap_lookup_init(globalCatalog);

	if (*ld == NULL) {
		logger(LOG_ERR, "adutils: ldap_lookup_init failed");
		return (NULL);
	}

	attrs[0] = "trustPartner";
	attrs[1] = "trustDirection";
	attrs[2] = NULL;

	/*
	 * Trust direction values:
	 * 1 - inbound (they trust us)
	 * 2 - outbound (we trust them)
	 * 3 - bidirectional (we trust each other)
	 */
	filter = "(&(objectclass=trustedDomain)"
	    "(|(trustDirection=3)(trustDirection=2)))";

	rc = ldap_search_s(*ld, base_dn, scope, filter, attrs, 0, &results);
	if (DBG(DISC, 1))
		logger(LOG_DEBUG, "Trusted domains:");
	if (rc == LDAP_SUCCESS) {
		for (entry = ldap_first_entry(*ld, results);
		    entry != NULL; entry = ldap_next_entry(*ld, entry)) {
			partner = ldap_get_values(*ld, entry, "trustPartner");
			direction = ldap_get_values(
			    *ld, entry, "trustDirection");

			if (partner != NULL && direction != NULL) {
				if (DBG(DISC, 1)) {
					logger(LOG_DEBUG, "    %s (%s)",
					    partner[0], direction[0]);
				}
				num++;
				void *tmp = realloc(trusted_domains,
				    (num + 1) *
				    sizeof (ad_disc_trusteddomains_t));
				if (tmp == NULL) {
					free(trusted_domains);
					ldap_value_free(partner);
					ldap_value_free(direction);
					(void) ldap_msgfree(results);
					return (NULL);
				}
				trusted_domains = tmp;
				/* Last element should be zero */
				(void) memset(&trusted_domains[num], 0,
				    sizeof (ad_disc_trusteddomains_t));
				(void) strcpy(trusted_domains[num - 1].domain,
				    partner[0]);
				trusted_domains[num - 1].direction =
				    atoi(direction[0]);
			}
			if (partner != NULL)
				ldap_value_free(partner);
			if (direction != NULL)
				ldap_value_free(direction);
		}
	} else if (rc == LDAP_NO_RESULTS_RETURNED) {
		/* This is not an error - return empty trusted domain */
		trusted_domains = calloc(1, sizeof (ad_disc_trusteddomains_t));
		if (DBG(DISC, 1))
			logger(LOG_DEBUG, "    not found");
	} else {
		if (DBG(DISC, 1))
			logger(LOG_DEBUG, "    rc=%d", rc);
	}
	if (results != NULL)
		(void) ldap_msgfree(results);

	return (trusted_domains);
}


/*
 * This functions finds all the domains in a forest.
 */
ad_disc_domainsinforest_t *
ldap_lookup_domains_in_forest(LDAP **ld, ad_disc_ds_t *globalCatalogs)
{
	static char	*attrs[] = {
		"objectSid",
		NULL,
	};
	int		rc;
	LDAPMessage	*result = NULL;
	LDAPMessage	*entry;
	int		ndomains = 0;
	int		nresults;
	ad_disc_domainsinforest_t *domains = NULL;

	if (DBG(DISC, 1))
		logger(LOG_DEBUG, "Looking for domains in forest...");

	if (*ld == NULL)
		*ld = ldap_lookup_init(globalCatalogs);

	if (*ld == NULL) {
		logger(LOG_ERR, "adutils: ldap_lookup_init failed");
		return (NULL);
	}

	/* Find domains */
	rc = ldap_search_s(*ld, "", LDAP_SCOPE_SUBTREE,
	    "(objectClass=Domain)", attrs, 0, &result);
	if (rc != LDAP_SUCCESS) {
		logger(LOG_ERR, "adutils: ldap_search, rc=%d", rc);
		goto err;
	}
	if (DBG(DISC, 1))
		logger(LOG_DEBUG, "Domains in forest:");

	nresults = ldap_count_entries(*ld, result);
	domains = calloc(nresults + 1, sizeof (*domains));
	if (domains == NULL) {
		if (DBG(DISC, 1))
			logger(LOG_DEBUG, "    (nomem)");
		goto err;
	}

	for (entry = ldap_first_entry(*ld, result);
	    entry != NULL;
	    entry = ldap_next_entry(*ld, entry)) {
		struct berval	**sid_ber;
		adutils_sid_t	sid;
		char		*sid_str;
		char		*name;
		char		*dn;

		sid_ber = ldap_get_values_len(*ld, entry,
		    "objectSid");
		if (sid_ber == NULL)
			continue;

		rc = adutils_getsid(sid_ber[0], &sid);
		ldap_value_free_len(sid_ber);
		if (rc < 0)
			goto err;

		if ((sid_str = adutils_sid2txt(&sid)) == NULL)
			goto err;

		(void) strcpy(domains[ndomains].sid, sid_str);
		free(sid_str);

		dn = ldap_get_dn(*ld, entry);
		name = DN_to_DNS(dn);
		free(dn);
		if (name == NULL)
			goto err;

		(void) strcpy(domains[ndomains].domain, name);
		free(name);

		if (DBG(DISC, 1))
			logger(LOG_DEBUG, "    %s", domains[ndomains].domain);

		ndomains++;
	}

	if (ndomains == 0) {
		if (DBG(DISC, 1))
			logger(LOG_DEBUG, "    not found");
		goto err;
	}

	if (ndomains < nresults) {
		ad_disc_domainsinforest_t *tmp;
		tmp = realloc(domains, (ndomains + 1) * sizeof (*domains));
		if (tmp == NULL)
			goto err;
		domains = tmp;
	}

	if (result != NULL)
		(void) ldap_msgfree(result);

	return (domains);

err:
	free(domains);
	if (result != NULL)
		(void) ldap_msgfree(result);
	return (NULL);
}


ad_disc_t
ad_disc_init(void)
{
	struct ad_disc *ctx;
	ctx = calloc(1, sizeof (struct ad_disc));
	if (ctx != NULL)
		DO_RES_NINIT(ctx);

	ctx->domain_name.type = AD_STRING;
	ctx->domain_guid.type = AD_UUID;
	ctx->domain_controller.type = AD_DIRECTORY;
	ctx->preferred_dc.type = AD_DIRECTORY;
	ctx->site_name.type = AD_STRING;
	ctx->forest_name.type = AD_STRING;
	ctx->global_catalog.type = AD_DIRECTORY;
	ctx->domains_in_forest.type = AD_DOMAINS_IN_FOREST;
	ctx->trusted_domains.type = AD_TRUSTED_DOMAINS;
	/* Site specific versions */
	ctx->site_domain_controller.type = AD_DIRECTORY;
	ctx->site_global_catalog.type = AD_DIRECTORY;
	return (ctx);
}

void
ad_disc_fini(ad_disc_t ctx)
{
	if (ctx == NULL)
		return;

	if (ctx->res_ninitted)
		res_ndestroy(&ctx->res_state);

	if (ctx->subnets != NULL)
		free(ctx->subnets);

	if (ctx->domain_name.value != NULL)
		free(ctx->domain_name.value);

	if (ctx->domain_guid.value != NULL)
		free(ctx->domain_guid.value);

	if (ctx->domain_controller.value != NULL)
		free(ctx->domain_controller.value);

	if (ctx->preferred_dc.value != NULL)
		free(ctx->preferred_dc.value);

	if (ctx->site_name.value != NULL)
		free(ctx->site_name.value);

	if (ctx->forest_name.value != NULL)
		free(ctx->forest_name.value);

	if (ctx->global_catalog.value != NULL)
		free(ctx->global_catalog.value);

	if (ctx->domains_in_forest.value != NULL)
		free(ctx->domains_in_forest.value);

	if (ctx->trusted_domains.value != NULL)
		free(ctx->trusted_domains.value);

	/* Site specific versions */
	if (ctx->site_domain_controller.value != NULL)
		free(ctx->site_domain_controller.value);

	if (ctx->site_global_catalog.value != NULL)
		free(ctx->site_global_catalog.value);

	free(ctx);
}

void
ad_disc_refresh(ad_disc_t ctx)
{
	if (ctx->res_ninitted) {
		res_ndestroy(&ctx->res_state);
		ctx->res_ninitted = 0;
	}
	(void) memset(&ctx->res_state, 0, sizeof (ctx->res_state));
	DO_RES_NINIT(ctx);

	if (ctx->domain_name.state == AD_STATE_AUTO)
		ctx->domain_name.state = AD_STATE_INVALID;

	if (ctx->domain_guid.state == AD_STATE_AUTO)
		ctx->domain_guid.state = AD_STATE_INVALID;

	if (ctx->domain_controller.state == AD_STATE_AUTO)
		ctx->domain_controller.state  = AD_STATE_INVALID;

	if (ctx->preferred_dc.state == AD_STATE_AUTO)
		ctx->preferred_dc.state  = AD_STATE_INVALID;

	if (ctx->site_name.state == AD_STATE_AUTO)
		ctx->site_name.state = AD_STATE_INVALID;

	if (ctx->forest_name.state == AD_STATE_AUTO)
		ctx->forest_name.state = AD_STATE_INVALID;

	if (ctx->global_catalog.state == AD_STATE_AUTO)
		ctx->global_catalog.state = AD_STATE_INVALID;

	if (ctx->domains_in_forest.state == AD_STATE_AUTO)
		ctx->domains_in_forest.state  = AD_STATE_INVALID;

	if (ctx->trusted_domains.state == AD_STATE_AUTO)
		ctx->trusted_domains.state  = AD_STATE_INVALID;

	if (ctx->site_domain_controller.state == AD_STATE_AUTO)
		ctx->site_domain_controller.state  = AD_STATE_INVALID;

	if (ctx->site_global_catalog.state == AD_STATE_AUTO)
		ctx->site_global_catalog.state = AD_STATE_INVALID;
}


/*
 * Called when the discovery cycle is done.  Sets a master TTL
 * that will avoid doing new time-based discoveries too soon after
 * the last discovery cycle.  Most interesting when the discovery
 * cycle failed, because then the TTLs on the individual items will
 * not be updated and may go stale.
 */
void
ad_disc_done(ad_disc_t ctx)
{
	time_t now = time(NULL);

	ctx->expires_not_before = now + MINIMUM_TTL;
	ctx->expires_not_after = now + MAXIMUM_TTL;
}

static void
log_cds(ad_disc_t ctx, ad_disc_cds_t *cds)
{
	char buf[INET6_ADDRSTRLEN];
	struct addrinfo *ai;

	if (!DBG(DISC, 1) && ctx->status_fp == NULL)
		return;

	DEBUG1STATUS(ctx, "Candidate servers:");
	if (cds->cds_ds.host[0] == '\0') {
		DEBUG1STATUS(ctx, "  (empty list)");
		return;
	}

	while (cds->cds_ds.host[0] != '\0') {

		DEBUG1STATUS(ctx, "  %s  p=%d w=%d",
		    cds->cds_ds.host,
		    cds->cds_ds.priority,
		    cds->cds_ds.weight);

		ai = cds->cds_ai;
		if (ai == NULL) {
			DEBUG1STATUS(ctx, "    (no address)");
		}
		while (ai != NULL) {
			int eai;

			eai = getnameinfo(ai->ai_addr, ai->ai_addrlen,
			    buf, sizeof (buf), NULL, 0, NI_NUMERICHOST);
			if (eai != 0)
				(void) strlcpy(buf, "?", sizeof (buf));

			DEBUG1STATUS(ctx, "    %s", buf);
			ai = ai->ai_next;
		}
		cds++;
	}
}

static void
log_ds(ad_disc_t ctx, ad_disc_ds_t *ds)
{
	char buf[INET6_ADDRSTRLEN];

	if (!DBG(DISC, 1) && ctx->status_fp == NULL)
		return;

	DEBUG1STATUS(ctx, "Responding servers:");
	if (ds->host[0] == '\0') {
		DEBUG1STATUS(ctx, "  (empty list)");
		return;
	}

	while (ds->host[0] != '\0') {

		DEBUG1STATUS(ctx, "  %s", ds->host);
		DO_GETNAMEINFO(buf, sizeof (buf), &ds->addr);
		DEBUG1STATUS(ctx, "    %s", buf);

		ds++;
	}
}

/* Discover joined Active Directory domainName */
static ad_item_t *
validate_DomainName(ad_disc_t ctx)
{
	char *dname, *srvname;
	int len, rc;

	if (is_valid(&ctx->domain_name))
		return (&ctx->domain_name);


	/* Try to find our domain by searching for DCs for it */
	DO_RES_NINIT(ctx);
	if (DBG(DISC, 1))
		logger(LOG_DEBUG, "Looking for our AD domain name...");
	rc = srv_getdom(&ctx->res_state,
	    LDAP_SRV_HEAD DC_SRV_TAIL, &srvname);

	/*
	 * If we can't find DCs by via res_nsearch() then there's no
	 * point in trying anything else to discover the AD domain name.
	 */
	if (rc < 0) {
		if (DBG(DISC, 1))
			logger(LOG_DEBUG, "Can't find our domain name.");
		return (NULL);
	}

	/*
	 * We have the FQDN of the SRV RR name, so now we extract the
	 * domainname suffix from it.
	 */
	dname = strdup(srvname + strlen(LDAP_SRV_HEAD DC_SRV_TAIL) +
	    1 /* for the dot between RR name and domainname */);

	free(srvname);

	if (dname == NULL) {
		logger(LOG_ERR, "Out of memory");
		return (NULL);
	}

	/* Eat any trailing dot */
	len = strlen(dname);
	if (len > 0 && dname[len - 1] == '.')
		dname[len - 1] = '\0';

	if (DBG(DISC, 1))
		logger(LOG_DEBUG, "Our domain name:  %s", dname);

	/*
	 * There is no "time to live" on the discovered domain,
	 * so passing zero as TTL here, making it non-expiring.
	 * Note that current consumers do not auto-discover the
	 * domain name, though a future installer could.
	 */
	update_item(&ctx->domain_name, dname, AD_STATE_AUTO, 0);

	return (&ctx->domain_name);
}


char *
ad_disc_get_DomainName(ad_disc_t ctx, boolean_t *auto_discovered)
{
	char *domain_name = NULL;
	ad_item_t *domain_name_item;

	domain_name_item = validate_DomainName(ctx);

	if (domain_name_item) {
		domain_name = strdup(domain_name_item->value);
		if (auto_discovered != NULL)
			*auto_discovered =
			    (domain_name_item->state == AD_STATE_AUTO);
	} else if (auto_discovered != NULL)
		*auto_discovered = B_FALSE;

	return (domain_name);
}


/* Discover domain controllers */
static ad_item_t *
validate_DomainController(ad_disc_t ctx, enum ad_disc_req req)
{
	ad_disc_ds_t *dc = NULL;
	ad_disc_cds_t *cdc = NULL;
	boolean_t validate_global = B_FALSE;
	boolean_t validate_site = B_FALSE;
	ad_item_t *domain_name_item;
	char *domain_name;
	ad_item_t *site_name_item = NULL;
	char *site_name;
	ad_item_t *prefer_dc_item;
	ad_disc_ds_t *prefer_dc = NULL;

	/* If the values is fixed there will not be a site specific version */
	if (is_fixed(&ctx->domain_controller))
		return (&ctx->domain_controller);

	domain_name_item = validate_DomainName(ctx);
	if (domain_name_item == NULL) {
		DEBUG1STATUS(ctx, "(no domain name)");
		return (NULL);
	}
	domain_name = (char *)domain_name_item->value;

	/* Get (optional) preferred DC. */
	prefer_dc_item = validate_PreferredDC(ctx);
	if (prefer_dc_item != NULL)
		prefer_dc = prefer_dc_item->value;

	if (req == AD_DISC_GLOBAL)
		validate_global = B_TRUE;
	else {
		if (is_fixed(&ctx->site_name))
			validate_site = B_TRUE;
		if (req == AD_DISC_PREFER_SITE)
			validate_global = B_TRUE;
	}

	/*
	 * If we're trying both site-specific and global,
	 * try the site-specific first, then fall-back.
	 */
	if (validate_site) {
		site_name_item = &ctx->site_name;
		site_name = (char *)site_name_item->value;

		if (!is_valid(&ctx->site_domain_controller) ||
		    is_changed(&ctx->site_domain_controller, PARAM1,
		    domain_name_item) ||
		    is_changed(&ctx->site_domain_controller, PARAM2,
		    site_name_item)) {
			char rr_name[DNS_MAX_NAME];

			/*
			 * Lookup DNS SRV RR named
			 * _ldap._tcp.<SiteName>._sites.dc._msdcs.<DomainName>
			 */
			DEBUG1STATUS(ctx, "DNS SRV query, dom=%s, site=%s",
			    domain_name, site_name);
			(void) snprintf(rr_name, sizeof (rr_name),
			    LDAP_SRV_HEAD SITE_SRV_MIDDLE DC_SRV_TAIL,
			    site_name);
			DO_RES_NINIT(ctx);
			cdc = srv_query(&ctx->res_state, rr_name,
			    domain_name, prefer_dc);

			if (cdc == NULL) {
				DEBUG1STATUS(ctx, "(no DNS response)");
				goto try_global;
			}
			log_cds(ctx, cdc);

			/*
			 * Filter out unresponsive servers, and
			 * save the domain info we get back.
			 */
			dc = ldap_ping(
			    ctx,
			    cdc,
			    domain_name,
			    DS_DS_FLAG);
			srv_free(cdc);
			cdc = NULL;

			if (dc == NULL) {
				DEBUG1STATUS(ctx, "(no LDAP response)");
				goto try_global;
			}
			log_ds(ctx, dc);

			update_item(&ctx->site_domain_controller, dc,
			    AD_STATE_AUTO, dc->ttl);
			update_version(&ctx->site_domain_controller, PARAM1,
			    domain_name_item);
			update_version(&ctx->site_domain_controller, PARAM2,
			    site_name_item);
		}
		return (&ctx->site_domain_controller);
	}

try_global:

	if (validate_global) {
		if (!is_valid(&ctx->domain_controller) ||
		    is_changed(&ctx->domain_controller, PARAM1,
		    domain_name_item)) {

			/*
			 * Lookup DNS SRV RR named
			 * _ldap._tcp.dc._msdcs.<DomainName>
			 */
			DEBUG1STATUS(ctx, "DNS SRV query, dom=%s",
			    domain_name);
			DO_RES_NINIT(ctx);
			cdc = srv_query(&ctx->res_state,
			    LDAP_SRV_HEAD DC_SRV_TAIL,
			    domain_name, prefer_dc);

			if (cdc == NULL) {
				DEBUG1STATUS(ctx, "(no DNS response)");
				return (NULL);
			}
			log_cds(ctx, cdc);

			/*
			 * Filter out unresponsive servers, and
			 * save the domain info we get back.
			 */
			dc = ldap_ping(
			    ctx,
			    cdc,
			    domain_name,
			    DS_DS_FLAG);
			srv_free(cdc);
			cdc = NULL;

			if (dc == NULL) {
				DEBUG1STATUS(ctx, "(no LDAP response)");
				return (NULL);
			}
			log_ds(ctx, dc);

			update_item(&ctx->domain_controller, dc,
			    AD_STATE_AUTO, dc->ttl);
			update_version(&ctx->domain_controller, PARAM1,
			    domain_name_item);
		}
		return (&ctx->domain_controller);
	}

	return (NULL);
}

ad_disc_ds_t *
ad_disc_get_DomainController(ad_disc_t ctx, enum ad_disc_req req,
    boolean_t *auto_discovered)
{
	ad_item_t *domain_controller_item;
	ad_disc_ds_t *domain_controller = NULL;

	domain_controller_item = validate_DomainController(ctx, req);

	if (domain_controller_item != NULL) {
		domain_controller = ds_dup(domain_controller_item->value);
		if (auto_discovered != NULL)
			*auto_discovered =
			    (domain_controller_item->state == AD_STATE_AUTO);
	} else if (auto_discovered != NULL)
		*auto_discovered = B_FALSE;

	return (domain_controller);
}


/*
 * Discover the Domain GUID
 * This info comes from validate_DomainController()
 */
static ad_item_t *
validate_DomainGUID(ad_disc_t ctx)
{
	ad_item_t *domain_controller_item;

	if (is_fixed(&ctx->domain_guid))
		return (&ctx->domain_guid);

	domain_controller_item = validate_DomainController(ctx, AD_DISC_GLOBAL);
	if (domain_controller_item == NULL)
		return (NULL);

	if (!is_valid(&ctx->domain_guid))
		return (NULL);

	return (&ctx->domain_guid);
}


uchar_t *
ad_disc_get_DomainGUID(ad_disc_t ctx, boolean_t *auto_discovered)
{
	ad_item_t *domain_guid_item;
	uchar_t	*domain_guid = NULL;

	domain_guid_item = validate_DomainGUID(ctx);
	if (domain_guid_item != NULL) {
		domain_guid = uuid_dup(domain_guid_item->value);
		if (auto_discovered != NULL)
			*auto_discovered =
			    (domain_guid_item->state == AD_STATE_AUTO);
	} else if (auto_discovered != NULL)
		*auto_discovered = B_FALSE;

	return (domain_guid);
}


/*
 * Discover site name (for multi-homed systems the first one found wins)
 * This info comes from validate_DomainController()
 */
static ad_item_t *
validate_SiteName(ad_disc_t ctx)
{
	ad_item_t *domain_controller_item;

	if (is_fixed(&ctx->site_name))
		return (&ctx->site_name);

	domain_controller_item = validate_DomainController(ctx, AD_DISC_GLOBAL);
	if (domain_controller_item == NULL)
		return (NULL);

	if (!is_valid(&ctx->site_name))
		return (NULL);

	return (&ctx->site_name);
}


char *
ad_disc_get_SiteName(ad_disc_t ctx, boolean_t *auto_discovered)
{
	ad_item_t *site_name_item;
	char	*site_name = NULL;

	site_name_item = validate_SiteName(ctx);
	if (site_name_item != NULL) {
		site_name = strdup(site_name_item->value);
		if (auto_discovered != NULL)
			*auto_discovered =
			    (site_name_item->state == AD_STATE_AUTO);
	} else if (auto_discovered != NULL)
		*auto_discovered = B_FALSE;

	return (site_name);
}



/*
 * Discover forest name
 * This info comes from validate_DomainController()
 */
static ad_item_t *
validate_ForestName(ad_disc_t ctx)
{
	ad_item_t *domain_controller_item;

	if (is_fixed(&ctx->forest_name))
		return (&ctx->forest_name);

	domain_controller_item = validate_DomainController(ctx, AD_DISC_GLOBAL);
	if (domain_controller_item == NULL)
		return (NULL);

	if (!is_valid(&ctx->forest_name))
		return (NULL);

	return (&ctx->forest_name);
}


char *
ad_disc_get_ForestName(ad_disc_t ctx, boolean_t *auto_discovered)
{
	ad_item_t *forest_name_item;
	char	*forest_name = NULL;

	forest_name_item = validate_ForestName(ctx);

	if (forest_name_item != NULL) {
		forest_name = strdup(forest_name_item->value);
		if (auto_discovered != NULL)
			*auto_discovered =
			    (forest_name_item->state == AD_STATE_AUTO);
	} else if (auto_discovered != NULL)
		*auto_discovered = B_FALSE;

	return (forest_name);
}


/* Discover global catalog servers */
static ad_item_t *
validate_GlobalCatalog(ad_disc_t ctx, enum ad_disc_req req)
{
	ad_disc_ds_t *gc = NULL;
	ad_disc_cds_t *cgc = NULL;
	boolean_t validate_global = B_FALSE;
	boolean_t validate_site = B_FALSE;
	ad_item_t *dc_item;
	ad_item_t *forest_name_item;
	ad_item_t *site_name_item;
	char *forest_name;
	char *site_name;

	/* If the values is fixed there will not be a site specific version */
	if (is_fixed(&ctx->global_catalog))
		return (&ctx->global_catalog);

	forest_name_item = validate_ForestName(ctx);
	if (forest_name_item == NULL) {
		DEBUG1STATUS(ctx, "(no forrest name)");
		return (NULL);
	}
	forest_name = (char *)forest_name_item->value;

	if (req == AD_DISC_GLOBAL)
		validate_global = B_TRUE;
	else {
		if (is_fixed(&ctx->site_name))
			validate_site = B_TRUE;
		if (req == AD_DISC_PREFER_SITE)
			validate_global = B_TRUE;
	}

	/*
	 * If we're trying both site-specific and global,
	 * try the site-specific first, then fall-back.
	 */
	if (validate_site) {
		site_name_item = &ctx->site_name;
		site_name = (char *)site_name_item->value;

		if (!is_valid(&ctx->site_global_catalog) ||
		    is_changed(&ctx->site_global_catalog, PARAM1,
		    forest_name_item) ||
		    is_changed(&ctx->site_global_catalog, PARAM2,
		    site_name_item)) {
			char rr_name[DNS_MAX_NAME];

			/*
			 * See if our DC is also a GC.
			 */
			dc_item = validate_DomainController(ctx, req);
			if (dc_item != NULL) {
				ad_disc_ds_t *ds = dc_item->value;
				if ((ds->flags & DS_GC_FLAG) != 0) {
					DEBUG1STATUS(ctx,
					    "DC is also a GC for %s in %s",
					    forest_name, site_name);
					gc = ds_dup(ds);
					if (gc != NULL) {
						gc->port = GC_PORT;
						goto update_site;
					}
				}
			}

			/*
			 * Lookup DNS SRV RR named:
			 * _ldap._tcp.<siteName>._sites.gc.
			 *	_msdcs.<ForestName>
			 */
			DEBUG1STATUS(ctx, "DNS SRV query, forest=%s, site=%s",
			    forest_name, site_name);
			(void) snprintf(rr_name, sizeof (rr_name),
			    LDAP_SRV_HEAD SITE_SRV_MIDDLE GC_SRV_TAIL,
			    site_name);
			DO_RES_NINIT(ctx);
			cgc = srv_query(&ctx->res_state, rr_name,
			    forest_name, NULL);

			if (cgc == NULL) {
				DEBUG1STATUS(ctx, "(no DNS response)");
				goto try_global;
			}
			log_cds(ctx, cgc);

			/*
			 * Filter out unresponsive servers, and
			 * save the domain info we get back.
			 */
			gc = ldap_ping(
			    NULL,
			    cgc,
			    forest_name,
			    DS_GC_FLAG);
			srv_free(cgc);
			cgc = NULL;

			if (gc == NULL) {
				DEBUG1STATUS(ctx, "(no LDAP response)");
				goto try_global;
			}
			log_ds(ctx, gc);

		update_site:
			update_item(&ctx->site_global_catalog, gc,
			    AD_STATE_AUTO, gc->ttl);
			update_version(&ctx->site_global_catalog, PARAM1,
			    forest_name_item);
			update_version(&ctx->site_global_catalog, PARAM2,
			    site_name_item);
		}
		return (&ctx->site_global_catalog);
	}

try_global:

	if (validate_global) {
		if (!is_valid(&ctx->global_catalog) ||
		    is_changed(&ctx->global_catalog, PARAM1,
		    forest_name_item)) {

			/*
			 * See if our DC is also a GC.
			 */
			dc_item = validate_DomainController(ctx, req);
			if (dc_item != NULL) {
				ad_disc_ds_t *ds = dc_item->value;
				if ((ds->flags & DS_GC_FLAG) != 0) {
					DEBUG1STATUS(ctx,
					    "DC is also a GC for %s",
					    forest_name);
					gc = ds_dup(ds);
					if (gc != NULL) {
						gc->port = GC_PORT;
						goto update_global;
					}
				}
			}

			/*
			 * Lookup DNS SRV RR named:
			 * _ldap._tcp.gc._msdcs.<ForestName>
			 */
			DEBUG1STATUS(ctx, "DNS SRV query, forest=%s",
			    forest_name);
			DO_RES_NINIT(ctx);
			cgc = srv_query(&ctx->res_state,
			    LDAP_SRV_HEAD GC_SRV_TAIL,
			    forest_name, NULL);

			if (cgc == NULL) {
				DEBUG1STATUS(ctx, "(no DNS response)");
				return (NULL);
			}
			log_cds(ctx, cgc);

			/*
			 * Filter out unresponsive servers, and
			 * save the domain info we get back.
			 */
			gc = ldap_ping(
			    NULL,
			    cgc,
			    forest_name,
			    DS_GC_FLAG);
			srv_free(cgc);
			cgc = NULL;

			if (gc == NULL) {
				DEBUG1STATUS(ctx, "(no LDAP response)");
				return (NULL);
			}
			log_ds(ctx, gc);

		update_global:
			update_item(&ctx->global_catalog, gc,
			    AD_STATE_AUTO, gc->ttl);
			update_version(&ctx->global_catalog, PARAM1,
			    forest_name_item);
		}
		return (&ctx->global_catalog);
	}
	return (NULL);
}


ad_disc_ds_t *
ad_disc_get_GlobalCatalog(ad_disc_t ctx, enum ad_disc_req req,
			boolean_t *auto_discovered)
{
	ad_disc_ds_t *global_catalog = NULL;
	ad_item_t *global_catalog_item;

	global_catalog_item = validate_GlobalCatalog(ctx, req);

	if (global_catalog_item != NULL) {
		global_catalog = ds_dup(global_catalog_item->value);
		if (auto_discovered != NULL)
			*auto_discovered =
			    (global_catalog_item->state == AD_STATE_AUTO);
	} else if (auto_discovered != NULL)
		*auto_discovered = B_FALSE;

	return (global_catalog);
}


static ad_item_t *
validate_TrustedDomains(ad_disc_t ctx)
{
	LDAP *ld = NULL;
	ad_item_t *global_catalog_item;
	ad_item_t *forest_name_item;
	ad_disc_trusteddomains_t *trusted_domains;
	char *dn = NULL;
	char *forest_name_dn;
	int len;
	int num_parts;

	if (is_fixed(&ctx->trusted_domains))
		return (&ctx->trusted_domains);

	global_catalog_item = validate_GlobalCatalog(ctx, AD_DISC_GLOBAL);
	if (global_catalog_item == NULL)
		return (NULL);

	forest_name_item = validate_ForestName(ctx);
	if (forest_name_item == NULL)
		return (NULL);

	if (!is_valid(&ctx->trusted_domains) ||
	    is_changed(&ctx->trusted_domains, PARAM1, global_catalog_item) ||
	    is_changed(&ctx->trusted_domains, PARAM2, forest_name_item)) {

		forest_name_dn = ldap_dns_to_dn(forest_name_item->value,
		    &num_parts);
		if (forest_name_dn == NULL)
			return (NULL);

		len = snprintf(NULL, 0, "CN=System,%s", forest_name_dn) + 1;
		dn = malloc(len);
		if (dn == NULL)  {
			free(forest_name_dn);
			return (NULL);
		}
		(void) snprintf(dn, len, "CN=System,%s", forest_name_dn);
		free(forest_name_dn);

		trusted_domains = ldap_lookup_trusted_domains(
		    &ld, global_catalog_item->value, dn);

		if (ld != NULL)
			(void) ldap_unbind(ld);
		free(dn);

		if (trusted_domains == NULL)
			return (NULL);

		update_item(&ctx->trusted_domains, trusted_domains,
		    AD_STATE_AUTO, 0);
		update_version(&ctx->trusted_domains, PARAM1,
		    global_catalog_item);
		update_version(&ctx->trusted_domains, PARAM2,
		    forest_name_item);
	}

	return (&ctx->trusted_domains);
}


ad_disc_trusteddomains_t *
ad_disc_get_TrustedDomains(ad_disc_t ctx, boolean_t *auto_discovered)
{
	ad_disc_trusteddomains_t *trusted_domains = NULL;
	ad_item_t *trusted_domains_item;

	trusted_domains_item = validate_TrustedDomains(ctx);

	if (trusted_domains_item != NULL) {
		trusted_domains = td_dup(trusted_domains_item->value);
		if (auto_discovered != NULL)
			*auto_discovered =
			    (trusted_domains_item->state == AD_STATE_AUTO);
	} else if (auto_discovered != NULL)
		*auto_discovered = B_FALSE;

	return (trusted_domains);
}


static ad_item_t *
validate_DomainsInForest(ad_disc_t ctx)
{
	ad_item_t *global_catalog_item;
	LDAP *ld = NULL;
	ad_disc_domainsinforest_t *domains_in_forest;

	if (is_fixed(&ctx->domains_in_forest))
		return (&ctx->domains_in_forest);

	global_catalog_item = validate_GlobalCatalog(ctx, AD_DISC_GLOBAL);
	if (global_catalog_item == NULL)
		return (NULL);

	if (!is_valid(&ctx->domains_in_forest) ||
	    is_changed(&ctx->domains_in_forest, PARAM1, global_catalog_item)) {

		domains_in_forest = ldap_lookup_domains_in_forest(
		    &ld, global_catalog_item->value);

		if (ld != NULL)
			(void) ldap_unbind(ld);

		if (domains_in_forest == NULL)
			return (NULL);

		update_item(&ctx->domains_in_forest, domains_in_forest,
		    AD_STATE_AUTO, 0);
		update_version(&ctx->domains_in_forest, PARAM1,
		    global_catalog_item);
	}
	return (&ctx->domains_in_forest);
}


ad_disc_domainsinforest_t *
ad_disc_get_DomainsInForest(ad_disc_t ctx, boolean_t *auto_discovered)
{
	ad_disc_domainsinforest_t *domains_in_forest = NULL;
	ad_item_t *domains_in_forest_item;

	domains_in_forest_item = validate_DomainsInForest(ctx);

	if (domains_in_forest_item != NULL) {
		domains_in_forest = df_dup(domains_in_forest_item->value);
		if (auto_discovered != NULL)
			*auto_discovered =
			    (domains_in_forest_item->state == AD_STATE_AUTO);
	} else if (auto_discovered != NULL)
		*auto_discovered = B_FALSE;

	return (domains_in_forest);
}

static ad_item_t *
validate_PreferredDC(ad_disc_t ctx)
{
	if (is_valid(&ctx->preferred_dc))
		return (&ctx->preferred_dc);

	return (NULL);
}

ad_disc_ds_t *
ad_disc_get_PreferredDC(ad_disc_t ctx, boolean_t *auto_discovered)
{
	ad_disc_ds_t *preferred_dc = NULL;
	ad_item_t *preferred_dc_item;

	preferred_dc_item = validate_PreferredDC(ctx);

	if (preferred_dc_item != NULL) {
		preferred_dc = ds_dup(preferred_dc_item->value);
		if (auto_discovered != NULL)
			*auto_discovered =
			    (preferred_dc_item->state == AD_STATE_AUTO);
	} else if (auto_discovered != NULL)
		*auto_discovered = B_FALSE;

	return (preferred_dc);
}



int
ad_disc_set_DomainName(ad_disc_t ctx, const char *domainName)
{
	char *domain_name = NULL;
	if (domainName != NULL) {
		domain_name = strdup(domainName);
		if (domain_name == NULL)
			return (-1);
		update_item(&ctx->domain_name, domain_name,
		    AD_STATE_FIXED, 0);
	} else if (ctx->domain_name.state == AD_STATE_FIXED)
		ctx->domain_name.state = AD_STATE_INVALID;
	return (0);
}

int
ad_disc_set_DomainGUID(ad_disc_t ctx, uchar_t *u)
{
	char *domain_guid = NULL;
	if (u != NULL) {
		domain_guid = uuid_dup(u);
		if (domain_guid == NULL)
			return (-1);
		update_item(&ctx->domain_guid, domain_guid,
		    AD_STATE_FIXED, 0);
	} else if (ctx->domain_guid.state == AD_STATE_FIXED)
		ctx->domain_guid.state = AD_STATE_INVALID;
	return (0);
}

void
auto_set_DomainGUID(ad_disc_t ctx, uchar_t *u)
{
	char *domain_guid = NULL;

	if (is_fixed(&ctx->domain_guid))
		return;

	domain_guid = uuid_dup(u);
	if (domain_guid == NULL)
		return;
	update_item(&ctx->domain_guid, domain_guid, AD_STATE_AUTO, 0);
}

int
ad_disc_set_DomainController(ad_disc_t ctx,
				const ad_disc_ds_t *domainController)
{
	ad_disc_ds_t *domain_controller = NULL;
	if (domainController != NULL) {
		domain_controller = ds_dup(domainController);
		if (domain_controller == NULL)
			return (-1);
		update_item(&ctx->domain_controller, domain_controller,
		    AD_STATE_FIXED, 0);
	} else if (ctx->domain_controller.state == AD_STATE_FIXED)
		ctx->domain_controller.state = AD_STATE_INVALID;
	return (0);
}

int
ad_disc_set_SiteName(ad_disc_t ctx, const char *siteName)
{
	char *site_name = NULL;
	if (siteName != NULL) {
		site_name = strdup(siteName);
		if (site_name == NULL)
			return (-1);
		update_item(&ctx->site_name, site_name, AD_STATE_FIXED, 0);
	} else if (ctx->site_name.state == AD_STATE_FIXED)
		ctx->site_name.state = AD_STATE_INVALID;
	return (0);
}

void
auto_set_SiteName(ad_disc_t ctx, char *siteName)
{
	char *site_name = NULL;

	if (is_fixed(&ctx->site_name))
		return;

	site_name = strdup(siteName);
	if (site_name == NULL)
		return;
	update_item(&ctx->site_name, site_name, AD_STATE_AUTO, 0);
}

int
ad_disc_set_ForestName(ad_disc_t ctx, const char *forestName)
{
	char *forest_name = NULL;
	if (forestName != NULL) {
		forest_name = strdup(forestName);
		if (forest_name == NULL)
			return (-1);
		update_item(&ctx->forest_name, forest_name,
		    AD_STATE_FIXED, 0);
	} else if (ctx->forest_name.state == AD_STATE_FIXED)
		ctx->forest_name.state = AD_STATE_INVALID;
	return (0);
}

void
auto_set_ForestName(ad_disc_t ctx, char *forestName)
{
	char *forest_name = NULL;

	if (is_fixed(&ctx->forest_name))
		return;

	forest_name = strdup(forestName);
	if (forest_name == NULL)
		return;
	update_item(&ctx->forest_name, forest_name, AD_STATE_AUTO, 0);
}

int
ad_disc_set_GlobalCatalog(ad_disc_t ctx,
    const ad_disc_ds_t *globalCatalog)
{
	ad_disc_ds_t *global_catalog = NULL;
	if (globalCatalog != NULL) {
		global_catalog = ds_dup(globalCatalog);
		if (global_catalog == NULL)
			return (-1);
		update_item(&ctx->global_catalog, global_catalog,
		    AD_STATE_FIXED, 0);
	} else if (ctx->global_catalog.state == AD_STATE_FIXED)
		ctx->global_catalog.state = AD_STATE_INVALID;
	return (0);
}

int
ad_disc_set_PreferredDC(ad_disc_t ctx, const ad_disc_ds_t *pref_dc)
{
	ad_disc_ds_t *new_pref_dc = NULL;
	if (pref_dc != NULL) {
		new_pref_dc = ds_dup(pref_dc);
		if (new_pref_dc == NULL)
			return (-1);
		update_item(&ctx->preferred_dc, new_pref_dc,
		    AD_STATE_FIXED, 0);
	} else if (ctx->preferred_dc.state == AD_STATE_FIXED)
		ctx->preferred_dc.state = AD_STATE_INVALID;
	return (0);
}

void
ad_disc_set_StatusFP(ad_disc_t ctx, struct __FILE_TAG *fp)
{
	ctx->status_fp = fp;
}


int
ad_disc_unset(ad_disc_t ctx)
{
	if (ctx->domain_name.state == AD_STATE_FIXED)
		ctx->domain_name.state =  AD_STATE_INVALID;

	if (ctx->domain_controller.state == AD_STATE_FIXED)
		ctx->domain_controller.state =  AD_STATE_INVALID;

	if (ctx->preferred_dc.state == AD_STATE_FIXED)
		ctx->preferred_dc.state =  AD_STATE_INVALID;

	if (ctx->site_name.state == AD_STATE_FIXED)
		ctx->site_name.state =  AD_STATE_INVALID;

	if (ctx->forest_name.state == AD_STATE_FIXED)
		ctx->forest_name.state =  AD_STATE_INVALID;

	if (ctx->global_catalog.state == AD_STATE_FIXED)
		ctx->global_catalog.state =  AD_STATE_INVALID;

	return (0);
}

/*
 * ad_disc_get_TTL
 *
 * This routines the time to live for AD
 * auto discovered items.
 *
 *	Returns:
 *		-1 if there are no TTL items
 *		0  if there are expired items
 *		else the number of seconds
 *
 * The MIN_GT_ZERO(x, y) macro return the lesser of x and y, provided it
 * is positive -- min() greater than zero.
 */
#define	MIN_GT_ZERO(x, y) (((x) <= 0) ? (((y) <= 0) ? \
		(-1) : (y)) : (((y) <= 0) ? (x) : (((x) > (y)) ? (y) : (x))))
int
ad_disc_get_TTL(ad_disc_t ctx)
{
	time_t expires;
	int ttl;

	expires = MIN_GT_ZERO(ctx->domain_controller.expires,
	    ctx->global_catalog.expires);
	expires = MIN_GT_ZERO(expires, ctx->site_domain_controller.expires);
	expires = MIN_GT_ZERO(expires, ctx->site_global_catalog.expires);

	if (expires == -1) {
		return (-1);
	}

	if (ctx->expires_not_before != 0 &&
	    expires < ctx->expires_not_before) {
		expires = ctx->expires_not_before;
	}

	if (ctx->expires_not_after != 0 &&
	    expires > ctx->expires_not_after) {
		expires = ctx->expires_not_after;
	}

	ttl = expires - time(NULL);

	if (ttl < 0) {
		return (0);
	}
	return (ttl);
}

boolean_t
ad_disc_SubnetChanged(ad_disc_t ctx)
{
	ad_subnet_t *subnets;

	if (ctx->subnets_changed || ctx->subnets == NULL)
		return (B_TRUE);

	if ((subnets = find_subnets()) != NULL) {
		if (cmpsubnets(subnets, ctx->subnets) != 0)
			ctx->subnets_changed = B_TRUE;
		free(subnets);
	}

	return (ctx->subnets_changed);
}