181 lines
5.1 KiB
Lua
181 lines
5.1 KiB
Lua
-- 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
|