summaryrefslogtreecommitdiffstats
path: root/modules/renumber
diff options
context:
space:
mode:
Diffstat (limited to 'modules/renumber')
-rw-r--r--modules/renumber/.packaging/test.config4
-rw-r--r--modules/renumber/README.rst27
-rw-r--r--modules/renumber/renumber.lua126
-rw-r--r--modules/renumber/renumber.test.lua87
4 files changed, 244 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..ea26eaf
--- /dev/null
+++ b/modules/renumber/README.rst
@@ -0,0 +1,27 @@
+.. 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'}
+ }
+ }
diff --git a/modules/renumber/renumber.lua b/modules/renumber/renumber.lua
new file mode 100644
index 0000000..25e8624
--- /dev/null
+++ b/modules/renumber/renumber.lua
@@ -0,0 +1,126 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+-- Module interface
+local ffi = require('ffi')
+local prefixes_global = {}
+
+-- Create subnet prefix rule
+local function matchprefix(subnet, 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)
+ -- Mask unspecified, renumber whole IP
+ if bitlen == 0 then
+ bitlen = #target * 8
+ end
+ return {subnet_cd, bitlen, target, addrtype}
+end
+
+-- Create name match rule
+local function matchname(name, addr)
+ 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}
+end
+
+-- Add subnet prefix rewrite rule
+local function add_prefix(subnet, addr)
+ table.insert(prefixes_global, matchprefix(subnet, addr))
+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 addr_buf = ffi.new('char[16]')
+local function renumber_record(tbl, rr)
+ for i = 1, #tbl do
+ local prefix = tbl[i]
+ -- Match record type to address family and record address to given subnet
+ -- If provided, compare record owner to prefix name
+ if match_subnet(prefix[1], prefix[2], prefix[4], rr) then
+ -- Replace part or whole address
+ local to_copy = prefix[2] or (#prefix[3] * 8)
+ local chunks = to_copy / 8
+ local rdlen = #rr.rdata
+ if rdlen < chunks then return rr end -- Address length mismatch
+ ffi.copy(addr_buf, rr.rdata, rdlen)
+ ffi.copy(addr_buf, prefix[3], chunks) -- Rewrite prefix
+ rr.rdata = ffi.string(addr_buf, rdlen)
+ 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
+ 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..c62bd77
--- /dev/null
+++ b/modules/renumber/renumber.test.lua
@@ -0,0 +1,87 @@
+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('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('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('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('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()
+
+verbose(true)
+modules.load('renumber < cache')
+renumber.config({
+ -- Source subnet, destination subnet
+ {'10.2.0.0/24', '192.168.2.0'},
+ {'166.66.0.0/16', '127.0.0.0'},
+ {'2001:db8:1::/48', '2001:db8:2::'},
+})
+
+return {
+ test_renumber,
+}