diff options
Diffstat (limited to '')
-rw-r--r-- | modules/rebinding/rebinding.lua | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/modules/rebinding/rebinding.lua b/modules/rebinding/rebinding.lua new file mode 100644 index 0000000..dc90d20 --- /dev/null +++ b/modules/rebinding/rebinding.lua @@ -0,0 +1,112 @@ +-- Protection from DNS rebinding attacks +local kres = require('kres') +local renumber = require('renumber') + +local M = {} +M.layer = {} +M.blacklist = { + -- https://www.iana.org/assignments/iana-ipv4-special-registry + -- + IPv4-to-IPv6 mapping + renumber.prefix('0.0.0.0/8', '0.0.0.0'), + renumber.prefix('::ffff:0.0.0.0/104', '::'), + renumber.prefix('10.0.0.0/8', '0.0.0.0'), + renumber.prefix('::ffff:10.0.0.0/104', '::'), + renumber.prefix('100.64.0.0/10', '0.0.0.0'), + renumber.prefix('::ffff:100.64.0.0/106', '::'), + renumber.prefix('127.0.0.0/8', '0.0.0.0'), + renumber.prefix('::ffff:127.0.0.0/104', '::'), + renumber.prefix('169.254.0.0/16', '0.0.0.0'), + renumber.prefix('::ffff:169.254.0.0/112', '::'), + renumber.prefix('172.16.0.0/12', '0.0.0.0'), + renumber.prefix('::ffff:172.16.0.0/108', '::'), + renumber.prefix('192.168.0.0/16', '0.0.0.0'), + renumber.prefix('::ffff:192.168.0.0/112', '::'), + -- https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml + renumber.prefix('::/128', '::'), + renumber.prefix('::1/128', '::'), + renumber.prefix('fc00::/7', '::'), + renumber.prefix('fe80::/10', '::'), +} -- second parameter for renumber module is ignored except for being v4 or v6 + +local function is_rr_blacklisted(rr) + for i = 1, #M.blacklist do + local prefix = M.blacklist[i] + -- Match record type to address family and record address to given subnet + if renumber.match_subnet(prefix[1], prefix[2], prefix[4], rr) then + return true + end + end + return false +end + +local function check_section(pkt, section) + local records = pkt:section(section) + local count = #records + if count == 0 then + return nil end + for i = 1, count do + local rr = records[i] + if rr.type == kres.type.A or rr.type == kres.type.AAAA then + local result = is_rr_blacklisted(rr) + if result then + return rr end + end + end +end + +local function check_pkt(pkt) + for _, section in ipairs({kres.section.ANSWER, + kres.section.AUTHORITY, + kres.section.ADDITIONAL}) do + local bad_rr = check_section(pkt, section) + if bad_rr then + return bad_rr + end + end +end + +local function refuse(req) + -- we are deleting packet in consume() phase so other modules + -- might have chosen some RRs from the original packet already + req.answ_selected.len = 0 + req.auth_selected.len = 0 + req.add_selected.len = 0 + + -- construct brand new answer packet + local pkt = req.answer + pkt:clear_payload() + pkt:rcode(kres.rcode.REFUSED) + pkt:ad(false) + pkt:aa(false) + pkt:begin(kres.section.ADDITIONAL) + + local msg = 'blocked by DNS rebinding protection' + pkt:put('\11explanation\7invalid\0', 10800, pkt:qclass(), kres.type.TXT, + string.char(#msg) .. msg) +end + +-- act on DNS queries which were not answered from cache +function M.layer.consume(state, req, pkt) + if state == kres.FAIL then + return state end + + req = kres.request_t(req) + local qry = req:current() + if qry.flags.CACHED then -- do not slow down cached queries + return state end + + pkt = kres.pkt_t(pkt) + local bad_rr = check_pkt(pkt) + if not bad_rr then + return state end + + qry.flags.RESOLVED = 1 -- stop iteration + qry.flags.CACHED = 1 -- do not cache + refuse(req) + log('[' .. string.format('%5d', qry.id) .. '][rebinding] ' + .. 'blocking blacklisted IP \'' .. kres.rr2str(bad_rr) + .. '\' received from IP ' .. tostring(kres.sockaddr_t(req.upstream.addr))) + return kres.FAIL +end + +return M |