]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
v4: Ground work for moving LDAP connections to per thread trunks (#4248)
authorNick Porter <nick@portercomputing.co.uk>
Thu, 7 Oct 2021 12:09:42 +0000 (13:09 +0100)
committerGitHub <noreply@github.com>
Thu, 7 Oct 2021 12:09:42 +0000 (07:09 -0500)
* Define fr_ldap_thread_t to hold thead specific data

* Define fr_ldap_thread_trunk_t to hold LDAP connection details

* Add fr_ldap_trunk_cmp() to compare two thread LDAP trunks

* Define mod_thread_instantiate and mod_thread_detach for rlm_ldap

* Define types of LDAP requests

* Define LDAP query result codes

* Define fr_ldap_query_t to store individual LDAP queries

* Add fr_ldap_query_cmp() to compare two fr_ldap_query_t

* Define fr_ldap_referral_t to hold parsed LDAP referrals while they are followed

A given LDAP resonse which contains referral details can consist of
multiple referral URLs all of which should be capable of returning the
same data.

We parse all of the URLs and firstly attempt to query a server that
already has an active trunk connection.

All the possible referral URLs may need to be parsed to find an active
connection.

If none of the referral URLs point to an active connection, then the
parsed data stored in this structure is used to create new connections,
rather than re-parsing.

* Define fr_ldap_referral_alloc and _fr_ldap_referral_free

fr_ldap_referrer_t contains a LDAPURLDesc which needs to be freed using
libldap function ldap_free_urldesc

* Define destructor for fr_ldap_query_t

Frees any remaining libldap allocated structures

* Define allocator for fr_ldap_query_t

* Add LDAP config options of referral_depth and idle_timeout

referral depth - maximum number of referrals to follow
idle_timeout - how log to keep unused LDAP connections open

* Add tree to hold outstanding LDAP queries to fr_ldap_connection_t

* Define callback for closing an idle LDAP trunk

* Add return code for when an LDAP query results in a referral

* Add lookup table to provide readable names of LDAP message types

* Thread connection specific config is not talloc'd

* Define ldap_trunk_connection_alloc() to allocate LDAP connections

* Define I/O callbacks for LDAP trunks

* Setup I/O callbacks requested by LDAP trunk connection

* Define callback for cancelling LDAP queries

* Define _ldap_referral_send - a trunk watcher function...

... for sending referral queries when a trunk becomes active.

This allows for referrals which have multiple potential URLs to follow
to launch a number of trunk connections and the first one to become
active will receive the query.

* Define fr_thread_ldap_trunk_state() to find the state of a LDAP trunk

Looked up by URI and bind DN

* Define fr_ldap_referral_follow() to parse referral URLs and despatch ...

... queries to chase the referrals

* Define fr_ldap_referral_next() to follow subsequent LDAP referrals if the ...

... first one followed returns an error.

* Define ldap_trunk_request_mux() for sending LDAP queries on trunk connections

* Define ldap_trunk_request_demux() to handle LDAP query responses

* Define fr_thread_ldap_trunk_get() to find / create an LDAP trunk...

... for the required host / bind dn

* Define fr_ldap_modify_async() to initiate async LDAP modifications

src/lib/ldap/all.mk.in
src/lib/ldap/base.c
src/lib/ldap/base.h
src/lib/ldap/connection.c
src/lib/ldap/referral.c [new file with mode: 0644]
src/modules/rlm_ldap/rlm_ldap.c

index 036d64742a0ed7e50f5e8fb09a9df884077156ee..9ff8e37a54abdc9753a697921bd1e0e3cd5c9e12 100644 (file)
@@ -4,7 +4,7 @@ ifneq "$(TARGETNAME)" ""
 TARGET         := $(TARGETNAME).a
 endif
 
-SOURCES                := base.c bind.c connection.c control.c directory.c edir.c map.c start_tls.c state.c util.c @SASL@
+SOURCES                := base.c bind.c connection.c control.c directory.c edir.c map.c referral.c start_tls.c state.c util.c @SASL@
 
 SRC_CFLAGS     := @mod_cflags@
 TGT_LDLIBS     := @mod_ldflags@
index d6c12d990d6c09d202ae9dae55328a8ecf9cd4b6..aeaeb9017ff25f4e38f238404aaa2bc0f45f1d75 100644 (file)
@@ -264,6 +264,11 @@ process_error:
                fr_strerror_const("Success");
                break;
 
+       case LDAP_REFERRAL:
+               fr_strerror_const("Referral");
+               status = LDAP_PROC_REFERRAL;
+               break;
+
        case LDAP_SASL_BIND_IN_PROGRESS:
                fr_strerror_const("Continuing");
                status = LDAP_PROC_CONTINUE;
@@ -838,6 +843,45 @@ finish:
        return status;
 }
 
+/** Modify something in the LDAP directory
+ *
+ * @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.
+ * @param[in] serverctrls      Search controls to pass to the server.  May be NULL.
+ * @param[in] clientctrls      Search controls for ldap_modify.  May be NULL.
+ * @return One of the LDAP_PROC_* (#fr_ldap_rcode_t) values.
+ */
+fr_ldap_rcode_t fr_ldap_modify_async(int *msgid, request_t *request, fr_ldap_connection_t **pconn,
+                              char const *dn, LDAPMod *mods[],
+                              LDAPControl **serverctrls, LDAPControl **clientctrls)
+{
+       LDAPControl     *our_serverctrls[LDAP_MAX_CONTROLS];
+       LDAPControl     *our_clientctrls[LDAP_MAX_CONTROLS];
+
+       fr_ldap_control_merge(our_serverctrls, our_clientctrls,
+                             NUM_ELEMENTS(our_serverctrls),
+                             NUM_ELEMENTS(our_clientctrls),
+                             *pconn, serverctrls, clientctrls);
+
+       fr_assert(*pconn && (*pconn)->handle);
+
+       if (RDEBUG_ENABLED4) fr_ldap_timeout_debug(request, *pconn, fr_time_delta_wrap(0), __FUNCTION__);
+
+       RDEBUG2("Modifying object with DN \"%s\"", dn);
+       if(ldap_modify_ext((*pconn)->handle, dn, mods, our_serverctrls, our_clientctrls, msgid) != LDAP_SUCCESS) {
+               int ldap_errno;
+
+               ldap_get_option((*pconn)->handle, LDAP_OPT_RESULT_CODE, &ldap_errno);
+               ROPTIONAL(RPEDEBUG, RPERROR, "Failed modifying object %s", ldap_err2string(ldap_errno));
+
+               return LDAP_PROC_ERROR;
+       }
+
+       return LDAP_PROC_SUCCESS;
+}
+
 /** Change settings global to libldap
  *
  * May only be called once.  Subsequent calls will be ignored.
@@ -884,6 +928,61 @@ int fr_ldap_global_config(int debug_level, char const *tls_random_file)
        return 0;
 }
 
+/** Free any libldap structures when an fr_ldap_query_t is freed
+ *
+ */
+static int _ldap_query_free(fr_ldap_query_t *query)
+{
+       int     i;
+
+       /*
+        *      Free any results which were retrieved
+        */
+       if (query->result) ldap_msgfree(query->result);
+
+       /*
+        *      Free any server and client controls that need freeing
+        */
+       for (i = 0; i < LDAP_MAX_CONTROLS; i++) {
+               if (!query->serverctrls[i].control) break;
+               if (query->serverctrls[i].freeit) ldap_control_free(query->serverctrls[i].control);
+       }
+
+       for (i = 0; i < LDAP_MAX_CONTROLS; i++) {
+               if (!query->clientctrls[i].control) break;
+               if (query->clientctrls[i].freeit) ldap_control_free(query->clientctrls[i].control);
+       }
+
+       /*
+        *      If a URL was parsed, free it.
+        */
+       if (query->ldap_url) ldap_free_urldesc(query->ldap_url);
+
+       /*
+        *      If any referrals were followed, the parsed referral URLS should be freed
+        */
+       if (query->referral_urls) ldap_memvfree((void **)query->referral_urls);
+
+       fr_dlist_talloc_free(&query->referrals);
+
+       return 0;
+}
+
+/** Allocate an fr_ldap_query_t, setting the talloc destructor
+ *
+ */
+fr_ldap_query_t *fr_ldap_query_alloc(TALLOC_CTX *ctx)
+{
+       fr_ldap_query_t *query;
+
+       MEM(query = talloc_zero(ctx, fr_ldap_query_t));
+       talloc_set_destructor(query, _ldap_query_free);
+
+       query->ret = LDAP_RESULT_PENDING;
+
+       return query;
+}
+
 /** Initialise libldap and check library versions
  *
  * @return
index bfa7723cdb781b6abe77e7fc0546264c192922d0..80756c14f93f4c25618e86224193ce89ce0c3887 100644 (file)
@@ -12,6 +12,7 @@
 #include <freeradius-devel/server/base.h>
 #include <freeradius-devel/server/connection.h>
 #include <freeradius-devel/server/map.h>
+#include <freeradius-devel/server/trunk.h>
 
 #define LDAP_DEPRECATED 0      /* Quiet warnings about LDAP_DEPRECATED not being defined */
 
