RecursorLua4::RecursorLua4() { prepareContext(); }
-static int getFakeAAAARecords(const DNSName& qname, const std::string& prefix, vector<DNSRecord>& ret)
-{
- int rcode=directResolve(qname, QType(QType::A), 1, ret);
-
- ComboAddress prefixAddress(prefix);
-
- // Remove double CNAME records
- std::set<DNSName> seenCNAMEs;
- ret.erase(std::remove_if(
- ret.begin(),
- ret.end(),
- [&seenCNAMEs](DNSRecord& rr) {
- if (rr.d_type == QType::CNAME) {
- auto target = getRR<CNAMERecordContent>(rr);
- if (target == nullptr) {
- return false;
- }
- if (seenCNAMEs.count(target->getTarget()) > 0) {
- // We've had this CNAME before, remove it
- return true;
- }
- seenCNAMEs.insert(target->getTarget());
- }
- return false;
- }),
- ret.end());
-
- bool seenA = false;
- for(DNSRecord& rr : ret)
- {
- if(rr.d_type == QType::A && rr.d_place==DNSResourceRecord::ANSWER) {
- if(auto rec = getRR<ARecordContent>(rr)) {
- ComboAddress ipv4(rec->getCA());
- uint32_t tmp;
- memcpy((void*)&tmp, &ipv4.sin4.sin_addr.s_addr, 4);
- // tmp=htonl(tmp);
- memcpy(((char*)&prefixAddress.sin6.sin6_addr.s6_addr)+12, &tmp, 4);
- rr.d_content = std::make_shared<AAAARecordContent>(prefixAddress);
- rr.d_type = QType::AAAA;
- }
- seenA = true;
- }
- }
-
- if (seenA) {
- // We've seen an A in the ANSWER section, so there is no need to keep any
- // SOA in the AUTHORITY section as this is not a NODATA response.
- ret.erase(std::remove_if(
- ret.begin(),
- ret.end(),
- [](DNSRecord& rr) {
- return (rr.d_type == QType::SOA && rr.d_place==DNSResourceRecord::AUTHORITY);
- }),
- ret.end());
- }
- return rcode;
-}
-
-static int getFakePTRRecords(const DNSName& qname, const std::string& prefix, vector<DNSRecord>& ret)
-{
- /* qname has a reverse ordered IPv6 address, need to extract the underlying IPv4 address from it
- and turn it into an IPv4 in-addr.arpa query */
- ret.clear();
- vector<string> parts = qname.getRawLabels();
-
- if(parts.size() < 8)
- return -1;
-
- string newquery;
- for(int n = 0; n < 4; ++n) {
- newquery +=
- std::to_string(stoll(parts[n*2], 0, 16) + 16*stoll(parts[n*2+1], 0, 16));
- newquery.append(1,'.');
- }
- newquery += "in-addr.arpa.";
-
-
- DNSRecord rr;
- rr.d_name = qname;
- rr.d_type = QType::CNAME;
- rr.d_content = std::make_shared<CNAMERecordContent>(newquery);
- ret.push_back(rr);
-
- int rcode = directResolve(DNSName(newquery), QType(QType::PTR), 1, ret);
-
- return rcode;
-
-}
-
boost::optional<dnsheader> RecursorLua4::DNSQuestion::getDH() const
{
if (dh)
ret = followCNAMERecords(dq.records, QType(dq.qtype));
}
else if(dq.followupFunction=="getFakeAAAARecords") {
- ret=getFakeAAAARecords(dq.followupName, dq.followupPrefix, dq.records);
+ ret=getFakeAAAARecords(dq.followupName, ComboAddress(dq.followupPrefix), dq.records);
}
else if(dq.followupFunction=="getFakePTRRecords") {
- ret=getFakePTRRecords(dq.followupName, dq.followupPrefix, dq.records);
+ ret=getFakePTRRecords(dq.followupName, dq.records);
}
else if(dq.followupFunction=="udpQueryResponse") {
dq.udpAnswer = GenUDPQueryResponse(dq.udpQueryDest, dq.udpQuery);
return false;
#endif /* F_SETPIPE_SZ */
}
+
+DNSName reverseNameFromIP(const ComboAddress& ip)
+{
+ if (ip.isIPv4()) {
+ std::string result("in-addr.arpa.");
+ auto ptr = reinterpret_cast<const uint8_t*>(&ip.sin4.sin_addr.s_addr);
+ for (size_t idx = 0; idx < sizeof(ip.sin4.sin_addr.s_addr); idx++) {
+ result = std::to_string(ptr[idx]) + "." + result;
+ }
+ return DNSName(result);
+ }
+ else if (ip.isIPv6()) {
+ std::string result("ip6.arpa.");
+ auto ptr = reinterpret_cast<const uint8_t*>(&ip.sin6.sin6_addr.s6_addr[0]);
+ for (size_t idx = 0; idx < sizeof(ip.sin6.sin6_addr.s6_addr); idx++) {
+ std::stringstream stream;
+ stream << std::hex << (ptr[idx] & 0x0F);
+ stream << '.';
+ stream << std::hex << (((ptr[idx]) >> 4) & 0x0F);
+ stream << '.';
+ result = stream.str() + result;
+ }
+ return DNSName(result);
+ }
+
+ throw std::runtime_error("Calling reverseNameFromIP() for an address which is neither an IPv4 nor an IPv6");
+}
int mapThreadToCPUList(pthread_t tid, const std::set<int>& cpus);
std::vector<ComboAddress> getResolvers(const std::string& resolvConfPath);
+
+DNSName reverseNameFromIP(const ComboAddress& ip);
static std::shared_ptr<NetmaskGroup> g_initialAllowFrom; // new thread needs to be setup with this
static NetmaskGroup g_XPFAcl;
static NetmaskGroup g_proxyProtocolACL;
+static boost::optional<ComboAddress> g_dns64Prefix{boost::none};
+static DNSName g_dns64PrefixReverse;
static size_t g_proxyProtocolMaximumSize;
static size_t g_tcpMaxQueriesPerConn;
static size_t s_maxUDPQueriesPerRound;
return rcode;
}
+int getFakeAAAARecords(const DNSName& qname, ComboAddress prefix, vector<DNSRecord>& ret)
+{
+ int rcode = directResolve(qname, QType(QType::A), QClass::IN, ret);
+
+ // Remove double CNAME records
+ std::set<DNSName> seenCNAMEs;
+ ret.erase(std::remove_if(
+ ret.begin(),
+ ret.end(),
+ [&seenCNAMEs](DNSRecord& rr) {
+ if (rr.d_type == QType::CNAME) {
+ auto target = getRR<CNAMERecordContent>(rr);
+ if (target == nullptr) {
+ return false;
+ }
+ if (seenCNAMEs.count(target->getTarget()) > 0) {
+ // We've had this CNAME before, remove it
+ return true;
+ }
+ seenCNAMEs.insert(target->getTarget());
+ }
+ return false;
+ }),
+ ret.end());
+
+ bool seenA = false;
+ for (DNSRecord& rr : ret) {
+ if (rr.d_type == QType::A && rr.d_place == DNSResourceRecord::ANSWER) {
+ if (auto rec = getRR<ARecordContent>(rr)) {
+ ComboAddress ipv4(rec->getCA());
+ uint32_t tmp;
+ memcpy(&tmp, &ipv4.sin4.sin_addr.s_addr, 4);
+ // tmp=htonl(tmp);
+ memcpy(((char*)&prefix.sin6.sin6_addr.s6_addr)+12, &tmp, 4);
+ rr.d_content = std::make_shared<AAAARecordContent>(prefix);
+ rr.d_type = QType::AAAA;
+ }
+ seenA = true;
+ }
+ }
+
+ if (seenA) {
+ // We've seen an A in the ANSWER section, so there is no need to keep any
+ // SOA in the AUTHORITY section as this is not a NODATA response.
+ ret.erase(std::remove_if(
+ ret.begin(),
+ ret.end(),
+ [](DNSRecord& rr) {
+ return (rr.d_type == QType::SOA && rr.d_place == DNSResourceRecord::AUTHORITY);
+ }),
+ ret.end());
+ }
+ return rcode;
+}
+
+int getFakePTRRecords(const DNSName& qname, vector<DNSRecord>& ret)
+{
+ /* qname has a reverse ordered IPv6 address, need to extract the underlying IPv4 address from it
+ and turn it into an IPv4 in-addr.arpa query */
+ ret.clear();
+ vector<string> parts = qname.getRawLabels();
+
+ if (parts.size() < 8) {
+ return -1;
+ }
+
+ string newquery;
+ for (int n = 0; n < 4; ++n) {
+ newquery +=
+ std::to_string(stoll(parts[n*2], 0, 16) + 16*stoll(parts[n*2+1], 0, 16));
+ newquery.append(1, '.');
+ }
+ newquery += "in-addr.arpa.";
+
+ DNSRecord rr;
+ rr.d_name = qname;
+ rr.d_type = QType::CNAME;
+ rr.d_content = std::make_shared<CNAMERecordContent>(newquery);
+ ret.push_back(rr);
+
+ int rcode = directResolve(DNSName(newquery), QType(QType::PTR), QClass::IN, ret);
+
+ return rcode;
+}
+
enum class PolicyResult : uint8_t { NoAction, HaveAnswer, Drop };
static PolicyResult handlePolicyHit(const DNSFilterEngine::Policy& appliedPolicy, const std::unique_ptr<DNSComboWriter>& dc, SyncRes& sr, int& res, vector<DNSRecord>& ret, DNSPacketWriter& pw)
}
// if there is a RecursorLua active, and it 'took' the query in preResolve, we don't launch beginResolve
- if(!t_pdl || !t_pdl->preresolve(dq, res)) {
+ if (!t_pdl || !t_pdl->preresolve(dq, res)) {
+
+ if (!g_dns64PrefixReverse.empty() && dq.qtype == QType::PTR && dq.qname.isPartOf(g_dns64PrefixReverse)) {
+ res = getFakePTRRecords(dq.qname, ret);
+ goto haveAnswer;
+ }
sr.setWantsRPZ(wantsRPZ);
if (wantsRPZ && appliedPolicy.d_kind != DNSFilterEngine::PolicyKind::NoAction) {
}
}
- if(t_pdl) {
- if(res == RCode::NoError) {
- auto i=ret.cbegin();
- for(; i!= ret.cend(); ++i)
- if(i->d_type == dc->d_mdp.d_qtype && i->d_place == DNSResourceRecord::ANSWER)
- break;
- if(i == ret.cend() && t_pdl->nodata(dq, res))
- shouldNotValidate = true;
+ if (t_pdl || g_dns64Prefix) {
+ if (res == RCode::NoError) {
+ auto i = ret.cbegin();
+ for(; i!= ret.cend(); ++i) {
+ if (i->d_type == dc->d_mdp.d_qtype && i->d_place == DNSResourceRecord::ANSWER) {
+ break;
+ }
+ }
+
+ if (i == ret.cend()) {
+ /* no record in the answer section, NODATA */
+ if (t_pdl && t_pdl->nodata(dq, res)) {
+ shouldNotValidate = true;
+ }
+ else if (g_dns64Prefix && dq.qtype == QType::AAAA && dq.validationState != Bogus) {
+ res = getFakeAAAARecords(dq.qname, *g_dns64Prefix, ret);
+ shouldNotValidate = true;
+ }
+ }
}
- else if(res == RCode::NXDomain && t_pdl->nxdomain(dq, res))
+ else if(res == RCode::NXDomain && t_pdl && t_pdl->nxdomain(dq, res)) {
shouldNotValidate = true;
+ }
- if(t_pdl->postresolve(dq, res))
+ if (t_pdl && t_pdl->postresolve(dq, res)) {
shouldNotValidate = true;
+ }
}
if (wantsRPZ) { //XXX This block is repeated, see above
g_proxyProtocolACL.toMasks(::arg()["proxy-protocol-from"]);
g_proxyProtocolMaximumSize = ::arg().asNum("proxy-protocol-maximum-size");
+ if (!::arg()["dns64-prefix"].empty()) {
+ auto dns64Prefix = Netmask(::arg()["dns64-prefix"]);
+ if (dns64Prefix.getBits() != 96) {
+ g_log << Logger::Error << "Invalid prefix for 'dns64-prefix', the current implementation only supports /96 prefixe: " << ::arg()["dns64-prefix"] << endl;
+ exit(1);
+ }
+ g_dns64Prefix = dns64Prefix.getNetwork();
+ g_dns64PrefixReverse = reverseNameFromIP(*g_dns64Prefix);
+ /* /96 is 24 nibbles + 2 for "ip6.arpa." */
+ while (g_dns64PrefixReverse.countLabels() > 26) {
+ g_dns64PrefixReverse.chopOff();
+ }
+ }
+
g_networkTimeoutMsec = ::arg().asNum("network-timeout");
g_initialDomainMap = parseAuthAndForwards();
::arg().set("proxy-protocol-from", "A Proxy Protocol header is only allowed from these subnets")="";
::arg().set("proxy-protocol-maximum-size", "The maximum size of a proxy protocol payload, including the TLV values")="512";
+ ::arg().set("dns64-prefix", "DNS64 prefix")="";
+
::arg().set("udp-source-port-min", "Minimum UDP port to bind on")="1024";
::arg().set("udp-source-port-max", "Maximum UDP port to bind on")="65535";
::arg().set("udp-source-port-avoid", "List of comma separated UDP port number to avoid")="11211";
We do this by retrieving the A records for ``www.example.com``, and translating them to AAAA records.
Elsewhere, a NAT64 device listens on these IPv6 addresses, and extracts the IPv4 address from each packet, and proxies it on.
-For maximum flexibility, DNS64 support is included in the :doc:`lua-scripting/index`.
+As of 4.4.0, an efficient implementation is built the recursor and can be enabled via the using the :ref:`dns64-prefix setting <setting-dns64-prefix>`.
+On earlier versions or for maximum flexibility, DNS64 support is included in the :doc:`lua-scripting/index`.
This allows for example to hand out custom IPv6 gateway ranges depending on the location of the requestor, enabling the use of NAT64 services close to the user.
Apart from faking AAAA records, it is also possible to also generate the associated PTR records.
handle incoming queries and distribute them to other threads based on a hash of the query, to maximize the cache hit
ratio.
+.. _setting-dns64-prefix:
+
+``dns64-prefix``
+----------------
+.. versionadded:: 4.4.0
+
+- Netmask, as a string
+- Default: None
+
+Enable DNS64 (:rfc:`6147`) support using the supplied /96 IPv6 prefix. This will generate 'fake' AAAA records for names
+with only `A` records, as well as 'fake' PTR records to make sure that reverse lookup of DNS64-generated IPv6 addresses
+generate the right name.
+See :doc:`dns64` for more flexible but slower alternatives using Lua.
+
.. _setting-dnssec:
``dnssec``
int directResolve(const DNSName& qname, const QType& qtype, int qclass, vector<DNSRecord>& ret);
int followCNAMERecords(std::vector<DNSRecord>& ret, const QType& qtype);
+int getFakeAAAARecords(const DNSName& qname, ComboAddress prefix, vector<DNSRecord>& ret);
+int getFakePTRRecords(const DNSName& qname, vector<DNSRecord>& ret);
template<class T> T broadcastAccFunction(const boost::function<T*()>& func);
#include <boost/assign/list_of.hpp>
#include <boost/tuple/tuple.hpp>
-#include "misc.hh"
-#include "dns.hh"
+
#include <arpa/inet.h>
-#include <utility>
+
+#include "dns.hh"
+#include "iputils.hh"
+#include "misc.hh"
+#include "utility.hh"
using std::string;
BOOST_CHECK(rfc1982check<uint64_t>(UINT64_MAX/2, UINT64_MAX-10));
}
-BOOST_AUTO_TEST_SUITE_END()
+BOOST_AUTO_TEST_CASE(test_reverse_name_to_ip)
+{
+ static const ComboAddress v4("192.0.2.1");
+ static const ComboAddress v6("2001:DB8::42");
+ BOOST_CHECK_EQUAL(reverseNameFromIP(v4).toString(), "1.2.0.192.in-addr.arpa.");
+ BOOST_CHECK_EQUAL(reverseNameFromIP(v6).toString(), "2.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.");
+}
+BOOST_AUTO_TEST_SUITE_END()
--- /dev/null
+import dns
+import os
+
+from recursortests import RecursorTest
+
+class DNS64RecursorTest(RecursorTest):
+
+ _confdir = 'DNS64'
+ _config_template = """
+ auth-zones=example.dns64=configs/%s/example.dns64.zone
+ auth-zones+=in-addr.arpa=configs/%s/in-addr.arpa.zone
+ auth-zones+=ip6.arpa=configs/%s/ip6.arpa.zone
+
+ dns64-prefix=64:ff9b::/96
+ """ % (_confdir, _confdir, _confdir)
+
+ @classmethod
+ def setUpClass(cls):
+
+ # we don't need all the auth stuff
+ cls.setUpSockets()
+ cls.startResponders()
+
+ confdir = os.path.join('configs', cls._confdir)
+ cls.createConfigDir(confdir)
+
+ cls.generateRecursorConfig(confdir)
+ cls.startRecursor(confdir, cls._recursorPort)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.tearDownRecursor()
+
+ @classmethod
+ def generateRecursorConfig(cls, confdir):
+ authzonepath = os.path.join(confdir, 'example.dns64.zone')
+ with open(authzonepath, 'w') as authzone:
+ authzone.write("""$ORIGIN example.dns64
+@ 3600 IN SOA {soa}
+www 3600 IN A 192.0.2.42
+www 3600 IN TXT "does exist"
+aaaa 3600 IN AAAA 2001:db8::1
+""".format(soa=cls._SOA))
+
+ authzonepath = os.path.join(confdir, 'in-addr.arpa.zone')
+ with open(authzonepath, 'w') as authzone:
+ authzone.write("""$ORIGIN in-addr.arpa
+@ 3600 IN SOA {soa}
+42.2.0.192 IN PTR www.example.dns64.
+""".format(soa=cls._SOA))
+
+ authzonepath = os.path.join(confdir, 'ip6.arpa.zone')
+ with open(authzonepath, 'w') as authzone:
+ authzone.write("""$ORIGIN ip6.arpa
+@ 3600 IN SOA {soa}
+1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2 IN PTR aaaa.example.dns64.
+""".format(soa=cls._SOA))
+
+ super(DNS64RecursorTest, cls).generateRecursorConfig(confdir)
+
+ # this type (A) exists for this name
+ def testExistingA(self):
+ qname = 'www.example.dns64.'
+ expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.42')
+
+ query = dns.message.make_query(qname, 'A', want_dnssec=True)
+ for method in ("sendUDPQuery", "sendTCPQuery"):
+ sender = getattr(self, method)
+ res = sender(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expected)
+
+ # there is no A record, we should get a NODATA
+ def testNonExistingA(self):
+ qname = 'aaaa.example.dns64.'
+
+ query = dns.message.make_query(qname, 'A', want_dnssec=True)
+ for method in ("sendUDPQuery", "sendTCPQuery"):
+ sender = getattr(self, method)
+ res = sender(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertEquals(len(res.answer), 0)
+
+ # this type (AAAA) does not exist for this name but there is an A record, we should get a DNS64-wrapped AAAA
+ def testNonExistingAAAA(self):
+ qname = 'www.example.dns64.'
+ expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'AAAA', '64:ff9b::c000:22a')
+
+ query = dns.message.make_query(qname, 'AAAA', want_dnssec=True)
+ for method in ("sendUDPQuery", "sendTCPQuery"):
+ sender = getattr(self, method)
+ res = sender(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expected)
+
+ # this type (AAAA) does not exist for this name and there is no A record either, we should get a NXDomain
+ def testNonExistingAAAA(self):
+ qname = 'nxd.example.dns64.'
+
+ query = dns.message.make_query(qname, 'AAAA', want_dnssec=True)
+ for method in ("sendUDPQuery", "sendTCPQuery"):
+ sender = getattr(self, method)
+ res = sender(query)
+ self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
+
+ # there is an AAAA record, we should get it
+ def testExistingAAAA(self):
+ qname = 'aaaa.example.dns64.'
+ expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'AAAA', '2001:db8::1')
+
+ query = dns.message.make_query(qname, 'AAAA', want_dnssec=True)
+ for method in ("sendUDPQuery", "sendTCPQuery"):
+ sender = getattr(self, method)
+ res = sender(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expected)
+
+ # there is a TXT record, we should get it
+ def testExistingTXT(self):
+ qname = 'www.example.dns64.'
+ expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'TXT', '"does exist"')
+
+ query = dns.message.make_query(qname, 'TXT', want_dnssec=True)
+ for method in ("sendUDPQuery", "sendTCPQuery"):
+ sender = getattr(self, method)
+ res = sender(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expected)
+
+ # the PTR records for the DNS64 prefix should be generated
+ def testNonExistingPTR(self):
+ qname = 'a.2.2.0.0.0.0.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.b.9.f.f.4.6.0.0.ip6.arpa.'
+ expectedCNAME = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'CNAME', '42.2.0.192.in-addr.arpa.')
+ expected = dns.rrset.from_text('42.2.0.192.in-addr.arpa.', 0, dns.rdataclass.IN, 'PTR', 'www.example.dns64.')
+
+ query = dns.message.make_query(qname, 'PTR', want_dnssec=True)
+ for method in ("sendUDPQuery", "sendTCPQuery"):
+ sender = getattr(self, method)
+ res = sender(query)
+ print(res)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expectedCNAME)
+ self.assertRRsetInAnswer(res, expected)
+
+ # but not for other prefixes
+ def testExistingPTR(self):
+ qname = '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.'
+ expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'PTR', 'aaaa.example.dns64.')
+
+ query = dns.message.make_query(qname, 'PTR', want_dnssec=True)
+ for method in ("sendUDPQuery", "sendTCPQuery"):
+ sender = getattr(self, method)
+ res = sender(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expected)