Project sponsored by
Project hosted on

The LDAP driver is obviously meant to be used in combination with a LDAP database. This driver has been tested on Windows 2K and Redhat Linux 7.2 using only OpenLDAP 2.1.22. Older versions of the LDAP library may work but have not been tested. The driver should build properly on any UN*X system that BIND and OpenLDAP both support. Be sure to specify --with-dlz-ldap when running configure so that the LDAP driver is built with BIND. By default, DLZ and its drivers are not built. When you specify a DLZ driver, the DLZ core is automatically built, too. The OpenLDAP libraries and header files are required to build the driver and are not included in this package. You can download the LDAP source from www.openldap.org. The configure script will search in the normal locations for the LDAP headers and libraries. You can specify the appropriate directory if it is not found by appending a path to the LDAP configure option. For example: --with-dlz-ldap=/path/to/files

The LDAP driver was built to be as flexible as possible. Most "drivers" tend to require a specific LDAP schema for the driver. This then requires modification to the driver to support any variation of the schema, introducing new problems and limiting the usefulness and adoption of the driver. This driver has been built specifically to avoid that problem. Instead of imposing a LDAP schema, the driver accepts LDAP queries as URL's with a few special tokens as parameters. These queries are then parsed and the tokens removed. When a query is run, the token is replaced with the appropriate value. This allows a variety of LDAP schemas to be used without modification to the driver's code. The only limitations are:

1. The query must return the appropriate data types (attributes) in the correct order.
2. The query must use the correct pre-defined tokens.
3. The query must be written so that when the tokens are replaced with search values, it is still valid.
4. This LDAP driver does NOT handle multi-valued attributes.

Below is a sample of a proper dlz_ldap_driver configuration. This configuration segment would be contained in BIND's config (named.conf) file, and is explained more below. The sample uses a custom schema developed for DLZ. Use of this schema by the LDAP driver is not required, although it may be easier to use the DLZ schema than create your own. The schema is distributed with the DLZ download and called dlz.schema.

This configuration sample has the long LDAP URL queries broken over multiple lines for readability on this page. When creating a real configuration, each LDAP URL should be on a single line with no spaces in any part of the URL.

When you are developing your own LDAP driver queries, be sure to pass the following parameters to BIND "-g -d 1". The first "-g" tells BIND to write all log messages to stdout instead of a log file. The second parameter "-d 1" sets BIND's debug level to 1. The LDAP driver will print each query it is about to run when the debug level is set to at least 1. This can be very helpful in debugging queries while you are setting up the driver. The queries will be printed only when BIND is trying to answer DNS queries, not when BIND loads. Run a few sample DNS queries to see the queries that are used.

    dlz "ldap zone" {
            database "ldap 2
            v3 simple {cn=Manager,o=bind-dlz} {secret} {192.168.2.12 192.168.2.14}
            ldap:///dlzZoneName=%zone%,ou=dns,o=bind-dlz???objectclass=dlzZone
            ldap:///dlzHostName=%record%,dlzZoneName=%zone%,ou=dns,o=bind-dlz?
                   dlzTTL,dlzType,dlzPreference,dlzData,dlzIPAddr?sub?
                   (&(objectclass=dlzAbstractRecord)(!(dlzType=soa)))
            ldap:///dlzHostName=@,dlzZoneName=%zone%,ou=dns,o=bind-dlz?
                   dlzTTL,dlzType,dlzData,dlzPrimaryNS,dlzAdminEmail,dlzSerial,
                   dlzRefresh,dlzRetry,dlzExpire,dlzMinimum?sub?
                   (&(objectclass=dlzAbstractRecord)(dlzType=soa))
            ldap:///dlzZoneName=%zone%,ou=dns,o=bind-dlz?
                   dlzTTL,dlzType,dlzHostName,dlzPreference,dlzData,dlzIPAddr,
                   dlzPrimaryNS,dlzAdminEmail,dlzSerial,dlzRefresh,dlzRetry,
                   dlzExpire,dlzMinimum?sub?
                   (&(objectclass=dlzAbstractRecord)(!(dlzType=soa)))
            ldap:///dlzZoneName=%zone%,ou=dns,o=bind-dlz??sub?
                   (&(objectclass=dlzXFR)(dlzIPAddr=%client%))";
    };

