Commit eb937f40 authored by Vladimír Čunát's avatar Vladimír Čunát Committed by Petr Špaček

modules/dns64: implement more properties from RFC

- don't synthesize if +CD
- bound synthesized TTL by SOA's TTL
- set AD flag if synthesizing from secure NODATA and A.
- review the RFC for properties that the module is missing
parent 4d2ddbd9
......@@ -13,6 +13,7 @@ Bugfixes
--------
- avoid turning off qname minimization in some cases, e.g. co.uk. (#339)
- fix validation of explicit wildcard queries (#274)
- dns64 module: more properties from the RFC implemented (incl. bug #375)
Improvements
------------
......
......@@ -31,7 +31,8 @@ struct kr_qflags {
bool NO_IPV6 : 1; /**< Disable IPv6 */
bool NO_IPV4 : 1; /**< Disable IPv4 */
bool TCP : 1; /**< Use TCP for this query. */
bool RESOLVED : 1; /**< Query is resolved. */
bool RESOLVED : 1; /**< Query is resolved. Note that kr_query gets
* RESOLVED before following a CNAME chain; see .CNAME. */
bool AWAIT_IPV4 : 1; /**< Query is waiting for A address. */
bool AWAIT_IPV6 : 1; /**< Query is waiting for AAAA address. */
bool AWAIT_CUT : 1; /**< Query is waiting for zone cut lookup */
......
......@@ -4,8 +4,11 @@ DNS64
-----
The module for :rfc:`6147` DNS64 AAAA-from-A record synthesis, it is used to enable client-server communication between an IPv6-only client and an IPv4-only server. See the well written `introduction`_ in the PowerDNS documentation.
If no address is passed (i.e. ``nil``), the well-known prefix ``64:ff9b::`` is used.
.. warning:: The module currently won't work well with :ref:`policy.STUB <mod-policy>`.
Also, the IPv6 passed in configuration is assumed to be ``/96``, and
PTR synthesis and "exclusion prefixes" aren't implemented.
.. tip:: The A record sub-requests will be DNSSEC secured, but the synthetic AAAA records can't be. Make sure the last mile between stub and resolver is secure to avoid spoofing.
......@@ -20,6 +23,4 @@ Example configuration
dns64.config('fe80::21b:aabb:0:0')
.. _RPZ: https://dnsrpz.info/
.. _introduction: https://doc.powerdns.com/md/recursor/dns64
......@@ -3,64 +3,101 @@ local ffi = require('ffi')
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
Subproject commit 314baab234262eb61a3e6cfd03941be5f3c94c9f
Subproject commit 41318bc5feb3b62db0305e9124175d0f0a6b8fb2
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment