]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
modules/dns64: implement "exclusion prefixes"
authorVladimír Čunát <vladimir.cunat@nic.cz>
Tue, 10 Aug 2021 17:42:28 +0000 (19:42 +0200)
committerVladimír Čunát <vladimir.cunat@nic.cz>
Thu, 19 Aug 2021 14:12:52 +0000 (16:12 +0200)
The RFC says we MUST do it, though this implementation is lazy and
avoids a SHOULD in the RFC.

NEWS
modules/dns64/dns64.lua

diff --git a/NEWS b/NEWS
index c64950e014ab22c8222d2ee941721957d534b26f..761d46a8f7761336b2de9bc9b9a601c1caf09a07 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,7 @@ Improvements
 ------------
 - dns64 module: also map the reverse (PTR) subtree (#478, !1201)
 - dns64 module: allow disabling based on client address (#368, !1201)
+- dns64 module: allow configuring AAAA subnets not allowed in answer (!1201)
 
 
 Knot Resolver 5.4.1 (2021-08-19)
index af8926f8f293caca87c136f5afe522ce4d299225..4b3403354af06e33b32ab86d45f97225a0d17786 100644 (file)
@@ -1,7 +1,8 @@
 -- SPDX-License-Identifier: GPL-3.0-or-later
 -- Module interface
 local ffi = require('ffi')
-local M = {}
+local C = ffi.C
+local M = { layer = { } }
 local addr_buf = ffi.new('char[16]')
 
 --[[
@@ -10,8 +11,6 @@ Missing parts of the RFC:
        > 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
 ]]
@@ -35,10 +34,53 @@ function M.config(conf)
                :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
 
--- Layers
-M.layer = { }
 function M.layer.consume(state, req, pkt)
        if state == kres.FAIL then return state end
        local qry = req:current()
@@ -47,13 +89,11 @@ function M.layer.consume(state, req, pkt)
                        or pkt:qclass() ~= kres.class.IN or req.qsource.packet: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
+       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