@@ -164,6 +165,30 @@ typedef enum {
        FR_LDAP_STATE_ERROR                             //!< Connection is in an error state.
 } fr_ldap_state_t;
 
+/** Types of LDAP requests
+ *
+ */
+typedef enum {
+       LDAP_REQUEST_SEARCH = 1,                        //!< A lookup in an LDAP directory
+       LDAP_REQUEST_MODIFY                             //!< A modification to an LDAP entity
+} fr_ldap_request_type_t;
+
+/** LDAP query result codes
+ *
+ */
+typedef enum {
+       LDAP_RESULT_PENDING = 1,                        //!< Result not yet returned
+       LDAP_RESULT_SUCCESS = 0,                        //!< Successfully got LDAP results
+       LDAP_RESULT_ERROR = -1,                         //!< A general error occurred
+       LDAP_RESULT_TIMEOUT = -2,                       //!< The query timed out
+       LDAP_RESULT_BAD_DN = -3,                        //!< The requested DN does not exist
+       LDAP_RESULT_NO_RESULT = -4,                     //!< No results returned
+       LDAP_RESULT_REFERRAL_FAIL = -5,                 //!< Initial results indicated a referral was needed
+                                                       ///< but the referral could not be followed
+       LDAP_RESULT_EXCESS_REFERRALS = -6,              //!< The referral chain took too many hops
+       LDAP_RESULT_MISSING_REFERRAL = -7,              //!< A referral was indicated but no URL was provided
+} fr_ldap_result_code_t;
+
 typedef struct {
        char const              *vendor_str;            //!< As returned from the vendorName attribute in the
                                                        ///< rootDSE.
@@ -206,6 +231,8 @@ typedef struct {
 
        bool                    use_referral_credentials;       //!< If true use credentials from the referral URL.
 
+       uint16_t                referral_depth;         //!< How many referrals to chase
+
        bool                    rebind;                 //!< Controls whether we set an ldad_rebind_proc function
                                                        ///< and so determines if we can bind to other servers whilst
                                                        ///< chasing referrals. If this is false, we will still chase
@@ -275,8 +302,12 @@ typedef struct {
        fr_time_delta_t         tls_handshake_timeout;  //!< How long we wait for the TLS handshake to complete.
 
        fr_time_delta_t         reconnection_delay;     //!< How long to wait before attempting to reconnect.
+
+       fr_time_delta_t         idle_timeout;           //!< How long to wait before closing unused connections.
 } fr_ldap_config_t;
 
+typedef struct fr_ldap_thread_trunk_s fr_ldap_thread_trunk_t;
+
 /** Tracks the state of a libldap connection handle
  *
  */
@@ -304,6 +335,8 @@ typedef struct {
 
        int                     fd;                     //!< File descriptor for this connection.
 
+       fr_rb_tree_t            *queries;               //!< Outstanding queries on this connection
+
        void                    *uctx;                  //!< User data associated with the handle.
 } fr_ldap_connection_t;
 
@@ -328,10 +361,114 @@ typedef struct {
        int             count;                          //!< Index on next free element.
 } fr_ldap_map_exp_t;
 
+typedef struct ldap_inst_s rlm_ldap_t;
+
+/** Thread specific structure to manage LDAP trunk connections.
+ *
+ */
+typedef struct {
+       fr_rb_tree_t            *trunks;        //!< Tree of LDAP trunks used by this thread
+       rlm_ldap_t              *inst;          //!< Module instance data
+       fr_ldap_config_t        *config;        //!< Module instance config
+       fr_trunk_conf_t         *trunk_conf;    //!< Module trunk config
+       fr_event_list_t         *el;            //!< Thread event list for callbacks / timeouts
+       fr_connection_t         *conn;          //!< LDAP connection used for bind auths
+       fr_rb_tree_t            *binds;         //!< Tree of outstanding bind auths
+} fr_ldap_thread_t;
+
+/** Thread LDAP trunk structure
+ *
+ * One fr_ldap_thread_trunk_t will be allocated for each destination a thread needs
+ * to create an LDAP trunk connection to.
+ *
+ * Used to hold config regarding the LDAP connection and associate pending queries
+ * with the trunk they are running on.
+ */
+typedef struct fr_ldap_thread_trunk_s {
+       fr_rb_node_t            node;           //!< Entry in the tree of connections
+       char const              *uri;           //!< Server URI for this connection
+       char const              *bind_dn;       //!< DN connection is bound as
+       fr_ldap_config_t        config;         //!< Config used for this connection
+       fr_ldap_directory_t     *directory;     //!< The type of directory we're connected to.
+       fr_trunk_t              *trunk;         //!< Connection trunk
+       fr_ldap_thread_t        *t;             //!< Thread this connection is associated with
+       fr_event_timer_t const  *ev;            //!< Event to close the thread when it has been idle.
+} fr_ldap_thread_trunk_t;
+
+typedef struct fr_ldap_referral_s fr_ldap_referral_t;
+
+/** LDAP query structure
+ *
+ * Used to hold the elements of an LDAP query and track its progress.
+ * libldap structures will be freed by the talloc destructor.
+ * The same structure is used both for search queries and modifications
+ */
+typedef struct fr_ldap_query_s {
+       fr_rb_node_t            node;           //!< Entry in the tree of outstanding queries.
+
+       LDAPURLDesc             *ldap_url;      //!< parsed URL for current query if the source
+                                               ///< of the query was a URL.
+
+       char const              *dn;            //!< Base DN for searches, DN for modifications.
+
+       union {
+               struct {
+                       char const      **attrs;        //!< Attributes being requested in a search.
+                       int             scope;          //!< Search scope.
+                       char const      *filter;        //!< Filter for search.
+               } search;
+               LDAPMod                 **mods;         //!< Changes to be applied if this query is a modification.
+       };
+
+       fr_ldap_request_type_t  type;                   //!< What type of query this is.
+
+       fr_ldap_control_t       serverctrls[LDAP_MAX_CONTROLS]; //!< Server controls specific to this query.
+       fr_ldap_control_t       clientctrls[LDAP_MAX_CONTROLS]; //!< Client controls specific to this query.
+
+
+       int                     msgid;          //!< The unique identifier for this query.
+                                               ///< Uniqueness is only per connection.
+
+       fr_ldap_thread_trunk_t  *ttrunk;        //!< Trunk this query is running on
+       fr_trunk_request_t      *treq;          //!< Trunk request this query is associated with
+       fr_ldap_connection_t    *ldap_conn;     //!< LDAP connection this query is running on.
+
+       request_t               *request;       //!< The request this query relates to
+
+       fr_event_timer_t const  *ev;            //!< Event for timing out the query
+
+       char                    **referral_urls;        //!< Referral results to follow
+       fr_dlist_head_t         referrals;      //!< List of parsed referrals
+       uint16_t                referral_depth; //!< How many referrals we have followed
+       fr_ldap_referral_t      *referral;      //!< Referral actually being followed
+
+       LDAPMessage             *result;        //!< Head of LDAP results list.
+
+       fr_ldap_result_code_t   ret;            //!< Result code
+} fr_ldap_query_t;
+
+/** Parsed LDAP referral structure
+ *
+ * When LDAP servers respond with a referral, it is parsed into one or more fr_ldap_referral_t
+ * and kept until the referral has been followed.
+ * Avoids repeated parsing of the referrals as provided by libldap.
+ */
+typedef struct fr_ldap_referral_s {
+       fr_dlist_t              entry;          //!< Entry in list of possible referrals
+       fr_ldap_query_t         *query;         //!< Query this referral relates to
+       LDAPURLDesc             *referral_url;  //!< URL for the referral
+       char                    *host_uri;      //!< Host URI used for referral conneciton
+       char const              *identity;      //!< Bind identity for referral connection
+       char const              *password;      //!< Bind password for referral connecition
+       fr_ldap_thread_trunk_t  *ttrunk;        //!< Trunk this referral should use
+} fr_ldap_referral_t;
+
+
 /** Codes returned by fr_ldap internal functions
  *
  */
 typedef enum {
+       LDAP_PROC_REFERRAL = 2,                         //!< LDAP server returned referral URLs.
        LDAP_PROC_CONTINUE = 1,                         //!< Operation is in progress.
        LDAP_PROC_SUCCESS = 0,                          //!< Operation was successfull.
 
@@ -384,6 +521,36 @@ static inline void fr_ldap_berval_to_value_shallow(fr_value_box_t *value, struct
        fr_value_box_memdup_shallow(value, NULL, (uint8_t *)berval->bv_val, berval->bv_len, true);
 }
 
+/** Compare two ldap trunk structures on connection URI / DN
+ *
+ * @param[in] one      first connection to compare.
+ * @param[in] two      second connection to compare.
+ * @return CMP(one, two)
+ */
+static inline int8_t fr_ldap_trunk_cmp(void const *one, void const *two)
+{
+       fr_ldap_thread_trunk_t const    *a = one, *b = two;
+       int8_t uricmp = CMP(strcmp(a->uri, b->uri), 0);
+
+       if (uricmp !=0) return uricmp;
+       return CMP(strcmp(a->bind_dn, b->bind_dn), 0);
+}
+
+/** Compare two ldap query structures on msgid
+ *
+ * @param[in] one      first query to compare.
+ * @param[in] two      second query to compare.
+ * @return CMP(one,two)
+ */
+static inline int8_t fr_ldap_query_cmp(void const *one, void const *two)
+{
+       fr_ldap_query_t const   *a = one, *b = two;
+
+       return CMP(a->msgid, b->msgid);
+}
+
+fr_ldap_query_t *fr_ldap_query_alloc(TALLOC_CTX *ctx);
+
 /*
  *     ldap.c - Wrappers arounds OpenLDAP functions.
  */
@@ -423,6 +590,10 @@ fr_ldap_rcode_t    fr_ldap_modify(request_t *request, fr_ldap_connection_t **pconn,
                               char const *dn, LDAPMod *mods[],
                               LDAPControl **serverctrls, LDAPControl **clientctrls);
 
+fr_ldap_rcode_t        fr_ldap_modify_async(int *msgid, request_t *request, fr_ldap_connection_t **pconn,
+                              char const *dn, LDAPMod *mods[],
+                              LDAPControl **serverctrls, LDAPControl **clientctrls);
+
 fr_ldap_rcode_t        fr_ldap_error_check(LDAPControl ***ctrls, fr_ldap_connection_t const *conn,
                                    LDAPMessage *msg, char const *dn);
 
@@ -507,6 +678,12 @@ int                fr_ldap_connection_timeout_set(fr_ldap_connection_t const *conn, fr_time_de
 
 int            fr_ldap_connection_timeout_reset(fr_ldap_connection_t const *conn);
 
+fr_ldap_thread_trunk_t *fr_thread_ldap_trunk_get(fr_ldap_thread_t *thread, char const *uri,
+                                              char const *bind_dn, char const *bind_password,
+                                              request_t *request, fr_ldap_config_t const *config);
+
+fr_trunk_state_t fr_thread_ldap_trunk_state(fr_ldap_thread_t *thread, char const *uri, char const *bind_dn);
+
 /*
  *     state.c - Connection state machine
  */
@@ -556,3 +733,12 @@ uint8_t            *fr_ldap_berval_to_bin(TALLOC_CTX *ctx, struct berval const *in);
 
 int            fr_ldap_parse_url_extensions(LDAPControl **sss, request_t *request,
                                             fr_ldap_connection_t *conn, char **extensions);
+
+/*
+ *     referral.c - Handle LDAP referrals
+ */
+fr_ldap_referral_t     *fr_ldap_referral_alloc(TALLOC_CTX *ctx);
+
+int            fr_ldap_referral_follow(fr_ldap_query_t *query);
+
+int            fr_ldap_referral_next(fr_ldap_query_t *query);
index 410fb30d1548f13fb7b8a7d6abe479994f5a577f..49eb103405b4b340b5cfe87fc4e9ef94199e6946 100644 (file)
@@ -28,6 +28,23 @@ USES_APPLE_DEPRECATED_API
 #include <freeradius-devel/ldap/base.h>
 #include <freeradius-devel/util/debug.h>
 
+/*
+ *     Lookup of libldap result message types to meaningful strings
+ */
+static char const *ldap_msg_types[UINT8_MAX] = {
+       [LDAP_RES_BIND]                 = "bind response",
+       [LDAP_RES_SEARCH_ENTRY]         = "search entry",
+       [LDAP_RES_SEARCH_REFERENCE]     = "search reference",
+       [LDAP_RES_SEARCH_RESULT]        = "search result",
+       [LDAP_RES_MODIFY]               = "modify response",
+       [LDAP_RES_ADD]                  = "add response",
+       [LDAP_RES_DELETE]               = "delete response",
+       [LDAP_RES_MODDN]                = "modify dn response",
+       [LDAP_RES_COMPARE]              = "compare response",
+       [LDAP_RES_EXTENDED]             = "extended response",
+       [LDAP_RES_INTERMEDIATE]         = "intermediate response"
+};
+
 #if LDAP_SET_REBIND_PROC_ARGS == 3
 /** Callback for OpenLDAP to rebind and chase referrals
  *
@@ -443,7 +460,7 @@ fr_ldap_connection_t *fr_ldap_connection_alloc(TALLOC_CTX *ctx)
  */
 static fr_connection_state_t _ldap_connection_init(void **h, fr_connection_t *conn, void *uctx)
 {
-       fr_ldap_config_t const  *config = talloc_get_type_abort(uctx, fr_ldap_config_t);
+       fr_ldap_config_t const  *config = uctx;
        fr_ldap_connection_t    *c;
        fr_ldap_state_t         state;
 
@@ -549,3 +566,530 @@ int fr_ldap_connection_timeout_reset(fr_ldap_connection_t const *c)
 error:
        return -1;
 }
+
+/** Callback for closing idle LDAP trunk
+ *
+ */
+static void _ldap_trunk_idle_timeout(fr_event_list_t *el, UNUSED fr_time_t now, void *uctx)
+{
+       fr_ldap_thread_trunk_t  *ttrunk = talloc_get_type_abort(uctx, fr_ldap_thread_trunk_t);
+
+       if (ttrunk->trunk->req_alloc == 0) {
+               DEBUG2("Removing idle LDAP trunk to %s", ttrunk->uri);
+               talloc_free(ttrunk->trunk);
+               talloc_free(ttrunk);
+       } else {
+               /*
+                *      There are still pending queries - insert a new event
+                */
+               fr_event_timer_in(ttrunk->t, el, &ttrunk->ev, ttrunk->t->config->idle_timeout,
+                                 _ldap_trunk_idle_timeout, ttrunk);
+       }
+}
+
+/** Callback to cancel LDAP queries
+ *
+ * Inform the remote LDAP server that we no longer want responses to specific queries.
+ *
+ * @param[in] tconn    The trunk connection handle
+ * @param[in] conn     The specific connection queries will be cancelled on
+ * @param[in] uctx     Context provided to fr_trunk_alloc
+ */
+static void ldap_request_cancel_mux(fr_trunk_connection_t *tconn, fr_connection_t *conn, UNUSED void *uctx)
+{
+       fr_trunk_request_t      *treq;
+       fr_ldap_connection_t    *ldap_conn = talloc_get_type_abort(conn->h, fr_ldap_connection_t);
+       fr_ldap_query_t         *query;
+
+       while ((fr_trunk_connection_pop_cancellation(&treq, tconn)) == 0) {
+               query = treq->preq;
+               ldap_abandon_ext(ldap_conn->handle, query->msgid, NULL, NULL);
+               fr_rb_remove(ldap_conn->queries, query);
+
+               fr_trunk_request_signal_cancel_complete(treq);
+       }
+}
+
+
+/** I/O read function
+ *
+ * Underlying FD is now readable - call the trunk to read any pending requests.
+ *
+ * @param[in] el       The event list signalling.
+ * @param[in] fd       that's now readable.
+ * @param[in] flags    describing the read event.
+ * @param[in] uctx     The trunk connection handle.
+ */
+static void ldap_conn_readable(UNUSED fr_event_list_t *el, UNUSED int fd, UNUSED int flags, void *uctx)
+{
+       fr_trunk_connection_t   *tconn = talloc_get_type_abort(uctx, fr_trunk_connection_t);
+
+       fr_trunk_connection_signal_readable(tconn);
+}
+
+
+/** I/O write function
+ *
+ * Underlying FD is now writable - call the trunk to write any pending requests.
+ *
+ * @param[in] el       The event list signalling.
+ * @param[in] fd       that's now writable.
+ * @param[in] flags    describing the write event.
+ * @param[in] uctx     The trunk connection handle
+ */
+static void ldap_conn_writable(UNUSED fr_event_list_t *el, UNUSED int fd, UNUSED int flags, void *uctx)
+{
+       fr_trunk_connection_t   *tconn = talloc_get_type_abort(uctx, fr_trunk_connection_t);
+
+       fr_trunk_connection_signal_writable(tconn);
+}
+
+
+/** I/O error function
+ *
+ * The event loop signalled that a fatal error occurec on this connection.
+ *
+ * @param[in] el       The event list signalling.
+ * @param[in] fd       that errored.
+ * @param[in] flags    EL flags.
+ * @param[in] fd_errno The nature of the error.
+ * @param[in] uctx     The trunk connection handle
+ */
+static void ldap_conn_error(UNUSED fr_event_list_t *el, UNUSED int fd, UNUSED int flags, int fd_errno, void *uctx)
+{
+       fr_trunk_connection_t   *tconn = talloc_get_type_abort(uctx, fr_trunk_connection_t);
+
+       ERROR("rlm_ldap - Connection failed: %s", fr_syserror(fd_errno));
+
+       fr_connection_signal_reconnect(tconn->conn, FR_CONNECTION_FAILED);
+}
+
+/** Setup callbacks requested by LDAP trunk connections
+ *
+ * @param[in] tconn    Trunk handle.
+ * @param[in] conn     Individual connection callbacks are to be installed for.
+ * @param[in] el       The event list to install events in.
+ * @param[in] notify_on        The types of event the trunk wants to be notified on.
+ * @param[in] uctx     Context provided to fr_trunk_alloc.
+ */
+static void ldap_trunk_connection_notify(fr_trunk_connection_t *tconn, fr_connection_t *conn,
+                                        fr_event_list_t *el,
+                                        fr_trunk_connection_event_t notify_on, UNUSED void *uctx)
+{
+       fr_ldap_connection_t    *ldap_conn = talloc_get_type_abort(conn->h, fr_ldap_connection_t);
+       fr_event_fd_cb_t        read_fn = NULL;
+       fr_event_fd_cb_t        write_fn = NULL;
+       switch (notify_on) {
+       case FR_TRUNK_CONN_EVENT_NONE:
+               fr_event_fd_delete(el, ldap_conn->fd, FR_EVENT_FILTER_IO);
+               return;
+
+       case FR_TRUNK_CONN_EVENT_READ:
+               read_fn = ldap_conn_readable;
+               break;
+
+       case FR_TRUNK_CONN_EVENT_WRITE:
+               write_fn = ldap_conn_writable;
+               break;
+
+       case FR_TRUNK_CONN_EVENT_BOTH:
+               read_fn = ldap_conn_readable;
+               write_fn = ldap_conn_writable;
+               break;
+       }
+
+       if (fr_event_fd_insert(ldap_conn, el, ldap_conn->fd,
+                              read_fn,
+                              write_fn,
+                              ldap_conn_error,
+                              tconn) < 0) {
+               PERROR("Failed inserting FD event");
+               fr_trunk_connection_signal_reconnect(tconn, FR_CONNECTION_FAILED);
+       }
+}
+
+/** Allocate an LDAP trunk connection
+ *
+ * @param[in] tconn            Trunk handle.
+ * @param[in] el               Event list which will be used for I/O and timer events.
+ * @param[in] conn_conf                Configuration of the connnection.
+ * @param[in] log_prefix       What to prefix log messages with.
+ * @param[in] uctx             User context passed to fr_trunk_alloc.
+ */
+static fr_connection_t *ldap_trunk_connection_alloc(fr_trunk_connection_t *tconn, fr_event_list_t *el,
+                                                   UNUSED fr_connection_conf_t const *conn_conf,
+                                                   char const *log_prefix, void *uctx)
+{
+       fr_ldap_thread_trunk_t  *thread_trunk = talloc_get_type_abort(uctx, fr_ldap_thread_trunk_t);
+
+       return fr_ldap_connection_state_alloc(tconn, el, &thread_trunk->config, log_prefix);
+}
+
+#define POPULATE_LDAP_CONTROLS(_dest, _src) do { \
+       int i; \
+       for (i = 0; (_src[i].control) && (i < LDAP_MAX_CONTROLS); i++) { \
+               _dest[i] = _src[i].control; \
+       } \
+       _dest[i] = NULL; \
+} while (0)
+
+/** Take LDAP pending queries from the queue and send them.
+ *
+ * @param[in] el       Event list for timers.
+ * @param[in] tconn    Trunk handle.
+ * @param[in] conn     on which to send the queries
+ * @param[in] uctx     User context passed to fr_trunk_alloc
+ */
+static void ldap_trunk_request_mux(UNUSED fr_event_list_t *el, fr_trunk_connection_t *tconn,
+                                  fr_connection_t *conn, UNUSED void *uctx)
+{
+       fr_ldap_connection_t    *ldap_conn = talloc_get_type_abort(conn->h, fr_ldap_connection_t);
+       fr_trunk_request_t      *treq;
+
+       LDAPURLDesc             *referral_url = NULL;
+
+       fr_ldap_query_t         *query = NULL;
+       fr_ldap_rcode_t         status = LDAP_PROC_ERROR;
+
+       while (fr_trunk_connection_pop_request(&treq, tconn) == 0) {
+
+               LDAPControl     *our_serverctrls[LDAP_MAX_CONTROLS + 1];
+               LDAPControl     *our_clientctrls[LDAP_MAX_CONTROLS + 1];
+
+               if (!treq) break;
+
+               query = talloc_get_type_abort(treq->preq, fr_ldap_query_t);
+
+               switch (query->type) {
+               case LDAP_REQUEST_SEARCH:
+                       /*
+                        *      This query is a LDAP search
+                        */
+                       if (query->referral) referral_url = query->referral->referral_url;
+
+                       /*
+                        *      Queries can be from parsed URLs, if so point at the relevant
+                        *      parts of the parsed structure
+                        */
+                       if (query->ldap_url) {
+                               query->dn = query->ldap_url->lud_dn;
+                               memcpy(&query->search.attrs, &query->ldap_url->lud_attrs, sizeof(query->search.attrs));
+                               query->search.scope = query->ldap_url->lud_scope;
+                               query->search.filter = query->ldap_url->lud_filter;
+
+                               /*
+                                *      Parsing LDAP server extensions from the URL is only
+                                *      possible once we know which conneciton the query will be
+                                *      handled by as the conneciton handle is used by the parsing
+                                *      function.
+                                */
+                               if (query->ldap_url->lud_exts) {
+                                       LDAPControl     *serverctrls[LDAP_MAX_CONTROLS];
+                                       int             i;
+
+                                       if (fr_ldap_parse_url_extensions(serverctrls, query->request,
+                                                                        ldap_conn, query->ldap_url->lud_exts) < 0) {
+                                       error:
+                                               fr_trunk_request_signal_fail(query->treq);
+                                               return;
+                                       }
+                                       for (i = 0; i < LDAP_MAX_CONTROLS; i++) {
+                                               if (!serverctrls[i]) break;
+                                               query->serverctrls[i].control = serverctrls[i];
+                                               query->serverctrls[i].freeit = true;
+                                       }
+                               }
+                       }
+
+                       POPULATE_LDAP_CONTROLS(our_serverctrls, query->serverctrls);
+                       POPULATE_LDAP_CONTROLS(our_clientctrls, query->clientctrls);
+
+                       /*
+                        *      If we are chasing a referral, referral_url will be populated and may
+                        *      have a base dn or scope to override the original query
+                        */
+                       status = fr_ldap_search_async(&query->msgid, query->request, &ldap_conn,
+                                                     (referral_url && referral_url->lud_dn) ?
+                                                       referral_url->lud_dn : query->dn,
+                                                     (referral_url && referral_url->lud_scope) ?
+                                                       referral_url->lud_scope : query->search.scope,
+                                                     query->search.filter, query->search.attrs,
+                                                     our_serverctrls, our_clientctrls);
+                       break;
+
+               case LDAP_REQUEST_MODIFY:
+                       /*
+                        *      This query is an LDAP modification
+                        */
+                       POPULATE_LDAP_CONTROLS(our_serverctrls, query->serverctrls);
+                       POPULATE_LDAP_CONTROLS(our_clientctrls, query->clientctrls);
+
+                       status = fr_ldap_modify_async(&query->msgid, query->request, &ldap_conn, query->dn, query->mods, our_serverctrls, our_clientctrls);
+                       break;
+
+               default:
+                       ERROR("Invalid LDAP query for trunk connection");
+                       goto error;
+
+               }
+
+               if (status != LDAP_PROC_SUCCESS) goto error;
+
+               /*
+                *      Record which connection was used for this query
+                *      - results processing often needs access to an LDAP handle
+                */
+               query->ldap_conn = ldap_conn;
+
+               /*
+                *      Add the query to the tree of pending queries for this trunk
+                */
+               fr_rb_insert(query->ldap_conn->queries, query);
+
+               fr_trunk_request_signal_sent(treq);
+       }
+
+}
+
+/** Read LDAP responses
+ *
+ * Responses from the LDAP server will cause the fd to become readable and trigger this
+ * callback.  Most LDAP search responses have multiple messages in their response - we
+ * only gather those which are complete before either following a referral or passing
+ * the head of the resulting chain of messages back.
+ *
+ * @param[in] tconn    Trunk connection associated with these results.
+ * @param[in] conn     Connection handle for these results.
+ * @param[in] uctx     Thread specific trunk structure - contains tree of pending queries.
+ */
+static void ldap_trunk_request_demux(UNUSED fr_trunk_connection_t *tconn, fr_connection_t *conn, void *uctx)
+{
+       fr_ldap_connection_t    *ldap_conn = talloc_get_type_abort(conn->h, fr_ldap_connection_t);
+       fr_ldap_thread_trunk_t  *t = talloc_get_type_abort(uctx, fr_ldap_thread_trunk_t);
+
+       int                     ret = 0, msgtype;
+       struct timeval          poll = { 0, 10 };
+       LDAPMessage             *result = NULL;
+       fr_ldap_rcode_t         rcode;
+       fr_ldap_query_t         find = { .msgid = -1 }, *query = NULL;
+       request_t               *request;
+
+       /*
+        *  Reset the idle timeout event
+        */
+       fr_event_timer_in(t->t, t->t->el, &t->ev, t->t->config->idle_timeout, _ldap_trunk_idle_timeout, t);
+
+       do {
+               /*
+                *      Look for any results for which we have the complete result message
+                *      ldap_result will return a pointer to a chain of messages.
+                */
+               ret = ldap_result(ldap_conn->handle, LDAP_RES_ANY, LDAP_MSG_ALL, &poll, &result);
+
+               switch (ret) {
+               case 0:
+                       return;
+
+               case -1:
+                       rcode = fr_ldap_error_check(NULL, ldap_conn, NULL, NULL);
+                       if (rcode == LDAP_PROC_BAD_CONN) ERROR("Bad LDAP connection");
+                       return;
+
+               default:
+                       break;
+               }
+
+               find.msgid = ldap_msgid(result);
+               query = fr_rb_find(ldap_conn->queries, &find);
+
+               if (!query) {
+                       WARN("Ignoring msgid %i - doesn't match any outstanding queries (it may have been cancelled)",
+                             find.msgid);
+                       continue;
+               }
+
+               msgtype = ldap_msgtype(result);
+
+               /*
+                *      Request to reference in debug output
+                */
+               request = query->request;
+
+               ROPTIONAL(RDEBUG2, DEBUG2, "Got LDAP response of type \"%s\" for message %d",
+                         ldap_msg_types[msgtype], query->msgid);
+               rcode = fr_ldap_error_check(NULL, ldap_conn, result, query->dn);
+
+               switch (rcode) {
+               case LDAP_PROC_SUCCESS:
+                       query->ret = ((!query->mods) && (ldap_count_entries(ldap_conn->handle, result) == 0)) ?
+                                    LDAP_RESULT_NO_RESULT : LDAP_RESULT_SUCCESS;
+                       break;
+
+               case LDAP_PROC_REFERRAL:
+                       if (!t->t->config->chase_referrals) {
+                               ROPTIONAL(REDEBUG, ERROR,
+                                         "LDAP referral received but 'chase_referrals' is set to 'no'");
+                               query->ret = LDAP_RESULT_EXCESS_REFERRALS;
+                               break;
+                       }
+
+                       if (query->referral_depth >= t->t->config->referral_depth) {
+                               ROPTIONAL(REDEBUG, ERROR, "Maximum LDAP referral depth (%d) exceeded",
+                                         t->t->config->referral_depth);
+                               query->ret = LDAP_RESULT_EXCESS_REFERRALS;
+                               break;
+                       }
+
+                       /*
+                        *      If we've come here as the result of an existing referral
+                        *      clear the previous list of URLs before getting the next list.
+                        */
+                       if (query->referral_urls) ldap_memvfree((void **)query->referral_urls);
+
+                       ldap_get_option(ldap_conn->handle, LDAP_OPT_REFERRAL_URLS, &query->referral_urls);
+                       if (!(query->referral_urls) || (!(query->referral_urls[0]))) {
+                               ROPTIONAL(REDEBUG, ERROR, "LDAP referral missing referral URL");
+                               query->ret = LDAP_RESULT_MISSING_REFERRAL;
+                               break;
+                       }
+
+                       query->referral_depth ++;
+
+                       if (fr_ldap_referral_follow(query) == 0) {
+                       next_follow:
+                               ldap_msgfree(result);
+                               continue;
+                       }
+
+                       ROPTIONAL(REDEBUG, ERROR, "Unable to follow any LDAP referral URLs");
+                       query->ret = LDAP_RESULT_REFERRAL_FAIL;
+                       break;
+
+               case LDAP_PROC_BAD_DN:
+                       ROPTIONAL(RDEBUG2, DEBUG2, "DN %s does not exist", query->ldap_url->lud_dn);
+                       query->ret = LDAP_RESULT_BAD_DN;
+                       break;
+
+               default:
+                       ROPTIONAL(RPERROR, PERROR, "LDAP server returned an error");
+
+                       if (query->referral_depth > 0) {
+                               /*
+                                *      We're processing a referral - see if there are any more to try
+                                */
+                               fr_dlist_talloc_free_item(&query->referrals, query->referral);
+
+                               if ((fr_dlist_num_elements(&query->referrals) > 0) &&
+                                   (fr_ldap_referral_next(query) == 0)) goto next_follow;
+                       }
+
+                       query->ret = LDAP_RESULT_REFERRAL_FAIL;
+                       break;
+               }
+
+               /*
+                *      Remove the timeout event
+                */
+               if (query->ev) fr_event_timer_delete(&query->ev);
+
+               query->result = result;
+
+               /*
+                *      Remove the query from the outstanding list and tidy up
+                */
+               fr_rb_remove(ldap_conn->queries, query);
+               fr_trunk_request_signal_complete(query->treq);
+               if (query->request) unlang_interpret_mark_runnable(query->request);
+
+       } while (1);
+}
+
+/** Find a thread specific LDAP connection for a specific URI / bind DN
+ *
+ * If no existing connection exists for that combination then create a new one
+ *
+ * @param[in] thread           to which the connection belongs
+ * @param[in] uri              of the host to find / create a connection to
+ * @param[in] bind_dn          to make the connection as
+ * @param[in] bind_password    for making connection
+ * @param[in] request          currently being processed (only for debug messages)
+ * @return
+ *     - an existing or new connection matching the URI and bind DN
+ *     - NULL on failure
+ */
+fr_ldap_thread_trunk_t *fr_thread_ldap_trunk_get(fr_ldap_thread_t *thread, char const *uri,
+                                                char const *bind_dn, char const *bind_password,
+                                                request_t *request, fr_ldap_config_t const *config)
+{
+       fr_ldap_thread_trunk_t  *found, find = {.uri = uri, .bind_dn = bind_dn};
+
+       ROPTIONAL(RDEBUG2, DEBUG2, "Looking for LDAP connection to %s bound as %s", uri, bind_dn);
+       found = fr_rb_find(thread->trunks, &find);
+
+       if (found) return found;
+
+       /*
+        *      No existing connection matching the requirement - create a new one
+        */
+       ROPTIONAL(RDEBUG2, DEBUG2, "No existing connection found - creating new one");
+       found = talloc_zero(thread, fr_ldap_thread_trunk_t);
+
+       /*
+        *      Buld config for this connection - start with module settings and
+        *      override server and bind details
+        */
+       memcpy(&found->config, config, sizeof(fr_ldap_config_t));
+       found->config.server = talloc_strdup(found, uri);
+       found->config.admin_identity = talloc_strdup(found, bind_dn);
+       found->config.admin_password = talloc_strdup(found, bind_password);
+
+       found->uri = found->config.server;
+       found->bind_dn = found->config.admin_identity;
+
+       found->trunk = fr_trunk_alloc(found, thread->el,
+                                     &(fr_trunk_io_funcs_t){
+                                             .connection_alloc = ldap_trunk_connection_alloc,
+                                             .connection_notify = ldap_trunk_connection_notify,
+                                             .request_mux = ldap_trunk_request_mux,
+                                             .request_demux = ldap_trunk_request_demux,
+                                             .request_cancel_mux = ldap_request_cancel_mux
+                                       },
+                                     thread->trunk_conf,
+                                     "rlm_ldap", found, false);
+
+       if (!found->trunk) {
+               ROPTIONAL(REDEBUG, ERROR, "Unable to create LDAP connection");
+               talloc_free(found);
+               return NULL;
+       }
+
+       found->t = thread;
+
+       /*
+        *  Insert event to close trunk if it becomes idle
+        */
+       fr_event_timer_in(thread, thread->el, &found->ev, thread->config->idle_timeout,
+                         _ldap_trunk_idle_timeout, found);
+
+       fr_rb_insert(thread->trunks, found);
+
+       return found;
+}
+
+/** Lookup the state of a thread specific LDAP connection trunk for a specific URI / bind DN
+ *
+ * @param[in] thread           to which the connection belongs
+ * @param[in] uri              of the host to find / create a connection to
+ * @param[in] bind_dn          to make the connection as
+ * @return
+ *     - State of a trunk matching the URI and bind DN
+ *     - FR_TRUNK_STATE_MAX if no matching trunk
+ */
+fr_trunk_state_t fr_thread_ldap_trunk_state(fr_ldap_thread_t *thread, char const *uri, char const *bind_dn)
+{
+       fr_ldap_thread_trunk_t  *found, find = {.uri = uri, .bind_dn = bind_dn};
+
+       found = fr_rb_find(thread->trunks, &find);
+
+       return (found) ? found->trunk->state : FR_TRUNK_STATE_MAX;
+}
diff --git a/src/lib/ldap/referral.c b/src/lib/ldap/referral.c
new file mode 100644 (file)
index 0000000..5eb4e90
--- /dev/null
@@ -0,0 +1,340 @@
+/*
+ *   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 lib/ldap/referral.c
+ * @brief Functions to handle ldap referrals
+ *
+ * @author Nick Porter <nick.porter@networkradius.com>
+ * @copyright 2021 The FreeRADIUS Server Project.
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/ldap/base.h>
+
+
+/** Clear up a fr_ldap_referral_t
+ *
+ * If there is a parsed referral_url, that must be freed using libldap's ldap_free_urldesc
+ */
+static int _fr_ldap_referral_free(fr_ldap_referral_t *referral)
+{
+        if (referral->referral_url) ldap_free_urldesc(referral->referral_url);
+        return 0;
+}
+
+/** Allocate a new structure to handle an LDAP referral, setting the destructor
+ *
+ * @param[in] ctx      to allocate the referral in
+ * @return
+ *     - a new referral structure on success
+ *     - NULL on failure
+ */
+fr_ldap_referral_t *fr_ldap_referral_alloc(TALLOC_CTX *ctx)
+{
+       fr_ldap_referral_t      *referral;
+
+       referral = talloc_zero(ctx, fr_ldap_referral_t);
+       if (!referral) {
+               PERROR("Failed to allocate LDAP referral container");
+               return NULL;
+       }
+       talloc_set_destructor(referral, _fr_ldap_referral_free);
+
+       return referral;
+}
+
+/** Callback to send LDAP referral queries when a trunk becomes active
+ *
+ */
+static void _ldap_referral_send (UNUSED fr_trunk_t *trunk, UNUSED fr_trunk_state_t prev,
+                               UNUSED fr_trunk_state_t state, void *uctx)
+{
+       fr_ldap_referral_t      *referral = talloc_get_type_abort(uctx, fr_ldap_referral_t);
+       fr_ldap_query_t         *query = referral->query;
+       request_t               *request = query->request;
+
+       /*
+        *      If referral is set, then another LDAP trunk has gone active first and sent the referral
+        */
+       if (query->referral) return;
+
+       /*
+        *      Enqueue referral query on active trunk connection
+        */
+       query->referral = referral;
+       query->ttrunk = referral->ttrunk;
+       query->treq = fr_trunk_request_alloc(query->ttrunk->trunk, query->request);
+       fr_trunk_request_enqueue(&query->treq, query->ttrunk->trunk, query->request, query, NULL);
+
+       RDEBUG4("Pending LDAP referral query queued on active trunk");
+}
+
+
+/** Follow an LDAP referral
+ *
+ * The returned list of LDAP referrals should already be in query->referrals.
+ * We check all the possible referrals and look for one where there already
+ * is an active trunk connection.
+ *
+ * @param query        whose result was one or more referral URLs
+ * @return
+ *     - 0 on success.
+ *     - < 0 on failure.
+ */
+int fr_ldap_referral_follow(fr_ldap_query_t *query)
+{
+       request_t               *request = query->request;
+       fr_ldap_config_t        *handle_config = query->ttrunk->t->config;
+       fr_ldap_thread_t        *thread = query->ttrunk->t;
+       fr_ldap_thread_trunk_t  *ttrunk = NULL;
+       int                     referral_no = -1;
+       fr_ldap_referral_t      *referral;
+
+       /*
+        *      In following a referral, firstly remove the query from the
+        *      tree of pending queries clear the message id.
+        */
+       fr_rb_remove(query->ldap_conn->queries, query);
+       query->msgid = 0;
+       fr_trunk_request_signal_complete(query->treq);
+       query->treq = NULL;
+
+       if (query->referral_depth > 1) {
+       /*
+        *      If we've already parsed a referral, clear the existing list of followers.
+        */
+               fr_dlist_talloc_free(&query->referrals);
+               query->referral = NULL;
+       } else {
+       /*
+        *      Otherwise initialise the list header for followers.
+        */
+               fr_dlist_talloc_init(&query->referrals, fr_ldap_referral_t, entry);
+       }
+
+       while (query->referral_urls[++referral_no]) {
+               if (!ldap_is_ldap_url(query->referral_urls[referral_no])) {
+                       RERROR("Referral %s does not look like an LDAP URL", query->referral_urls[referral_no]);
+                       continue;
+               }
+
+               referral = fr_ldap_referral_alloc(query);
+               if (!referral) continue;
+
+               referral->query = query;
+
+               if (ldap_url_parse(query->referral_urls[referral_no], &referral->referral_url)) {
+                       RERROR("Failed parsing referral LDAP URL %s", query->referral_urls[referral_no]);
+               free_referral:
+                       talloc_free(referral);
+                       continue;
+               }
+
+               referral->host_uri = talloc_asprintf(referral, "%s://%s:%d", referral->referral_url->lud_scheme,
+                                                    referral->referral_url->lud_host, referral->referral_url->lud_port);
+
+               if (handle_config->use_referral_credentials) {
+                       char    **ext;
+
+                       /*
+                        *      If there are no extensions, OpenLDAP doesn't
+                        *      bother allocating an array.
+                        */
+                       for (ext = referral->referral_url->lud_exts; ext && *ext; ext++) {
+                               char const *p;
+                               bool critical = false;
+
+                               p = *ext;
+
+                               if (*p == '!') {
+                                       critical = true;
+                                       p++;
+                               }
+
+                               /*
+                                *      LDAP Parse URL unescapes the extensions for us
+                                */
+                               switch (fr_table_value_by_substr(fr_ldap_supported_extensions, p, -1, LDAP_EXT_UNSUPPORTED)) {
+                               case LDAP_EXT_BINDNAME:
+                                       p = strchr(p, '=');
+                                       if (!p) {
+                                       bad_ext:
+                                               RERROR("Failed parsing extension \"%s\": "
+                                                      "No attribute/value delimiter '='", *ext);
+                                               goto free_referral;
+                                       }
+                                       referral->identity = p + 1;
+                                       break;
+
+                               case LDAP_EXT_BINDPW:
+                                       p = strchr(p, '=');
+                                       if (!p) goto bad_ext;
+                                       referral->password = p + 1;
+                                       break;
+
+                               default:
+                                       if (critical) {
+                                               RERROR("Failed parsing critical extension \"%s\": "
+                                                      "Not supported by FreeRADIUS", *ext);
+                                               goto free_referral;
+                                       }
+                                       DEBUG2("Skipping unsupported extension \"%s\"", *ext);
+                                       continue;
+                               }
+                       }
+               } else {
+                       if (handle_config->rebind) {
+                               referral->identity = handle_config->admin_identity;
+                               referral->password = handle_config->admin_password;
+                       }
+               }
+
+               fr_dlist_insert_tail(&query->referrals, referral);
+               if (fr_thread_ldap_trunk_state(thread, referral->host_uri,
+                                              referral->identity) != FR_TRUNK_STATE_ACTIVE) {
+                       RDEBUG3("No active LDAP trunk for URI %s, bind DN %s", referral->host_uri, referral->identity);
+                       continue;
+               }
+
+               ttrunk = fr_thread_ldap_trunk_get(thread, referral->host_uri, referral->identity,
+                                                 referral->password, request, handle_config);
+
+               if (!ttrunk) {
+                       RERROR("Unable to connect to LDAP referral URL");
+                       fr_dlist_talloc_free_item(&query->referrals, referral);
+                       continue;
+               }
+
+               /*
+                *      We have an active trunk enqueue the request
+                */
+               query->ttrunk = ttrunk;
+               query->referral = referral;
+               query->treq = fr_trunk_request_alloc(ttrunk->trunk, request);
+               fr_trunk_request_enqueue(&query->treq, ttrunk->trunk, request, query, NULL);
+               return 0;
+       }
+
+       /*
+        *      None of the referrals parsed successfully
+        */
+       if (fr_dlist_num_elements(&query->referrals) == 0) {
+               RERROR("No valid LDAP referrals to follow");
+               return -1;
+       }
+
+       /*
+        *      We have parsed referrals, but none of them matched an existing active connection.
+        *      Launch new trunks with callbacks so the first to become active will run the query.
+        */
+       referral = NULL;
+       while ((referral = fr_dlist_next(&query->referrals, referral))) {
+               ttrunk = fr_thread_ldap_trunk_get(thread, referral->host_uri, referral->identity,
+                                                referral->password, request, handle_config);
+               if (!ttrunk) {
+                       fr_dlist_talloc_free_item(&query->referrals, referral);
+                       continue;
+               }
+               referral->ttrunk = ttrunk;
+               fr_trunk_add_watch(ttrunk->trunk, FR_TRUNK_STATE_ACTIVE, _ldap_referral_send, true, referral);
+               RDEBUG4("Watch inserted to send referral query on active trunk.");
+       }
+
+       return 0;
+}
+
+/** Follow an alternative LDAP referral
+ *
+ * If an initial chase of an LDAP referral results in an error being returned
+ * this function can be used to attempt one of the other referral URLs given
+ * in the initial query results.
+ *
+ * The initial use of fr_ldap_referral_follow may have launched trunks for
+ * any referral URLs which parsed successfully, so this starts by looking
+ * for the first which has an active state and sends the query that way.
+ *
+ * If no active trunks match the remaining servers listed in referrals then
+ * new trunks are launched with watchers to send the query on the first
+ * active trunk.
+ *
+ * @param query whose referrals are being chased
+ * @return
+ *     - 0 on success.
+ *     - < 0 on failure.
+ */
+int fr_ldap_referral_next(fr_ldap_query_t *query)
+{
+        request_t              *request = query->request;
+       fr_ldap_config_t        *handle_config = query->ttrunk->t->config;
+       fr_ldap_thread_t        *thread = query->ttrunk->t;
+       fr_ldap_referral_t      *referral = NULL;
+       fr_ldap_thread_trunk_t  *ttrunk;
+
+       while ((referral = fr_dlist_next(&query->referrals, referral))) {
+               if (fr_thread_ldap_trunk_state(thread, referral->host_uri,
+                                              referral->identity) != FR_TRUNK_STATE_ACTIVE) {
+                       RDEBUG3("No active LDAP trunk for URI %s, bind DN %s", referral->host_uri, referral->identity);
+                       continue;
+               }
+
+               ttrunk = fr_thread_ldap_trunk_get(thread, referral->host_uri, referral->identity,
+                                                 referral->password, request, handle_config);
+
+               if (!ttrunk) {
+                       RERROR("Unable to connect to LDAP referral URL");
+                       fr_dlist_talloc_free_item(&query->referrals, referral);
+                       continue;
+               }
+
+               /*
+                *      We have an active trunk enqueue the request
+                */
+               query->ttrunk = ttrunk;
+               query->referral = referral;
+               query->treq = fr_trunk_request_alloc(ttrunk->trunk, request);
+               fr_trunk_request_enqueue(&query->treq, ttrunk->trunk, request, query, NULL);
+               return 0;
+       }
+
+       /*
+        *      None of the referrals parsed successfully
+        */
+       if (fr_dlist_num_elements(&query->referrals) == 0) {
+               RERROR("No valid LDAP referrals to follow");
+               return -1;
+       }
+
+       /*
+        *      None of the remaining referrals have an active trunk.
+        *      Launch new trunks with callbacks so the first to become active will run the query.
+        */
+       referral = NULL;
+       while ((referral = fr_dlist_next(&query->referrals, referral))) {
+               ttrunk = fr_thread_ldap_trunk_get(thread, referral->host_uri, referral->identity,
+                                                 referral->password, request, handle_config);
+               if (!ttrunk) {
+                       fr_dlist_talloc_free_item(&query->referrals, referral);
+                       continue;
+               }
+               referral->ttrunk = ttrunk;
+               fr_trunk_add_watch(ttrunk->trunk, FR_TRUNK_STATE_ACTIVE, _ldap_referral_send, true, referral);
+               RDEBUG4("Watch inserted to send referral query on active trunk.");
+       }
+
+       return 0;
+}
index 777755077b6767a1065c075e3691330062419891..737b776289934ff5c050b9a2d61669235b77b110 100644 (file)
@@ -145,6 +145,8 @@ static CONF_PARSER option_config[] = {
 
        { FR_CONF_OFFSET("use_referral_credentials", FR_TYPE_BOOL, rlm_ldap_t, handle_config.use_referral_credentials), .dflt = "no" },
 
+       { FR_CONF_OFFSET("referral_depth", FR_TYPE_UINT16, rlm_ldap_t, handle_config.referral_depth), .dflt = "5" },
+
        { FR_CONF_OFFSET("rebind", FR_TYPE_BOOL, rlm_ldap_t, handle_config.rebind) },
 
        { FR_CONF_OFFSET("sasl_secprops", FR_TYPE_STRING, rlm_ldap_t, handle_config.sasl_secprops) },
@@ -179,6 +181,8 @@ static CONF_PARSER option_config[] = {
        /* timeout for search results */
        { FR_CONF_OFFSET("res_timeout", FR_TYPE_TIME_DELTA, rlm_ldap_t, handle_config.res_timeout), .dflt = "20" },
 
+       { FR_CONF_OFFSET("idle_timeout", FR_TYPE_TIME_DELTA, rlm_ldap_t, handle_config.idle_timeout), .dflt = "300" },
+
        CONF_PARSER_TERMINATOR
 };
 
@@ -1559,6 +1563,45 @@ static int parse_sub_section(rlm_ldap_t *inst, CONF_SECTION *parent, ldap_acct_s
        return 0;
 }
 
+/** Initialise thread specific data structure
+ *
+ */
+static int mod_thread_instatiate(UNUSED CONF_SECTION const *conf, void *instance,
+                                fr_event_list_t *el, void *thread)
+{
+       rlm_ldap_t              *inst = instance;
+       fr_ldap_thread_t        *this_thread = thread;
+
+       /*
+        *      Initialise tree for connection trunks used by this thread
+        */
+       MEM(this_thread->trunks = fr_rb_inline_talloc_alloc(this_thread, fr_ldap_thread_trunk_t, node, fr_ldap_trunk_cmp, NULL));
+
+       this_thread->inst = inst;
+       this_thread->config = &inst->handle_config;
+       this_thread->el = el;
+
+       return 0;
+}
+
+/** Clean up thread specific data structure
+ *
+ */
+static int mod_thread_detach(UNUSED fr_event_list_t *el, void *thread)
+{
+       fr_ldap_thread_t        *this_thread = thread;
+       fr_ldap_thread_trunk_t  *ttrunk;
+       fr_rb_iter_preorder_t   iter;
+
+       for (ttrunk = fr_rb_iter_init_preorder(&iter, this_thread->trunks);
+            ttrunk;
+            ttrunk = fr_rb_iter_next_preorder(&iter)) {
+               talloc_free(ttrunk->trunk);
+            }
+       talloc_free(this_thread->trunks);
+       return 0;
+}
+
 /** Bootstrap the module
  *
  * Define attributes.
@@ -2139,6 +2182,10 @@ module_t rlm_ldap = {
        .bootstrap      = mod_bootstrap,
        .instantiate    = mod_instantiate,
        .detach         = mod_detach,
+       .thread_inst_size       = sizeof(fr_ldap_thread_t),
+       .thread_inst_type       = "fr_ldap_thread_t",
+       .thread_instantiate     = mod_thread_instatiate,
+       .thread_detach          = mod_thread_detach,
        .methods = {
                [MOD_AUTHENTICATE]      = mod_authenticate,
                [MOD_AUTHORIZE]         = mod_authorize,