
While it is easy enough to write a client application that will directly access DNSDB API via https, imagine that you want to write an intermediate layer that will do some sort of pre-processing of DNSDB requests (or post-processing of DNSDB results) “in between” the client and DNSDB API. Conceptually:


What might an intermediate layer do? You might write code to pre-process queries going into DNSDB API, perhaps electing to:
Similarly, you might use an intermediate layer to post-process output from DNSDB API, too. For example, you could write code to:
You could do simply many of these things in a single monolithic client application, but there may be advantages to decoupling some functions into a separate intermediate layer instead (even if that layer is just running on your own workstation):
If you do choose to create and run an intermediate layer, it’s critically important that you implement strong access controls to prevent your intermediate layer from inadvertently acting as a “backdoor” or “proxy” that enables unauthorized access to DNSDB.
If you’re just running your intermediate layer on your own dedicated workstation, a simple solution may be to simply run your application on localhost (e.g., IPv4 127.0.0.1) rather than binding your application to a routable IP address. That’s the approach we’ll illustrate in the code shown below.
If you need to run a more generally-available intermediate layer, you’ll want to use strong authentication (perhaps something like PKI client certificate authentication) over a carefully-configured encrypted connection. The mechanics and complexity of appropriately doing that are beyond the scope of this article.
There are many ways that one could create the necessary client-to-intermediate-layer connection, including simply by using standard Un*x sockets, but we wanted to use 0mq as implemented by pyzmq.
You may wonder “Why 0mq?” Well, 0mq is an intriguing alternative worthy of investigation for many reasons, including:
There are also some limitations associated with 0mq that you should be thinking about, including:
On balance, however, we think 0mq makes sense for our example.
For the Intermediate layer to DNSDB API connection, we’re simply going to use pycurl.
Because this is just a demonstration application, we’re going to keep this bare bones. Here’s our sample minimalist client:
$ cat simple-client.py#!/usr/local/bin/python3
import sys
import zmq
myarg = sys.argv[1]
context = zmq.Context()
socket = context.socket(zmq.REQ)
socket.connect("tcp://127.0.0.1:5556")
socket.send_string(myarg)
result = socket.recv_string()
print (result)
Pretty short, isn’t it? It just picks up the domain name to investigate from the command line, and then uses 0mq to send the query to the server we’ll create next.
Our “server” (actually, our intermediate layer application) is longer (because it uses
libcurl
to connect to DNSDB API, and makes at least a rudimentary pass at one error condition), but it’s still less than a page of code:
$ cat simple-server.py#!/usr/local/bin/python3
import zmq
from pathlib import Path
from io import BytesIO
import pycurl
def make_query(fqdn):
filepath = str(Path.home()) + "/.dnsdb-apikey.txt"
with open(filepath) as stream:
myapikey = stream.read().rstrip()
url = "https://api.dnsdb.info/dnsdb/v2/lookup/rrset/name/" + str(fqdn)
requestHeader = []
requestHeader.append('X-API-Key: ' + myapikey)
requestHeader.append('Accept: application/jsonl')
buffer = BytesIO()
c = pycurl.Curl()
c.setopt(pycurl.URL, url)
c.setopt(pycurl.HTTPHEADER, requestHeader)
c.setopt(pycurl.WRITEDATA, buffer)
c.perform()
rc = c.getinfo(c.RESPONSE_CODE)
body = buffer.getvalue()
content = body.decode('iso-8859-1')
if rc == 200:
return content
else:
return str(rc)
def main():
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.bind("tcp://127.0.0.1:5556")
while True:
fqdn = socket.recv()
fqdn2 = fqdn.decode("utf-8")
# preprocess fqdn2 here if desired
content = make_query(fqdn2)
if content.isdigit():
socket.send_string("Error making query! Return code = " + content)
else:
# postprocess content here if desired
socket.send_string(content)
if __name__ == "__main__":
main()
To try the above demonstration application:
$ chmod a+rx simple-client.py
$ chmod a+rx simple-server.py
$ chmod go-rwx ~/.dnsdb-apikey.txt
$ python3 simple-server.py
$ python3 simple-client.py www.whitman.edu/A
{"cond":"begin"}
{"obj":{"count":1299072,"time_first":1395869220,"time_last":1603327211,
"rrname":"www.whitman.edu.","rrtype":"A","bailiwick":"whitman.edu.",
"rdata":["199.89.174.11"]}}
{"obj":{"count":940920,"time_first":1277387381,"time_last":1395938425,
"rrname":"www.whitman.edu.","rrtype":"A","bailiwick":"whitman.edu.",
"rdata":["199.89.174.13"]}}
{"cond":"succeeded"}
The sample application shown above truly is just a skeleton, particularly when it comes to handling exceptions and being hardened against various attacks — it’s NOT meant to be “sailor proof,” it’s just a proof of concept.
That said, primitive as this brief example may be, it’s still quite a powerful one — you now have a working framework that you can easily enhance to prototype the sort of things we discussed in Section I.
For part two of this series, let’s try building a DNSDB Flexible Search regular expression enrichment pipeline and a global domain “kill file” that we can use to do filtering of our DNSDB output prior to delivery. We’ll also handle displaying our output as something easier-to-read than just a blob of raw JSON Lines with times expressed in Un*x ticks.
Joe St Sauver is a Distinguished Scientist and Director of Research with Farsight Security, Inc..
Read the next part in this series: Creating a DNSDB-Flexible-Search-To-DNSDB-Standard-Search Pipeline With 0mq