/* $OpenBSD: ldapclient.c,v 1.53 2024/11/21 13:17:02 claudio Exp $ */ /* * Copyright (c) 2008 Alexander Schrijver * Copyright (c) 2008 Pierre-Yves Ritschard * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "aldap.h" #include "log.h" #include "ypldap.h" void client_sig_handler(int, short, void *); void client_dispatch_dns(int, short, void *); void client_dispatch_parent(int, short, void *); void client_shutdown(void); void client_configure(struct env *); void client_periodic_update(int, short, void *); int client_build_req(struct idm *, struct idm_req *, struct aldap_message *, int, int); int client_search_idm(struct env *, struct idm *, struct aldap *, char **, char *, int, int, enum imsg_type); int client_try_idm(struct env *, struct idm *, struct ypldap_addr *); void client_addr_init(struct idm *); int client_addr_free(struct idm *); void client_addr_init(struct idm *idm) { struct sockaddr_in *sa_in; struct sockaddr_in6 *sa_in6; struct ypldap_addr *h; int defport; if (idm->idm_port != 0) defport = idm->idm_port; else if (idm->idm_flags & F_SSL) defport = LDAPS_PORT; else defport = LDAP_PORT; TAILQ_FOREACH(h, &idm->idm_addr, next) { switch (h->ss.ss_family) { case AF_INET: sa_in = (struct sockaddr_in *)&h->ss; if (ntohs(sa_in->sin_port) == 0) sa_in->sin_port = htons(defport); idm->idm_state = STATE_DNS_DONE; break; case AF_INET6: sa_in6 = (struct sockaddr_in6 *)&h->ss; if (ntohs(sa_in6->sin6_port) == 0) sa_in6->sin6_port = htons(defport); idm->idm_state = STATE_DNS_DONE; break; default: fatalx("king bula sez: wrong AF in client_addr_init"); /* not reached */ } } } int client_addr_free(struct idm *idm) { struct ypldap_addr *h; while (!TAILQ_EMPTY(&idm->idm_addr)) { h = TAILQ_FIRST(&idm->idm_addr); TAILQ_REMOVE(&idm->idm_addr, h, next); free(h); } return (0); } void client_sig_handler(int sig, short event, void *p) { switch (sig) { case SIGINT: case SIGTERM: client_shutdown(); break; case SIGHUP: /* ingore */ break; default: fatalx("unexpected signal"); } } void client_dispatch_dns(int fd, short events, void *p) { struct imsg imsg; u_int16_t dlen; u_char *data; struct ypldap_addr *h; int n, wait_cnt = 0; struct idm *idm; int shut = 0; struct env *env = p; struct imsgev *iev = env->sc_iev_dns; struct imsgbuf *ibuf = &iev->ibuf; if ((events & (EV_READ | EV_WRITE)) == 0) fatalx("unknown event"); if (events & EV_READ) { if ((n = imsgbuf_read(ibuf)) == -1 && errno != EAGAIN) fatal("imsgbuf_read error"); if (n == 0) shut = 1; } if (events & EV_WRITE) { if (imsgbuf_write(ibuf) == -1) { if (errno == EPIPE) /* connection closed */ shut = 1; else fatal("imsgbuf_write"); } } for (;;) { if ((n = imsg_get(ibuf, &imsg)) == -1) fatal("client_dispatch_dns: imsg_get error"); if (n == 0) break; switch (imsg.hdr.type) { case IMSG_HOST_DNS: TAILQ_FOREACH(idm, &env->sc_idms, idm_entry) if (idm->idm_id == imsg.hdr.peerid) break; if (idm == NULL) { log_warnx("IMSG_HOST_DNS with invalid peerID"); break; } if (!TAILQ_EMPTY(&idm->idm_addr)) { log_warnx("IMSG_HOST_DNS but addrs set!"); break; } dlen = imsg.hdr.len - IMSG_HEADER_SIZE; if (dlen == 0) { /* no data -> temp error */ idm->idm_state = STATE_DNS_TEMPFAIL; break; } data = (u_char *)imsg.data; while (dlen >= sizeof(struct sockaddr_storage)) { if ((h = calloc(1, sizeof(*h))) == NULL) fatal(NULL); memcpy(&h->ss, data, sizeof(h->ss)); TAILQ_INSERT_HEAD(&idm->idm_addr, h, next); data += sizeof(h->ss); dlen -= sizeof(h->ss); } if (dlen != 0) fatalx("IMSG_HOST_DNS: dlen != 0"); client_addr_init(idm); break; default: break; } imsg_free(&imsg); } TAILQ_FOREACH(idm, &env->sc_idms, idm_entry) { TAILQ_FOREACH(h, &idm->idm_addr, next) { if (client_try_idm(env, idm, h) == -1) idm->idm_state = STATE_LDAP_FAIL; else break; } if (idm->idm_state < STATE_LDAP_DONE) wait_cnt++; } if (wait_cnt == 0) imsg_compose_event(env->sc_iev, IMSG_END_UPDATE, 0, 0, -1, NULL, 0); if (!shut) imsg_event_add(iev); else { /* this pipe is dead, so remove the event handler */ event_del(&iev->ev); event_loopexit(NULL); } } void client_dispatch_parent(int fd, short events, void *p) { int n; int shut = 0; struct imsg imsg; struct env *env = p; struct imsgev *iev = env->sc_iev; struct imsgbuf *ibuf = &iev->ibuf; if ((events & (EV_READ | EV_WRITE)) == 0) fatalx("unknown event"); if (events & EV_READ) { if ((n = imsgbuf_read(ibuf)) == -1 && errno != EAGAIN) fatal("imsgbuf_read error"); if (n == 0) shut = 1; } if (events & EV_WRITE) { if (imsgbuf_write(ibuf) == -1) { if (errno == EPIPE) /* connection closed */ shut = 1; else fatal("imsgbuf_write"); } } for (;;) { if ((n = imsg_get(ibuf, &imsg)) == -1) fatal("client_dispatch_parent: imsg_get error"); if (n == 0) break; switch (imsg.hdr.type) { case IMSG_CONF_START: { struct env params; if (env->sc_flags & F_CONFIGURING) { log_warnx("configuration already in progress"); break; } memcpy(¶ms, imsg.data, sizeof(params)); log_debug("configuration starting"); env->sc_flags |= F_CONFIGURING; purge_config(env); memcpy(&env->sc_conf_tv, ¶ms.sc_conf_tv, sizeof(env->sc_conf_tv)); env->sc_flags |= params.sc_flags; break; } case IMSG_CONF_IDM: { struct idm *idm; if (!(env->sc_flags & F_CONFIGURING)) break; if ((idm = calloc(1, sizeof(*idm))) == NULL) fatal(NULL); memcpy(idm, imsg.data, sizeof(*idm)); idm->idm_env = env; TAILQ_INSERT_TAIL(&env->sc_idms, idm, idm_entry); break; } case IMSG_CONF_END: env->sc_flags &= ~F_CONFIGURING; log_debug("applying configuration"); client_configure(env); break; default: log_debug("client_dispatch_parent: unexpect imsg %d", imsg.hdr.type); break; } imsg_free(&imsg); } if (!shut) imsg_event_add(iev); else { /* this pipe is dead, so remove the event handler */ event_del(&iev->ev); event_loopexit(NULL); } } void client_shutdown(void) { log_info("ldap client exiting"); _exit(0); } pid_t ldapclient(int pipe_main2client[2]) { pid_t pid; int pipe_dns[2]; struct passwd *pw; struct event ev_sigint; struct event ev_sigterm; struct event ev_sighup; struct env env; switch (pid = fork()) { case -1: fatal("cannot fork"); break; case 0: break; default: return (pid); } memset(&env, 0, sizeof(env)); TAILQ_INIT(&env.sc_idms); if ((pw = getpwnam(YPLDAP_USER)) == NULL) fatal("getpwnam"); if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipe_dns) == -1) fatal("socketpair"); ypldap_dns(pipe_dns, pw); close(pipe_dns[1]); #ifndef DEBUG if (chroot(pw->pw_dir) == -1) fatal("chroot"); if (chdir("/") == -1) fatal("chdir"); #else #warning disabling chrooting in DEBUG mode #endif setproctitle("ldap client"); ypldap_process = PROC_CLIENT; log_procname = log_procnames[ypldap_process]; #ifndef DEBUG if (setgroups(1, &pw->pw_gid) || setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) fatal("cannot drop privileges"); #else #warning disabling privilege revocation in DEBUG mode #endif if (pledge("stdio inet", NULL) == -1) fatal("pledge"); event_init(); signal(SIGPIPE, SIG_IGN); signal_set(&ev_sigint, SIGINT, client_sig_handler, NULL); signal_set(&ev_sigterm, SIGTERM, client_sig_handler, NULL); signal_set(&ev_sighup, SIGHUP, client_sig_handler, NULL); signal_add(&ev_sigint, NULL); signal_add(&ev_sigterm, NULL); signal_add(&ev_sighup, NULL); close(pipe_main2client[0]); if ((env.sc_iev = calloc(1, sizeof(*env.sc_iev))) == NULL) fatal(NULL); if ((env.sc_iev_dns = calloc(1, sizeof(*env.sc_iev_dns))) == NULL) fatal(NULL); env.sc_iev->events = EV_READ; env.sc_iev->data = &env; imsgbuf_init(&env.sc_iev->ibuf, pipe_main2client[1]); env.sc_iev->handler = client_dispatch_parent; event_set(&env.sc_iev->ev, env.sc_iev->ibuf.fd, env.sc_iev->events, env.sc_iev->handler, &env); event_add(&env.sc_iev->ev, NULL); env.sc_iev_dns->events = EV_READ; env.sc_iev_dns->data = &env; imsgbuf_init(&env.sc_iev_dns->ibuf, pipe_dns[0]); env.sc_iev_dns->handler = client_dispatch_dns; event_set(&env.sc_iev_dns->ev, env.sc_iev_dns->ibuf.fd, env.sc_iev_dns->events, env.sc_iev_dns->handler, &env); event_add(&env.sc_iev_dns->ev, NULL); event_dispatch(); client_shutdown(); return (0); } int client_build_req(struct idm *idm, struct idm_req *ir, struct aldap_message *m, int min_attr, int max_attr) { struct aldap_stringset *ldap_attrs; int i; size_t k; memset(ir, 0, sizeof(*ir)); for (i = min_attr; i < max_attr; i++) { if (idm->idm_flags & F_FIXED_ATTR(i)) { if (strlcat(ir->ir_line, idm->idm_attrs[i], sizeof(ir->ir_line)) >= sizeof(ir->ir_line)) /* * entry yields a line > 1024, trash it. */ return (-1); if (i == ATTR_UID) { ir->ir_key.ik_uid = strtonum( idm->idm_attrs[i], 0, UID_MAX, NULL); } else if (i == ATTR_GR_GID) { ir->ir_key.ik_gid = strtonum( idm->idm_attrs[i], 0, GID_MAX, NULL); } } else if (idm->idm_list & F_LIST(i)) { aldap_match_attr(m, idm->idm_attrs[i], &ldap_attrs); for (k = 0; k >= 0 && ldap_attrs && k < ldap_attrs->len; k++) { /* XXX: Fail when attributes have illegal characters e.g. ',' */ if (strlcat(ir->ir_line, ldap_attrs->str[k].ostr_val, sizeof(ir->ir_line)) >= sizeof(ir->ir_line)) continue; if (k + 1 < ldap_attrs->len) if (strlcat(ir->ir_line, ",", sizeof(ir->ir_line)) >= sizeof(ir->ir_line)) { aldap_free_attr(ldap_attrs); return (-1); } } aldap_free_attr(ldap_attrs); } else { if (aldap_match_attr(m, idm->idm_attrs[i], &ldap_attrs) == -1) return (-1); if (strlcat(ir->ir_line, ldap_attrs->str[0].ostr_val, sizeof(ir->ir_line)) >= sizeof(ir->ir_line)) { aldap_free_attr(ldap_attrs); return (-1); } if (i == ATTR_UID) { ir->ir_key.ik_uid = strtonum( ldap_attrs->str[0].ostr_val, 0, UID_MAX, NULL); } else if (i == ATTR_GR_GID) { ir->ir_key.ik_uid = strtonum( ldap_attrs->str[0].ostr_val, 0, GID_MAX, NULL); } aldap_free_attr(ldap_attrs); } if (i + 1 != max_attr) if (strlcat(ir->ir_line, ":", sizeof(ir->ir_line)) >= sizeof(ir->ir_line)) return (-1); } return (0); } int client_search_idm(struct env *env, struct idm *idm, struct aldap *al, char **attrs, char *filter, int min_attr, int max_attr, enum imsg_type type) { struct idm_req ir; struct aldap_message *m; struct aldap_page_control *pg = NULL; const char *errstr; char *dn; dn = idm->idm_basedn; if (type == IMSG_GRP_ENTRY && idm->idm_groupdn[0] != '\0') dn = idm->idm_groupdn; do { if (aldap_search(al, dn, LDAP_SCOPE_SUBTREE, filter, attrs, 0, 0, 0, pg) == -1) { aldap_get_errno(al, &errstr); log_debug("%s", errstr); return (-1); } if (pg != NULL) { aldap_freepage(pg); pg = NULL; } while ((m = aldap_parse(al)) != NULL) { if (al->msgid != m->msgid) { goto fail; } if (m->message_type == LDAP_RES_SEARCH_RESULT) { if (m->page != NULL && m->page->cookie_len != 0) pg = m->page; else pg = NULL; aldap_freemsg(m); break; } if (m->message_type != LDAP_RES_SEARCH_ENTRY) { goto fail; } if (client_build_req(idm, &ir, m, min_attr, max_attr) == 0) imsg_compose_event(env->sc_iev, type, 0, 0, -1, &ir, sizeof(ir.ir_key) + strlen(ir.ir_line) + 1); aldap_freemsg(m); } } while (pg != NULL); return (0); fail: aldap_freemsg(m); if (pg != NULL) { aldap_freepage(pg); } return (-1); } int client_try_idm(struct env *env, struct idm *idm, struct ypldap_addr *addr) { const char *where; char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV]; char *attrs[ATTR_MAX+1]; int fd = -1; int i, j; struct sockaddr *sa = (struct sockaddr *)&addr->ss; struct aldap_message *m; struct aldap *al; if (getnameinfo(sa, SA_LEN(sa), hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV)) errx(1, "could not get numeric hostname"); where = "connect"; if ((fd = socket(sa->sa_family, SOCK_STREAM, 0)) == -1) return (-1); if (connect(fd, sa, SA_LEN(sa)) != 0) { log_warn("connect to %s port %s failed", hbuf, sbuf); close(fd); return (-1); } al = aldap_init(fd); if (al == NULL) { close(fd); return (-1); } if (idm->idm_flags & F_STARTTLS) { log_debug("requesting starttls"); where = "starttls"; if (aldap_req_starttls(al) == -1) goto bad; where = "parsing"; if ((m = aldap_parse(al)) == NULL) goto bad; where = "verifying msgid"; if (al->msgid != m->msgid) { aldap_freemsg(m); goto bad; } where = "starttls result"; if (aldap_get_resultcode(m) != LDAP_SUCCESS) { aldap_freemsg(m); goto bad; } aldap_freemsg(m); } if (idm->idm_flags & (F_STARTTLS | F_SSL)) { log_debug("starting tls"); where = "enabling tls"; if (aldap_tls(al, idm->idm_tls_config, idm->idm_name) < 0) { const char *err; aldap_get_errno(al, &err); log_warnx("TLS handshake with %s(%s) failed: %s", idm->idm_name, hbuf, err); goto bad; } } if (idm->idm_flags & F_NEEDAUTH) { int rc; where = "binding"; if (idm->idm_bindext != 0) rc = aldap_bind_sasl_external(al, idm->idm_bindextid); else rc = aldap_bind(al, idm->idm_binddn, idm->idm_bindcred); if (rc == -1) goto bad; where = "parsing"; if ((m = aldap_parse(al)) == NULL) goto bad; where = "verifying msgid"; if (al->msgid != m->msgid) { aldap_freemsg(m); goto bad; } where = "bind response"; rc = aldap_get_resultcode(m); if (rc != LDAP_SUCCESS) { log_warnx("LDAP bind with %s(%s) failed: result code" " %d", idm->idm_name, hbuf, rc); aldap_freemsg(m); goto bad; } aldap_freemsg(m); } memset(attrs, 0, sizeof(attrs)); for (i = 0, j = 0; i < ATTR_MAX; i++) { if (idm->idm_flags & F_FIXED_ATTR(i)) continue; attrs[j++] = idm->idm_attrs[i]; } attrs[j] = NULL; /* * build password line. */ where = "search"; log_debug("searching password entries"); if (client_search_idm(env, idm, al, attrs, idm->idm_filters[FILTER_USER], 0, ATTR_MAX, IMSG_PW_ENTRY) == -1) goto bad; memset(attrs, 0, sizeof(attrs)); for (i = ATTR_GR_MIN, j = 0; i < ATTR_GR_MAX; i++) { if (idm->idm_flags & F_FIXED_ATTR(i)) continue; attrs[j++] = idm->idm_attrs[i]; } attrs[j] = NULL; /* * build group line. */ where = "search"; log_debug("searching group entries"); if (client_search_idm(env, idm, al, attrs, idm->idm_filters[FILTER_GROUP], ATTR_GR_MIN, ATTR_GR_MAX, IMSG_GRP_ENTRY) == -1) goto bad; aldap_close(al); idm->idm_state = STATE_LDAP_DONE; return (0); bad: aldap_close(al); log_debug("directory %s(%s) errored out in %s", idm->idm_name, hbuf, where); return (-1); } void client_periodic_update(int fd, short event, void *p) { struct env *env = p; struct idm *idm; int fail_cnt = 0; /* If LDAP isn't finished, notify the master process to trash the * update. */ TAILQ_FOREACH(idm, &env->sc_idms, idm_entry) { if (idm->idm_state < STATE_LDAP_DONE) fail_cnt++; idm->idm_state = STATE_NONE; client_addr_free(idm); } if (fail_cnt > 0) { log_debug("trash the update"); imsg_compose_event(env->sc_iev, IMSG_TRASH_UPDATE, 0, 0, -1, NULL, 0); } client_configure(env); } void client_configure(struct env *env) { struct timeval tv; struct idm *idm; u_int16_t dlen; log_debug("connecting to directories"); imsg_compose_event(env->sc_iev, IMSG_START_UPDATE, 0, 0, -1, NULL, 0); /* Start the DNS lookups */ TAILQ_FOREACH(idm, &env->sc_idms, idm_entry) { dlen = strlen(idm->idm_name) + 1; imsg_compose_event(env->sc_iev_dns, IMSG_HOST_DNS, idm->idm_id, 0, -1, idm->idm_name, dlen); } tv.tv_sec = env->sc_conf_tv.tv_sec; tv.tv_usec = env->sc_conf_tv.tv_usec; evtimer_set(&env->sc_conf_ev, client_periodic_update, env); evtimer_add(&env->sc_conf_ev, &tv); }