1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
|
-- 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
|