The first line: dlz "ldap zone" {

This line tells BIND we want to use a DLZ driver. The word "dlz" is a new BIND keyword added by the DLZ patch. The next section "ldap zone" is the label for this configuration segment. It is used in any error messages BIND displays while parsing its config file. The last piece "{" starts the DLZ configuration section in BIND's config file.

The second line: database "ldap 2

This line is indented just to make it easier to read the configuration file. The keyword "database" is the only parameter that can be specified in a DLZ configuration segment. It is required. The double quote (") begins the command line that is passed to the DLZ driver--in this case, the LDAP driver. Notice that the command line is actually broken over many lines. This is done to make the configuration segment easier to read. The next piece is the word "ldap". This is the official name of the DLZ LDAP driver. We are telling BIND that we want to use the LDAP driver. The word "ldap" is located at argv[0]. I.E. This is the command line array passed to the driver, and the driver name must always be at argv[0]; it is not optional. The next piece on the line is the number "2". This parameter is located at argv[1] and is required. This tells BIND how many database connections we want the LDAP driver to use. When BIND is built single threaded, this parameter is ignored BUT STILL REQUIRED! The LDAP driver only uses one connection when built for single threaded operation. When built for multithreaded operation, the driver will open the specified number of connections and ALWAYS keep them open. The driver does NOT release connections during periods of inactivity and then re-open them when needed. I considered this approach and decided against it. A DNS server should always be ready to answer an onslaught of queries with all of its resources available.

The third line: v3 simple {cn=Manager,o=bind-dlz} {secret} {192.168.2.12 192.168.2.14}

This specifies LDAP connection parameters; it is required. Notice some of the contents on this line are surrounded by braces "{" and "}". Braces can be used inside of DLZ command lines to group several items together into one parameter. Braces cannot be nested, so "{some commands {more commands} other stuff}" would be invalid and have unpredictable results. Spaces are not removed inside of braces, so be careful. For example, {some commands} would be "some commands", but { some commands } would be " some commands ". Notice the extra spaces in the second set! Usually, for LDAP queries this does not have a negative effect, but it should still be avoided. The use of braces is similar to the use of double quotes on a standard command line. Double quotes could not be used, as the entire DLZ command line is already inside of double quotes and the escaping would be nasty.

The first part of this line is "v3"; it is located at argv[2] and is required. This specifies the LDAP protocol version to use when connecting to the LDAP server. Use "v3" for LDAP version 3, and "v2" for LDAP version 2. Next is the word "simple"; it is located at argv[3] and is required. This parameter specifies the LDAP authentication method to use. Supported options are: "simple", "krb41" and "krb42". "simple" uses basic user / password authentication. "krb41" uses kerberos 4.1, and "krb42" uses kerberos 4.2. The next two parameters "{cn=Manager,o=bind-dlz}" and "{secret}" are the user and credential parameters used to authenticate to the LDAP server. They are required and located at argv[4] and argv[5]. When using the "simple" authentication method, they should be a username and password as seen here. Last on this line is "{192.168.2.12 192.168.2.14}". This specifies the LDAP server to connect to. OpenLDAP allows a space separated list of host names to be passed, and the DLZ LDAP driver supports this option also. Notice the list of host names is surrounded by braces as discussed earlier. This groups the list of host names together as a single parameter located at argv[6]. Obviously, it is required. If you are providing a list of host names to connect to, each host name must be separated by a space and the entire list of host names should be surround by a set of braces.

Fourth line: ldap:///dlzZoneName=%zone%,ou=dns,o=bind-dlz???objectclass=dlzZone

This query is used by findzone() in the LDAP driver. This parameter is required and located at argv[7]. All LDAP queries must be specified as LDAP URL's conforming to RFC 2255. See RFC 2255 or the OpenLDAP documentation for more information. Notice the string %zone%. This is one of the tokens discussed previously. When BIND needs to ask if a zone is supported by the LDAP database, it will use the above query and replace %zone% with the zone name it's asking about. BIND makes no guesses about syntax, so it's up to you to make sure the query will have the correct syntax when %zone% is replaced. Only the token %zone% should be used in the findzone() query. Other tokens will be recognized and parsed out, but they will never be replaced by any value because only zone is used in the findzone() operation. Results returned by the findzone query are not really used, so what you return and in what order doesn't really matter. What does matter is the number of entries that are returned. If zero entries are returned, the database does not support the zone (i.e. it is not authoritative for the zone). If at least 1 entry is returned, then the database does support the zone, and thus is authoritative for it. Findzone MUST return at least 1 entry for a zone query before the lookup query will ever be called. In the LDAP driver, the find zone query is also used as the first query during the allowzonexfr() query operation. This lets allowzonexfr() determine if the zone is supported by the database before it attempts to determine if a zone transfer is allowed on the zone by the client.

Red LDAP query: ldap:///dlzHostName=%record%,dlzZoneName=%zone%,ou=dns,o=bind-dlz?
dlzTTL,dlzType,dlzPreference,dlzData,dlzIPAddr?sub?
(&(objectclass=dlzAbstractRecord)(!(dlzType=soa)))

This query is used by lookup() in the LDAP driver. This parameter is required and located at argv[8]. Notice the string %record%. This is the second of the tokens discussed previously. Once BIND has determined the zone is supported by calling findzone(), it will call lookup() during domain name resolution DNS queries. Only the %zone% and %record% tokens are useful in a lookup query. You don't have to use both tokens if your database query doesn't require it. Again, you are responsible for making sure the syntax of the query is correct once %zone% and %record% are replaced. The token %zone% is replaced with the zone name, and %record% is replaced with the host name we are searching for. If we are searching at the zone apex, then %record% is replaced with the string "@" (i.e. a string consisting of the single at (@) character). If we have searched for the host name and not found it, lookup will search for a "wild card" hostname.

When searching for a wild card hostname, %record% is replaced with "~". The standard wildcard "*" could not be used because it has special meaning in LDAP queries. During queries & zone transfers "~" is converted by DLZ to the standard "*" wildcard character.

If your lookup query will be returning TXT dns records, be sure the TXT records are properly stored wrapped in double quotes. The LDAP driver will not wrap TXT records in double quotes for you, and it is not possible to add the double quotes as part of an LDAP query like it is in the SQL based drivers.

Let's say your TXT data is: a long example

If your TXT data is not stored wrapped in double quotes the DNS query will return

"a" "long" "example"

If you do store the TXT data wrapped in double quotes the DNS query will return

"a long example"

Notice that there is no token to tell the query what type of DNS record we are looking for. The query should return all records matching the record and zone parameters. Returning NS and SOA records by lookup is optional. If your lookup query will not return NS and SOA records, you must implement an authority query that will return those types of records. If your lookup query will return NS and SOA records, the authority query is not required. You should NOT implement an authority query if your lookup query will return NS and SOA records, as it will cause an error! In this sample query, we are artificially preventing NS and SOA records from being returned by the query so that we can show the use of the authority function later in this document. The order and type of data returned by a lookup query IS EXTREMELY IMPORTANT!! Also, the number of attributes returned by the lookup query can very from 3 to an unlimited number. How many attributes are returned is also of great importance. All of this is discussed later in the documentation, after we have introduced all of the queries that can be passed to the LDAP driver.

Green SQL query: ldap:///dlzHostName=@,dlzZoneName=%zone%,ou=dns,o=bind-dlz?
dlzTTL,dlzType,dlzData,dlzPrimaryNS,dlzAdminEmail,dlzSerial,
dlzRefresh,dlzRetry,dlzExpire,dlzMinimum?sub?
(&(objectclass=dlzAbstractRecord)(dlzType=soa))

This query is used by authority() in the LDAP driver. This parameter is optional and always located at argv[9]. Only the token %zone% is useful in this query. Other tokens will never be replaced with a value, although they are parsed out if you use them. Again, you are responsible for making sure the syntax of the query is correct once %zone% is replaced. The token %zone% is replaced with the zone name. This query should be used to return NS and SOA records if the lookup query does not. If the lookup query will return NS and SOA records, DO NOT USE an authority query, as it will cause errors! Like the lookup query, the number, type and order of attributes returned by the query is extremely important. All of this is discussed later in the documentation.

Blue SQL query: ldap:///dlzZoneName=%zone%,ou=dns,o=bind-dlz?
dlzTTL,dlzType,dlzHostName,dlzPreference,dlzData,dlzIPAddr,
dlzPrimaryNS,dlzAdminEmail,dlzSerial,dlzRefresh,dlzRetry,
dlzExpire,dlzMinimum?sub?
(&(objectclass=dlzAbstractRecord)(!(dlzType=soa)))

This query is used by allnodes() in the LDAP driver. This parameter is optional and always located at argv[10]. If your lookup function will return SOA and NS data, and you will not be using an authority query, you still need to specify an empty authority query so that the allnodes query will be located at argv[10]. To specify an empty authority query, use "{}" as your authority query. Notice there are no characters or spaces between the brackets. If you accidentally use "{ }" the driver will think you are using a single space as your authority query and error. BE CAREFUL! In order to support zone transfer, you must specify an allnodes AND an allowzonexfr query. If only one query is supplied, zone transfers will fail! This query should be used to return ALL records in the zone. Only the token %zone% is useful in this query. Other tokens will never be replaced with a value, although they are parsed out if you use them. Again, you are responsible for making sure the syntax of the query is correct once %zone% is replaced. The token %zone% is replaced with the zone name. Like the lookup query, the number, type and order of attributes returned by the query is extremely important. All of this is discussed later in the documentation.

Violet SQL query: ldap:///dlzZoneName=%zone%,ou=dns,o=bind-dlz??sub?
(&(objectclass=dlzXFR)(dlzIPAddr=%client%))";

This query is used by allowzonexfr() in the LDAP driver. This parameter is optional and always located at argv[11]. In order to support zone transfer, you must specify an allnodes AND an allowzonexfr query. If only one query is supplied, zone transfers will fail! Notice the string %client%. This is the third token discussed previously. The %client% token is only useful in the allowzonexfr query. Like all the other queries, %zone% will be replaced with the zone name. The token %client% will be replaced with the client's IP address. The IP address will be either an IPV4 or IPV6 address, depending upon how the client connected to the DNS server (using IPV4 or IPV6). The client address will be ASCII text like all the other token replacement strings. You are still responsible to make sure the query is correct once the tokens have been replaced. Like the findzone query, the results returned by the allowzonexfr query are not really used, so what you return and in what order doesn't really matter. What does matter is the number of entries that are returned. If zero entries are returned, the client is not allowed to perform zone transfers on this zone. If at least 1 entry is returned, the client is allowed to perform zone transfers for this zone, and the allnodes query will be executed next. Before using this query, the LDAP driver will use the findzone query to determine if the zone is supported by the database. Only if the zone is supported will the allowzonexfr query be run. Also notice the double quote and semi-colon at the end of the line. The double quote closes the command line string that was started on the second line. The semi-colon is required after a BIND parameter, and is part of BIND's standard config file syntax.

Ninth line: };

This closes the DLZ configuration section in BIND's config file. It is part of BIND's standard configuration file syntax.

Returned Attributes:

Now that we have covered all the different types of queries that can be used by the LDAP driver, we can discuss the number, type and order of attributes returned by the queries. This can be a bit difficult to understand, so be sure to read the next section of the documentation thoroughly and carefully!

The chart below shows the order in which attributes should be returned in queries. Attribute 1 is the left most attribute in a query. The chart below shows 12 attributes but not all attributes are required in all queries. You can divide your data up into even more attributes if you like, as long as the concatenation of the data makes sense to BIND. An explanation of how attributes are used and concatenated together is detailed below.

Order | Name | Data Type | Description
--- | --- | --- | ---
1 | ttl | string (num) | Time to live
2 | type | string | DNS data type
3 | host | string | Host name or IP address
4 | mx_priority | string (num) | MX Priority
5 | data | string | IP address / Host name / Full domain name
6 | primary_ns | string | Primary name server for SOA record
7 | resp_person | string | Responsible person for SOA record
8 | serial | string (num) | serial # for SOA record
9 | refresh | string (num) | Refresh time for SOA record
10 | retry | string (num) | Retry time for SOA record
11 | expire | string (num) | Expire time for SOA record
12 | minimum | string (num) | Minimum time for SOA record

Notice the data type column. Some attributes are labeled as "string (num)". LDAP only really returns strings from a query. The values of the attributes may be held as integers, floats, whatever by the database, but the return from the OpenLDAP functions is only a string. Eventually, however, the attributess marked with "string (num)" are turned into numbers by BIND or the LDAP driver. Your database can hold these attributess as strings or numbers, but they MUST be able to be properly parsed into a number without any extra characters. If they cannot, BIND will error and fail to answer the DNS query. I recommend the attributess labeled as "string (num)" be held as numbers in your LDAP schema.

There are five queries that can be used by the LDAP driver. Of those, only the lookup, allnodes, and authority queries make use of the attributes returned by the query. In order for the attributes to be interpreted properly by the driver, they must be in the correct order.

Allnodes:

The allnodes query is actually the simplest to understand because it makes use of all the attributes and has the fewest options. When the allnodes() function loops through the entries returned by the query, it passes that data back to BIND using the dns_sdlz_putnamedrr() function. This function takes five parameters and has the following signature:

dns_sdlz_putnamedrr(allnodes, name, type, ttl, data)

The first parameter "allnodes" is used internally by the driver and BIND so you can just ignore it. The next parameter "name" is the hostname of the record or attribute 3 from the chart above. Next is "type"--this is the DNS data type of the record, attribute 2 from the chart above. Next is "ttl" or time to live, attribute 1 in our chart. The last parameter is "data". Data accepts all the other information about this record as a string. This string must be in the proper format for BIND to use it properly.

The data string is "built" by concatenating attributess 4 through 12 together, with a space added in between each. Fortunately, the number of spaces in between doesn't matter, as long as there is at least one. This makes it easy for one query to return a number of different DNS types. If an attribute returned by the allnodes query is NULL, nothing is appended by that attribute. The driver then appends a space to the end of the string and continues to the next attribute. This is done until attributess 4 through 12 (or however many attributes you have in the query) are processed.

For example, if we had an all nodes query that returned 12 attributes, the entry for a mx record might have the following data:

    ttl: 3600
    type: MX
    host: @
    mx_priority: 20
    data: mail
    all other attributes in the entry are NULL.

This would result in the following call to dns_sdlz_putnamedrr().

dns_sdlz_putnamedrr(allnodes, "@", "MX", 3600, "20 mail ")

Notice all the extra spaces at the end of the data string. That is caused by the other attributes in the entry being NULL. This is correct operation, and BIND will be able to understand and use this data.

In the same query respone, say the next entry is an NS record.

    ttl: 3600
    type: NS
    host: @
    data: NS1
    all other attributes in the entry are NULL.

The call to dns_sdlz_putnamedrr() now would look like this:

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1 ")

Notice the space at the beginning. This is caused by the attribute dlzPreference being NULL.

Now obviously, only 4 attributes are required for the dns_sdlz_putnamedrr() function. So if your query returns only four attributes, it should work properly as long as the first three are ttl, type, host (in that order), and the fourth has all the "data" needed for the last parameter of dns_sdlz_putnamedrr(). The advantage of splitting the "data" string into many attributes in your database is easier data management.

Relative return data:

Another important point is that the LDAP driver uses relative data. In the last example, the call to dns_sdlz_putnamedrr() looked like this:

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1 ")

In the "data" string, we only have NS1. NS1 is not a fully qualified domain name. BIND only returns fully qualified answers though. Our driver knows that NS1 is relative to the zone we are searching in. So if we were searching in the zone "example.com", the above would actually result in BIND returning "NS1.example.com" (case doesn't matter in DNS queries). This makes it easy to manage our DNS data in the database using separate attributes for zone and host names. Sometimes, though, we want to return data that is NOT relative to the zone. For example, if the authoritative name server for this zone isn't "ns1.example.com", but is "ns1.domain.com", we need to be able to properly return that information to BIND so the DNS response will be correct. To override the default behavior, use an extra "." on the end of the domain name.

for example:

    ttl: 3600
    type: NS
    host: @
    data: NS1
    all other attributes in the entry are NULL, our zone is "example.com"

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1 ")

tells BIND our name server is "NS1.example.com"

if data = NS1.domain.com

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1.domain.com ")

Tells BIND our name server is "NS1.domain.com.example.com". This is correct behavior! It is not the right answer, but the behavior is correct.

if data = NS1.domain.com.

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1.domain.com. ")

Tells BIND our name server is "NS1.domain.com". This is the answer we want to give. Notice the extra "." at the end of the host name. This tells BIND that the domain name is absolute and not to be used relative to our zone of "example.com".

To summarize, the allnodes query MUST have at least 4 attributes in the query response, and those attributes must be ttl, type, host and data. The attributes must be in that order, and the data attribute (or attributes concatenated together) must hold the remainder of any data required for that DNS record type. To return absolute instead of relative hostname data, an extra "." should be used at the end of the domain name.

Lookup and Authority:

Lookup and authority operate in a manner similar to allnodes. Instead of dns_sdlz_putnamedrr(), lookup and authority use dns_sdlz_putrr(). This function is similar to dns_sdlz_putnamedrr(), except it only takes 4 parameters. The function dns_sdlz_putrr() has the following signature:

dns_sdlz_putrr(lookup, type, ttl, data)

The first parameter "lookup" is used internally by the driver and BIND so you can just ignore it. The next three parameters "type", "ttl", and "data" are all the same as in the dns_sdlz_putnamedrr() function.

Some of the DLZ drivers allow the use of "default values". I.E. you have the option of returning only 1 or 2 attributes and the other attributes will have defaults built into the driver. The LDAP driver does not supply any "default values", so lookup and authority queries must all return at least three attributes.

Just like the allnodes query, the first attribute in the entry MUST be the ttl. The second attribute MUST be the DNS data type, and the last attribute MUST be the DNS data in the format expected by BIND. The format is the same as discussed for the allnodes query.

dns_sdlz_putrr(lookup, "dns_type_here", "ttl_attribute_here", "data_attribute_here")

If the query response for a lookup or authority query has more than three attributes, all remaining attributes will be concatenated together to create the last parameter passed to dns_sdlz_putrr(). This string must be formatted as BIND expects it. The format is the same as discussed for the allnodes query.

dns_sdlz_putrr(lookup, "dns_type", "ttl_here", "concatenated_attributes_here")

As with the allnodes query, append a "." to the end of a hostname if you need to return an absolute hostname instead of a relative one.

It may be confusing at first to understand how to properly create a query for the LDAP driver. But the flexibility afforded by this way of doing things is very powerful. It sure beats being limited to a set schema, or building an entirely new driver!