diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:26:00 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:26:00 +0000 |
commit | 830407e88f9d40d954356c3754f2647f91d5c06a (patch) | |
tree | d6a0ece6feea91f3c656166dbaa884ef8a29740e /modules/renumber | |
parent | Initial commit. (diff) | |
download | knot-resolver-upstream.tar.xz knot-resolver-upstream.zip |
Adding upstream version 5.6.0.upstream/5.6.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | modules/renumber/.packaging/test.config | 4 | ||||
-rw-r--r-- | modules/renumber/README.rst | 36 | ||||
-rw-r--r-- | modules/renumber/renumber.lua | 181 | ||||
-rw-r--r-- | modules/renumber/renumber.test.lua | 103 |
4 files changed, 324 insertions, 0 deletions
diff --git a/modules/renumber/.packaging/test.config b/modules/renumber/.packaging/test.config new file mode 100644 index 0000000..37f136a --- /dev/null +++ b/modules/renumber/.packaging/test.config @@ -0,0 +1,4 @@ +-- SPDX-License-Identifier: GPL-3.0-or-later +modules.load('renumber') +assert(renumber) +quit() diff --git a/modules/renumber/README.rst b/modules/renumber/README.rst new file mode 100644 index 0000000..2e68991 --- /dev/null +++ b/modules/renumber/README.rst @@ -0,0 +1,36 @@ +.. SPDX-License-Identifier: GPL-3.0-or-later + +.. _mod-renumber: + +IP address renumbering +====================== + +The module renumbers addresses in answers to different address space. +e.g. you can redirect malicious addresses to a blackhole, or use private address ranges +in local zones, that will be remapped to real addresses by the resolver. + + +.. warning:: While requests are still validated using DNSSEC, the signatures + are stripped from final answer. The reason is that the address synthesis + breaks signatures. You can see whether an answer was valid or not based on + the AD flag. + +Example configuration +--------------------- + +.. code-block:: lua + + modules = { + renumber = { + -- Source subnet, destination subnet + {'10.10.10.0/24', '192.168.1.0'}, + -- Remap /16 block to localhost address range + {'166.66.0.0/16', '127.0.0.0'}, + -- Remap /26 subnet (64 ip addresses) + {'166.55.77.128/26', '127.0.0.192'}, + -- Remap a /32 block to a single address + {'2001:db8::/32', '::1!'}, + } + } + +.. TODO: renumber.name() hangs in vacuum, kind of. No occurrences in code or docs, and probably bad UX. diff --git a/modules/renumber/renumber.lua b/modules/renumber/renumber.lua new file mode 100644 index 0000000..60803d5 --- /dev/null +++ b/modules/renumber/renumber.lua @@ -0,0 +1,181 @@ +-- SPDX-License-Identifier: GPL-3.0-or-later +-- Module interface +local ffi = require('ffi') +local prefixes_global = {} + +-- get address from config: either subnet prefix or fixed endpoint +local function extract_address(target) + local idx = string.find(target, "!", 1, true) + if idx == nil then + return target, false + end + if idx ~= #target then + error("[renumber] \"!\" symbol in target is only accepted at the end of address") + end + return string.sub(target, 1, idx - 1), true +end + +-- Create bitmask from integer mask for single octet: 2 -> 11000000 +local function getOctetBitmask(intMask) + return bit.lshift(bit.rshift(255, 8 - intMask), 8 - intMask) +end + +-- Merge ipNet with ipHost, using intMask +local function mergeIps(ipNet, ipHost, intMask) + local octetMask + local result = "" + + if #ipNet ~= #ipHost then + return nil + end + + if intMask == nil then + return ipNet + end + + for currentOctetNo = 1, #ipNet do + if intMask >= 8 then + result = result .. ipNet:sub(currentOctetNo,currentOctetNo) + elseif (intMask <= 0) then + result = result .. ipHost:sub(currentOctetNo,currentOctetNo) + else + octetMask = getOctetBitmask(intMask) + result = result .. string.char(bit.bor( + bit.band(string.byte(ipNet:sub(currentOctetNo,currentOctetNo)), octetMask), + bit.band(string.byte(ipHost:sub(currentOctetNo,currentOctetNo)), bit.bnot(octetMask)) + )) + end + intMask = intMask - 8 + end + + return result +end + +-- Create subnet prefix rule +local function matchprefix(subnet, addr) + local is_exact + addr, is_exact = extract_address(addr) + local target = kres.str2ip(addr) + if target == nil then error('[renumber] invalid address: '..addr) end + local addrtype = string.find(addr, ':', 1, true) and kres.type.AAAA or kres.type.A + local subnet_cd = ffi.new('char[16]') + local bitlen = ffi.C.kr_straddr_subnet(subnet_cd, subnet) + if bitlen < 0 then error('[renumber] invalid subnet: '..subnet) end + return {subnet_cd, bitlen, target, addrtype, is_exact} +end + +-- Create name match rule +local function matchname(name, addr) + local is_exact + addr, is_exact = extract_address(addr) -- though matchname() always leads to replacing whole address + local target = kres.str2ip(addr) + if target == nil then error('[renumber] invalid address: '..addr) end + local owner = todname(name) + if not name then error('[renumber] invalid name: '..name) end + local addrtype = string.find(addr, ':', 1, true) and kres.type.AAAA or kres.type.A + return {owner, nil, target, addrtype, is_exact} +end + +-- Add subnet prefix rewrite rule +local function add_prefix(subnet, addr) + local prefix = matchprefix(subnet, addr) + table.insert(prefixes_global, prefix) +end + +-- Match IP against given subnet or record owner +local function match_subnet(subnet, bitlen, addrtype, rr) + local addr = rr.rdata + return addrtype == rr.type and + ((bitlen and (#addr >= bitlen / 8) and (ffi.C.kr_bitcmp(subnet, addr, bitlen) == 0)) or subnet == rr.owner) +end + +-- Renumber address record +local function renumber_record(tbl, rr) + for i = 1, #tbl do + local prefix = tbl[i] + local subnet = prefix[1] + local bitlen = prefix[2] + local target = prefix[3] + local addrtype = prefix[4] + local is_exact = prefix[5] + + -- Match record type to address family and record address to given subnet + -- If provided, compare record owner to prefix name + if match_subnet(subnet, bitlen, addrtype, rr) then + if is_exact then + rr.rdata = target + else + local mergedHost = mergeIps(target, rr.rdata, bitlen) + if mergedHost ~= nil then rr.rdata = mergedHost end + end + + return rr + end + end + return nil +end + +-- Renumber addresses based on config +local function rule(prefixes) + return function (state, req) + if state == kres.FAIL then return state end + local pkt = req.answer + -- Only successful answers + local records = pkt:section(kres.section.ANSWER) + local ancount = #records + if ancount == 0 then return state end + -- Find renumber candidates + local changed = false + for i = 1, ancount do + local rr = records[i] + if rr.type == kres.type.A or rr.type == kres.type.AAAA then + local new_rr = renumber_record(prefixes, rr) + if new_rr ~= nil then + records[i] = new_rr + changed = true + end + end + end + -- If not rewritten, chain action + if not changed then return state end + -- Replace section if renumbering + local qname = pkt:qname() + local qclass = pkt:qclass() + local qtype = pkt:qtype() + pkt:recycle() + pkt:question(qname, qclass, qtype) + for i = 1, ancount do + local rr = records[i] + -- Strip signatures as rewritten data cannot be validated + if rr.type ~= kres.type.RRSIG then + pkt:put(rr.owner, rr.ttl, rr.class, rr.type, rr.rdata) + end + end + req:set_extended_error(kres.extended_error.FORGED, "DUQR") + return state + end +end + +-- Export module interface +local M = { + prefix = matchprefix, + name = matchname, + rule = rule, + match_subnet = match_subnet, +} + +-- Config +function M.config (conf) + if conf == nil then return end + if type(conf) ~= 'table' or type(conf[1]) ~= 'table' then + error('[renumber] expected { {prefix, target}, ... }') + end + for i = 1, #conf do add_prefix(conf[i][1], conf[i][2]) end +end + +-- Layers +M.layer = { + finish = rule(prefixes_global), +} + +return M diff --git a/modules/renumber/renumber.test.lua b/modules/renumber/renumber.test.lua new file mode 100644 index 0000000..97d6a6f --- /dev/null +++ b/modules/renumber/renumber.test.lua @@ -0,0 +1,103 @@ +local function gen_rrset(owner, rrtype, rdataset) + assert(type(rdataset) == 'table' or type(rdataset) == 'string') + if type(rdataset) ~= 'table' then + rdataset = { rdataset } + end + local rrset = kres.rrset(todname(owner), rrtype, kres.class.IN, 3600) + for _, rdata in pairs(rdataset) do + assert(rrset:add_rdata(rdata, #rdata)) + end + return rrset +end + +local function prepare_cache() + cache.open(100*MB) + cache.clear() + + local ffi = require('ffi') + local c = kres.context().cache + + assert(c:insert( + gen_rrset('a10-0.test.', + kres.type.A, kres.str2ip('10.0.0.1')), + nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH)) + assert(c:insert( + gen_rrset('a10-2.test.', + kres.type.A, kres.str2ip('10.2.0.1')), + nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH)) + assert(c:insert( + gen_rrset('a10-0plus2.test.', + kres.type.A, { + kres.str2ip('10.0.0.1'), + kres.str2ip('10.2.0.1') + }), + nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH)) + assert(c:insert( + gen_rrset('a10-3plus4.test.', + kres.type.A, { + kres.str2ip('10.3.0.1'), + kres.str2ip('10.4.0.1') + }), + nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH)) + assert(c:insert( + gen_rrset('a166-66.test.', + kres.type.A, kres.str2ip('166.66.42.123')), + nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH)) + assert(c:insert( + gen_rrset('a167-81.test.', + kres.type.A, kres.str2ip('167.81.254.221')), + nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH)) + assert(c:insert( + gen_rrset('aaaa-db8-1.test.', + kres.type.AAAA, { + kres.str2ip('2001:db8:1::1'), + kres.str2ip('2001:db8:1::2'), + }), + nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH)) + + c:commit() +end + +local check_answer = require('test_utils').check_answer + +local function test_renumber() + check_answer('unknown IPv4 range passes through unaffected', + 'a10-0.test.', kres.type.A, kres.rcode.NOERROR, '10.0.0.1') + check_answer('known IPv4 range is remapped when matching first-defined rule', + 'a10-2.test.', kres.type.A, kres.rcode.NOERROR, '192.168.2.1') + check_answer('mix of known and unknown IPv4 ranges is remapped correctly', + 'a10-0plus2.test.', kres.type.A, kres.rcode.NOERROR, {'192.168.2.1', '10.0.0.1'}) + check_answer('mix of known and unknown IPv4 ranges is remapped correctly to exact address', + 'a10-3plus4.test.', kres.type.A, kres.rcode.NOERROR, {'10.3.0.1', '192.168.3.10'}) + check_answer('known IPv4 range is remapped when matching second-defined rule', + 'a166-66.test.', kres.type.A, kres.rcode.NOERROR, '127.0.42.123') + check_answer('known IPv4 range is remapped when matching a rule with netmask not on a byte boundary', + 'a167-81.test.', kres.type.A, kres.rcode.NOERROR, {'127.0.30.221'}) + + check_answer('two AAAA records', + 'aaaa-db8-1.test.', kres.type.AAAA, kres.rcode.NOERROR, + {'2001:db8:2::2', '2001:db8:2::1'}) +end + +net.ipv4 = false +net.ipv6 = false + +trust_anchors.remove('.') +policy.add(policy.all(policy.DEBUG_ALWAYS)) +policy.add(policy.suffix(policy.PASS, {todname('test.')})) +prepare_cache() + +log_level('debug') +modules.load('renumber < cache') +renumber.config({ + -- Source subnet, destination subnet + {'10.2.0.0/24', '192.168.2.0'}, + {'10.4.0.0/24', '192.168.3.10!'}, + {'166.66.0.0/16', '127.0.0.0'}, + {'167.81.255.0/19', '127.0.0.0'}, + {'2001:db8:1::/48', '2001:db8:2::'}, +}) + +return { + test_renumber, +} |