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
|