1
0
Fork 0
knot-resolver/modules/dns64/dns64.lua
Daniel Baumann fbc604e215
Adding upstream version 5.7.5.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-21 13:56:17 +02:00

220 lines
7.5 KiB
Lua

-- SPDX-License-Identifier: GPL-3.0-or-later
-- Module interface
local kres = require('kres')
local ffi = require('ffi')
local C = ffi.C
local M = { layer = { } }
local addr_buf = ffi.new('char[16]')
--[[
Missing parts of the RFC:
> The implementation SHOULD support mapping of separate IPv4 address
> ranges to separate IPv6 prefixes for AAAA record synthesis. This
> allows handling of special use IPv4 addresses [RFC5735].
TODO: support different prefix lengths, defaulting to /96 if not specified
https://tools.ietf.org/html/rfc6052#section-2.2
]]
-- Config
function M.config(conf)
if type(conf) ~= 'table' then
conf = { prefix = conf }
end
M.proxy = kres.str2ip(tostring(conf.prefix or '64:ff9b::'))
if M.proxy == nil or #M.proxy ~= 16 then
error(string.format('[dns64] %q is not a valid IPv6 address', conf.prefix), 2)
end
M.rev_ttl = conf.rev_ttl or 60
M.rev_suffix = kres.str2dname(M.proxy
:sub(1, 96/8)
-- hexdump, reverse, intersperse by dots
:gsub('.', function (ch) return string.format('%02x', string.byte(ch)) end)
:reverse()
:gsub('.', '%1.')
.. 'ip6.arpa.'
)
-- RFC 6147.5.1.4
M.exclude_subnets = {}
if conf.exclude_subnets ~= nil and type(conf.exclude_subnets) ~= 'table' then
error('[dns64] .exclude_subnets is not a table')
end
for _, subnet_cfg in ipairs(conf.exclude_subnets or { '::ffff/96' }) do
local subnet = {}
subnet.prefix = ffi.new('char[16]')
subnet.bitlen = C.kr_straddr_subnet(subnet.prefix, tostring(subnet_cfg))
if subnet.bitlen < 0 or not string.find(subnet_cfg, ':', 1, true) then
error(string.format('[dns64] failed to parse IPv6 subnet: %q', subnet_cfg))
end
table.insert(M.exclude_subnets, subnet)
end
end
-- Filter the AAAA records from the last ANSWER, return iff it's NODATA afterwards.
-- Currently the implementation is lazy and kills it all if any AAAA is excluded.
local function do_exclude_prefixes(qry)
local rrsel = qry.request.answ_selected
for i = 0, tonumber(rrsel.len) - 1 do
local rr_e = rrsel.at[i] -- struct ranked_rr_array_entry
if rr_e.qry_uid ~= qry.uid or rr_e.rr.type ~= kres.type.AAAA or not rr_e.to_wire
then goto next_rrset end
-- Found answer AAAA RRset
for _, subnet in ipairs(M.exclude_subnets) do
for j = 0, rr_e.rr:rdcount() - 1 do
local rd = rr_e.rr:rdata_pt(j)
if rd.len == 16 and C.kr_bitcmp(subnet.prefix, rd.data, subnet.bitlen) == 0 then
-- We can't use this RR. TODO: and we're lazy,
-- so we kill the whole RRset instead of filtering.
rr_e.to_wire = false
return true
end
end
end
-- We can use the answer -> return false
-- We use a nonsensical if to fool the parser; is return adjacent to a label forbidden?
if true then return false end
::next_rrset::
end
-- No RRset found, it was probably NODATA.
return true
end
function M.layer.consume(state, req, pkt)
if state == kres.FAIL then return state end
local qry = req:current()
-- Observe only final answers in IN class where request has no CD flag.
if M.proxy == nil or not qry.flags.RESOLVED or qry.flags.DNS64_DISABLE
or pkt:qclass() ~= kres.class.IN or req.qsource.packet:cd() then
return state
end
-- Observe final AAAA NODATA responses to the current SNAME.
if pkt:qtype() == kres.type.AAAA and pkt:qname() == qry:name()
and qry.flags.RESOLVED and not qry.flags.CNAME and qry.parent == nil
and pkt:rcode() == kres.rcode.NOERROR and do_exclude_prefixes(qry) then
-- Start a *marked* corresponding A sub-query.
local extraFlags = kres.mk_qflags({})
extraFlags.DNSSEC_WANT = qry.flags.DNSSEC_WANT
extraFlags.AWAIT_CUT = true
extraFlags.DNS64_MARK = true
req:push(pkt:qname(), kres.type.A, kres.class.IN, extraFlags, qry)
return state
end
-- Observe answer to the marked sub-query, and convert all A records in ANSWER
-- to corresponding AAAA records to be put into the request's answer.
if not qry.flags.DNS64_MARK then return state end
-- Find rank for the NODATA answer.
-- That will result into corresponding AD flag. See RFC 6147 5.5.2.
local neg_rank
if qry.parent.flags.DNSSEC_WANT and not qry.parent.flags.DNSSEC_INSECURE
then neg_rank = ffi.C.KR_RANK_SECURE
else neg_rank = ffi.C.KR_RANK_INSECURE
end
-- Find TTL bound from SOA, according to RFC 6147 5.1.7.4.
local max_ttl = 600
for i = 1, tonumber(req.auth_selected.len) do
local entry = req.auth_selected.at[i - 1]
if entry.qry_uid == qry.parent.uid and entry.rr
and entry.rr.type == kres.type.SOA
and entry.rr.rclass == kres.class.IN then
max_ttl = entry.rr:ttl()
end
end
-- Find the As and do the conversion itself.
for i = 1, tonumber(req.answ_selected.len) do
local orig = req.answ_selected.at[i - 1]
if orig.qry_uid == qry.uid and orig.rr.type == kres.type.A then
local rank = neg_rank
if orig.rank < rank then rank = orig.rank end
-- Disable GC, as this object doesn't own owner or RDATA, it's just a reference
local ttl = orig.rr:ttl()
if ttl > max_ttl then ttl = max_ttl end
local rrs = ffi.gc(kres.rrset(nil, kres.type.AAAA, orig.rr.rclass, ttl), nil)
rrs._owner = orig.rr._owner
for k = 1, orig.rr.rrs.count do
local rdata = orig.rr:rdata( k - 1 )
ffi.copy(addr_buf, M.proxy, 12)
ffi.copy(addr_buf + 12, rdata, 4)
ffi.C.knot_rrset_add_rdata(rrs, ffi.string(addr_buf, 16), 16, req.pool)
end
ffi.C.kr_ranked_rrarray_add(
req.answ_selected,
rrs,
rank,
true,
qry.uid,
req.pool)
end
end
ffi.C.kr_ranked_rrarray_finalize(req.answ_selected, qry.uid, req.pool)
req:set_extended_error(kres.extended_error.FORGED, "BHD4: DNS64 synthesis")
end
local function hexchar2int(char)
if char >= string.byte('0') and char <= string.byte('9') then
return char - string.byte('0')
elseif char >= string.byte('a') and char <= string.byte('f') then
return 10 + char - string.byte('a')
else
return nil
end
end
-- Map the reverse subtree by generating CNAMEs; similarly to the hints module.
--
-- RFC 6147.5.3.1.2 says we SHOULD only generate CNAME if it points to data,
-- but I can't see what's wrong with a CNAME to an NXDOMAIN/NODATA
-- Reimplementation idea: as-if we had a DNAME in policy/cache?
function M.layer.produce(_, req, pkt)
local qry = req.current_query
local sname = qry.sname
if ffi.C.knot_dname_in_bailiwick(sname, M.rev_suffix) < 0 or qry.flags.DNS64_DISABLE
then return end
-- Update packet question if it was minimized.
qry.flags.NO_MINIMIZE = true
if not ffi.C.knot_dname_is_equal(pkt.wire + 12, sname) or not pkt:has_wire() then
if not pkt:recycle() or not pkt:question(sname, qry.sclass, qry.stype)
then return end
end
-- Generate a CNAME iff the full address is queried; otherwise leave NODATA.
local labels_missing = 16*2 + 2 - ffi.C.knot_dname_labels(sname, nil)
if labels_missing == 0 then
-- Transforming v6 labels (hex) to v4 ones (decimal) isn't trivial:
local labels = sname
local v4name = ''
for _ = 1, 4 do -- append one IPv4 label at a time into v4name
local v4octet = 0
for i = 0, 1 do
if labels[0] ~= 1 then return end
local ch = hexchar2int(labels[1])
if not ch then return end
v4octet = v4octet + ch * 16^i
labels = labels + 2
end
v4octet = tostring(v4octet)
v4name = v4name .. string.char(#v4octet) .. v4octet
end
v4name = v4name .. '\7in-addr\4arpa\0'
if not pkt:put(sname, M.rev_ttl, kres.class.IN, kres.type.CNAME, v4name)
then return end
end
-- Simple finishing touches.
if labels_missing < 0 then -- and use NXDOMAIN for too long queries
pkt:rcode(kres.rcode.NXDOMAIN)
else
pkt:rcode(kres.rcode.NOERROR)
end
pkt.parsed = pkt.size
pkt:aa(true)
pkt:qr(true)
qry.flags.CACHED = true
end
return M