Ondřej, hello.
On 20 Jan 2023, at 10:47, Ondřej Kuzník wrote:
> That said, patches implementing some kind of SRV are welcome. The easiest
> way might be to introduce an lloadd tier implementation that manages its
> backend collection accordingly.
It's not an OpenLDAP patch, but I've attached a module which might be of
interest here. This exposes a function
char* get_sorted_srv_records(const char* domain);
which does a SRV lookup, and orders the records that come back according to the
specification of RFC 2782 (though in a single pass, rather than the clumsy
multiple pass algorithm that the RFC suggests).
Best wishes,
Norman
--
Norman Gray : https://nxg.me.uk
// DNS support -- return information about LDAP servers,
// by looking up a SRV record.
//
// SRV records, as discussed in RFC 2782, point to locations of
// services. In the words of that RFC,
//
// If a SRV-cognizant LDAP client wants to discover a LDAP server that
// supports TCP protocol and provides LDAP service for the domain
// example.com., it does a lookup of
//
// _ldap._tcp.example.com
//
// The get_sorted_srv_records() function below will take a domain example.com,
// prepend the "_ldap._tcp",
// do the lookup to obtain the SRV record,
// and return a string containing the hosts and ports,
// sorted as described in the RFC.
//
// It's unexpectedly hard to find clear documentation about DNS lookups,
// but the following does seem to be portable,
// though without much in the way of intelligible manpages on any platform.
//
// See:
// Scrappy documentation:
https://docstore.mik.ua/orelly/networking_2ndEd/dns/ch15_02.htm
// Wikipedia: https://en.wikipedia.org/wiki/List_of_DNS_record_types
// RFC 1035, Domain names -- implementation and specification
//
// This module exposes a function
//
// char* get_sorted_srv_records(const char* domain);
//
// which returns a space-separated list of ldap:// URIs obtained from
// the SRV record corresponding to the given domain, in a random order
// appropriate to the weights obtained.
//
// This can be compiled as a standalone (test?) program; see STANDALONE below.
// CentOS features.h requires _BSD_SOURCE in order to get types.h to define
u_char,
// which resolv.h refers to.
#define _BSD_SOURCE 1
// Debian prefers _DEFAULT_SOURCE, and issues a deprecation warning
// if _BSD_SOURCE is defined and _DEFAULT_SOURCE isn't.
#define _DEFAULT_SOURCE 1
// ... and _XOPEN_SOURCE to get getopt
// (on eg CentOS, this also happens with _POSIX_C_SOURCE >= 2, but
// that _prevents_ macOS types.h from defining u_char).
#define _XOPEN_SOURCE 1
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
#include "config.h"
// See the docs for autoconf AC_HEADER_RESOLV
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#ifdef HAVE_NETINET_IN_H
# include <netinet/in.h> /* inet_ functions / structs */
#endif
#ifdef HAVE_ARPA_NAMESER_H
# include <arpa/nameser.h> /* DNS HEADER struct */
#endif
#ifdef HAVE_NETDB_H
# include <netdb.h>
#endif
#include <resolv.h>
// There is a test main program at the bottom of this file.
// To compile:
// cc -Wall -std=c99 -o dns-support -DSTANDALONE=1 -lresolv dns-support.c
//
#ifndef STANDALONE
#define STANDALONE 0
#endif
#if STANDALONE
#include <stdarg.h>
#include <string.h>
// dummy out functions provided by the other modules in this kit
static int _chatter = 2;
int get_verbosity(void) { return _chatter; }
void change_verbosity(int inc) { _chatter += inc; }
void log_err_message(void* dummy, const char* fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
#else
// we call get_verbosity and log_err_message
#include "ldaputil.h"
#include "ldap-config.h"
#endif
typedef unsigned char byte;
// Return a uniform random deviate in [0,1)
//
// random(3) returns a number in [0, 2^31-1].
// We convert this to a double by dividing it by 2^31 exactly.
// Thus the return value is always strictly less than one.
static double random_f(void)
{
static byte initialised_p = 0;
static double denom;
if (! initialised_p) {
srandom(time(NULL));
denom = ldexp(1, 31);
initialised_p = 1;
// Explore a few properties of 'denom'
// int exp;
// double mantissa = frexp(denom, &exp);
// printf("%g -> %f.2^%d; ", denom, mantissa, exp);
// double two31 = (double)2147483647; // 2^31-1 as an integer
// mantissa = frexp(two31, &exp);
// printf("%g -> %f.2^%d; ", two31, mantissa, exp);
// printf("ratio = %g, less than one? %s\n",
// two31/denom, two31/denom < 1.0 ? "yes" : "no");
}
double univariate = (double)random();
return univariate / denom;
}
// Uncompress a domain-name.
// The returned pointer points to storage which is reused on each invocation.
static const char* uncompress(ns_msg msg, const byte* p, int* skiplen_p)
{
static char ans[NS_MAXDNAME];
int skiplen = ns_name_uncompress(ns_msg_base(msg), ns_msg_end(msg),
p,
ans, sizeof(ans));
if (skiplen_p != NULL) *skiplen_p = skiplen;
return (skiplen >= 0 ? ans : NULL);
}
typedef struct srv_result_s {
char host[NS_MAXDNAME+1];
unsigned int port;
unsigned int priority;
float order;
} srv_result;
static int srv_result_compar(const void* a_v, const void* b_v)
{
srv_result* a = (srv_result*)a_v;
srv_result* b = (srv_result*)b_v;
if (a->priority == b->priority) {
return a->order < b->order ? -1 : +1;
} else {
return a->priority - b->priority;
}
}
/*
* For a given domain, retrieve a sequence of SRV records,
* sort them, and display the result.
*
* RFC 2782
*
* The SRV record is defined (textually) to be (priority, weight,
* port, domain-name), where the first three are
* two-byte integers. The domain-name is not to be compressed,
* according to the RFC, 'unless and until permitted by future
* standards action', but uncompress doesn't seem to mind, and it
* seems future-proof.
*
* See the RFC for other observations on how best to use SRV
* records, as both a server and as a client. The RFC says,
* of the host selection algorithm:
*
* Priority
* The priority of this target host. A client MUST attempt to
* contact the target host with the lowest-numbered priority it can
* reach; target hosts with the same priority SHOULD be tried in an
* order defined by the weight field. The range is 0-65535. This
* is a 16 bit unsigned integer in network byte order.
*
* Weight
* A server selection mechanism. The weight field specifies a
* relative weight for entries with the same priority. Larger
* weights SHOULD be given a proportionately higher probability of
* being selected. The range of this number is 0-65535. This is a
* 16 bit unsigned integer in network byte order. Domain
* administrators SHOULD use Weight 0 when there isn't any server
* selection to do, to make the RR easier to read for humans (less
* noisy). In the presence of records containing weights greater
* than 0, records with weight 0 should have a very small chance of
* being selected.
*
* In the absence of a protocol whose specification calls for the
* use of other weighting information, a client arranges the SRV
* RRs of the same Priority in the order in which target hosts,
* specified by the SRV RRs, will be contacted.
*
* The algorithm there is rather cumbersome.
*
* Instead, for each host, i, record the priority and an ordering
* parameter which is drawn from an exponential distribution with
* parameter weight_i (which we can obtain with
* -log(1-uniform())/weight_i, where uniform() is a uniform random
* deviate in [0,1)). The minimum of an ensemble of such random
* variables is item i, with probability weight_i/(sum_j(weight_j)).
* See eg
<https://en.wikipedia.org/wiki/Exponential_distribution#Distribution_of_the_minimum_of_exponential_random_variables>.
*
* With this done, sort the records. The comparator function orders
* host with lower priority first, and when two hosts have equal
* priority, it orders first the one with the lower ordering parameter.
*/
char* get_sorted_srv_records(const char* domain)
{
byte buf[NS_PACKETSZ];
size_t retlen = 0; // length of return string
int chatter = get_verbosity();
char ldapdomain[NS_MAXDNAME+1];
snprintf(ldapdomain, NS_MAXDNAME, "_ldap._tcp.%s", domain);
ldapdomain[NS_MAXDNAME] = '\0'; // just in case
errno = 0;
int res_len = res_query(ldapdomain, ns_c_in, ns_t_srv, buf, sizeof(buf));
if (res_len < 0) {
if (errno != 0) {
log_err_message(NULL, "res_query: error %s\n", strerror(errno));
}
return NULL;
}
ns_msg msg;
ns_rr rr;
ns_initparse(buf, res_len, &msg);
int nmsg = ns_msg_count(msg, ns_s_an);
srv_result* srv = (srv_result*)malloc(nmsg * sizeof(srv_result));
if (srv == NULL) {
log_err_message(NULL, "Unable to allocate %zu\n", nmsg *
sizeof(srv_result));
return NULL;
}
if (chatter > 2) fprintf(stderr, "SRV for %s:\n", domain);
for (int recnum=0; recnum<nmsg; recnum++) {
if (ns_parserr(&msg, ns_s_an, recnum, &rr)) {
log_err_message(NULL, "ns_parserr: %s\n", strerror(errno));
return NULL;
}
const char* domainname = uncompress(msg, ns_rr_rdata(rr)+6, NULL);
if (domainname) {
const byte* prefs = ns_rr_rdata(rr);
strncpy(srv[recnum].host, domainname, NS_MAXDNAME);
srv[recnum].host[NS_MAXDNAME] = '\0'; // just in case
// add 7 for "ldap://", 5 for portnum, 1 for space
retlen += strlen(domainname) + 7 + 5 + 1;
srv[recnum].priority = ns_get16(prefs);
int16_t weight = ns_get16(prefs+2);
if (weight == 0) {
// This should only happen when there is no
// weight-based selection to do, but insert a
// placeholder and very high value, in case this has
// been bungled.
srv[recnum].order = 1e100;
} else {
// log(random_f()) would also work, but using
// (1-random_f()) guarantees that we will never try to
// evaluate log(0) (yes, I know that would be unlikely).
//
// Also, choose the log function appropriately, if the
// argument is likely to be close to 1 (yes, this
// is a degree of numerical care that is quite
// unnecessary for this particular application).
double r = random_f(); // in [0,1)
double lograndom = (r < 0.5) ? log1p(-r) : log(1-r);
srv[recnum].order = -lograndom/weight;
}
srv[recnum].port = ns_get16(prefs+4);
if (chatter > 2) {
fprintf(stderr, "%30s:%d\t%d %d -> %f\n",
srv[recnum].host, srv[recnum].port,
srv[recnum].priority,
weight, srv[recnum].order);
}
} else {
log_err_message(NULL, "failed to uncompress domainname! %s\n",
strerror(errno));
return NULL;
}
}
qsort(srv, nmsg, sizeof(srv_result), srv_result_compar);
char* rbuf = (char*)malloc(retlen);
if (rbuf == NULL) {
log_err_message(NULL, "Can't allocate %zd!\n", retlen);
return NULL;
}
if (chatter > 2) {
fprintf(stderr, "Order:\n");
for (int recnum=0; recnum<nmsg; recnum++) {
fprintf(stderr, " %d: %s:%d\n",
recnum, srv[recnum].host, srv[recnum].port);
}
}
char* p = rbuf;
for (int recnum=0; recnum<nmsg; recnum++) {
size_t l = sprintf(p, "ldap://%s:%d ",
srv[recnum].host, srv[recnum].port);
p += l;
}
p--;
*p = '\0';
return rbuf;
}
#if STANDALONE
// This is essentially a test program, but it might be useful in some other way.
static const char* progname;
static void Usage(void)
{
fprintf(stderr, "Usage: %s [-v] domain\n", progname);
exit(1);
}
int main(int argc, char** argv)
{
progname = argv[0];
res_init();
const char* domain = NULL;
for (argc--, argv++; argc>0; argc--, argv++) {
if (**argv == '-') {
while (*++*argv) {
switch (**argv) {
case 'v':
change_verbosity(+1);
break;
default:
Usage();
}
}
} else {
if (domain) Usage();
domain = *argv;
}
}
if (domain == NULL) Usage();
char* ldap_search_list = get_sorted_srv_records(domain);
printf("%s\n", ldap_search_list);
exit(0);
}
#endif