--- /dev/null
+-- Load modules
+local sqlite3 = require("lsqlite3")
+
+local DB_PATH = "/var/lib/knot-resolver/dhcp-leases.db"
+local TTL = 60
+
+local M = {}
+local db
+local sql_fwd
+local sql_rev
+
+local function log_error(s)
+ print(s)
+end
+
+local function log_debug(s)
+ print(s)
+end
+
+-- Initializes the module
+function M.init()
+ -- Open the database
+ db = sqlite3.open(DB_PATH, sqlite3.OPEN_READONLY)
+
+ -- Fail if we cannot open the database
+ if not db then
+ log_error("leases: Failed to open " .. DB_PATH)
+ return -1
+ end
+
+ -- Don't ever block
+ db:exec("PRAGMA query_only = 1")
+
+ -- Prepare the forward lookup query
+ sql_fwd = db:prepare(
+ "SELECT address FROM leases WHERE hostname = ?1 COLLATE NOCASE LIMIT 1"
+ )
+
+ -- Prepare the reverse lookup query
+ sql_rev = db:prepare(
+ "SELECT hostname FROM leases WHERE address = ?1 LIMIT 1"
+ )
+end
+
+-- Cleans up the module
+function M.deinit()
+ -- Cleanup the statements
+ if sql_fwd then
+ sql_fwd:finalize()
+ end
+ if sql_rev then
+ sql_rev:finalize()
+ end
+
+ -- Close the database
+ if db then
+ db:close()
+ end
+end
+
+-- Parses an IPv4 address from the reverse pointer query name
+local function address_from_reverse_pointer(qname)
+ local d, c, b, a = qname:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)%.in%-addr%.arpa%.$")
+
+ -- Return nil if we could not parse the name
+ if not a or not b or not c or not d then
+ return
+ end
+
+ -- Concatenate the address
+ return string.format("%s.%s.%s.%s", a, b, c, d)
+end
+
+local function lookup_fwd(hostname)
+ -- Reset the statement
+ sql_fwd:reset()
+
+ -- Bind the query name
+ sql_fwd:bind_values(hostname)
+
+ -- Execute the statement
+ if sql_fwd:step() == sqlite3.ROW then
+ local address = sql_fwd:get_value(0)
+
+ -- Convert the address to wire format
+ if address then
+ return kres.str2ip(address)
+ end
+ end
+end
+
+local function lookup_rev(qname)
+ -- Parse the address from the query name
+ local address = address_from_reverse_pointer(qname)
+
+ -- Fail if we could not parse the address
+ if not address then
+ return
+ end
+
+ -- Reset the statement
+ sql_rev:reset()
+
+ -- Bind the address
+ sql_rev:bind_values(address)
+
+ -- Execute the statement
+ if sql_rev:step() == sqlite3.ROW then
+ local hostname = sql_rev:get_value(0)
+
+ -- Convert the hostname to wire format
+ if hostname then
+ return todname(hostname)
+ end
+ end
+end
+
+-- Function that will try to answer the query
+function M.answer()
+ return function(state, req)
+ -- Fetch the current query
+ local query = req:current()
+
+ -- Fetch the query name
+ local qname = kres.dname2str(query.sname)
+
+ -- Fetch the query type
+ local qtype = query.stype
+
+ -- Log action
+ log_debug(
+ string.format("Called for %s (%d)", qname, qtype)
+ )
+
+ local answer = {}
+
+ -- Is this a forward lookup?
+ if qtype == kres.type.A then
+ -- Perform a forward lookup
+ local address = lookup_fwd(qname)
+
+ if address then
+ answer[qtype] = { rdata = address, ttl = TTL }
+ end
+
+ -- Or is this a reverse lookup?
+ elseif qtype == kres.type.PTR then
+ -- Perform a reverse lookup
+ local hostname = lookup_rev(qname)
+
+ if hostname then
+ answer[qtype] = { rdata = hostname, ttl = TTL }
+ end
+ end
+
+ -- If we have an answer, use the policy module to send it
+ if answer then
+ answer = policy.ANSWER(answer)
+
+ -- Otherwise we send NXDOMAIN
+ else
+ answer = policy.DENY
+ end
+
+ -- Pass the state and request
+ return answer(state, req)
+ end
+end
+
+return M