Your corporate telephone directory queryable via the DNS? Sure, I've shown you how to do it in Perl using Stanford::DNSserver, but I was recently asked wether a separate authoritative server really was required. An authoritative server isn't required if you use Unbound.
- Authoritative responses to queries can be generated on the fly. (This is the functionality we'll be using here.)
- Responses to queries obtained from the public DNS can be modified before being returned to the DNS client. This could be interesting, say, to increase or lower the TTL of a particular set of records, or to lie.
- Packets can be logged.
The first item is particularly interesting because we can obtain data from the myriad of sources Python and its modules have to offer: do you want to build a customized DNS blacklist that uses a fast database, Redis, say, as a back-end? Want to query Twitter statuses over DNS, or provide a DNS gateway to a REST weather API? Do you have a parts database you want exposed over the DNS? Any and all of these and more are possible using Unbound, some Python and a bit of imagination.
I'm going to show you how to gateway a DNS query to an LDAP search result. This was a real requirement years ago, and at the time I wrote something that would allow me to query the DNS for a user's unique identifier and get back her workstation's IP address, telephone number, name, and e-mail address. The module I'm showing you here delivers a telephone number only, but you'll quickly get the idea and be able to expand on it at your heart's content.
My Telephone directory module is based on the response generation example.
It provides DNS
TXT records as responses to queries of the form
first.last.tele in my arbitrarily chosen top-level domain
.tele. In the former example, all LDAP entries for the surname are searched,
in the latter case LDAP entries for common name
Here's a sample LDAP entry in my directory:
dn: cn=Jane Jolie,o=mens.de description: Actress telephoneNumber: +49 555 6302547 mail: firstname.lastname@example.org givenName: Jane objectClass: top objectClass: person objectClass: inetOrgPerson sn: Jolie displayName: Jane Jolie cn: Jane Jolie
Let me query for Jane's number; I know her first and surnames, so I use both:
dig jane.jolie.tele txt ... ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; ANSWER SECTION: jane.jolie.tele. 3600 IN TXT "Jane Jolie: +49 555 6302547"
Note how the reply is authoritative (flags
Specifying a surname only, I get a list of all
jolie* in the LDAP
directory. (Careful: a search for
*.tele will also work!)
dig jolie.tele txt ... ;; ANSWER SECTION: jolie.tele. 3600 IN TXT "Jane Jolie: +49 555 6302547" jolie.tele. 3600 IN TXT "Brad Jolie: +1 555 2341983"
This data is provided by a recursive Unbound server running on my workstation or on a central DNS server your organization may have, and it logs the LDAP search filter used:
unbound[13092:0] info: tele: filter == (&(objectclass=person)(cn=jane jolie)) unbound[13092:0] info: tele: filter == (&(objectclass=person)(sn=jolie))
And here is the source code for the module; keep the structure documentation handy. Each python module must define four functions:
initfunction is invoked once at Unbound startup, when our module is loaded. It is here we can connect to databases, initialize data, etc.
cfgis a class which provides attributes on Unbound configured settings. For example, I can determine the name of the DLV trust anchor file so:
deinitis also invoked once, when our module is unloaded.
- The documentation states, that
inform_superis called when the "querystate is finished". I've never seen this function invoked in my code.
operatefunction is where the action happens; this function handles DNS queries received by the server. It accepts a new query or handles a pending query.
import sys import ldap LDAPURI = "ldap://127.0.0.1" BASE = "o=mens.de" BINDDN = "" BINDPW = "" FILTER = "(&(objectclass=person)(%s=%s))" attrs = [ 'cn', 'telephonenumber' ] global ld def init(id, cfg): global ld ld = ldap.initialize(LDAPURI) ld.simple_bind_s(BINDDN, BINDPW) return True def deinit(id): return True def inform_super(id, qstate, superqstate, qdata): return True def operate(id, event, qstate, qdata): # Handle a new DNS query or a query passed us by another # Unbound module if (event == MODULE_EVENT_NEW) or (event == MODULE_EVENT_PASS): qdn = qstate.qinfo.qname_str if qdn.endswith(".tele."): # Build a common name (CN) from the label parts: a query for # "a.b.tele" will result in a search for "CN=a b" # whereas a query for "s.tele" will result in an LDAP search # for "SN=s" parts = qstate.qinfo.qname_list[0:-2] args = ' '.join(parts) if len(parts) == 1: filter = FILTER % ("sn", args) else: filter = FILTER % ("cn", args) log_info("tele: filter == %s" % filter) # Create a DNS packet for a TXT RR msg = DNSMessage(qdn, RR_TYPE_TXT, RR_CLASS_IN, PKT_QR | PKT_RA | PKT_AA) if (qstate.qinfo.qtype == RR_TYPE_TXT) or (qstate.qinfo.qtype == RR_TYPE_ANY): res = ld.search_s(BASE, ldap.SCOPE_SUBTREE, filter, attrs) if len(res) == 0: qstate.return_rcode = RCODE_NXDOMAIN else: for entry in res: ttl = 3600 try: tel = entry['telephoneNumber'] except KeyError: tel = '' cn = entry['cn'] msg.answer.append("%s %d IN TXT \"%s: %s\"" % (qdn, ttl, cn, tel)) qstate.return_rcode = RCODE_NOERROR # Set qstate.return_msg if not msg.set_return_msg(qstate): qstate.ext_state[id] = MODULE_ERROR return True # Indicate valid result qstate.return_msg.rep.security = 2 qstate.ext_state[id] = MODULE_FINISHED return True else: # Pass the query to validator qstate.ext_state[id] = MODULE_WAIT_MODULE return True if event == MODULE_EVENT_MODDONE: log_info("tele: iterator module done") qstate.ext_state[id] = MODULE_FINISHED return True log_err("tele: bad event") qstate.ext_state[id] = MODULE_ERROR return True
As far as Unbound's configuration goes, adding a Python module script is
easy. I configure
python in the
module-config and add the my script's path
server: verbosity: 1 chroot: "" username: "unbound" module-config: "validator python iterator" auto-trust-anchor-file: "/usr/local/etc/unbound/root.key" dlv-anchor-file: "/usr/local/etc/unbound/dlv.isc.org.key" trust-anchor-file: "/usr/local/etc/unbound/uno.aa" val-log-level: 2 python: python-script: "/usr/local/etc/unbound/u-ldap.py"
When Unbound processes a DNS query it calls the modules listed in
from left to right. In our case the validator doesn't have an answer (it only
knows how to validate a reply) so the python module gets the query. It can either
generate a response or pass the event on to the iterator, the last module in the list.
When our python module is called with
MODULE_EVENT_PASS it means the event has
already been handled by the validator.