summaryrefslogtreecommitdiffstats
path: root/src/modules/rlm_ldap/ldap.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/rlm_ldap/ldap.c')
-rw-r--r--src/modules/rlm_ldap/ldap.c1661
1 files changed, 1661 insertions, 0 deletions
diff --git a/src/modules/rlm_ldap/ldap.c b/src/modules/rlm_ldap/ldap.c
new file mode 100644
index 0000000..c356921
--- /dev/null
+++ b/src/modules/rlm_ldap/ldap.c
@@ -0,0 +1,1661 @@
+/*
+ * This program is is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file ldap.c
+ * @brief LDAP module library functions.
+ *
+ * @author Arran Cudbard-Bell <a.cudbardb@freeradius.org>
+ * @copyright 2015 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
+ * @copyright 2013-2015 Network RADIUS SARL <info@networkradius.com>
+ * @copyright 2013-2015 The FreeRADIUS Server Project.
+ */
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/modules.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <stdarg.h>
+#include <ctype.h>
+
+#ifdef HAVE_OPENSSL_SSL_H
+# include <openssl/ssl.h>
+#endif
+
+#include "ldap.h"
+
+/** Converts "bad" strings into ones which are safe for LDAP
+ *
+ * @note RFC 4515 says filter strings can only use the @verbatim \<hex><hex> @endverbatim
+ * format, whereas RFC 4514 indicates that some chars in DNs, may be escaped simply
+ * with a backslash. For simplicity, we always use the hex escape sequences.
+ * In other areas where we're doing DN comparison, the DNs need to be normalised first
+ * so that they both use only hex escape sequences.
+ *
+ * @note This is a callback for xlat operations.
+ *
+ * Will escape any characters in input strings that would cause the string to be interpreted
+ * as part of a DN and or filter. Escape sequence is @verbatim \<hex><hex> @endverbatim.
+ *
+ * @param request The current request.
+ * @param out Pointer to output buffer.
+ * @param outlen Size of the output buffer.
+ * @param in Raw unescaped string.
+ * @param arg Any additional arguments (unused).
+ */
+size_t rlm_ldap_escape_func(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, UNUSED void *arg)
+{
+ static char const encode[] = ",+\"\\<>;*=()";
+ static char const hextab[] = "0123456789abcdef";
+ size_t left = outlen;
+
+ if (*in && ((*in == ' ') || (*in == '#'))) goto encode;
+
+ while (*in) {
+ /*
+ * Encode unsafe characters.
+ */
+ if (memchr(encode, *in, sizeof(encode) - 1)) {
+ encode:
+ /*
+ * Only 3 or less bytes available.
+ */
+ if (left <= 3) break;
+
+ *out++ = '\\';
+ *out++ = hextab[(*in >> 4) & 0x0f];
+ *out++ = hextab[*in & 0x0f];
+ in++;
+ left -= 3;
+
+ continue;
+ }
+
+ if (left <= 1) break;
+
+ /*
+ * Doesn't need encoding
+ */
+ *out++ = *in++;
+ left--;
+ }
+
+ *out = '\0';
+
+ return outlen - left;
+}
+
+/** Check whether a string looks like a DN
+ *
+ * @param[in] in Str to check.
+ * @param[in] inlen Length of string to check.
+ * @return
+ * - true if string looks like a DN.
+ * - false if string does not look like DN.
+ */
+bool rlm_ldap_is_dn(char const *in, size_t inlen)
+{
+ char const *p;
+
+ char want = '=';
+ bool too_soon = true;
+ int comp = 1;
+
+ for (p = in; inlen > 0; p++, inlen--) {
+ if (p[0] == '\\') {
+ char c;
+
+ too_soon = false;
+
+ /*
+ * Invalid escape sequence, not a DN
+ */
+ if (inlen < 2) return false;
+
+ /*
+ * Double backslash, consume two chars
+ */
+ if (p[1] == '\\') {
+ inlen--;
+ p++;
+ continue;
+ }
+
+ /*
+ * Special, consume two chars
+ */
+ switch (p[1]) {
+ case ' ':
+ case '#':
+ case '=':
+ case '"':
+ case '+':
+ case ',':
+ case ';':
+ case '<':
+ case '>':
+ case '\'':
+ inlen -= 1;
+ p += 1;
+ continue;
+
+ default:
+ break;
+ }
+
+ /*
+ * Invalid escape sequence, not a DN
+ */
+ if (inlen < 3) return false;
+
+ /*
+ * Hex encoding, consume three chars
+ */
+ if (fr_hex2bin((uint8_t *) &c, 1, p + 1, 2) == 1) {
+ inlen -= 2;
+ p += 2;
+ continue;
+ }
+
+ /*
+ * Invalid escape sequence, not a DN
+ */
+ return false;
+ }
+
+ switch (*p) {
+ case '=':
+ if (too_soon || (*p != want)) return false; /* Too soon after last , or = */
+ want = ',';
+ too_soon = true;
+ break;
+
+ case ',':
+ if (too_soon || (*p != want)) return false; /* Too soon after last , or = */
+ want = '=';
+ too_soon = true;
+ comp++;
+ break;
+
+ default:
+ too_soon = false;
+ break;
+ }
+ }
+
+ /*
+ * If the string ended with , or =, or the number
+ * of components was less than 2
+ *
+ * i.e. we don't have <attr>=<val>,<attr>=<val>
+ */
+ if (too_soon || (comp < 2)) return false;
+
+ return true;
+}
+
+/** Convert a berval to a talloced string
+ *
+ * The ldap_get_values function is deprecated, and ldap_get_values_len
+ * does not guarantee the berval buffers it returns are \0 terminated.
+ *
+ * For some cases this is fine, for others we require a \0 terminated
+ * buffer (feeding DNs back into libldap for example).
+ *
+ * @param ctx to allocate in.
+ * @param in Berval to copy.
+ * @return \0 terminated buffer containing in->bv_val.
+ */
+char *rlm_ldap_berval_to_string(TALLOC_CTX *ctx, struct berval const *in)
+{
+ char *out;
+
+ out = talloc_array(ctx, char, in->bv_len + 1);
+ if (!out) return NULL;
+
+ memcpy(out, in->bv_val, in->bv_len);
+ out[in->bv_len] = '\0';
+
+ return out;
+}
+
+/** Normalise escape sequences in a DN
+ *
+ * Characters in a DN can either be escaped as
+ * @verbatim \<hex><hex> @endverbatim or @verbatim \<special> @endverbatim
+ *
+ * The LDAP directory chooses how characters are escaped, which can make
+ * local comparisons of DNs difficult.
+ *
+ * Here we search for hex sequences that match special chars, and convert
+ * them to the @verbatim \<special> @endverbatim form.
+ *
+ * @note the resulting output string will only ever be shorter than the
+ * input, so it's fine to use the same buffer for both out and in.
+ *
+ * @param out Where to write the normalised DN.
+ * @param in The input DN.
+ * @return The number of bytes written to out.
+ */
+size_t rlm_ldap_normalise_dn(char *out, char const *in)
+{
+ char const *p;
+ char *o = out;
+
+ for (p = in; *p != '\0'; p++) {
+ if (p[0] == '\\') {
+ char c;
+
+ /*
+ * Double backslashes get processed specially
+ */
+ if (p[1] == '\\') {
+ p += 1;
+ *o++ = p[0];
+ *o++ = p[1];
+ continue;
+ }
+
+ /*
+ * Hex encodings that have an alternative
+ * special encoding, get rewritten to the
+ * special encoding.
+ */
+ if (fr_hex2bin((uint8_t *) &c, 1, p + 1, 2) == 1) {
+ switch (c) {
+ case ' ':
+ case '#':
+ case '=':
+ case '"':
+ case '+':
+ case ',':
+ case ';':
+ case '<':
+ case '>':
+ case '\'':
+ *o++ = '\\';
+ *o++ = c;
+ p += 2;
+ continue;
+
+ default:
+ break;
+ }
+ }
+ }
+ *o++ = *p;
+ }
+ *o = '\0';
+
+ return o - out;
+}
+
+/** Find the place at which the two DN strings diverge
+ *
+ * Returns the length of the non matching string in full.
+ *
+ * @param full DN.
+ * @param part Partial DN as returned by ldap_parse_result.
+ * @return
+ * - Length of the portion of full which wasn't matched
+ * - -1 on failure.
+ */
+static size_t rlm_ldap_common_dn(char const *full, char const *part)
+{
+ size_t f_len, p_len, i;
+
+ if (!full) {
+ return -1;
+ }
+
+ f_len = strlen(full);
+
+ if (!part) {
+ return -1;
+ }
+
+ p_len = strlen(part);
+ if (!p_len) {
+ return f_len;
+ }
+
+ if ((f_len < p_len) || !f_len) {
+ return -1;
+ }
+
+ for (i = 0; i < p_len; i++) {
+ if (part[p_len - i] != full[f_len - i]) {
+ return -1;
+ }
+ }
+
+ return f_len - p_len;
+}
+
+/** Combine and expand filters
+ *
+ * @param request Current request.
+ * @param out Where to write the expanded string.
+ * @param outlen Length of output buffer.
+ * @param sub Array of subfilters (may contain NULLs).
+ * @param sublen Number of potential subfilters in array.
+ * @return length of expanded data.
+ */
+ssize_t rlm_ldap_xlat_filter(REQUEST *request, char const **sub, size_t sublen, char *out, size_t outlen)
+{
+ char buffer[LDAP_MAX_FILTER_STR_LEN + 1];
+ char const *in = NULL;
+ char *p = buffer;
+
+ ssize_t len = 0;
+
+ unsigned int i;
+ int cnt = 0;
+
+ /*
+ * Figure out how many filter elements we need to integrate
+ */
+ for (i = 0; i < sublen; i++) {
+ if (sub[i] && *sub[i]) {
+ in = sub[i];
+ cnt++;
+ }
+ }
+
+ if (!cnt) {
+ out[0] = '\0';
+ return 0;
+ }
+
+ if (cnt > 1) {
+ if (outlen < 3) {
+ goto oob;
+ }
+
+ p[len++] = '(';
+ p[len++] = '&';
+
+ for (i = 0; i < sublen; i++) {
+ if (sub[i] && (*sub[i] != '\0')) {
+ len += strlcpy(p + len, sub[i], outlen - len);
+
+ if ((size_t) len >= outlen) {
+ oob:
+ REDEBUG("Out of buffer space creating filter");
+
+ return -1;
+ }
+ }
+ }
+
+ if ((outlen - len) < 2) {
+ goto oob;
+ }
+
+ p[len++] = ')';
+ p[len] = '\0';
+
+ in = buffer;
+ }
+
+ len = radius_xlat(out, outlen, request, in, rlm_ldap_escape_func, NULL);
+ if (len < 0) {
+ REDEBUG("Failed creating filter");
+
+ return -1;
+ }
+
+ return len;
+}
+
+/** Return the error string associated with a handle
+ *
+ * @param conn to retrieve error from.
+ * @return error string.
+ */
+char const *rlm_ldap_error_str(ldap_handle_t const *conn)
+{
+ int lib_errno;
+ ldap_get_option(conn->handle, LDAP_OPT_ERROR_NUMBER, &lib_errno);
+ if (lib_errno == LDAP_SUCCESS) {
+ return "unknown";
+ }
+
+ return ldap_err2string(lib_errno);
+}
+
+/** Parse response from LDAP server dealing with any errors
+ *
+ * Should be called after an LDAP operation. Will check result of operation and if it was successful, then attempt
+ * to retrieve and parse the result.
+ *
+ * Will also produce extended error output including any messages the server sent, and information about partial
+ * DN matches.
+ *
+ * @param[in] inst of LDAP module.
+ * @param[in] conn Current connection.
+ * @param[in] msgid returned from last operation. May be -1 if no result processing is required.
+ * @param[in] dn Last search or bind DN.
+ * @param[out] result Where to write result, if NULL result will be freed.
+ * @param[out] error Where to write the error string, may be NULL, must not be freed.
+ * @param[out] extra Where to write additional error string to, may be NULL (faster) or must be freed
+ * (with talloc_free).
+ * @return One of the LDAP_PROC_* (#ldap_rcode_t) values.
+ */
+ldap_rcode_t rlm_ldap_result(rlm_ldap_t const *inst, ldap_handle_t const *conn, int msgid, char const *dn,
+ LDAPMessage **result, char const **error, char **extra)
+{
+ ldap_rcode_t status = LDAP_PROC_SUCCESS;
+
+ int lib_errno = LDAP_SUCCESS; // errno returned by the library.
+ int srv_errno = LDAP_SUCCESS; // errno in the result message.
+
+ char *part_dn = NULL; // Partial DN match.
+ char *our_err = NULL; // Our extended error message.
+ char *srv_err = NULL; // Server's extended error message.
+ char *p, *a;
+
+ bool freeit = false; // Whether the message should be freed after being processed.
+ int len;
+
+ struct timeval tv; // Holds timeout values.
+
+ LDAPMessage *tmp_msg = NULL; // Temporary message pointer storage if we weren't provided with one.
+
+ char const *tmp_err; // Temporary error pointer storage if we weren't provided with one.
+
+ if (!error) error = &tmp_err;
+ *error = NULL;
+
+ if (extra) *extra = NULL;
+ if (result) *result = NULL;
+
+ /*
+ * We always need the result, but our caller may not
+ */
+ if (!result) {
+ result = &tmp_msg;
+ freeit = true;
+ }
+
+ /*
+ * Check if there was an error sending the request
+ */
+ ldap_get_option(conn->handle, LDAP_OPT_ERROR_NUMBER, &lib_errno);
+ if (lib_errno != LDAP_SUCCESS) goto process_error;
+ if (msgid < 0) return LDAP_SUCCESS; /* No msgid and no error, return now */
+
+ memset(&tv, 0, sizeof(tv));
+ tv.tv_sec = inst->res_timeout;
+
+ /*
+ * Now retrieve the result and check for errors
+ * ldap_result returns -1 on failure, and 0 on timeout
+ */
+ lib_errno = ldap_result(conn->handle, msgid, 1, &tv, result);
+ if (lib_errno == 0) {
+ lib_errno = LDAP_TIMEOUT;
+
+ goto process_error;
+ }
+
+ if (lib_errno == -1) {
+ ldap_get_option(conn->handle, LDAP_OPT_ERROR_NUMBER, &lib_errno);
+
+ goto process_error;
+ }
+
+ /*
+ * Parse the result and check for errors sent by the server
+ */
+ lib_errno = ldap_parse_result(conn->handle, *result,
+ &srv_errno,
+ extra ? &part_dn : NULL,
+ extra ? &srv_err : NULL,
+ NULL, NULL, freeit);
+ if (freeit) *result = NULL;
+
+ if (lib_errno != LDAP_SUCCESS) {
+ ldap_get_option(conn->handle, LDAP_OPT_ERROR_NUMBER, &lib_errno);
+ goto process_error;
+ }
+
+process_error:
+ if ((lib_errno == LDAP_SUCCESS) && (srv_errno != LDAP_SUCCESS)) {
+ lib_errno = srv_errno;
+ } else if ((lib_errno != LDAP_SUCCESS) && (srv_errno == LDAP_SUCCESS)) {
+ srv_errno = lib_errno;
+ }
+
+ switch (lib_errno) {
+ case LDAP_SUCCESS:
+ *error = "Success";
+ break;
+
+ case LDAP_SASL_BIND_IN_PROGRESS:
+ *error = "Continuing";
+ status = LDAP_PROC_CONTINUE;
+ break;
+
+ case LDAP_NO_SUCH_OBJECT:
+ *error = "The specified DN wasn't found";
+ status = LDAP_PROC_BAD_DN;
+
+ if (!extra) break;
+
+ /*
+ * Build our own internal diagnostic string
+ */
+ len = rlm_ldap_common_dn(dn, part_dn);
+ if (len < 0) break;
+
+ our_err = talloc_typed_asprintf(conn, "Match stopped here: [%.*s]%s", len, dn, part_dn ? part_dn : "");
+ goto error_string;
+
+ case LDAP_INSUFFICIENT_ACCESS:
+ *error = "Insufficient access. Check the identity and password configuration directives";
+ status = LDAP_PROC_NOT_PERMITTED;
+ break;
+
+ case LDAP_UNWILLING_TO_PERFORM:
+ *error = "Server was unwilling to perform";
+ status = LDAP_PROC_NOT_PERMITTED;
+ break;
+
+ case LDAP_FILTER_ERROR:
+ *error = "Bad search filter";
+ status = LDAP_PROC_ERROR;
+ break;
+
+ case LDAP_TIMEOUT:
+ *error = "Timed out while waiting for server to respond";
+ goto timeout;
+
+ case LDAP_TIMELIMIT_EXCEEDED:
+ *error = "Time limit exceeded";
+ timeout:
+ exec_trigger(NULL, inst->cs, "modules.ldap.timeout", true);
+ /* FALL-THROUGH */
+
+ case LDAP_BUSY:
+ case LDAP_UNAVAILABLE:
+ case LDAP_SERVER_DOWN:
+ status = LDAP_PROC_RETRY;
+ goto error_string;
+
+ case LDAP_INVALID_CREDENTIALS:
+ case LDAP_CONSTRAINT_VIOLATION:
+ status = LDAP_PROC_REJECT;
+ goto error_string;
+
+ case LDAP_OPERATIONS_ERROR:
+ if (inst->chase_referrals) {
+ *error = "Operations error with LDAP database. Please see the LDAP server configuration / documentation for more information.";
+ } else {
+ *error = "Please set 'chase_referrals=yes' and 'rebind=yes'. See the ldap module configuration "
+ "for details.";
+ }
+
+ /* FALL-THROUGH */
+ default:
+ status = LDAP_PROC_ERROR;
+
+ error_string:
+ if (!*error) *error = ldap_err2string(lib_errno);
+
+ if (!extra || ((lib_errno == srv_errno) && !our_err && !srv_err)) break;
+
+ /*
+ * Output the error codes from the library and server
+ */
+ p = talloc_zero_array(conn, char, 1);
+ if (!p) break;
+
+ if (lib_errno != srv_errno) {
+ a = talloc_asprintf_append(p, "LDAP lib error: %s (%u), srv error: %s (%u). ",
+ ldap_err2string(lib_errno), lib_errno,
+ ldap_err2string(srv_errno), srv_errno);
+ if (!a) {
+ talloc_free(p);
+ break;
+ }
+
+ p = a;
+ }
+
+ if (our_err) {
+ a = talloc_asprintf_append_buffer(p, "%s. ", our_err);
+ if (!a) {
+ talloc_free(p);
+ break;
+ }
+
+ p = a;
+ }
+
+ if (srv_err) {
+ a = talloc_asprintf_append_buffer(p, "Server said: %s. ", srv_err);
+ if (!a) {
+ talloc_free(p);
+ break;
+ }
+
+ p = a;
+ }
+
+ *extra = p;
+
+ break;
+ }
+
+ /*
+ * Cleanup memory
+ */
+ if (srv_err) ldap_memfree(srv_err);
+ if (part_dn) ldap_memfree(part_dn);
+
+ talloc_free(our_err);
+
+ if ((status < 0) && *result) {
+ ldap_msgfree(*result);
+ *result = NULL;
+ }
+
+ return status;
+}
+
+/** Bind to the LDAP directory as a user
+ *
+ * Performs a simple bind to the LDAP directory, and handles any errors that occur.
+ *
+ * @param[in] inst rlm_ldap configuration.
+ * @param[in] request Current request, this may be NULL, in which case all debug logging is done with radlog.
+ * @param[in,out] pconn to use. May change as this function calls functions which auto re-connect.
+ * @param[in] dn of the user, may be NULL to bind anonymously.
+ * @param[in] password of the user, may be NULL if no password is specified.
+ * @param[in] sasl mechanism to use for bind, and additional parameters.
+ * @param[in] retry if the server is down.
+ * @return One of the LDAP_PROC_* (#ldap_rcode_t) values.
+ */
+ldap_rcode_t rlm_ldap_bind(rlm_ldap_t const *inst, REQUEST *request, ldap_handle_t **pconn, char const *dn,
+ char const *password, ldap_sasl *sasl, bool retry)
+{
+ ldap_rcode_t status = LDAP_PROC_ERROR;
+
+ int msgid = -1;
+
+ char const *error = NULL;
+ char *extra = NULL;
+
+ int i, num;
+
+ rad_assert(*pconn && (*pconn)->handle);
+ rad_assert(!retry || inst->pool);
+
+#ifndef WITH_SASL
+ if (sasl && sasl->mech) {
+ REDEBUG("Server is built without SASL, but is being asked to do SASL.");
+ return status;
+ }
+#endif
+
+ /*
+ * Bind as anonymous user
+ */
+ if (!dn) dn = "";
+
+ /*
+ * For sanity, for when no connections are viable,
+ * and we can't make a new one.
+ */
+ num = retry ? fr_connection_pool_get_retries(inst->pool) : 0;
+ for (i = num; i >= 0; i--) {
+#ifdef WITH_SASL
+ if (sasl && sasl->mech) {
+ status = rlm_ldap_sasl_interactive(inst, request, *pconn, dn, password, sasl,
+ &error, &extra);
+ } else
+#endif
+ {
+ msgid = ldap_bind((*pconn)->handle, dn, password, LDAP_AUTH_SIMPLE);
+
+ /* We got a valid message ID */
+ if (msgid >= 0) {
+ if (request) {
+ RDEBUG2("Waiting for bind result...");
+ } else {
+ DEBUG2("rlm_ldap (%s): Waiting for bind result...", inst->name);
+ }
+ }
+
+ status = rlm_ldap_result(inst, *pconn, msgid, dn, NULL, &error, &extra);
+ }
+
+ switch (status) {
+ case LDAP_PROC_SUCCESS:
+ LDAP_DBG_REQ("Bind successful");
+ break;
+
+ case LDAP_PROC_NOT_PERMITTED:
+ LDAP_ERR_REQ("Bind was not permitted: %s", error);
+ LDAP_EXT_REQ();
+
+ break;
+
+ case LDAP_PROC_REJECT:
+ LDAP_ERR_REQ("Bind credentials incorrect: %s", error);
+ LDAP_EXT_REQ();
+
+ break;
+
+ case LDAP_PROC_RETRY:
+ if (retry) {
+ *pconn = fr_connection_reconnect(inst->pool, *pconn);
+ if (*pconn) {
+ LDAP_DBGW_REQ("Bind with %s to %s failed: %s. Got new socket, retrying...",
+ *dn ? dn : "(anonymous)", inst->server, error);
+
+ talloc_free(extra); /* don't leak debug info */
+
+ continue;
+ }
+ };
+ status = LDAP_PROC_ERROR;
+
+ /*
+ * Were not allowed to retry, or there are no more
+ * sockets, treat this as a hard failure.
+ */
+ /* FALL-THROUGH */
+ default:
+ LDAP_ERR_REQ("Bind with %s to %s failed: %s", *dn ? dn : "(anonymous)",
+ inst->server, error);
+ LDAP_EXT_REQ();
+
+ break;
+ }
+
+ break;
+ }
+
+ if (retry && (i < 0)) {
+ LDAP_ERR_REQ("Hit reconnection limit");
+ status = LDAP_PROC_ERROR;
+ }
+
+ talloc_free(extra);
+
+ return status; /* caller closes the connection */
+}
+
+/** Search for something in the LDAP directory
+ *
+ * Binds as the administrative user and performs a search, dealing with any errors.
+ *
+ * @param[out] result Where to store the result. Must be freed with ldap_msgfree if LDAP_PROC_SUCCESS is returned.
+ * May be NULL in which case result will be automatically freed after use.
+ * @param[in] inst rlm_ldap configuration.
+ * @param[in] request Current request.
+ * @param[in,out] pconn to use. May change as this function calls functions which auto re-connect.
+ * @param[in] dn to use as base for the search.
+ * @param[in] scope to use (LDAP_SCOPE_BASE, LDAP_SCOPE_ONE, LDAP_SCOPE_SUB).
+ * @param[in] filter to use, should be pre-escaped.
+ * @param[in] attrs to retrieve.
+ * @param[in] serverctrls Search controls to pass to the server. May be NULL.
+ * @param[in] clientctrls Search controls for ldap_search. May be NULL.
+ * @return One of the LDAP_PROC_* (#ldap_rcode_t) values.
+ */
+ldap_rcode_t rlm_ldap_search(LDAPMessage **result, rlm_ldap_t const *inst, REQUEST *request,
+ ldap_handle_t **pconn,
+ char const *dn, int scope, char const *filter, char const * const *attrs,
+ LDAPControl **serverctrls, LDAPControl **clientctrls)
+{
+ ldap_rcode_t status = LDAP_PROC_ERROR;
+ LDAPMessage *our_result = NULL;
+
+ int msgid; // Message id returned by
+ // ldap_search_ext.
+
+ int count = 0; // Number of results we got.
+
+ struct timeval tv; // Holds timeout values.
+
+ char const *error = NULL;
+ char *extra = NULL;
+
+ int i;
+
+ rad_assert(*pconn && (*pconn)->handle);
+
+ /*
+ * OpenLDAP library doesn't declare attrs array as const, but
+ * it really should be *sigh*.
+ */
+ char **search_attrs;
+ memcpy(&search_attrs, &attrs, sizeof(attrs));
+
+ /*
+ * Do all searches as the admin user.
+ */
+ if ((*pconn)->rebound) {
+ status = rlm_ldap_bind(inst, request, pconn, (*pconn)->inst->admin_identity,
+ (*pconn)->inst->admin_password, &(*pconn)->inst->admin_sasl, true);
+ if (status != LDAP_PROC_SUCCESS) {
+ return LDAP_PROC_ERROR;
+ }
+
+ rad_assert(*pconn);
+
+ (*pconn)->rebound = false;
+ }
+
+ if (filter) {
+ LDAP_DBG_REQ("Performing search in \"%s\" with filter \"%s\", scope \"%s\"", dn, filter,
+ fr_int2str(ldap_scope, scope, "<INVALID>"));
+ } else {
+ LDAP_DBG_REQ("Performing unfiltered search in \"%s\", scope \"%s\"", dn,
+ fr_int2str(ldap_scope, scope, "<INVALID>"));
+ }
+ /*
+ * If LDAP search produced an error it should also be logged
+ * to the ld. result should pick it up without us
+ * having to pass it explicitly.
+ */
+ memset(&tv, 0, sizeof(tv));
+ tv.tv_sec = inst->res_timeout;
+
+ /*
+ * For sanity, for when no connections are viable,
+ * and we can't make a new one.
+ */
+ for (i = fr_connection_pool_get_retries(inst->pool); i >= 0; i--) {
+ (void) ldap_search_ext((*pconn)->handle, dn, scope, filter, search_attrs,
+ 0, serverctrls, clientctrls, &tv, 0, &msgid);
+
+ LDAP_DBG_REQ("Waiting for search result...");
+ status = rlm_ldap_result(inst, *pconn, msgid, dn, &our_result, &error, &extra);
+ switch (status) {
+ case LDAP_PROC_SUCCESS:
+ break;
+
+ /*
+ * Invalid DN isn't a failure when searching.
+ * The DN may be xlat expanded so may point directly
+ * to an LDAP object. If that can't be located, it's
+ * the same as notfound.
+ */
+ case LDAP_PROC_BAD_DN:
+ LDAP_DBG_REQ("%s", error);
+ if (extra) LDAP_DBG_REQ("%s", extra);
+ break;
+
+ case LDAP_PROC_RETRY:
+ *pconn = fr_connection_reconnect(inst->pool, *pconn);
+ if (*pconn) {
+ LDAP_DBGW_REQ("Search failed: %s. Got new socket, retrying...", error);
+
+ talloc_free(extra); /* don't leak debug info */
+
+ continue;
+ }
+
+ status = LDAP_PROC_ERROR;
+
+ /* FALL-THROUGH */
+ default:
+ LDAP_ERR_REQ("Failed performing search: %s", error);
+ if (extra) LDAP_ERR_REQ("%s", extra);
+
+ goto finish;
+ }
+
+ break;
+ }
+
+ if (i < 0) {
+ LDAP_ERR_REQ("Hit reconnection limit");
+ status = LDAP_PROC_ERROR;
+
+ goto finish;
+ }
+
+ count = ldap_count_entries((*pconn)->handle, our_result);
+ if (count < 0) {
+ LDAP_ERR_REQ("Error counting results: %s", rlm_ldap_error_str(*pconn));
+ status = LDAP_PROC_ERROR;
+
+ ldap_msgfree(our_result);
+ our_result = NULL;
+ } else if (count == 0) {
+ LDAP_DBG_REQ("Search returned no results");
+ status = LDAP_PROC_NO_RESULT;
+
+ ldap_msgfree(our_result);
+ our_result = NULL;
+ }
+
+finish:
+ talloc_free(extra);
+
+ /*
+ * We always need to get the result to count entries, but the caller
+ * may not of requested one. If that's the case, free it, else write
+ * it to where our caller said.
+ */
+ if (!result) {
+ if (our_result) ldap_msgfree(our_result);
+ } else {
+ *result = our_result;
+ }
+
+ return status;
+}
+
+/** Modify something in the LDAP directory
+ *
+ * Binds as the administrative user and attempts to modify an LDAP object.
+ *
+ * @param[in] inst rlm_ldap configuration.
+ * @param[in] request Current request.
+ * @param[in,out] pconn to use. May change as this function calls functions which auto re-connect.
+ * @param[in] dn of the object to modify.
+ * @param[in] mods to make, see 'man ldap_modify' for more information.
+ * @return One of the LDAP_PROC_* (#ldap_rcode_t) values.
+ */
+ldap_rcode_t rlm_ldap_modify(rlm_ldap_t const *inst, REQUEST *request, ldap_handle_t **pconn,
+ char const *dn, LDAPMod *mods[])
+{
+ ldap_rcode_t status = LDAP_PROC_ERROR;
+
+ int msgid; // Message id returned by ldap_search_ext.
+
+ char const *error = NULL;
+ char *extra = NULL;
+
+ int i;
+
+ rad_assert(*pconn && (*pconn)->handle);
+
+ /*
+ * Perform all modifications as the admin user.
+ */
+ if ((*pconn)->rebound) {
+ status = rlm_ldap_bind(inst, request, pconn, (*pconn)->inst->admin_identity,
+ (*pconn)->inst->admin_password, &(*pconn)->inst->admin_sasl, true);
+ if (status != LDAP_PROC_SUCCESS) {
+ return LDAP_PROC_ERROR;
+ }
+
+ rad_assert(*pconn);
+
+ (*pconn)->rebound = false;
+ }
+
+ /*
+ * For sanity, for when no connections are viable,
+ * and we can't make a new one.
+ */
+ for (i = fr_connection_pool_get_retries(inst->pool); i >= 0; i--) {
+ RDEBUG2("Modifying object with DN \"%s\"", dn);
+ (void) ldap_modify_ext((*pconn)->handle, dn, mods, NULL, NULL, &msgid);
+
+ RDEBUG2("Waiting for modify result...");
+ status = rlm_ldap_result(inst, *pconn, msgid, dn, NULL, &error, &extra);
+ switch (status) {
+ case LDAP_PROC_SUCCESS:
+ break;
+
+ case LDAP_PROC_RETRY:
+ *pconn = fr_connection_reconnect(inst->pool, *pconn);
+ if (*pconn) {
+ RWDEBUG("Modify failed: %s. Got new socket, retrying...", error);
+
+ talloc_free(extra); /* don't leak debug info */
+ continue;
+ }
+
+ status = LDAP_PROC_ERROR;
+
+ /* FALL-THROUGH */
+ default:
+ REDEBUG("Failed modifying object: %s", error);
+ REDEBUG("%s", extra);
+
+ goto finish;
+ }
+
+ break;
+ }
+
+ if (i < 0) {
+ LDAP_ERR_REQ("Hit reconnection limit");
+ status = LDAP_PROC_ERROR;
+ }
+
+finish:
+ talloc_free(extra);
+
+ return status;
+}
+
+/** Retrieve the DN of a user object
+ *
+ * Retrieves the DN of a user and adds it to the control list as LDAP-UserDN. Will also retrieve any
+ * attributes passed and return the result in *result.
+ *
+ * This potentially allows for all authorization and authentication checks to be performed in one
+ * ldap search operation, which is a big bonus given the number of crappy, slow *cough*AD*cough*
+ * LDAP directory servers out there.
+ *
+ * @param[in] inst rlm_ldap configuration.
+ * @param[in] request Current request.
+ * @param[in,out] pconn to use. May change as this function calls functions which auto re-connect.
+ * @param[in] attrs Additional attributes to retrieve, may be NULL.
+ * @param[in] force Query even if the User-DN already exists.
+ * @param[out] result Where to write the result, may be NULL in which case result is discarded.
+ * @param[out] rcode The status of the operation, one of the RLM_MODULE_* codes.
+ * @return The user's DN or NULL on error.
+ */
+char const *rlm_ldap_find_user(rlm_ldap_t const *inst, REQUEST *request, ldap_handle_t **pconn,
+ char const *attrs[], bool force, LDAPMessage **result, rlm_rcode_t *rcode)
+{
+ static char const *tmp_attrs[] = { NULL };
+
+ ldap_rcode_t status;
+ VALUE_PAIR *vp = NULL;
+ LDAPMessage *tmp_msg = NULL, *entry = NULL;
+ int ldap_errno;
+ int cnt;
+ char *dn = NULL;
+ char const *filter = NULL;
+ char filter_buff[LDAP_MAX_FILTER_STR_LEN];
+ char const *base_dn;
+ char base_dn_buff[LDAP_MAX_DN_STR_LEN];
+ LDAPControl *serverctrls[] = { inst->userobj_sort_ctrl, NULL };
+
+ bool freeit = false; //!< Whether the message should
+ //!< be freed after being processed.
+
+ *rcode = RLM_MODULE_FAIL;
+
+ if (!result) {
+ result = &tmp_msg;
+ freeit = true;
+ }
+ *result = NULL;
+
+ if (!attrs) {
+ memset(&attrs, 0, sizeof(tmp_attrs));
+ }
+
+ /*
+ * If the caller isn't looking for the result we can just return the current userdn value.
+ */
+ if (!force) {
+ vp = fr_pair_find_by_da(request->config, inst->user_dn_da, TAG_ANY);
+ if (vp) {
+ RDEBUG("Using user DN from request \"%s\"", vp->vp_strvalue);
+ *rcode = RLM_MODULE_OK;
+ return vp->vp_strvalue;
+ }
+ }
+
+ /*
+ * Perform all searches as the admin user.
+ */
+ if ((*pconn)->rebound) {
+ status = rlm_ldap_bind(inst, request, pconn, (*pconn)->inst->admin_identity,
+ (*pconn)->inst->admin_password, &(*pconn)->inst->admin_sasl, true);
+ if (status != LDAP_PROC_SUCCESS) {
+ *rcode = RLM_MODULE_FAIL;
+ return NULL;
+ }
+
+ rad_assert(*pconn);
+
+ (*pconn)->rebound = false;
+ }
+
+ if (inst->userobj_filter) {
+ if (tmpl_expand(&filter, filter_buff, sizeof(filter_buff), request, inst->userobj_filter,
+ rlm_ldap_escape_func, NULL) < 0) {
+ REDEBUG("Unable to create filter");
+ *rcode = RLM_MODULE_INVALID;
+
+ return NULL;
+ }
+ }
+
+ if (tmpl_expand(&base_dn, base_dn_buff, sizeof(base_dn_buff), request,
+ inst->userobj_base_dn, rlm_ldap_escape_func, NULL) < 0) {
+ REDEBUG("Unable to create base_dn");
+ *rcode = RLM_MODULE_INVALID;
+
+ return NULL;
+ }
+
+ status = rlm_ldap_search(result, inst, request, pconn, base_dn,
+ inst->userobj_scope, filter, attrs, serverctrls, NULL);
+ switch (status) {
+ case LDAP_PROC_SUCCESS:
+ break;
+
+ case LDAP_PROC_BAD_DN:
+ case LDAP_PROC_NO_RESULT:
+ *rcode = RLM_MODULE_NOTFOUND;
+ return NULL;
+
+ default:
+ *rcode = RLM_MODULE_FAIL;
+ return NULL;
+ }
+
+ rad_assert(*pconn);
+
+ /*
+ * Forbid the use of unsorted search results that
+ * contain multiple entries, as it's a potential
+ * security issue, and likely non deterministic.
+ */
+ if (!inst->userobj_sort_ctrl) {
+ cnt = ldap_count_entries((*pconn)->handle, *result);
+ if (cnt > 1) {
+ REDEBUG("Ambiguous search result, returned %i unsorted entries (should return 1 or 0). "
+ "Enable sorting, or specify a more restrictive base_dn, filter or scope", cnt);
+ REDEBUG("The following entries were returned:");
+ RINDENT();
+ for (entry = ldap_first_entry((*pconn)->handle, *result);
+ entry;
+ entry = ldap_next_entry((*pconn)->handle, entry)) {
+ dn = ldap_get_dn((*pconn)->handle, entry);
+ REDEBUG("%s", dn);
+ ldap_memfree(dn);
+ }
+ REXDENT();
+ *rcode = RLM_MODULE_INVALID;
+ goto finish;
+ }
+ }
+
+ entry = ldap_first_entry((*pconn)->handle, *result);
+ if (!entry) {
+ ldap_get_option((*pconn)->handle, LDAP_OPT_RESULT_CODE, &ldap_errno);
+ REDEBUG("Failed retrieving entry: %s",
+ ldap_err2string(ldap_errno));
+
+ goto finish;
+ }
+
+ dn = ldap_get_dn((*pconn)->handle, entry);
+ if (!dn) {
+ ldap_get_option((*pconn)->handle, LDAP_OPT_RESULT_CODE, &ldap_errno);
+ REDEBUG("Retrieving object DN from entry failed: %s", ldap_err2string(ldap_errno));
+
+ goto finish;
+ }
+ rlm_ldap_normalise_dn(dn, dn);
+
+ /*
+ * We can't use fr_pair_make here to copy the value into the
+ * attribute, as the dn must be copied into the attribute
+ * verbatim (without de-escaping).
+ *
+ * Special chars are pre-escaped by libldap, and because
+ * we pass the string back to libldap we must not alter it.
+ */
+ RDEBUG("User object found at DN \"%s\"", dn);
+ vp = fr_pair_afrom_da(request, inst->user_dn_da);
+ if (vp) {
+ fr_pair_add(&request->config, vp);
+ fr_pair_value_strcpy(vp, dn);
+ *rcode = RLM_MODULE_OK;
+ }
+ ldap_memfree(dn);
+
+finish:
+ if ((freeit || (*rcode != RLM_MODULE_OK)) && *result) {
+ ldap_msgfree(*result);
+ *result = NULL;
+ }
+
+ return vp ? vp->vp_strvalue : NULL;
+}
+
+/** Check for presence of access attribute in result
+ *
+ * @param[in] inst rlm_ldap configuration.
+ * @param[in] request Current request.
+ * @param[in] conn used to retrieve access attributes.
+ * @param[in] entry retrieved by rlm_ldap_find_user or rlm_ldap_search.
+ * @return
+ * - #RLM_MODULE_USERLOCK if the user was denied access.
+ * - #RLM_MODULE_OK otherwise.
+ */
+rlm_rcode_t rlm_ldap_check_access(rlm_ldap_t const *inst, REQUEST *request,
+ ldap_handle_t const *conn, LDAPMessage *entry)
+{
+ rlm_rcode_t rcode = RLM_MODULE_OK;
+ struct berval **values = NULL;
+
+ values = ldap_get_values_len(conn->handle, entry, inst->userobj_access_attr);
+ if (values) {
+ if (inst->access_positive) {
+ if ((values[0]->bv_len >= 5) && (strncasecmp(values[0]->bv_val, "false", 5) == 0)) {
+ RDEBUG("\"%s\" attribute exists but is set to 'false' - user locked out",
+ inst->userobj_access_attr);
+ rcode = RLM_MODULE_USERLOCK;
+ }
+ /* RLM_MODULE_OK set above... */
+ } else if ((values[0]->bv_len < 5) || (strncasecmp(values[0]->bv_val, "false", 5) != 0)) {
+ RDEBUG("\"%s\" attribute exists - user locked out", inst->userobj_access_attr);
+ rcode = RLM_MODULE_USERLOCK;
+ }
+ ldap_value_free_len(values);
+ } else if (inst->access_positive) {
+ RDEBUG("No \"%s\" attribute - user locked out", inst->userobj_access_attr);
+ rcode = RLM_MODULE_USERLOCK;
+ }
+
+ return rcode;
+}
+
+/** Verify we got a password from the search
+ *
+ * Checks to see if after the LDAP to RADIUS mapping has been completed that a reference password.
+ *
+ * @param inst rlm_ldap configuration.
+ * @param request Current request.
+ */
+void rlm_ldap_check_reply(rlm_ldap_t const *inst, REQUEST *request)
+{
+ /*
+ * More warning messages for people who can't be bothered to read the documentation.
+ *
+ * Expect_password is set when we process the mapping, and is only true if there was a mapping between
+ * an LDAP attribute and a password reference attribute in the control list.
+ */
+ if (inst->expect_password && (rad_debug_lvl > 1)) {
+ if (!fr_pair_find_by_num(request->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY) &&
+ !fr_pair_find_by_num(request->config, PW_NT_PASSWORD, 0, TAG_ANY) &&
+ !fr_pair_find_by_num(request->config, PW_USER_PASSWORD, 0, TAG_ANY) &&
+ !fr_pair_find_by_num(request->config, PW_PASSWORD_WITH_HEADER, 0, TAG_ANY) &&
+ !fr_pair_find_by_num(request->config, PW_CRYPT_PASSWORD, 0, TAG_ANY)) {
+ RWDEBUG("No \"known good\" password added. Ensure the admin user has permission to "
+ "read the password attribute");
+ RWDEBUG("PAP authentication will *NOT* work with Active Directory (if that is what you "
+ "were trying to configure)");
+ }
+ }
+}
+
+#if LDAP_SET_REBIND_PROC_ARGS == 3
+/** Callback for OpenLDAP to rebind and chase referrals
+ *
+ * Called by OpenLDAP when it receives a referral and has to rebind.
+ *
+ * @param handle to rebind.
+ * @param url to bind to.
+ * @param request that triggered the rebind.
+ * @param msgid that triggered the rebind.
+ * @param ctx rlm_ldap configuration.
+ */
+static int rlm_ldap_rebind(LDAP *handle, LDAP_CONST char *url, UNUSED ber_tag_t request, UNUSED ber_int_t msgid,
+ void *ctx)
+{
+ ldap_rcode_t status;
+ ldap_handle_t *conn = talloc_get_type_abort(ctx, ldap_handle_t);
+
+ int ldap_errno;
+
+ rad_assert(handle == conn->handle);
+
+ DEBUG("rlm_ldap (%s): Rebinding to URL %s", conn->inst->name, url);
+
+ status = rlm_ldap_bind(conn->inst, NULL, &conn, conn->inst->admin_identity, conn->inst->admin_password,
+ &(conn->inst->admin_sasl), false);
+ if (status != LDAP_PROC_SUCCESS) {
+ ldap_get_option(handle, LDAP_OPT_ERROR_NUMBER, &ldap_errno);
+
+ return ldap_errno;
+ }
+
+ return LDAP_SUCCESS;
+}
+#endif
+
+int rlm_ldap_global_init(rlm_ldap_t *inst)
+{
+ int ldap_errno;
+#if defined(LDAP_OPT_X_TLS_PACKAGE) && defined(LDAP_OPT_X_TLS_CTX) && defined(HAVE_OPENSSL_SSL_H)
+ bool use_openssl = false;
+#endif
+
+#define do_ldap_global_option(_option, _name, _value) \
+ if (ldap_set_option(NULL, _option, _value) != LDAP_OPT_SUCCESS) { \
+ ldap_get_option(NULL, LDAP_OPT_ERROR_NUMBER, &ldap_errno); \
+ ERROR("Failed setting global option %s: %s", _name, \
+ (ldap_errno != LDAP_SUCCESS) ? ldap_err2string(ldap_errno) : "Unknown error"); \
+ return -1;\
+ }
+
+#define maybe_ldap_global_option(_option, _name, _value) \
+ if (_value) do_ldap_global_option(_option, _name, _value)
+
+#ifdef LDAP_OPT_DEBUG_LEVEL
+ /*
+ * Can't use do_ldap_global_option
+ */
+ if (inst->ldap_debug) do_ldap_global_option(LDAP_OPT_DEBUG_LEVEL, "ldap_debug", &(inst->ldap_debug));
+#endif
+
+#ifdef LDAP_OPT_X_TLS_RANDOM_FILE
+ /*
+ * OpenLDAP will error out if we attempt to set
+ * this on a handle. Presumably it's global in
+ * OpenSSL too.
+ */
+ maybe_ldap_global_option(LDAP_OPT_X_TLS_RANDOM_FILE, "random_file", inst->tls_random_file);
+#endif
+
+#ifdef LDAP_OPT_X_TLS_PACKAGE
+ {
+ char *name = NULL;
+
+ if (ldap_get_option(NULL, LDAP_OPT_X_TLS_PACKAGE, (void *) &name) == LDAP_OPT_SUCCESS) {
+ if (strcmp(name, "OpenSSL") != 0) {
+ WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+ WARN("!! libldap is using %s, while FreeRADIUS is using OpenSSL", name);
+ WARN("!! There may be random issues with TLS connections due to this conflict.");
+ WARN("!! The server may also crash.");
+ WARN("!! See https://wiki.freeradius.org/modules/Rlm_ldap for more information.");
+ WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+ }
+#if defined(LDAP_OPT_X_TLS_CTX) && defined(HAVE_OPENSSL_SSL_H)
+ else {
+ use_openssl = true;
+ }
+#endif
+
+ ldap_memfree(name);
+ }
+ }
+#endif
+
+#ifdef LDAP_OPT_X_TLS_CTX
+#ifdef HAVE_OPENSSL_SSL_H
+ {
+ X509_STORE *store;
+ SSL_CTX *ssl_ctx;
+
+ if (inst->tls_check_crl &&
+#ifdef LDAP_OPT_X_TLS_PACKAGE
+ use_openssl &&
+#endif
+
+ (ldap_get_option(NULL, LDAP_OPT_X_TLS_CTX, (void *) &ssl_ctx) == LDAP_OPT_SUCCESS)) {
+ store = SSL_CTX_get_cert_store(ssl_ctx);
+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
+ }
+ }
+#endif
+#endif
+
+ return 0;
+}
+
+/** Close and delete a connection
+ *
+ * Unbinds the LDAP connection, informing the server and freeing any memory, then releases the memory used by the
+ * connection handle.
+ *
+ * @param conn to destroy.
+ * @return always indicates success.
+ */
+static int _mod_conn_free(ldap_handle_t *conn)
+{
+ if (conn->handle) {
+ DEBUG3("rlm_ldap: Closing libldap handle %p", conn->handle);
+#ifdef HAVE_LDAP_UNBIND_EXT_S
+ ldap_unbind_ext_s(conn->handle, NULL, NULL);
+#else
+ ldap_unbind_s(conn->handle);
+#endif
+ }
+
+ return 0;
+}
+
+/** Create and return a new connection
+ *
+ * Create a new ldap connection and allocate memory for a new rlm_handle_t
+ */
+void *mod_conn_create(TALLOC_CTX *ctx, void *instance)
+{
+ ldap_rcode_t status;
+
+ int ldap_errno, ldap_version;
+ struct timeval tv;
+
+ rlm_ldap_t *inst = instance;
+ ldap_handle_t *conn;
+
+ /*
+ * Allocate memory for the handle.
+ */
+ conn = talloc_zero(ctx, ldap_handle_t);
+ if (!conn) return NULL;
+ talloc_set_destructor(conn, _mod_conn_free);
+
+ conn->inst = inst;
+ conn->rebound = false;
+
+ DEBUG("rlm_ldap (%s): Connecting to %s", inst->name, inst->server);
+#ifdef HAVE_LDAP_INITIALIZE
+ ldap_errno = ldap_initialize(&conn->handle, inst->server);
+ if (ldap_errno != LDAP_SUCCESS) {
+ LDAP_ERR("ldap_initialize failed: %s", ldap_err2string(ldap_errno));
+ goto error;
+ }
+#else
+ conn->handle = ldap_init(inst->server, inst->port);
+ if (!conn->handle) {
+ LDAP_ERR("ldap_init failed");
+ goto error;
+ }
+#endif
+ DEBUG3("rlm_ldap (%s): New libldap handle %p", inst->name, conn->handle);
+
+ /*
+ * We now have a connection structure, but no actual connection.
+ *
+ * Set a bunch of LDAP options, using common code.
+ */
+#define do_ldap_option(_option, _name, _value) \
+ if (ldap_set_option(conn->handle, _option, _value) != LDAP_OPT_SUCCESS) { \
+ ldap_get_option(conn->handle, LDAP_OPT_ERROR_NUMBER, &ldap_errno); \
+ LDAP_ERR("Failed setting connection option %s: %s", _name, \
+ (ldap_errno != LDAP_SUCCESS) ? ldap_err2string(ldap_errno) : "Unknown error"); \
+ goto error;\
+ }
+
+#define maybe_ldap_option(_option, _name, _value) \
+ if (_value) do_ldap_option(_option, _name, _value)
+
+ /*
+ * Leave "dereference" unset to use the OpenLDAP default.
+ */
+ if (inst->dereference_str) {
+ do_ldap_option(LDAP_OPT_DEREF, "dereference", &(inst->dereference));
+ }
+
+ /*
+ * Leave "chase_referrals" unset to use the OpenLDAP default.
+ */
+ if (!inst->chase_referrals_unset) {
+ if (inst->chase_referrals) {
+ do_ldap_option(LDAP_OPT_REFERRALS, "chase_referrals", LDAP_OPT_ON);
+
+ if (inst->rebind == true) {
+#if LDAP_SET_REBIND_PROC_ARGS == 3
+ ldap_set_rebind_proc(conn->handle, rlm_ldap_rebind, conn);
+#endif
+ }
+ } else {
+ do_ldap_option(LDAP_OPT_REFERRALS, "chase_referrals", LDAP_OPT_OFF);
+ }
+ }
+
+#ifdef LDAP_OPT_NETWORK_TIMEOUT
+ if (inst->net_timeout) {
+ memset(&tv, 0, sizeof(tv));
+ tv.tv_sec = inst->net_timeout;
+
+ do_ldap_option(LDAP_OPT_NETWORK_TIMEOUT, "net_timeout", &tv);
+ }
+#endif
+
+ do_ldap_option(LDAP_OPT_TIMELIMIT, "srv_timelimit", &(inst->srv_timelimit));
+
+ ldap_version = LDAP_VERSION3;
+ do_ldap_option(LDAP_OPT_PROTOCOL_VERSION, "ldap_version", &ldap_version);
+
+#ifdef LDAP_OPT_X_KEEPALIVE_IDLE
+ do_ldap_option(LDAP_OPT_X_KEEPALIVE_IDLE, "keepalive idle", &(inst->keepalive_idle));
+#endif
+
+#ifdef LDAP_OPT_X_KEEPALIVE_PROBES
+ do_ldap_option(LDAP_OPT_X_KEEPALIVE_PROBES, "keepalive probes", &(inst->keepalive_probes));
+#endif
+
+#ifdef LDAP_OPT_X_KEEPALIVE_INTERVAL
+ do_ldap_option(LDAP_OPT_X_KEEPALIVE_INTERVAL, "keepalive interval", &(inst->keepalive_interval));
+#endif
+
+#ifdef HAVE_LDAP_START_TLS_S
+ /*
+ * Set all of the TLS options
+ */
+ if (inst->tls_mode) {
+ do_ldap_option(LDAP_OPT_X_TLS, "tls_mode", &(inst->tls_mode));
+ }
+
+ maybe_ldap_option(LDAP_OPT_X_TLS_CACERTFILE, "ca_file", inst->tls_ca_file);
+ maybe_ldap_option(LDAP_OPT_X_TLS_CACERTDIR, "ca_path", inst->tls_ca_path);
+
+
+ /*
+ * Set certificate options
+ */
+ maybe_ldap_option(LDAP_OPT_X_TLS_CERTFILE, "certificate_file", inst->tls_certificate_file);
+ maybe_ldap_option(LDAP_OPT_X_TLS_KEYFILE, "private_key_file", inst->tls_private_key_file);
+
+# ifdef LDAP_OPT_X_TLS_REQUIRE_CERT
+ if (inst->tls_require_cert_str) {
+ do_ldap_option(LDAP_OPT_X_TLS_REQUIRE_CERT, "require_cert", &inst->tls_require_cert);
+ }
+# endif
+
+# ifdef LDAP_OPT_X_TLS_PROTOCOL_MIN
+ if (inst->tls_min_version_str) {
+ do_ldap_option(LDAP_OPT_X_TLS_PROTOCOL_MIN, "tls_min_version", &inst->tls_min_version);
+ }
+# endif
+
+ /*
+ * Counter intuitively the TLS context appears to need to be initialised
+ * after all the TLS options are set on the handle.
+ */
+# ifdef LDAP_OPT_X_TLS_NEWCTX
+ {
+ /* Always use the new TLS configuration context */
+ int is_server = 0;
+ do_ldap_option(LDAP_OPT_X_TLS_NEWCTX, "new TLS context", &is_server);
+
+ }
+# endif
+
+# ifdef LDAP_OPT_X_TLS_CIPHER_SUITE
+ if (inst->tls_cipher_list) {
+ do_ldap_option(LDAP_OPT_X_TLS_CIPHER_SUITE, "cipher_list", inst->tls_cipher_list);
+ }
+# endif
+
+ /*
+ * And finally start the TLS code.
+ */
+ if (inst->start_tls) {
+ if (inst->port == 636) {
+ WARN("Told to Start TLS on LDAPS port this will probably fail, please correct the "
+ "configuration");
+ }
+
+ if (ldap_start_tls_s(conn->handle, NULL, NULL) != LDAP_SUCCESS) {
+ ldap_get_option(conn->handle, LDAP_OPT_ERROR_NUMBER, &ldap_errno);
+
+ LDAP_ERR("Could not start TLS: %s", ldap_err2string(ldap_errno));
+ goto error;
+ }
+ }
+#endif /* HAVE_LDAP_START_TLS_S */
+
+ if (inst->sasl_secprops) {
+ do_ldap_option(LDAP_OPT_X_SASL_SECPROPS, "SASL_SECPROPS", inst->sasl_secprops);
+ }
+
+ status = rlm_ldap_bind(inst, NULL, &conn, conn->inst->admin_identity, conn->inst->admin_password,
+ &(conn->inst->admin_sasl), false);
+ if (status != LDAP_PROC_SUCCESS) {
+ goto error;
+ }
+
+ return conn;
+
+error:
+ talloc_free(conn);
+
+ return NULL;
+}
+
+/** Gets an LDAP socket from the connection pool
+ *
+ * Retrieve a socket from the connection pool, or NULL on error (of if no sockets are available).
+ *
+ * @param inst rlm_ldap configuration.
+ * @param request Current request (may be NULL).
+ */
+ldap_handle_t *mod_conn_get(rlm_ldap_t const *inst, UNUSED REQUEST *request)
+{
+ return fr_connection_get(inst->pool);
+}
+
+/** Frees an LDAP socket back to the connection pool
+ *
+ * If the socket was rebound chasing a referral onto another server then we destroy it.
+ * If the socket was rebound to another user on the same server, we let the next caller rebind it.
+ *
+ * @param inst rlm_ldap configuration.
+ * @param conn to release.
+ */
+void mod_conn_release(rlm_ldap_t const *inst, ldap_handle_t *conn)
+{
+ /*
+ * Could have already been free'd due to a previous error.
+ */
+ if (!conn) return;
+
+ fr_connection_release(inst->pool, conn);
+ return;
+}