Project sponsored by
Project hosted on

The Postgres driver is obviously meant to be used in combination with a Postgres database. This driver has been tested on Windows 2K and Redhat Linux 7.2 using only Postgres 7.2. Older versions of the library may work but have not been tested. The driver should build properly on any UN*X system that BIND and Postgres both support. Be sure to specify --with-dlz-postgres when running configure so that the Postgres 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 Postgres libpq libraries and header files are required to build the driver and are not included in this package. You can download the PostgreSQL source from www.postgresql.org. The configure script will search in the normal locations for the Postgres headers and libraries. You can specify the appropriate directory if it is not found by appending a path to the Postgres configure option. For example: --with-dlz-postgres=/path/to/files

To find out where your Postgres headers and libraries are you can run the the pg_config command. "pg_config --includedir" will return the path to Postgres header files, and "pg_config --libdir" will return the path Postgres library files.

The Postgres driver was built to be as flexible as possible. Most "drivers" tend to require a specific database schema for the driver to be able to interact with the database. 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 database schema, the driver accepts SQL queries 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 database schemas to be used without modification to the driver's code. The only limitations are:

1. The query must return the appropriate data types in the correct order.
2. The query must use the correct pre-defined tokens.
3. The query must be written to that when the tokens are replaced with search values it is still valid.

Below is a sample of a proper dlz_postgres_driver configuration. This configuration segment would be contained in BIND's config (named.conf) file. It is explained more below. When you are developing your own Postgres driver SQL 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 Postgres driver will print each SQL query it is about to run when the debug level is set to at least 1. This can be very helpful in debugging SQL queries while you are setting up the driver. The SQL 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 SQL queries that are used.

    dlz "postgres zone" {
       database "postgres 2
       {host=localhost port=5432 dbname=dns_data user=rob}
       {select zone from dns_records where zone = '%zone%'}
       {select ttl, type, mx_priority, case when lower(type)='txt' then '\"' || data
            || '\"' else data end from dns_records where zone = '%zone%' and host = '%record%'
            and not (type = 'SOA' or type = 'NS')}
       {select ttl, type, mx_priority, data, resp_person, serial, refresh, retry, expire,
            minimum from dns_records where zone = '%zone%' and (type = 'SOA' or type='NS')}
       {select ttl, type, host, mx_priority, data, resp_person, serial, refresh, retry, expire,
            minimum from dns_records where zone = '%zone%'}
       {select zone from xfr_table where zone = '%zone%' and client = '%client%'}";
    };

The first line: dlz "postgres 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 "postgres 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 "postgres 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 PostgreSQL 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 "postgres". This is the official name of the DLZ postgres driver. We are telling BIND that we want to use the Postgres driver. The word "postgres" 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 last 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 Postgres driver to use. When BIND is built single threaded, this parameter is ignored BUT STILL REQUIRED! The Postgres 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: {host=localhost port=5432 dbname=dns_data user=rob}

This is the database connection string; it is required. For more information on PostgreSQL's database connection parameters, refer to the PostreSQL manual. Notice that the contents of this line are surrounded by braces "{" and "}". Braces can be used inside of DLZ command lines to group several items together into one parameter. The entire line above is one parameter located at argv[2]. 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 SQL queries this does not have a negative effect, but it should still be avoided. For this reason, the queries are all on one line. 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. Additionally, single quotes were not used because they have special meaning in SQL queries. Using single quotes would have made escaping SQL queries nasty, too.

Fourth line: {select zone from dns_records where zone = '%zone%'}

This query is used by findzone() in the Postgres driver. This parameter is required and located at argv[3]. Notice the string %zone%. This is one of the tokens discussed previously. When BIND needs to ask if a zone is supported by the Postgres database, it will use the above query and replace %zone% with the zone name it's asking about. Also, notice that the %zone% is inside a set of single quotes. BIND makes no guesses about syntax. Zone is always a string, but DLZ doesn't know or care that SQL strings should be inside of single quotes. It's up to you to make sure the query will have the correct syntax when %zone% is replaced. The Postgres driver is smart enough, however, to properly escape the zone string. So "www.dom'ain.com" will be properly escaped so as not to cause a problem when used in the query. 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 rows (or tuples in PostgreSQL terms) that are returned. If zero rows are returned, the database does not support the zone (i.e. it is not authoritative for the zone). If at least 1 row is returned, then the database does support the zone, and thus is authoritative for it. Findzone MUST return at least 1 row for a zone query before the lookup query will ever be called. In the Postgres 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 SQL query: {select ttl, type, mx_priority, case when lower(type)='txt' then '\"' || data || '\"' else data end from dns_records where zone = '%zone%' and host = '%record%' and not (type = 'SOA' or type = 'NS')}

This query is used by lookup() in the Postgres driver. This parameter is required and located at argv[4]. 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 driver will properly escape the strings used to replace the tokens. 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 "*". If your lookup query will be returning TXT dns records be sure to wrap your TXT data with double quotes before returning the data.

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

