bulkDNS modules are written in Lua programming language. This tutorial shows how to write a module for a customized scan scenarios using Lua and bulkDNS. Before writing modules, you need to make sure to compile bulkDNS with Lua (using `make with-lua', by following the instruction in the main README file.)
BulkDNS accepts a Lua script file using the switch --lua-script
. After launching the scanner, each
thread (pthread) creates a Lua state in the memory, run the file one time and then for each entry, it
calls the main
function in the Lua file. Therefore, your Lua script must have a main
function which
accepts only one parameter: one line of the input file passed to bulkDNS.
The main
function must return exactly one value: whatever you want to log in the output file.
Therefore, here is the structure of the Lua script you pass to bulkDNS.
-- whatever import module you want
--
--
-- whatever code or function you want to have
-- if you define a global variable here, it will be
-- available as long as bulkDNS is running
-- for examle:
-- global_cache = {}
function main(input_line)
-- input_line: one of the entries of the input file or nil (only for the last call)
-- you passed to bulkDNS
-- body of the function
-- body of the function
-- body of the function
return "whatever you return will be stored in the output file specifed with -o switch"
end
If you return nil
from the main
function, then nothing will be logged in the output. This is very useful and we'll see an example later.
It's also very important to note that whatever global variable you define in your Lua file will be available until the end of the scan. This is on purpose! In this way, you can keep the states for different entries and have a dynamic scan. For examle, one use-case of this feature is to implement a global LRU DNS cache in your Lua file!
When there is no more entry for scan, the C code will call the Lua 'main' function for the last time by passing nil
to the
main function. With this last call, the Lua script knows that it's time to save global variables or do whatever you want since
there won't be any other call to this Lua script.
Here is the scan scenario: I have a list of domain names and I want to output only those with DNS response code of NXDomain.
We know that NXDomain is rcode=3
in a DNS packet.
So here is the code in Lua:
-- save this code in a file nxscanner.lua
local sdns = require("libsdns")
assert(sdns)
function main(line)
-- we don't care about the last call as we don't have global vars
if line == nil then return nil end
-- create a query packet
local query = sdns.create_query(line, "A", "IN")
-- make sure the query packet created successfully
if query == nil then return nil end
-- parameters for sending to cloudflare servers
tbl_send = {dstport=53, timeout=3, dstip="1.1.1.1"}
-- make a payload from our query packet
to_send = sdns.to_network(query)
-- make sure we created the payload successfully
if to_send == nil then return nil end
-- add it to our parameters
tbl_send.to_send = to_send
-- send it using sdns library
from_udp = sdns.send_udp(tbl_send)
-- make sure we have the answer payload
if from_udp == nil then return nil end
-- convert the payload to DNS packet
answer = sdns.from_network(from_udp)
-- make sure the conversion was successful
if answer == nil then return nil end
-- get the header of the DNS packet
header = sdns.get_header(answer)
-- make sure you got the header
if header == nil then return nil end
-- check if rcode==3 or not
if header.rcode == 3 then
-- send it to the output
return line
else
return nil
end
end
That's it! You just made a nice NX scanner!
Before running bulkDNS, make sure you don't have any syntax error. You can do it by running lua nxscanner.lua
in your bash. You should get nothing as output.
Now create a list of domain names and store it in input_file.txt:
microsoft.com
google.com
yahoo.com
filovirid.com
urlabuse.com
nonexistentdomainslakfjas.com
secondnotexistdomain234234.net
The last two domains must be in the output as they don't exist.
Run bulkDNS like this:
./bulkdns --lua-script=nxscanner.lua --concurrency=10 input_file.txt
it prints out the output in your terminal (you can specify a file to save the output using -o
or --output
switch).
You can download both nxscanner.lua
and input_file.txt
from this directory.
It's important to note that I used sdns
lua library for doing DNS operation in Lua. However, you can use whatever Lua library that you prefer. For socket operation, you can also use other Lua libraries like this one. However, if you want to use sdns
Lua library, make sure you follow this tutorial.
In case you forgot, SPF stands for Sender Policy Framework.
We want to extract only the SPF records (not the whole TXT records) of a list of domain names. Here is the code to do the job:
-- save it in a file: spfscanner.lua
local json = require "json"
local sdns = require "libsdns"
local find = string.find
local json_encode = json.encode
local insert = table.insert
function main(line)
if line == nil then return nil end
local query = sdns.create_query(line, "TXT", "IN")
if query == nil then return nil end
tbl_send = {dstport=53, timeout=3, dstip="1.1.1.1"}
to_send = sdns.to_network(query)
if to_send == nil then return nil end
tbl_send.to_send = to_send
from_udp = sdns.send_udp(tbl_send)
if from_udp == nil then return nil end
answer = sdns.from_network(from_udp)
if answer == nil then return nil end
local header = sdns.get_header(answer)
if header == nil then return nil end
if header.tc == 1 then
-- we need to do TCP
from_tcp = sdns.send_tcp(tbl_send)
if from_tcp == nil then return nil end
answer = sdns.from_network(from_tcp)
if answer == nil then return nil end
header = sdns.get_header(answer)
end
local spf = {}
question = sdns.get_question(answer) or {}
local num = header.ancount or 0
if num == 0 then return nil end
for i=1, num do
a = sdns.get_answer(answer, i)
a = ((a or {}).rdata or {}).txtdata or nil
if a == nil then goto continue end
if find(a, "^[vV]=[sS][pP][fF]1%s+") ~= nil then
table.insert(spf, a)
end
::continue::
end
return json_encode({name=line, data=spf});
end
And run it like:
./bulkdns --lua-script=spfscanner.lua --concurrency=10 input_file.txt
I am using the json library from here. I stored it in the module directory.
You can find more modules in this directory and all are documented.
This awesome feature lets bulkDNS to work as a DNS server. Note that the purpose of having this feature is not to use bulkDNS as a product-level DNS server. You must use software like Bind9 or PowerDNS or Unbound for that.
However, if you are a researcher or a curious person want to have some experience with DNS and play a little bit by running your own customized server, then this is exactly what you need.
How can you use this feature:
./bulkdns --server-mode --lua-script=<your-lua-file> --bind-ip=YOUR-IPv4 -p PORT
--server-mode
tells bulkDNS that instead of running the scanner, we want to run a
DNS server.
--lua-script
specifies the Lua script file which is responsible for handling
DNS requests.
--bind-ip
tells bulkDNS to listen on this IP address for incoming connections.
-p
or --port
tells bulkDNS to listen to this port of the given IPv4.
bulkDNS listens to both TCP and UDP connections.
for example:
./bulkdns --server-mode --lua-script=server.lua -p 5300 --bind-ip=127.0.0.1
This command will listen and bind to 127.0.0.1:5300 on both TCP and UDP.
The structure of the Lua script must be like this:
-- import whatever module you want
-- you can have whatever global variable and function you want
function main(raw_data, client_info)
--[[
this function will be called for each requests that is received by bulkDNS
the function has two parameters in each call.
raw_data: this is the binary data the client sent to the socket
client_info: This is a Lua table containing the client information
client_info = {
ip="client-IP-address",
port=client-port-number,
proto="TCP or UDP"
}
This function MUST return exactly two values:
1. string/nil: What you want to log in the output or file
2. string/nil: What you want to send back to the client
for example, if you return:
nil, nil
it means that you don't want to log anything and you don't want
to return anything to the client.
--]]
-- body of the main function goes here.
-- you can convert the raw_data to a DNS packet and decide to answer
-- the user or not. You can also create the log string here.
return log_string, response
end
Each time the client ask for something, the bulkDNS thread will call the
main
function in Lua script and return the response back to the client and
log the data in the output.
Let's write a simple DNS forwarder as an example. Here is the explanation of Digicert about DNS forwarder in case you don't know what it is: DNS forwarder.
local json = require("json")
local sdns = require("libsdns")
-- Let's make sure we have all our libriries loaded
assert(json)
assert(sdns)
function main(raw_data, client_info)
local dns = sdns.from_network(raw_data)
if (dns == nil) then return nil, nil end
local question = sdns.get_question(dns)
if (question == nil) then return nil, nil end
-- get the question to check the name of the query
question.qname = question.qname or ""
-- create the log anyway
to_log = create_log(dns, client_info, question)
-- do the forward operation
-- ask question from 1.1.1.1 and forward the answer to the client
return to_log, do_forward(raw_data, client_info)
end
function do_forward(raw_data, client_info)
-- this function will do the forwarding part
-- this function will do both UDP and
-- TCP (if it's necessary) to serve the answer
local RR = {dstip="1.1.1.1", dstport=53, timeout=3}
RR.to_send = raw_data
local proto = client_info.proto
if proto == "UDP" then return sdns.send_udp(RR) end
if proto == "TCP" then return sdns.send_tcp(RR) end
end
function create_log(dns, client_info, question)
-- this function will create the log line for us
-- We send the log back to C code to store (or print) it.
-- this is due to the fact that the code is multithreaded and we
-- don't want any race condition
local err, msg;
local ts = os.time(os.date("!*t"))
local ip = client_info.ip or ""
port = client_info.port or ""
local data = {
ip=ip, port=port,
proto=client_info.proto,
["error"]="success",
ts=ts
}
data.question = {
name=question.qname,
["type"]=question.qtype,
["class"]=question.qclass
}
return json.encode(data)
end
That's it! Now run bulkDNS with the following command:
./bulkdns --server-mode --bind-ip=127.0.0.1 -p 5300 --lua-script=forwarder.lua
Now use dig to query for the A
record of google.com
dig @127.0.0.1 -p 5300 A google.com
If you want to run it on your server, you can use 0.0.0.0
for --bind-ip
and
run it on port 5300 but use iptables
to reroute the packets from port 53 to 5300.
Something like this would work:
# change PUBLICIP to your server's public IPv4
iptables -t nat -I PREROUTING -p udp --dport 53 -j DNAT --to PUBLICIP:5300
Now let's see another poor example of an authorative server: Let's say we want to manage
our own DNS server for a domain name. Our domain name is example.com
and we want
to serve our IP address for this domain which is 1.2.3.4
. we have the same IP address
for our name-server which is ns1.example.com
.
local sdns = require("libsdns")
local json = require("json")
assert(sdns)
assert(json)
function main(raw_data, client_info)
local dns = sdns.from_network(raw_data)
local lower = string.lower
if raw_data == nil then return nil, nil end
local question = sdns.get_question(dns)
if question.qclass ~= "IN" then return nil, nil end
if question.qtype ~= "A" and question.qtype ~= "NS" then return nil, nil end
if question.qname == nil then return nil, nil end
if question == nil then return nil, nil end
local response = sdns.create_response_from_query(dns)
print(question.qname)
if lower(question.qname) == 'example.com.' then
if question.qtype == 'A' then
sdns.add_rr_A(response, {
ttl=300, rdata={ip="1.2.3.4"},
name=question.qname, section="answer"}
)
local response_raw = sdns.to_network(response)
return create_log(nil, client_info, question), response_raw
else
sdns.add_rr_NS(response, {
ttl=300, rdata={nsname="ns1.example.com"},
name=question.qname, section="answer"}
)
sdns.add_rr_A(response, {
ttl=300, rdata={ip="1.2.3.4"},
name=question.qname, section="additional"}
)
local response_raw = sdns.to_network(response)
return create_log(nil, client_info, question), response_raw
end
end
if lower(question.qname) == 'ns1.example.com.' and question.qtype== 'A' then
sdns.add_rr_A(response, {
ttl=300, rdata={ip="1.2.3.4"},
name=question.qname, section="answer"}
)
local response_raw = sdns.to_network(response)
return create_log(nil, client_info, question), response_raw
end
return nil, nil
end
function create_log(dns, client_info, question)
-- this function will create the log line for us
-- We send the log back to C code to store (or print) it.
-- this is due to the fact that the code is multithreaded and we
-- don't want any race condition
local err, msg;
local ts = os.time(os.date("!*t"))
local ip = client_info.ip or ""
port = client_info.port or ""
local data = {
ip=ip, port=port,
proto=client_info.proto,
["error"]="success",
ts=ts
}
data.question = {
name=question.qname,
["type"]=question.qtype,
["class"]=question.qclass
}
return json.encode(data)
end
Now you can run bulkDNS and use dig for asking the following questions:
# asking for the A record of the nameserver
dig @127.0.0.1 -p 5300 A ns1.example.com
# asking for the NS record of the domain name
dig @127.0.0.1 -p 5300 NS example.com
# asking for the A record of the domain name
dig @127.0.0.1 -p 5300 A example.com
# this is an authoritative server. So any other question you ask, the packet will
# be dropped.