summaryrefslogtreecommitdiffstats
path: root/modules/dns64/dns64.lua
diff options
context:
space:
mode:
Diffstat (limited to 'modules/dns64/dns64.lua')
-rw-r--r--modules/dns64/dns64.lua103
1 files changed, 103 insertions, 0 deletions
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