/* * 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 2008 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ #pragma ident "%Z%%M% %I% %E% SMI" /* * 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_(). Each such function calls validate_() * for any items that it depends on. * * This API is not thread-safe. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "addisc.h" #define DNS_MAX_NAME NS_MAXDNAME #define DN_MAX_NAME (DNS_MAX_NAME + 512) /* 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) \ (ctx)->res_ninitted = res_ninit(&ctx->state) != -1 #define is_fixed(item) \ ((item)->type == AD_TYPE_FIXED) #define is_changed(item, num, param) \ ((item)->param_version[num] != (param)->version) #define is_valid(item) \ ((item)->type != AD_TYPE_INVALID && (item)->value.str != NULL) /*LINTLIBRARY*/ /* * Function definitions */ static void validate_SiteName(ad_disc_t ctx); static idmap_ad_disc_ds_t *dsdup(const idmap_ad_disc_ds_t *); static void update_version(ad_item_t *item, int num, ad_item_t *param) { item->param_version[num] = param->version; } static int is_expired(ad_item_t *item) { if (item->type == AD_TYPE_FIXED) return (FALSE); if (item->type == AD_TYPE_AUTO && (item->ttl == 0 || item->ttl > time(NULL))) return (FALSE); return (TRUE); } static void update_string(ad_item_t *item, char *value, enum ad_item_type type, uint32_t ttl) { if (item->value.str != NULL && value != NULL) { if (strcmp(item->value.str, value) != 0) item->version++; } else if (item->value.str != value) item->version++; if (item->value.str != NULL) free(item->value.str); item->value.str = value; item->type = type; if (ttl == 0) item->ttl = 0; else item->ttl = time(NULL) + ttl; } static void update_ds(ad_item_t *item, idmap_ad_disc_ds_t *value, enum ad_item_type type, uint32_t ttl) { if (item->value.ds != NULL && value != NULL) { if (ad_disc_compare_ds(item->value.ds, value) != 0) item->version++; } else if (item->value.ds != value) item->version++; if (item->value.ds != NULL) free(item->value.ds); item->value.ds = value; item->type = type; if (ttl == 0) item->ttl = 0; else item->ttl = time(NULL) + ttl; } static ad_item_t * get_item(ad_item_t *global, ad_item_t *site, enum ad_disc_req req) { ad_item_t *item; if (is_fixed(global)) return (global); if (req == AD_DISC_GLOBAL) item = global; else if (req == AD_DISC_SITE_SPECIFIC) item = site; else if (is_valid(site)) item = site; else item = global; if (!is_valid(item)) return (NULL); return (item); } /* * 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) { idmapdlog(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) { idmapdlog(LOG_ERR, "Failed to find the number of network interfaces (%s)", strerror(errno)); close(sock); return (NULL); } if (lifn.lifn_count < 1) { idmapdlog(LOG_ERR, "No IPv4 network interfaces found"); 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) { idmapdlog(LOG_ERR, "Out of memory"); close(sock); return (NULL); } if (ioctl(sock, SIOCGLIFCONF, (char *)&lifc) < 0) { idmapdlog(LOG_ERR, "Failed to list network interfaces (%s)", strerror(errno)); free(lifc.lifc_buf); 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); 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); close(sock); return (results); } static int cmpsubnets(ad_subnet_t *subnets1, ad_subnet_t *subnets2) { int num_subnets1; int num_subnets2; int matched; int i, j; for (i = 0; subnets1[i].subnet[0] != '\0'; i++) ; num_subnets1 = i; for (i = 0; subnets2[i].subnet[0] != '\0'; i++) ; num_subnets2 = i; if (num_subnets1 != num_subnets2) return (1); for (i = 0; i < num_subnets1; i++) { matched = FALSE; for (j = 0; j < num_subnets2; j++) { if (strcmp(subnets1[i].subnet, subnets2[j].subnet) == 0) { matched = TRUE; break; } } if (!matched) return (1); } return (0); } /* Convert a DN's DC components into a DNS domainname */ static 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; /* * Find all DC= and form DNS name of the * form .... */ while (dn_name[i] != NULL) { if (strncasecmp(&dn_name[i], "DC=", 3) == 0) { i += 3; if (dn_name[i] != NULL && num > 0) dns[j++] = '.'; while (dn_name[i] != NULL && 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] != NULL && dn_name[i] != ',' && dn_name[i] != '+') i++; } /* Skip over separator ',' or '+' */ if (dn_name[i] != NULL) i++; } dns[j] = '\0'; dns_name = malloc(j + 1); if (dns_name != NULL) (void) strlcpy(dns_name, dns, j + 1); return (dns_name); } /* Format the DN of an AD LDAP subnet object for some subnet */ static char * subnet_to_DN(const char *subnet, const char *baseDN) { char *result; int len; len = snprintf(NULL, 0, "CN=%s,CN=Subnets,CN=Sites,%s", subnet, baseDN) + 1; result = malloc(len); if (result != NULL) (void) snprintf(result, len, "CN=%s,CN=Subnets,CN=Sites,%s", subnet, baseDN); return (result); } /* Make a list of subnet object DNs from a list of subnets */ static char ** subnets_to_DNs(ad_subnet_t *subnets, const char *base_dn) { char **results; int i, j; for (i = 0; subnets[i].subnet[0] != '\0'; i++) ; results = calloc(i + 1, sizeof (char *)); if (results == NULL) return (NULL); for (i = 0; subnets[i].subnet[0] != '\0'; i++) { if ((results[i] = subnet_to_DN(subnets[i].subnet, base_dn)) == NULL) { for (j = 0; j < i; j++) free(results[j]); free(results); return (NULL); } } return (results); } /* Compare DS lists */ int ad_disc_compare_ds(idmap_ad_disc_ds_t *ds1, idmap_ad_disc_ds_t *ds2) { int i, j; int num_ds1; int num_ds2; int match; for (i = 0; ds1[i].host[0] != '\0'; i++) ; num_ds1 = i; for (j = 0; ds2[j].host[0] != '\0'; j++) ; num_ds2 = j; if (num_ds1 != num_ds2) return (1); for (i = 0; i < num_ds1; i++) { match = FALSE; for (j = 0; j < num_ds1; j++) { if (strcmp(ds1[i].host, ds2[i].host) == 0) { match = TRUE; break; } } if (!match) return (1); } return (0); } /* Copy a list of DSs */ static idmap_ad_disc_ds_t * dsdup(const idmap_ad_disc_ds_t *srv) { int i; int size; idmap_ad_disc_ds_t *new = NULL; for (i = 0; srv[i].host[0] != '\0'; i++) ; size = (i + 1) * sizeof (idmap_ad_disc_ds_t); new = malloc(size); if (new != NULL) memcpy(new, srv, size); return (new); } /* Compare SRC RRs; used with qsort() */ static int srvcmp(idmap_ad_disc_ds_t *s1, idmap_ad_disc_ds_t *s2) { if (s1->priority < s2->priority) return (1); else if (s1->priority > s2->priority) return (-1); if (s1->weight < s2->weight) return (1); else if (s1->weight > s2->weight) return (-1); return (0); } /* * Query or search the SRV RRs for a given name. * * If name == NULL then search (as in res_nsearch(3RESOLV), honoring any * search list/option), else query (as in res_nquery(3RESOLV)). * * The output TTL will be the one of the SRV RR with the lowest TTL. */ idmap_ad_disc_ds_t * srv_query(res_state state, const char *svc_name, const char *dname, char **rrname, uint32_t *ttl) { idmap_ad_disc_ds_t *srv; idmap_ad_disc_ds_t *srv_res; union { HEADER hdr; uchar_t buf[NS_MAXMSG]; } msg; int len, cnt, qdcount, ancount; uchar_t *ptr, *eom; uchar_t *end; uint16_t type; /* LINTED E_FUNC_SET_NOT_USED */ uint16_t class; uint32_t rttl; uint16_t size; char *query_type; char namebuf[NS_MAXDNAME]; if (state == NULL) return (NULL); /* Set negative result TTL */ *ttl = 5 * 60; /* 1. query necessary resource records */ /* Search, querydomain or query */ if (rrname != NULL) { query_type = "search"; *rrname = NULL; len = res_nsearch(state, svc_name, C_IN, T_SRV, msg.buf, sizeof (msg.buf)); } else if (dname != NULL) { query_type = "query"; len = res_nquerydomain(state, svc_name, dname, C_IN, T_SRV, msg.buf, sizeof (msg.buf)); } idmapdlog(LOG_DEBUG, "%sing DNS for SRV RRs named '%s'", query_type, svc_name); if (len < 0) { idmapdlog(LOG_DEBUG, "DNS %s for '%s' failed (%s)", query_type, svc_name, hstrerror(state->res_h_errno)); return (NULL); } if (len > sizeof (msg.buf)) { idmapdlog(LOG_ERR, "DNS query %ib message doesn't fit" " into %ib buffer", len, sizeof (msg.buf)); return (NULL); } /* 2. parse the reply, skip header and question sections */ ptr = msg.buf + sizeof (msg.hdr); eom = msg.buf + len; qdcount = ntohs(msg.hdr.qdcount); ancount = ntohs(msg.hdr.ancount); for (cnt = qdcount; cnt > 0; --cnt) { if ((len = dn_skipname(ptr, eom)) < 0) { idmapdlog(LOG_ERR, "DNS query invalid message format"); return (NULL); } ptr += len + QFIXEDSZ; } /* 3. walk through the answer section */ srv_res = calloc(ancount + 1, sizeof (idmap_ad_disc_ds_t)); *ttl = (uint32_t)-1; for (srv = srv_res, cnt = ancount; cnt > 0; --cnt, srv++) { len = dn_expand(msg.buf, eom, ptr, namebuf, sizeof (namebuf)); if (len < 0) { idmapdlog(LOG_ERR, "DNS query invalid message format"); return (NULL); } if (rrname != NULL && *rrname == NULL) *rrname = strdup(namebuf); ptr += len; NS_GET16(type, ptr); NS_GET16(class, ptr); NS_GET32(rttl, ptr); NS_GET16(size, ptr); if ((end = ptr + size) > eom) { idmapdlog(LOG_ERR, "DNS query invalid message format"); return (NULL); } if (type != T_SRV) { ptr = end; continue; } NS_GET16(srv->priority, ptr); NS_GET16(srv->weight, ptr); NS_GET16(srv->port, ptr); len = dn_expand(msg.buf, eom, ptr, srv->host, sizeof (srv->host)); if (len < 0) { idmapdlog(LOG_ERR, "DNS query invalid SRV record"); return (NULL); } if (rttl < *ttl) *ttl = rttl; idmapdlog(LOG_DEBUG, "Found %s %d IN SRV [%d][%d] %s:%d", namebuf, rttl, srv->priority, srv->weight, srv->host, srv->port); /* 3. move ptr to the end of current record */ ptr = end; } if (ancount > 1) qsort(srv_res, ancount, sizeof (*srv_res), (int (*)(const void *, const void *))srvcmp); return (srv_res); } static int /* ARGSUSED */ saslcallback(LDAP *ld, unsigned flags, void *defaults, void *prompts) { 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); } /* * A utility function to get the value of some attribute of one of one * or more AD LDAP objects named by the dn_list; first found one wins. */ static char * ldap_lookup_entry_attr(LDAP **ld, idmap_ad_disc_ds_t *domainControllers, char **dn_list, char *attr) { int i; int rc, ldversion; int zero = 0; int timeoutms = 5 * 1000; char *saslmech = "GSSAPI"; uint32_t saslflags = LDAP_SASL_INTERACTIVE; int scope = LDAP_SCOPE_BASE; char *attrs[2]; LDAPMessage *results = NULL; LDAPMessage *entry; char **values = NULL; char *val = NULL; attrs[0] = attr; attrs[1] = NULL; rc = LDAP_INVALID_CREDENTIALS; if (*ld == NULL) { for (i = 0; domainControllers[i].host[0] != '\0'; i++) { *ld = ldap_init(domainControllers[i].host, domainControllers[i].port); if (*ld == NULL) { idmapdlog(LOG_INFO, "Couldn't connect to " "AD DC %s:%d (%s)", domainControllers[i].host, domainControllers[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 = ldap_sasl_interactive_bind_s(*ld, "" /* binddn */, saslmech, NULL, NULL, saslflags, &saslcallback, NULL /* defaults */); if (rc == LDAP_SUCCESS) break; idmapdlog(LOG_INFO, "LDAP SASL bind to %s:%d " "failed (%s)", domainControllers[i].host, domainControllers[i].port, ldap_err2string(rc)); (void) ldap_unbind(*ld); *ld = NULL; } } if (*ld == NULL) { idmapdlog(LOG_NOTICE, "Couldn't open and SASL bind LDAP " "connections to any domain controllers; discovery of " "some items will fail"); return (NULL); } for (i = 0; dn_list[i] != NULL; i++) { rc = ldap_search_s(*ld, dn_list[i], scope, "(objectclass=*)", attrs, 0, &results); if (rc == LDAP_SUCCESS) { for (entry = ldap_first_entry(*ld, results); entry != NULL && values == NULL; entry = ldap_next_entry(*ld, entry)) { values = ldap_get_values( *ld, entry, attr); } if (values != NULL) { (void) ldap_msgfree(results); val = strdup(values[0]); ldap_value_free(values); return (val); } } if (results != NULL) { (void) ldap_msgfree(results); results = NULL; } } (void) ldap_unbind(*ld); *ld = NULL; 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); return (ctx); } void ad_disc_fini(ad_disc_t ctx) { if (ctx == NULL) return; if (ctx->res_ninitted) res_ndestroy(&ctx->state); if (ctx->subnets != NULL) free(ctx->subnets); if (ctx->domain_name.value.str != NULL) free(ctx->domain_name.value.str); if (ctx->domain_controller.value.str != NULL) free(ctx->domain_controller.value.str); if (ctx->site_name.value.str != NULL) free(ctx->site_name.value.str); if (ctx->forest_name.value.str != NULL) free(ctx->forest_name.value.str); if (ctx->global_catalog.value.str != NULL) free(ctx->global_catalog.value.str); if (ctx->site_domain_controller.value.str != NULL) free(ctx->site_domain_controller.value.str); if (ctx->site_global_catalog.value.str != NULL) free(ctx->site_global_catalog.value.str); free(ctx); } void ad_disc_refresh(ad_disc_t ctx) { if (ctx->res_ninitted) res_ndestroy(&ctx->state); (void) memset(&ctx->state, 0, sizeof (ctx->state)); ctx->res_ninitted = res_ninit(&ctx->state) != -1; if (ctx->domain_name.type == AD_TYPE_AUTO) ctx->domain_name.type = AD_TYPE_INVALID; if (ctx->domain_controller.type == AD_TYPE_AUTO) ctx->domain_controller.type = AD_TYPE_INVALID; if (ctx->site_name.type == AD_TYPE_AUTO) ctx->site_name.type = AD_TYPE_INVALID; if (ctx->forest_name.type == AD_TYPE_AUTO) ctx->forest_name.type = AD_TYPE_INVALID; if (ctx->global_catalog.type == AD_TYPE_AUTO) ctx->global_catalog.type = AD_TYPE_INVALID; if (ctx->site_domain_controller.type == AD_TYPE_AUTO) ctx->site_domain_controller.type = AD_TYPE_INVALID; if (ctx->site_global_catalog.type == AD_TYPE_AUTO) ctx->site_global_catalog.type = AD_TYPE_INVALID; } /* Discover joined Active Directory domainName */ static void validate_DomainName(ad_disc_t ctx) { idmap_ad_disc_ds_t *domain_controller = NULL; char *dname, *srvname; uint32_t ttl = 0; if (is_fixed(&ctx->domain_name)) return; if (!is_expired(&ctx->domain_name)) return; /* Try to find our domain by searching for DCs for it */ DO_RES_NINIT(ctx); domain_controller = srv_query(&ctx->state, LDAP_SRV_HEAD DC_SRV_TAIL, ctx->domain_name.value.str, &srvname, &ttl); /* * 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 (domain_controller == NULL) return; free(domain_controller); /* * 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) { idmapdlog(LOG_ERR, "Out of memory"); return; } /* Eat any trailing dot */ if (*(dname + strlen(dname)) == '.') *(dname + strlen(dname)) = '\0'; update_string(&ctx->domain_name, dname, AD_TYPE_AUTO, 0); } char * ad_disc_get_DomainName(ad_disc_t ctx) { char *domain_name = NULL; validate_DomainName(ctx); if (is_valid(&ctx->domain_name)) domain_name = strdup(ctx->domain_name.value.str); return (domain_name); } /* Discover domain controllers */ static void validate_DomainController(ad_disc_t ctx, enum ad_disc_req req) { uint32_t ttl = 0; idmap_ad_disc_ds_t *domain_controller = NULL; int validate_global = FALSE; int validate_site = FALSE; if (is_fixed(&ctx->domain_controller)) return; validate_DomainName(ctx); if (req == AD_DISC_GLOBAL) validate_global = TRUE; else { validate_SiteName(ctx); if (is_valid(&ctx->site_name)) validate_site = TRUE; if (req == AD_DISC_PREFER_SITE) validate_global = TRUE; } if (validate_global && (is_expired(&ctx->domain_controller) || is_changed(&ctx->domain_controller, PARAM1, &ctx->domain_name))) { update_version(&ctx->domain_controller, PARAM1, &ctx->domain_name); if (is_valid(&ctx->domain_name)) { /* * Lookup DNS SRV RR named * _ldap._tcp.dc._msdcs. */ DO_RES_NINIT(ctx); domain_controller = srv_query(&ctx->state, LDAP_SRV_HEAD DC_SRV_TAIL, ctx->domain_name.value.str, NULL, &ttl); } update_ds(&ctx->domain_controller, domain_controller, AD_TYPE_AUTO, ttl); } if (validate_site && (is_expired(&ctx->site_domain_controller) || is_changed(&ctx->site_domain_controller, PARAM1, &ctx->domain_name) || is_changed(&ctx->site_domain_controller, PARAM2, &ctx->site_name))) { update_version(&ctx->site_domain_controller, PARAM1, &ctx->domain_name); update_version(&ctx->site_domain_controller, PARAM2, &ctx->site_name); if (is_valid(&ctx->domain_name)) { char rr_name[DNS_MAX_NAME]; /* * Lookup DNS SRV RR named * _ldap._tcp.._sites.dc._msdcs. */ (void) snprintf(rr_name, sizeof (rr_name), LDAP_SRV_HEAD SITE_SRV_MIDDLE DC_SRV_TAIL, ctx->site_name.value.str); DO_RES_NINIT(ctx); domain_controller = srv_query(&ctx->state, rr_name, ctx->domain_name.value.str, NULL, &ttl); } update_ds(&ctx->site_domain_controller, domain_controller, AD_TYPE_AUTO, ttl); } } idmap_ad_disc_ds_t * ad_disc_get_DomainController(ad_disc_t ctx, enum ad_disc_req req) { idmap_ad_disc_ds_t *domain_controller = NULL; ad_item_t *item; validate_DomainController(ctx, req); item = get_item(&ctx->domain_controller, &ctx->site_domain_controller, req); if (item != NULL && is_valid(item)) domain_controller = dsdup(item->value.ds); return (domain_controller); } /* Discover site name (for multi-homed systems the first one found wins) */ static void validate_SiteName(ad_disc_t ctx) { LDAP *ld = NULL; ad_subnet_t *subnets = NULL; char **dn_subnets = NULL; char *dn_root[2]; char *config_naming_context = NULL; char *site_object = NULL; char *site_name = NULL; char *forest_name; int len; int i; int update_required = FALSE; if (is_fixed(&ctx->site_name)) return; /* Can't rely on site-specific DCs */ validate_DomainController(ctx, AD_DISC_GLOBAL); if (is_expired(&ctx->site_name) || is_changed(&ctx->site_name, PARAM1, &ctx->domain_controller) || ctx->subnets == NULL || ctx->subnets_changed) { subnets = find_subnets(); ctx->subnets_last_check = time(NULL); update_required = TRUE; } else if (ctx->subnets_last_check + 60 < time(NULL)) { subnets = find_subnets(); ctx->subnets_last_check = time(NULL); if (cmpsubnets(ctx->subnets, subnets) != 0) update_required = TRUE; } if (!update_required) { free(subnets); return; } update_version(&ctx->site_name, PARAM1, &ctx->domain_controller); if (is_valid(&ctx->domain_name) && is_valid(&ctx->domain_controller) && subnets != NULL) { dn_root[0] = ""; dn_root[1] = NULL; config_naming_context = ldap_lookup_entry_attr( &ld, ctx->domain_controller.value.ds, dn_root, "configurationNamingContext"); if (config_naming_context == NULL) goto out; /* * configurationNamingContext also provides the Forest * Name. */ if (!is_fixed(&ctx->forest_name)) { /* * The configurationNamingContext should be of * form: * CN=Configuration, * Remove the first part and convert to DNS form * (replace ",DC=" with ".") */ char *str = "CN=Configuration,"; int len = strlen(str); if (strncasecmp(config_naming_context, str, len) == 0) { forest_name = DN_to_DNS( config_naming_context + len); update_string(&ctx->forest_name, forest_name, AD_TYPE_AUTO, 0); } } dn_subnets = subnets_to_DNs(subnets, config_naming_context); if (dn_subnets == NULL) goto out; site_object = ldap_lookup_entry_attr( &ld, ctx->domain_controller.value.ds, dn_subnets, "siteobject"); if (site_object != NULL) { /* * The site object should be of the form * CN=,CN=Sites,CN=Configuration, * */ if (strncasecmp(site_object, "CN=", 3) == 0) { for (len = 0; site_object[len + 3] != ','; len++) ; site_name = malloc(len + 1); (void) strncpy(site_name, &site_object[3], len); site_name[len] = '\0'; } } if (ctx->subnets != NULL) { free(ctx->subnets); ctx->subnets = NULL; } ctx->subnets = subnets; subnets = NULL; ctx->subnets_changed = FALSE; } out: if (ld != NULL) (void) ldap_unbind(ld); update_string(&ctx->site_name, site_name, AD_TYPE_AUTO, 0); if (site_name == NULL || *site_name == '\0') { /* No site name -> no site-specific DSs */ update_ds(&ctx->site_domain_controller, NULL, AD_TYPE_AUTO, 0); update_ds(&ctx->site_global_catalog, NULL, AD_TYPE_AUTO, 0); } if (dn_subnets != NULL) { for (i = 0; dn_subnets[i] != NULL; i++) free(dn_subnets[i]); free(dn_subnets); } if (config_naming_context != NULL) free(config_naming_context); if (site_object != NULL) free(site_object); free(subnets); } char * ad_disc_get_SiteName(ad_disc_t ctx) { char *site_name = NULL; validate_SiteName(ctx); if (is_valid(&ctx->site_name)) site_name = strdup(ctx->site_name.value.str); return (site_name); } /* Discover forest name */ static void validate_ForestName(ad_disc_t ctx) { LDAP *ld = NULL; char *config_naming_context; char *forest_name = NULL; char *dn_list[2]; if (is_fixed(&ctx->forest_name)) return; /* * We may not have a site name yet, so we won't rely on * site-specific DCs. (But maybe we could replace * validate_ForestName() with validate_siteName()?) */ validate_DomainController(ctx, AD_DISC_GLOBAL); if (is_expired(&ctx->forest_name) || is_changed(&ctx->forest_name, PARAM1, &ctx->domain_controller)) { update_version(&ctx->forest_name, PARAM1, &ctx->domain_controller); if (is_valid(&ctx->domain_controller)) { dn_list[0] = ""; dn_list[1] = NULL; config_naming_context = ldap_lookup_entry_attr( &ld, ctx->domain_controller.value.ds, dn_list, "configurationNamingContext"); if (config_naming_context != NULL) { /* * The configurationNamingContext should be of * form: * CN=Configuration, * Remove the first part and convert to DNS form * (replace ",DC=" with ".") */ char *str = "CN=Configuration,"; int len = strlen(str); if (strncasecmp(config_naming_context, str, len) == 0) { forest_name = DN_to_DNS( config_naming_context + len); } free(config_naming_context); } if (ld != NULL) (void) ldap_unbind(ld); } update_string(&ctx->forest_name, forest_name, AD_TYPE_AUTO, 0); } } char * ad_disc_get_ForestName(ad_disc_t ctx) { char *forest_name = NULL; validate_ForestName(ctx); if (is_valid(&ctx->forest_name)) forest_name = strdup(ctx->forest_name.value.str); return (forest_name); } /* Discover global catalog servers */ static void validate_GlobalCatalog(ad_disc_t ctx, enum ad_disc_req req) { idmap_ad_disc_ds_t *global_catalog = NULL; uint32_t ttl = 0; int validate_global = FALSE; int validate_site = FALSE; if (is_fixed(&ctx->global_catalog)) return; validate_ForestName(ctx); if (req == AD_DISC_GLOBAL) validate_global = TRUE; else { validate_SiteName(ctx); if (is_valid(&ctx->site_name)) validate_site = TRUE; if (req == AD_DISC_PREFER_SITE) validate_global = TRUE; } if (validate_global && (is_expired(&ctx->global_catalog) || is_changed(&ctx->global_catalog, PARAM1, &ctx->forest_name))) { update_version(&ctx->global_catalog, PARAM1, &ctx->forest_name); if (is_valid(&ctx->forest_name)) { /* * Lookup DNS SRV RR named * _ldap._tcp.gc._msdcs. */ DO_RES_NINIT(ctx); global_catalog = srv_query(&ctx->state, LDAP_SRV_HEAD GC_SRV_TAIL, ctx->forest_name.value.str, NULL, &ttl); } update_ds(&ctx->global_catalog, global_catalog, AD_TYPE_AUTO, ttl); } if (validate_site && (is_expired(&ctx->site_global_catalog) || is_changed(&ctx->site_global_catalog, PARAM1, &ctx->forest_name) || is_changed(&ctx->site_global_catalog, PARAM2, &ctx->site_name))) { update_version(&ctx->site_global_catalog, PARAM1, &ctx->forest_name); update_version(&ctx->site_global_catalog, PARAM2, &ctx->site_name); if (is_valid(&ctx->forest_name) && is_valid(&ctx->site_name)) { char rr_name[DNS_MAX_NAME]; /* * Lookup DNS SRV RR named: * _ldap._tcp.._sites.gc. * _msdcs. */ (void) snprintf(rr_name, sizeof (rr_name), LDAP_SRV_HEAD SITE_SRV_MIDDLE GC_SRV_TAIL, ctx->site_name.value.str); DO_RES_NINIT(ctx); global_catalog = srv_query(&ctx->state, rr_name, ctx->forest_name.value.str, NULL, &ttl); } update_ds(&ctx->site_global_catalog, global_catalog, AD_TYPE_AUTO, ttl); } } idmap_ad_disc_ds_t * ad_disc_get_GlobalCatalog(ad_disc_t ctx, enum ad_disc_req req) { idmap_ad_disc_ds_t *global_catalog = NULL; ad_item_t *item; validate_GlobalCatalog(ctx, req); item = get_item(&ctx->global_catalog, &ctx->site_global_catalog, req); if (item != NULL && is_valid(item)) global_catalog = dsdup(item->value.ds); return (global_catalog); } 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_string(&ctx->domain_name, domain_name, AD_TYPE_FIXED, 0); } else if (ctx->domain_name.type == AD_TYPE_FIXED) ctx->domain_name.type = AD_TYPE_INVALID; return (0); } int ad_disc_set_DomainController(ad_disc_t ctx, const idmap_ad_disc_ds_t *domainController) { idmap_ad_disc_ds_t *domain_controller = NULL; if (domainController != NULL) { domain_controller = dsdup(domainController); if (domain_controller == NULL) return (-1); update_ds(&ctx->domain_controller, domain_controller, AD_TYPE_FIXED, 0); } else if (ctx->domain_controller.type == AD_TYPE_FIXED) ctx->domain_controller.type = AD_TYPE_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_string(&ctx->site_name, site_name, AD_TYPE_FIXED, 0); } else if (ctx->site_name.type == AD_TYPE_FIXED) ctx->site_name.type = AD_TYPE_INVALID; return (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_string(&ctx->forest_name, forest_name, AD_TYPE_FIXED, 0); } else if (ctx->forest_name.type == AD_TYPE_FIXED) ctx->forest_name.type = AD_TYPE_INVALID; return (0); } int ad_disc_set_GlobalCatalog(ad_disc_t ctx, const idmap_ad_disc_ds_t *globalCatalog) { idmap_ad_disc_ds_t *global_catalog = NULL; if (globalCatalog != NULL) { global_catalog = dsdup(globalCatalog); if (global_catalog == NULL) return (-1); update_ds(&ctx->global_catalog, global_catalog, AD_TYPE_FIXED, 0); } else if (ctx->global_catalog.type == AD_TYPE_FIXED) ctx->global_catalog.type = AD_TYPE_INVALID; return (0); } int ad_disc_unset(ad_disc_t ctx) { if (ctx->domain_name.type == AD_TYPE_FIXED) ctx->domain_name.type = AD_TYPE_INVALID; if (ctx->domain_controller.type == AD_TYPE_FIXED) ctx->domain_controller.type = AD_TYPE_INVALID; if (ctx->site_name.type == AD_TYPE_FIXED) ctx->site_name.type = AD_TYPE_INVALID; if (ctx->forest_name.type == AD_TYPE_FIXED) ctx->forest_name.type = AD_TYPE_INVALID; if (ctx->global_catalog.type == AD_TYPE_FIXED) ctx->global_catalog.type = AD_TYPE_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) { int ttl; ttl = MIN_GT_ZERO(ctx->domain_controller.ttl, ctx->global_catalog.ttl); ttl = MIN_GT_ZERO(ttl, ctx->site_domain_controller.ttl); ttl = MIN_GT_ZERO(ttl, ctx->site_global_catalog.ttl); if (ttl == -1) return (-1); ttl -= time(NULL); if (ttl < 0) return (0); return (ttl); } int ad_disc_SubnetChanged(ad_disc_t ctx) { ad_subnet_t *subnets; if (ctx->subnets_changed || ctx->subnets == NULL) return (TRUE); if ((subnets = find_subnets()) != NULL) { if (cmpsubnets(subnets, ctx->subnets) != 0) ctx->subnets_changed = TRUE; free(subnets); } return (ctx->subnets_changed); }