summaryrefslogtreecommitdiffstats
path: root/modules/dns64
diff options
context:
space:
mode:
Diffstat (limited to 'modules/dns64')
-rw-r--r--modules/dns64/.packaging/test.config4
-rw-r--r--modules/dns64/README.rst28
-rw-r--r--modules/dns64/dns64.lua103
-rw-r--r--modules/dns64/dns64.test.lua53
4 files changed, 188 insertions, 0 deletions
diff --git a/modules/dns64/.packaging/test.config b/modules/dns64/.packaging/test.config
new file mode 100644
index 0000000..5abf524
--- /dev/null
+++ b/modules/dns64/.packaging/test.config
@@ -0,0 +1,4 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+modules.load('dns64')
+assert(dns64)
+quit()
diff --git a/modules/dns64/README.rst b/modules/dns64/README.rst
new file mode 100644
index 0000000..feceb1c
--- /dev/null
+++ b/modules/dns64/README.rst
@@ -0,0 +1,28 @@
+.. SPDX-License-Identifier: GPL-3.0-or-later
+
+.. _mod-dns64:
+
+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 :func:`policy.STUB`.
+ 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.
+
+Example configuration
+---------------------
+
+.. code-block:: lua
+
+ -- Load the module with a NAT64 address
+ modules = { dns64 = 'fe80::21b:77ff:0:0' }
+ -- Reconfigure later
+ dns64.config('fe80::21b:aabb:0:0')
+
+
+.. _introduction: https://doc.powerdns.com/md/recursor/dns64
diff --git a/modules/dns64/dns64.lua b/modules/dns64/dns64.lua
new file mode 100644
index 0000000..a389b78
--- /dev/null
+++ b/modules/dns64/dns64.lua
@@ -0,0 +1,103 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+-- Module interface
+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)
+ 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 = { }
+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 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
+ -- 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);
+end
+
+return M
diff --git a/modules/dns64/dns64.test.lua b/modules/dns64/dns64.test.lua
new file mode 100644
index 0000000..45956a4
--- /dev/null
+++ b/modules/dns64/dns64.test.lua
@@ -0,0 +1,53 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+local condition = require('cqueues.condition')
+
+-- setup resolver
+modules = { 'hints', 'dns64' }
+hints['dns64.example'] = '192.168.1.1'
+hints.use_nodata(true) -- Respond NODATA to AAAA query
+hints.ttl(60)
+dns64.config('fe80::21b:77ff:0:0')
+
+-- helper to wait for query resolution
+local function wait_resolve(qname, qtype)
+ local waiting, done, cond = false, false, condition.new()
+ local rcode, answers = kres.rcode.SERVFAIL, {}
+ resolve {
+ name = qname,
+ type = qtype,
+ finish = function (answer, _)
+ rcode = answer:rcode()
+ answers = answer:section(kres.section.ANSWER)
+ -- Signal as completed
+ if waiting then
+ cond:signal()
+ end
+ done = true
+ end,
+ }
+ -- Wait if it didn't finish immediately
+ if not done then
+ waiting = true
+ cond:wait()
+ end
+ return rcode, answers
+end
+
+-- test builtin rules
+local function test_builtin_rules()
+ local rcode, answers = wait_resolve('dns64.example', kres.type.AAAA)
+ same(rcode, kres.rcode.NOERROR, 'dns64.example returns NOERROR')
+ same(#answers, 1, 'dns64.example synthesised answer')
+ local expect = {'dns64.example.', '60', 'AAAA', 'fe80::21b:77ff:c0a8:101'}
+ if #answers > 0 then
+ local rr = {kres.rr2str(answers[1]):match('(%S+)%s+(%S+)%s+(%S+)%s+(%S+)')}
+ same(rr, expect, 'dns64.example synthesised correct AAAA record')
+ end
+end
+
+-- plan tests
+local tests = {
+ test_builtin_rules,
+}
+
+return tests