If your SQL does not wrap the data in double quotes the DNS query will return

"a" "long" "example"

If you do wrap the TXT data with 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 fields returned by the lookup query can very from 1 to an unlimited number of fields. How many fields 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 Postgres driver.

Green SQL query: {select ttl, type, mx_priority, data, resp_person, serial, refresh, retry, expire, minimum from dns_records where zone = '%zone%' and (type = 'SOA' or type='NS')}

This query is used by authority() in the Postgres driver. This parameter is optional and always located at argv[5]. 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 driver will properly escape the string used to replace the token. 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 fields returned by the query is extremely important. All of this is discussed later in the documentation.

Blue SQL query: {select ttl, type, host, mx_priority, data, resp_person, serial, refresh, retry, expire, minimum from dns_records where zone = '%zone%'}

This query is used by allnodes() in the Postgres driver. This parameter is optional and always located at argv[6]. 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[6]. 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. Notice in this query we have NOT taken the efforts to wrap TXT records with double quotes. If you will have TXT records in your database be sure to wrap the TXT records with double quotes in both the lookup and allnode queries. 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 driver will properly escape the string used to replace the token. The token %zone% is replaced with the zone name. Like the lookup query, the number, type and order of fields returned by the query is extremely important. All of this is discussed later in the documentation.

Violet SQL query: {select zone from xfr_table where zone = '%zone%' and client = '%client%'}";

This query is used by allowzonexfr() in the Postgres driver. This parameter is optional and always located at argv[7]. 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. Also, both the %zone% and %client% replacement strings will be properly escaped. 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 rows that are returned. If zero rows are returned, the client is not allowed to perform zone transfers on this zone. If at least 1 row 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 Postgres 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 Fields:

Now that we have covered all the different types of queries that can be used by the Postgres driver, we can discuss the number, type and order of fields 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 fields should be returned in queries. Field 1 is the left most field in a SQL query. The chart below shows 12 fields but not all fields are required in all queries. You can divide your data up into even more fields if you like, as long as the concatenation of the data makes sense to BIND. An explanation of how fields 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 fields are labeled as "string (num)". PostgreSQL only really returns strings from a database when using the libpq functions. The values of the fields may be held as integers, floats, whatever by the database, but the return from the libpq functions is only a string. Eventually, however, the fields marked with "string (num)" are turned into numbers by BIND or the Postgres driver. Your database can hold these fields 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 fields labeled as "string (num)" be held as numbers in your database schema.

There are five queries that can be used by the Postgres driver. Of those, only the lookup, allnodes, and authority queries make use of the fields returned by the query. In order for the fields 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 fields and has the fewest options. When the allnodes() function loops through the rows 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 field 3 from the chart above. Next is "type"--this is the DNS data type of the record, field 2 from the chart above. Next is "ttl" or time to live, field 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 fields 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 a field returned by the allnodes query is NULL, nothing is appended by that field. The driver then appends a space to the end of the string and continues to the next field. This is done until fields 4 through 12 (or however many fields you have in the query) are processed.

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

    ttl = 3600
    type = MX
    host = @
    mx_priority = 20
    data = mail
    all other fields in the row 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 fields in the row being NULL. This is correct operation, and BIND will be able to understand and use this data.

In the same result set, say the next row is an NS record.

    ttl = 3600
    type = NS
    host = @
    data = NS1
    all other fields in the row 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 field mx_priority being NULL.

Now obviously, only 4 fields are required for the dns_sdlz_putnamedrr() function. So if your query returns only four fields, 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" field into many fields in your database is easier data management.

Relative return data:

Another important point is that the Postgres 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" field, 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 fields 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 fields in the row 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 fields in the result set, and those fields must be ttl, type, host and data. The fields must be in that order, and the data field (or fields 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.

Lookup and authority work a little differently in that they have default values. If a result set for a lookup or authority query only has one field, the driver will use the default value of "a" for type and 86400 for ttl. The call to dns_sdlz_putrr will look something like:

dns_sdlz_putrr(lookup, "a", 86400, "your_data_field_here")

The field returned by your result set should be just a hostname or IP address (for reverse DNS queries). This is fine for a lookup query, but the purpose of the authority query is to return NS and SOA records, so an authority query should never return just one field!

If the result set for a lookup or authority query only has two fields, the driver will use the default value of 86400 for ttl. The first field in the result set MUST be the DNS record type. The second field 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", 86400, "data_field_here")

If the result set for a lookup or authority query has three fields, the driver will not use any default values. The first field in the result set MUST be the ttl. The second field MUST be the DNS data type, and the last field 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_field_here", "data_field_here")

If the result set for a lookup or authority query has more than three fields, the driver will not use any default values. The first field in the result set MUST be the ttl. The second field MUST be the DNS data type. All remaining fields 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_fields_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 an SQL query for the Postgres 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!