xref: /illumos-gate/usr/src/lib/libadutils/common/ldap_ping.c (revision b327cd3f3b4dab4f29e7140159b1e01ed2ceef2a)
1 /*
2  * This file and its contents are supplied under the terms of the
3  * Common Development and Distribution License ("CDDL"), version 1.0.
4  * You may only use this file in accordance with the terms of version
5  * 1.0 of the CDDL.
6  *
7  * A full copy of the text of the CDDL should have accompanied this
8  * source.  A copy of the CDDL is also available via the Internet at
9  * http://www.illumos.org/license/CDDL.
10  */
11 
12 /*
13  * Copyright 2014 Nexenta Systems, Inc.  All rights reserved.
14  */
15 
16 #include <stdio.h>
17 #include <string.h>
18 #include <strings.h>
19 #include <unistd.h>
20 #include <assert.h>
21 #include <stdlib.h>
22 #include <net/if.h>
23 #include <net/if.h>
24 #include <sys/types.h>
25 #include <sys/socket.h>
26 #include <sys/sockio.h>
27 #include <netinet/in.h>
28 #include <arpa/inet.h>
29 #include <arpa/nameser.h>
30 #include <resolv.h>
31 #include <netdb.h>
32 #include <ctype.h>
33 #include <errno.h>
34 #include <ldap.h>
35 #include <lber.h>
36 #include <syslog.h>
37 #include "adutils_impl.h"
38 #include "addisc_impl.h"
39 
40 #define	LDAP_PORT	389
41 
42 #define	NETLOGON_ATTR_NAME			"NetLogon"
43 #define	NETLOGON_NT_VERSION_1			0x00000001
44 #define	NETLOGON_NT_VERSION_5			0x00000002
45 #define	NETLOGON_NT_VERSION_5EX			0x00000004
46 #define	NETLOGON_NT_VERSION_5EX_WITH_IP		0x00000008
47 #define	NETLOGON_NT_VERSION_WITH_CLOSEST_SITE	0x00000010
48 #define	NETLOGON_NT_VERSION_AVOID_NT4EMUL	0x01000000
49 
50 typedef enum {
51 	OPCODE = 0,
52 	SBZ,
53 	FLAGS,
54 	DOMAIN_GUID,
55 	FOREST_NAME,
56 	DNS_DOMAIN_NAME,
57 	DNS_HOST_NAME,
58 	NET_DOMAIN_NAME,
59 	NET_COMP_NAME,
60 	USER_NAME,
61 	DC_SITE_NAME,
62 	CLIENT_SITE_NAME,
63 	SOCKADDR_SIZE,
64 	SOCKADDR,
65 	NEXT_CLOSEST_SITE_NAME,
66 	NTVER,
67 	LM_NT_TOKEN,
68 	LM_20_TOKEN
69 } field_5ex_t;
70 
71 struct _berelement {
72 	char	*ber_buf;
73 	char	*ber_ptr;
74 	char	*ber_end;
75 };
76 
77 extern int ldap_put_filter(BerElement *ber, char *);
78 static void send_to_cds(ad_disc_cds_t *, char *, size_t, int);
79 static ad_disc_cds_t *find_cds_by_addr(ad_disc_cds_t *, struct sockaddr_in6 *);
80 static boolean_t addrmatch(struct addrinfo *, struct sockaddr_in6 *);
81 static void save_ai(ad_disc_cds_t *, struct addrinfo *);
82 
83 static void
84 cldap_escape_le64(char *buf, uint64_t val, int bytes)
85 {
86 	char *p = buf;
87 
88 	while (bytes != 0) {
89 		p += sprintf(p, "\\%.2x", (uint8_t)(val & 0xff));
90 		val >>= 8;
91 		bytes--;
92 	}
93 	*p = '\0';
94 }
95 
96 /*
97  * Construct CLDAPMessage PDU for NetLogon search request.
98  *
99  *  CLDAPMessage ::= SEQUENCE {
100  *      messageID       MessageID,
101  *      protocolOp      searchRequest   SearchRequest;
102  *  }
103  *
104  *  SearchRequest ::=
105  *      [APPLICATION 3] SEQUENCE {
106  *          baseObject    LDAPDN,
107  *          scope         ENUMERATED {
108  *                             baseObject            (0),
109  *                             singleLevel           (1),
110  *                             wholeSubtree          (2)
111  *                        },
112  *          derefAliases  ENUMERATED {
113  *                                     neverDerefAliases     (0),
114  *                                     derefInSearching      (1),
115  *                                     derefFindingBaseObj   (2),
116  *                                     derefAlways           (3)
117  *                                },
118  *          sizeLimit     INTEGER (0 .. MaxInt),
119  *          timeLimit     INTEGER (0 .. MaxInt),
120  *          attrsOnly     BOOLEAN,
121  *          filter        Filter,
122  *          attributes    SEQUENCE OF AttributeType
123  *  }
124  */
125 BerElement *
126 cldap_build_request(const char *dname,
127 	const char *host, uint32_t ntver, uint16_t msgid)
128 {
129 	BerElement 	*ber;
130 	int		len = 0;
131 	char		*basedn = "";
132 	int scope = LDAP_SCOPE_BASE, deref = LDAP_DEREF_NEVER,
133 	    sizelimit = 0, timelimit = 0, attrsonly = 0;
134 	char		filter[512];
135 	char		ntver_esc[13];
136 	char		*p, *pend;
137 
138 	/*
139 	 * Construct search filter in LDAP format.
140 	 */
141 	p = filter;
142 	pend = p + sizeof (filter);
143 
144 	len = snprintf(p, pend - p, "(&(DnsDomain=%s)", dname);
145 	if (len >= (pend - p))
146 		goto fail;
147 	p += len;
148 
149 	if (host != NULL) {
150 		len = snprintf(p, (pend - p), "(Host=%s)", host);
151 		if (len >= (pend - p))
152 			goto fail;
153 		p += len;
154 	}
155 
156 	if (ntver != 0) {
157 		/*
158 		 * Format NtVer as little-endian with LDAPv3 escapes.
159 		 */
160 		cldap_escape_le64(ntver_esc, ntver, sizeof (ntver));
161 		len = snprintf(p, (pend - p), "(NtVer=%s)", ntver_esc);
162 		if (len >= (pend - p))
163 			goto fail;
164 		p += len;
165 	}
166 
167 	len = snprintf(p, pend - p, ")");
168 	if (len >= (pend - p))
169 		goto fail;
170 	p += len;
171 
172 	/*
173 	 * Encode CLDAPMessage and beginning of SearchRequest sequence.
174 	 */
175 
176 	if ((ber = ber_alloc()) == NULL)
177 		goto fail;
178 
179 	if (ber_printf(ber, "{it{seeiib", msgid,
180 	    LDAP_REQ_SEARCH, basedn, scope, deref,
181 	    sizelimit, timelimit, attrsonly) < 0)
182 		goto fail;
183 
184 	/*
185 	 * Encode Filter sequence.
186 	 */
187 	if (ldap_put_filter(ber, filter) < 0)
188 		goto fail;
189 	/*
190 	 * Encode attribute and close Filter and SearchRequest sequences.
191 	 */
192 	if (ber_printf(ber, "{s}}}", NETLOGON_ATTR_NAME) < 0)
193 		goto fail;
194 
195 	/*
196 	 * Success
197 	 */
198 	return (ber);
199 
200 fail:
201 	if (ber != NULL)
202 		ber_free(ber, 1);
203 	return (NULL);
204 }
205 
206 /*
207  * Parse incoming search responses and attribute to correct hosts.
208  *
209  *  CLDAPMessage ::= SEQUENCE {
210  *     messageID       MessageID,
211  *                     searchResponse  SEQUENCE OF
212  *                                         SearchResponse;
213  *  }
214  *
215  *  SearchResponse ::=
216  *    CHOICE {
217  *         entry          [APPLICATION 4] SEQUENCE {
218  *                             objectName     LDAPDN,
219  *                             attributes     SEQUENCE OF SEQUENCE {
220  *                                              AttributeType,
221  *                                              SET OF
222  *                                                AttributeValue
223  *                                            }
224  *                        },
225  *         resultCode     [APPLICATION 5] LDAPResult
226  *    }
227  */
228 
229 static int
230 decode_name(uchar_t *base, uchar_t *cp, char *str)
231 {
232 	uchar_t *tmp = NULL, *st = cp;
233 	uint8_t len;
234 
235 	/*
236 	 * there should probably be some boundary checks on str && cp
237 	 * maybe pass in strlen && msglen ?
238 	 */
239 	while (*cp != 0) {
240 		if (*cp == 0xc0) {
241 			if (tmp == NULL)
242 				tmp = cp + 2;
243 			cp = base + *(cp + 1);
244 		}
245 		for (len = *cp++; len > 0; len--)
246 			*str++ = *cp++;
247 		*str++ = '.';
248 	}
249 	if (cp != st)
250 		*(str-1) = '\0';
251 	else
252 		*str = '\0';
253 
254 	return ((tmp == NULL ? cp + 1 : tmp) - st);
255 }
256 
257 static int
258 cldap_parse(ad_disc_t ctx, ad_disc_cds_t *cds, BerElement *ber)
259 {
260 	ad_disc_ds_t *dc = &cds->cds_ds;
261 	uchar_t *base = NULL, *cp = NULL;
262 	char val[512]; /* how big should val be? */
263 	int l, msgid, rc = 0;
264 	uint16_t opcode;
265 	field_5ex_t f = OPCODE;
266 
267 	/*
268 	 * Later, compare msgid's/some validation?
269 	 */
270 
271 	if (ber_scanf(ber, "{i{x{{x[la", &msgid, &l, &cp) == LBER_ERROR) {
272 		rc = 1;
273 		goto out;
274 	}
275 
276 	for (base = cp; ((cp - base) < l) && (f <= LM_20_TOKEN); f++) {
277 		val[0] = '\0';
278 		switch (f) {
279 		case OPCODE:
280 			/* opcode = *(uint16_t *)cp; */
281 			/* cp +=2; */
282 			opcode = *cp++;
283 			opcode |= (*cp++ << 8);
284 			break;
285 		case SBZ:
286 			cp += 2;
287 			break;
288 		case FLAGS:
289 			/* dci->Flags = *(uint32_t *)cp; */
290 			/* cp +=4; */
291 			dc->flags = *cp++;
292 			dc->flags |= (*cp++ << 8);
293 			dc->flags |= (*cp++ << 16);
294 			dc->flags |= (*cp++ << 26);
295 			break;
296 		case DOMAIN_GUID:
297 			if (ctx != NULL)
298 				auto_set_DomainGUID(ctx, cp);
299 			cp += 16;
300 			break;
301 		case FOREST_NAME:
302 			cp += decode_name(base, cp, val);
303 			if (ctx != NULL)
304 				auto_set_ForestName(ctx, val);
305 			break;
306 		case DNS_DOMAIN_NAME:
307 			/*
308 			 * We always have this already.
309 			 * (Could validate it here.)
310 			 */
311 			cp += decode_name(base, cp, val);
312 			break;
313 		case DNS_HOST_NAME:
314 			cp += decode_name(base, cp, val);
315 			if (0 != strcasecmp(val, dc->host)) {
316 				logger(LOG_ERR, "DC name %s != %s?",
317 				    val, dc->host);
318 			}
319 			break;
320 		case NET_DOMAIN_NAME:
321 			/*
322 			 * This is the "Flat" domain name.
323 			 * (i.e. the NetBIOS name)
324 			 * ignore for now.
325 			 */
326 			cp += decode_name(base, cp, val);
327 			break;
328 		case NET_COMP_NAME:
329 			/* not needed */
330 			cp += decode_name(base, cp, val);
331 			break;
332 		case USER_NAME:
333 			/* not needed */
334 			cp += decode_name(base, cp, val);
335 			break;
336 		case DC_SITE_NAME:
337 			cp += decode_name(base, cp, val);
338 			(void) strlcpy(dc->site, val, sizeof (dc->site));
339 			break;
340 		case CLIENT_SITE_NAME:
341 			cp += decode_name(base, cp, val);
342 			if (ctx != NULL)
343 				auto_set_SiteName(ctx, val);
344 			break;
345 		/*
346 		 * These are all possible, but we don't really care about them.
347 		 * Sockaddr_size && sockaddr might be useful at some point
348 		 */
349 		case SOCKADDR_SIZE:
350 		case SOCKADDR:
351 		case NEXT_CLOSEST_SITE_NAME:
352 		case NTVER:
353 		case LM_NT_TOKEN:
354 		case LM_20_TOKEN:
355 			break;
356 		default:
357 			rc = 3;
358 			goto out;
359 		}
360 	}
361 
362 out:
363 	if (base)
364 		free(base);
365 	else if (cp)
366 		free(cp);
367 	return (rc);
368 }
369 
370 
371 /*
372  * Filter out unresponsive servers, and save the domain info
373  * returned by the "LDAP ping" in the returned object.
374  * If ctx != NULL, this is a query for a DC, in which case we
375  * also save the Domain GUID, Site name, and Forest name as
376  * "auto" (discovered) values in the ctx.
377  *
378  * Only return the "winner".  (We only want one DC/GC)
379  */
380 ad_disc_ds_t *
381 ldap_ping(ad_disc_t ctx, ad_disc_cds_t *dclist, char *dname, int reqflags)
382 {
383 	struct sockaddr_in6 addr6;
384 	socklen_t addrlen;
385 	struct pollfd pingchk;
386 	ad_disc_cds_t *send_ds;
387 	ad_disc_cds_t *recv_ds = NULL;
388 	ad_disc_ds_t *ret_ds = NULL;
389 	BerElement *req = NULL;
390 	BerElement *res = NULL;
391 	struct _berelement *be, *rbe;
392 	size_t be_len, rbe_len;
393 	int fd = -1;
394 	int tries = 3;
395 	int waitsec;
396 	int r;
397 	uint16_t msgid;
398 
399 	/* One plus a null entry. */
400 	ret_ds = calloc(2, sizeof (ad_disc_ds_t));
401 	if (ret_ds == NULL)
402 		goto fail;
403 
404 	if ((fd = socket(PF_INET6, SOCK_DGRAM, 0)) < 0)
405 		goto fail;
406 
407 	(void) memset(&addr6, 0, sizeof (addr6));
408 	addr6.sin6_family = AF_INET6;
409 	addr6.sin6_addr = in6addr_any;
410 	if (bind(fd, (struct sockaddr *)&addr6, sizeof (addr6)) < 0)
411 		goto fail;
412 
413 	/*
414 	 * semi-unique msgid...
415 	 */
416 	msgid = gethrtime() & 0xffff;
417 
418 	/*
419 	 * Is ntver right? It certainly works on w2k8... If others are needed,
420 	 * that might require changes to cldap_parse
421 	 */
422 	req = cldap_build_request(dname, NULL,
423 	    NETLOGON_NT_VERSION_5EX, msgid);
424 	if (req == NULL)
425 		goto fail;
426 	be = (struct _berelement *)req;
427 	be_len = be->ber_end - be->ber_buf;
428 
429 	if ((res = ber_alloc()) == NULL)
430 		goto fail;
431 	rbe = (struct _berelement *)res;
432 	rbe_len = rbe->ber_end - rbe->ber_buf;
433 
434 	pingchk.fd = fd;
435 	pingchk.events = POLLIN;
436 	pingchk.revents = 0;
437 
438 try_again:
439 	send_ds = dclist;
440 	waitsec = 5;
441 	while (recv_ds == NULL && waitsec > 0) {
442 
443 		/*
444 		 * If there is another candidate, send to it.
445 		 */
446 		if (send_ds->cds_ds.host[0] != '\0') {
447 			send_to_cds(send_ds, be->ber_buf, be_len, fd);
448 			send_ds++;
449 
450 			/*
451 			 * Wait 1/10 sec. before the next send.
452 			 */
453 			r = poll(&pingchk, 1, 100);
454 #if 0 /* DEBUG */
455 			/* Drop all responses 1st pass. */
456 			if (waitsec == 5)
457 				r = 0;
458 #endif
459 		} else {
460 			/*
461 			 * No more candidates to "ping", so
462 			 * just wait a sec for responses.
463 			 */
464 			r = poll(&pingchk, 1, 1000);
465 			if (r == 0)
466 				--waitsec;
467 		}
468 
469 		if (r > 0) {
470 			/*
471 			 * Got a response.
472 			 */
473 			(void) memset(&addr6, 0, addrlen = sizeof (addr6));
474 			r = recvfrom(fd, rbe->ber_buf, rbe_len, 0,
475 			    (struct sockaddr *)&addr6, &addrlen);
476 
477 			recv_ds = find_cds_by_addr(dclist, &addr6);
478 			if (recv_ds == NULL)
479 				continue;
480 
481 			(void) cldap_parse(ctx, recv_ds, res);
482 			if ((recv_ds->cds_ds.flags & reqflags) != reqflags) {
483 				logger(LOG_ERR, "Skip %s"
484 				    "due to flags 0x%X",
485 				    recv_ds->cds_ds.host,
486 				    recv_ds->cds_ds.flags);
487 				recv_ds = NULL;
488 			}
489 		}
490 	}
491 
492 	if (recv_ds == NULL) {
493 		if (--tries <= 0)
494 			goto fail;
495 		goto try_again;
496 	}
497 
498 	(void) memcpy(ret_ds, recv_ds, sizeof (*ret_ds));
499 
500 	ber_free(res, 1);
501 	ber_free(req, 1);
502 	(void) close(fd);
503 	return (ret_ds);
504 
505 fail:
506 	ber_free(res, 1);
507 	ber_free(req, 1);
508 	(void) close(fd);
509 	free(ret_ds);
510 	return (NULL);
511 }
512 
513 /*
514  * Attempt a send of the LDAP request to all known addresses
515  * for this candidate server.
516  */
517 static void
518 send_to_cds(ad_disc_cds_t *send_cds, char *ber_buf, size_t be_len, int fd)
519 {
520 	struct sockaddr_in6 addr6;
521 	struct addrinfo *ai;
522 	int err;
523 
524 	if (DBG(DISC, 2)) {
525 		logger(LOG_DEBUG, "send to: %s", send_cds->cds_ds.host);
526 	}
527 
528 	for (ai = send_cds->cds_ai; ai != NULL; ai = ai->ai_next) {
529 
530 		/*
531 		 * Build the "to" address.
532 		 */
533 		(void) memset(&addr6, 0, sizeof (addr6));
534 		if (ai->ai_family == AF_INET6) {
535 			(void) memcpy(&addr6, ai->ai_addr, sizeof (addr6));
536 		} else if (ai->ai_family == AF_INET) {
537 			struct sockaddr_in *sin =
538 			    (void *)ai->ai_addr;
539 			addr6.sin6_family = AF_INET6;
540 			IN6_INADDR_TO_V4MAPPED(&sin->sin_addr,
541 			    &addr6.sin6_addr);
542 		} else {
543 			continue;
544 		}
545 		addr6.sin6_port = htons(LDAP_PORT);
546 
547 		/*
548 		 * Send the "ping" to this address.
549 		 */
550 		err = sendto(fd, ber_buf, be_len, 0,
551 		    (struct sockaddr *)&addr6, sizeof (addr6));
552 		err = (err < 0) ? errno : 0;
553 
554 		if (DBG(DISC, 2)) {
555 			char abuf[INET6_ADDRSTRLEN];
556 			const char *pa;
557 
558 			pa = inet_ntop(AF_INET6,
559 			    &addr6.sin6_addr,
560 			    abuf, sizeof (abuf));
561 			logger(LOG_ERR, "  > %s rc=%d",
562 			    pa ? pa : "?", err);
563 		}
564 	}
565 }
566 
567 /*
568  * We have a response from some address.  Find the candidate with
569  * this address.  In case a candidate had multiple addresses, we
570  * keep track of which the response came from.
571  */
572 static ad_disc_cds_t *
573 find_cds_by_addr(ad_disc_cds_t *dclist, struct sockaddr_in6 *sin6from)
574 {
575 	char abuf[INET6_ADDRSTRLEN];
576 	ad_disc_cds_t *ds;
577 	struct addrinfo *ai;
578 	int eai;
579 
580 	if (DBG(DISC, 1)) {
581 		eai = getnameinfo((void *)sin6from, sizeof (*sin6from),
582 		    abuf, sizeof (abuf), NULL, 0, NI_NUMERICHOST);
583 		if (eai != 0)
584 			(void) strlcpy(abuf, "?", sizeof (abuf));
585 		logger(LOG_DEBUG, "LDAP ping resp: addr=%s", abuf);
586 	}
587 
588 	/*
589 	 * Find the DS this response came from.
590 	 * (don't accept unexpected responses)
591 	 */
592 	for (ds = dclist; ds->cds_ds.host[0] != '\0'; ds++) {
593 		ai = ds->cds_ai;
594 		while (ai != NULL) {
595 			if (addrmatch(ai, sin6from))
596 				goto found;
597 			ai = ai->ai_next;
598 		}
599 	}
600 	if (DBG(DISC, 1)) {
601 		logger(LOG_DEBUG, "  (unexpected)");
602 	}
603 	return (NULL);
604 
605 found:
606 	if (DBG(DISC, 2)) {
607 		logger(LOG_DEBUG, "  from %s", ds->cds_ds.host);
608 	}
609 	save_ai(ds, ai);
610 	return (ds);
611 }
612 
613 static boolean_t
614 addrmatch(struct addrinfo *ai, struct sockaddr_in6 *sin6from)
615 {
616 
617 	/*
618 	 * Note: on a GC query, the ds->addr port numbers are
619 	 * the GC port, and our from addr has the LDAP port.
620 	 * Just compare the IP addresses.
621 	 */
622 
623 	if (ai->ai_family == AF_INET6) {
624 		struct sockaddr_in6 *sin6p = (void *)ai->ai_addr;
625 
626 		if (!memcmp(&sin6from->sin6_addr, &sin6p->sin6_addr,
627 		    sizeof (struct in6_addr)))
628 			return (B_TRUE);
629 	}
630 
631 	if (ai->ai_family == AF_INET) {
632 		struct in6_addr in6;
633 		struct sockaddr_in *sin4p = (void *)ai->ai_addr;
634 
635 		IN6_INADDR_TO_V4MAPPED(&sin4p->sin_addr, &in6);
636 		if (!memcmp(&sin6from->sin6_addr, &in6,
637 		    sizeof (struct in6_addr)))
638 			return (B_TRUE);
639 	}
640 
641 	return (B_FALSE);
642 }
643 
644 static void
645 save_ai(ad_disc_cds_t *cds, struct addrinfo *ai)
646 {
647 	ad_disc_ds_t *ds = &cds->cds_ds;
648 	struct sockaddr_in *sin;
649 	struct sockaddr_in6 *sin6;
650 
651 	/*
652 	 * If this DS already saw a response, keep the first
653 	 * address from which we received a response.
654 	 */
655 	if (ds->addr.ss_family != 0) {
656 		if (DBG(DISC, 2))
657 			logger(LOG_DEBUG, "already have an address");
658 		return;
659 	}
660 
661 	switch (ai->ai_family) {
662 	case AF_INET:
663 		sin = (void *)&ds->addr;
664 		(void) memcpy(sin, ai->ai_addr, sizeof (*sin));
665 		sin->sin_port = htons(ds->port);
666 		break;
667 
668 	case AF_INET6:
669 		sin6 = (void *)&ds->addr;
670 		(void) memcpy(sin6, ai->ai_addr, sizeof (*sin6));
671 		sin6->sin6_port = htons(ds->port);
672 		break;
673 
674 	default:
675 		logger(LOG_ERR, "bad AF %d", ai->ai_family);
676 	}
677 }
678