local M = {}
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].
+
+ Also the exclusion prefixes are not implemented, sec. 5.1.4 (MUST).
+
+ TODO: support different prefix lengths, defaulting to /96 if not specified
+ https://tools.ietf.org/html/rfc6052#section-2.2
+
+ PTR queries aren't supported (MUST), sec. 5.3.1.2
+]]
+
-- Config
function M.config (confstr)
- if confstr == nil then return end
- M.proxy = kres.str2ip(confstr)
+ M.proxy = kres.str2ip(confstr or '64:ff9b::')
if M.proxy == nil then error('[dns64] "'..confstr..'" is not a valid address') end
end
-- Layers
-M.layer = {
- consume = function (state, req, pkt)
- if state == kres.FAIL then return state end
- pkt = kres.pkt_t(pkt)
- req = kres.request_t(req)
- local qry = req:current()
- -- Observe only authoritative answers
- if M.proxy == nil or not qry.flags.RESOLVED then
- return state
+M.layer = { }
+function M.layer.consume(state, req, pkt)
+ if state == kres.FAIL then return state end
+ pkt = kres.pkt_t(pkt)
+ req = kres.request_t(req)
+ 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 pkt:qclass() ~= kres.class.IN or req.answer:cd() then
+ return state
+ end
+ -- Synthetic AAAA from marked A responses
+ local answer = pkt:section(kres.section.ANSWER)
+
+ -- Observe final AAAA NODATA responses to the current SNAME.
+ local is_nodata = pkt:rcode() == kres.rcode.NOERROR and #answer == 0
+ if pkt:qtype() == kres.type.AAAA and is_nodata and pkt:qname() == qry:name()
+ and qry.flags.RESOLVED and not qry.flags.CNAME and qry.parent == nil 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
- -- Synthetic AAAA from marked A responses
- local answer = pkt:section(kres.section.ANSWER)
- if qry.flags.DNS64_MARK then -- Marked request
- local section = ffi.C.knot_pkt_section(pkt, kres.section.ANSWER)
- for i = 1, section.count do
- local orig = ffi.C.knot_pkt_rr(section, i - 1)
- if orig.type == kres.type.A then
- -- Disable GC, as this object doesn't own either owner or RDATA, it's just a reference
- local rrs = ffi.gc(kres.rrset(nil, kres.type.AAAA, orig.rclass), nil)
- rrs._owner = ffi.cast('knot_dname_t *', orig:owner()) -- explicit cast needed here
- for k = 1, orig.rrs.rr_count do
- local rdata = orig:rdata( k - 1 )
- ffi.copy(addr_buf, M.proxy, 16)
- ffi.copy(addr_buf + 12, rdata, 4)
- ffi.C.knot_rrset_add_rdata(rrs, ffi.string(addr_buf, 16), 16, orig:ttl(), req.pool)
- end
- -- All referred memory is copied within the function,
- -- so it doesn't matter that lua GCs our variables.
- ffi.C.kr_ranked_rrarray_add(
- req.answ_selected,
- rrs,
- ffi.C.KR_RANK_OMIT,
- true,
- qry.uid,
- req.pool)
- end
- end
- else -- Observe AAAA NODATA responses
- local is_nodata = (pkt:rcode() == kres.rcode.NOERROR) and (#answer == 0)
- if pkt:qtype() == kres.type.AAAA and is_nodata and pkt:qname() == qry:name()
- and (qry.flags.RESOLVED and qry.parent == nil) then
- 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)
+ 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 rrs = ffi.gc(kres.rrset(nil, kres.type.AAAA, orig.rr.rclass), nil)
+ rrs._owner = ffi.cast('knot_dname_t *', orig.rr:owner()) -- explicit cast needed here
+ for k = 1, orig.rr.rrs.rr_count do
+ local rdata = orig.rr:rdata( k - 1 )
+ ffi.copy(addr_buf, M.proxy, 12)
+ ffi.copy(addr_buf + 12, rdata, 4)
+ local ttl = orig.rr:ttl()
+ if ttl > max_ttl then ttl = max_ttl end
+ ffi.C.knot_rrset_add_rdata(rrs, ffi.string(addr_buf, 16), 16, ttl, req.pool)
end
+ ffi.C.kr_ranked_rrarray_add(
+ req.answ_selected,
+ rrs,
+ rank,
+ true,
+ qry.uid,
+ req.pool)
end
- return state
end
-}
+end
return M