/* * This file and its contents are supplied under the terms of the * Common Development and Distribution License ("CDDL"), version 1.0. * You may only use this file in accordance with the terms of version * 1.0 of the CDDL. * * A full copy of the text of the CDDL should have accompanied this * source. A copy of the CDDL is also available via the Internet at * http://www.illumos.org/license/CDDL. */ /* * Copyright 2014 Nexenta Systems, Inc. All rights reserved. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "adutils_impl.h" #include "addisc_impl.h" #define LDAP_PORT 389 #define NETLOGON_ATTR_NAME "NetLogon" #define NETLOGON_NT_VERSION_1 0x00000001 #define NETLOGON_NT_VERSION_5 0x00000002 #define NETLOGON_NT_VERSION_5EX 0x00000004 #define NETLOGON_NT_VERSION_5EX_WITH_IP 0x00000008 #define NETLOGON_NT_VERSION_WITH_CLOSEST_SITE 0x00000010 #define NETLOGON_NT_VERSION_AVOID_NT4EMUL 0x01000000 typedef enum { OPCODE = 0, SBZ, FLAGS, DOMAIN_GUID, FOREST_NAME, DNS_DOMAIN_NAME, DNS_HOST_NAME, NET_DOMAIN_NAME, NET_COMP_NAME, USER_NAME, DC_SITE_NAME, CLIENT_SITE_NAME, SOCKADDR_SIZE, SOCKADDR, NEXT_CLOSEST_SITE_NAME, NTVER, LM_NT_TOKEN, LM_20_TOKEN } field_5ex_t; struct _berelement { char *ber_buf; char *ber_ptr; char *ber_end; }; extern int ldap_put_filter(BerElement *ber, char *); static void send_to_cds(ad_disc_cds_t *, char *, size_t, int); static ad_disc_cds_t *find_cds_by_addr(ad_disc_cds_t *, struct sockaddr_in6 *); static boolean_t addrmatch(struct addrinfo *, struct sockaddr_in6 *); static void save_ai(ad_disc_cds_t *, struct addrinfo *); static void cldap_escape_le64(char *buf, uint64_t val, int bytes) { char *p = buf; while (bytes != 0) { p += sprintf(p, "\\%.2x", (uint8_t)(val & 0xff)); val >>= 8; bytes--; } *p = '\0'; } /* * Construct CLDAPMessage PDU for NetLogon search request. * * CLDAPMessage ::= SEQUENCE { * messageID MessageID, * protocolOp searchRequest SearchRequest; * } * * SearchRequest ::= * [APPLICATION 3] SEQUENCE { * baseObject LDAPDN, * scope ENUMERATED { * baseObject (0), * singleLevel (1), * wholeSubtree (2) * }, * derefAliases ENUMERATED { * neverDerefAliases (0), * derefInSearching (1), * derefFindingBaseObj (2), * derefAlways (3) * }, * sizeLimit INTEGER (0 .. MaxInt), * timeLimit INTEGER (0 .. MaxInt), * attrsOnly BOOLEAN, * filter Filter, * attributes SEQUENCE OF AttributeType * } */ BerElement * cldap_build_request(const char *dname, const char *host, uint32_t ntver, uint16_t msgid) { BerElement *ber; int len = 0; char *basedn = ""; int scope = LDAP_SCOPE_BASE, deref = LDAP_DEREF_NEVER, sizelimit = 0, timelimit = 0, attrsonly = 0; char filter[512]; char ntver_esc[13]; char *p, *pend; /* * Construct search filter in LDAP format. */ p = filter; pend = p + sizeof (filter); len = snprintf(p, pend - p, "(&(DnsDomain=%s)", dname); if (len >= (pend - p)) goto fail; p += len; if (host != NULL) { len = snprintf(p, (pend - p), "(Host=%s)", host); if (len >= (pend - p)) goto fail; p += len; } if (ntver != 0) { /* * Format NtVer as little-endian with LDAPv3 escapes. */ cldap_escape_le64(ntver_esc, ntver, sizeof (ntver)); len = snprintf(p, (pend - p), "(NtVer=%s)", ntver_esc); if (len >= (pend - p)) goto fail; p += len; } len = snprintf(p, pend - p, ")"); if (len >= (pend - p)) goto fail; p += len; /* * Encode CLDAPMessage and beginning of SearchRequest sequence. */ if ((ber = ber_alloc()) == NULL) goto fail; if (ber_printf(ber, "{it{seeiib", msgid, LDAP_REQ_SEARCH, basedn, scope, deref, sizelimit, timelimit, attrsonly) < 0) goto fail; /* * Encode Filter sequence. */ if (ldap_put_filter(ber, filter) < 0) goto fail; /* * Encode attribute and close Filter and SearchRequest sequences. */ if (ber_printf(ber, "{s}}}", NETLOGON_ATTR_NAME) < 0) goto fail; /* * Success */ return (ber); fail: if (ber != NULL) ber_free(ber, 1); return (NULL); } /* * Parse incoming search responses and attribute to correct hosts. * * CLDAPMessage ::= SEQUENCE { * messageID MessageID, * searchResponse SEQUENCE OF * SearchResponse; * } * * SearchResponse ::= * CHOICE { * entry [APPLICATION 4] SEQUENCE { * objectName LDAPDN, * attributes SEQUENCE OF SEQUENCE { * AttributeType, * SET OF * AttributeValue * } * }, * resultCode [APPLICATION 5] LDAPResult * } */ static int decode_name(uchar_t *base, uchar_t *cp, char *str) { uchar_t *tmp = NULL, *st = cp; uint8_t len; /* * there should probably be some boundary checks on str && cp * maybe pass in strlen && msglen ? */ while (*cp != 0) { if (*cp == 0xc0) { if (tmp == NULL) tmp = cp + 2; cp = base + *(cp + 1); } for (len = *cp++; len > 0; len--) *str++ = *cp++; *str++ = '.'; } if (cp != st) *(str-1) = '\0'; else *str = '\0'; return ((tmp == NULL ? cp + 1 : tmp) - st); } static int cldap_parse(ad_disc_t ctx, ad_disc_cds_t *cds, BerElement *ber) { ad_disc_ds_t *dc = &cds->cds_ds; uchar_t *base = NULL, *cp = NULL; char val[512]; /* how big should val be? */ int l, msgid, rc = 0; uint16_t opcode; field_5ex_t f = OPCODE; /* * Later, compare msgid's/some validation? */ if (ber_scanf(ber, "{i{x{{x[la", &msgid, &l, &cp) == LBER_ERROR) { rc = 1; goto out; } for (base = cp; ((cp - base) < l) && (f <= LM_20_TOKEN); f++) { val[0] = '\0'; switch (f) { case OPCODE: /* opcode = *(uint16_t *)cp; */ /* cp +=2; */ opcode = *cp++; opcode |= (*cp++ << 8); break; case SBZ: cp += 2; break; case FLAGS: /* dci->Flags = *(uint32_t *)cp; */ /* cp +=4; */ dc->flags = *cp++; dc->flags |= (*cp++ << 8); dc->flags |= (*cp++ << 16); dc->flags |= (*cp++ << 26); break; case DOMAIN_GUID: if (ctx != NULL) auto_set_DomainGUID(ctx, cp); cp += 16; break; case FOREST_NAME: cp += decode_name(base, cp, val); if (ctx != NULL) auto_set_ForestName(ctx, val); break; case DNS_DOMAIN_NAME: /* * We always have this already. * (Could validate it here.) */ cp += decode_name(base, cp, val); break; case DNS_HOST_NAME: cp += decode_name(base, cp, val); if (0 != strcasecmp(val, dc->host)) { logger(LOG_ERR, "DC name %s != %s?", val, dc->host); } break; case NET_DOMAIN_NAME: /* * This is the "Flat" domain name. * (i.e. the NetBIOS name) * ignore for now. */ cp += decode_name(base, cp, val); break; case NET_COMP_NAME: /* not needed */ cp += decode_name(base, cp, val); break; case USER_NAME: /* not needed */ cp += decode_name(base, cp, val); break; case DC_SITE_NAME: cp += decode_name(base, cp, val); (void) strlcpy(dc->site, val, sizeof (dc->site)); break; case CLIENT_SITE_NAME: cp += decode_name(base, cp, val); if (ctx != NULL) auto_set_SiteName(ctx, val); break; /* * These are all possible, but we don't really care about them. * Sockaddr_size && sockaddr might be useful at some point */ case SOCKADDR_SIZE: case SOCKADDR: case NEXT_CLOSEST_SITE_NAME: case NTVER: case LM_NT_TOKEN: case LM_20_TOKEN: break; default: rc = 3; goto out; } } out: if (base) free(base); else if (cp) free(cp); return (rc); } /* * Filter out unresponsive servers, and save the domain info * returned by the "LDAP ping" in the returned object. * If ctx != NULL, this is a query for a DC, in which case we * also save the Domain GUID, Site name, and Forest name as * "auto" (discovered) values in the ctx. * * Only return the "winner". (We only want one DC/GC) */ ad_disc_ds_t * ldap_ping(ad_disc_t ctx, ad_disc_cds_t *dclist, char *dname, int reqflags) { struct sockaddr_in6 addr6; socklen_t addrlen; struct pollfd pingchk; ad_disc_cds_t *send_ds; ad_disc_cds_t *recv_ds = NULL; ad_disc_ds_t *ret_ds = NULL; BerElement *req = NULL; BerElement *res = NULL; struct _berelement *be, *rbe; size_t be_len, rbe_len; int fd = -1; int tries = 3; int waitsec; int r; uint16_t msgid; /* One plus a null entry. */ ret_ds = calloc(2, sizeof (ad_disc_ds_t)); if (ret_ds == NULL) goto fail; if ((fd = socket(PF_INET6, SOCK_DGRAM, 0)) < 0) goto fail; (void) memset(&addr6, 0, sizeof (addr6)); addr6.sin6_family = AF_INET6; addr6.sin6_addr = in6addr_any; if (bind(fd, (struct sockaddr *)&addr6, sizeof (addr6)) < 0) goto fail; /* * semi-unique msgid... */ msgid = gethrtime() & 0xffff; /* * Is ntver right? It certainly works on w2k8... If others are needed, * that might require changes to cldap_parse */ req = cldap_build_request(dname, NULL, NETLOGON_NT_VERSION_5EX, msgid); if (req == NULL) goto fail; be = (struct _berelement *)req; be_len = be->ber_end - be->ber_buf; if ((res = ber_alloc()) == NULL) goto fail; rbe = (struct _berelement *)res; rbe_len = rbe->ber_end - rbe->ber_buf; pingchk.fd = fd; pingchk.events = POLLIN; pingchk.revents = 0; try_again: send_ds = dclist; waitsec = 5; while (recv_ds == NULL && waitsec > 0) { /* * If there is another candidate, send to it. */ if (send_ds->cds_ds.host[0] != '\0') { send_to_cds(send_ds, be->ber_buf, be_len, fd); send_ds++; /* * Wait 1/10 sec. before the next send. */ r = poll(&pingchk, 1, 100); #if 0 /* DEBUG */ /* Drop all responses 1st pass. */ if (waitsec == 5) r = 0; #endif } else { /* * No more candidates to "ping", so * just wait a sec for responses. */ r = poll(&pingchk, 1, 1000); if (r == 0) --waitsec; } if (r > 0) { /* * Got a response. */ (void) memset(&addr6, 0, addrlen = sizeof (addr6)); r = recvfrom(fd, rbe->ber_buf, rbe_len, 0, (struct sockaddr *)&addr6, &addrlen); recv_ds = find_cds_by_addr(dclist, &addr6); if (recv_ds == NULL) continue; (void) cldap_parse(ctx, recv_ds, res); if ((recv_ds->cds_ds.flags & reqflags) != reqflags) { logger(LOG_ERR, "Skip %s" "due to flags 0x%X", recv_ds->cds_ds.host, recv_ds->cds_ds.flags); recv_ds = NULL; } } } if (recv_ds == NULL) { if (--tries <= 0) goto fail; goto try_again; } (void) memcpy(ret_ds, recv_ds, sizeof (*ret_ds)); ber_free(res, 1); ber_free(req, 1); (void) close(fd); return (ret_ds); fail: ber_free(res, 1); ber_free(req, 1); (void) close(fd); free(ret_ds); return (NULL); } /* * Attempt a send of the LDAP request to all known addresses * for this candidate server. */ static void send_to_cds(ad_disc_cds_t *send_cds, char *ber_buf, size_t be_len, int fd) { struct sockaddr_in6 addr6; struct addrinfo *ai; int err; if (DBG(DISC, 2)) { logger(LOG_DEBUG, "send to: %s", send_cds->cds_ds.host); } for (ai = send_cds->cds_ai; ai != NULL; ai = ai->ai_next) { /* * Build the "to" address. */ (void) memset(&addr6, 0, sizeof (addr6)); if (ai->ai_family == AF_INET6) { (void) memcpy(&addr6, ai->ai_addr, sizeof (addr6)); } else if (ai->ai_family == AF_INET) { struct sockaddr_in *sin = (void *)ai->ai_addr; addr6.sin6_family = AF_INET6; IN6_INADDR_TO_V4MAPPED(&sin->sin_addr, &addr6.sin6_addr); } else { continue; } addr6.sin6_port = htons(LDAP_PORT); /* * Send the "ping" to this address. */ err = sendto(fd, ber_buf, be_len, 0, (struct sockaddr *)&addr6, sizeof (addr6)); err = (err < 0) ? errno : 0; if (DBG(DISC, 2)) { char abuf[INET6_ADDRSTRLEN]; const char *pa; pa = inet_ntop(AF_INET6, &addr6.sin6_addr, abuf, sizeof (abuf)); logger(LOG_ERR, " > %s rc=%d", pa ? pa : "?", err); } } } /* * We have a response from some address. Find the candidate with * this address. In case a candidate had multiple addresses, we * keep track of which the response came from. */ static ad_disc_cds_t * find_cds_by_addr(ad_disc_cds_t *dclist, struct sockaddr_in6 *sin6from) { char abuf[INET6_ADDRSTRLEN]; ad_disc_cds_t *ds; struct addrinfo *ai; int eai; if (DBG(DISC, 1)) { eai = getnameinfo((void *)sin6from, sizeof (*sin6from), abuf, sizeof (abuf), NULL, 0, NI_NUMERICHOST); if (eai != 0) (void) strlcpy(abuf, "?", sizeof (abuf)); logger(LOG_DEBUG, "LDAP ping resp: addr=%s", abuf); } /* * Find the DS this response came from. * (don't accept unexpected responses) */ for (ds = dclist; ds->cds_ds.host[0] != '\0'; ds++) { ai = ds->cds_ai; while (ai != NULL) { if (addrmatch(ai, sin6from)) goto found; ai = ai->ai_next; } } if (DBG(DISC, 1)) { logger(LOG_DEBUG, " (unexpected)"); } return (NULL); found: if (DBG(DISC, 2)) { logger(LOG_DEBUG, " from %s", ds->cds_ds.host); } save_ai(ds, ai); return (ds); } static boolean_t addrmatch(struct addrinfo *ai, struct sockaddr_in6 *sin6from) { /* * Note: on a GC query, the ds->addr port numbers are * the GC port, and our from addr has the LDAP port. * Just compare the IP addresses. */ if (ai->ai_family == AF_INET6) { struct sockaddr_in6 *sin6p = (void *)ai->ai_addr; if (!memcmp(&sin6from->sin6_addr, &sin6p->sin6_addr, sizeof (struct in6_addr))) return (B_TRUE); } if (ai->ai_family == AF_INET) { struct in6_addr in6; struct sockaddr_in *sin4p = (void *)ai->ai_addr; IN6_INADDR_TO_V4MAPPED(&sin4p->sin_addr, &in6); if (!memcmp(&sin6from->sin6_addr, &in6, sizeof (struct in6_addr))) return (B_TRUE); } return (B_FALSE); } static void save_ai(ad_disc_cds_t *cds, struct addrinfo *ai) { ad_disc_ds_t *ds = &cds->cds_ds; struct sockaddr_in *sin; struct sockaddr_in6 *sin6; /* * If this DS already saw a response, keep the first * address from which we received a response. */ if (ds->addr.ss_family != 0) { if (DBG(DISC, 2)) logger(LOG_DEBUG, "already have an address"); return; } switch (ai->ai_family) { case AF_INET: sin = (void *)&ds->addr; (void) memcpy(sin, ai->ai_addr, sizeof (*sin)); sin->sin_port = htons(ds->port); break; case AF_INET6: sin6 = (void *)&ds->addr; (void) memcpy(sin6, ai->ai_addr, sizeof (*sin6)); sin6->sin6_port = htons(ds->port); break; default: logger(LOG_ERR, "bad AF %d", ai->ai_family); } }