From: Remi Gacogne Date: Tue, 13 Aug 2019 13:44:31 +0000 (+0200) Subject: dnsdist: Support for plaintext KVS lookups, suffix matching limits X-Git-Tag: dnsdist-1.4.0-rc2~9^2~6 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=b5b3fc9b5d24dee13a06dabc1c3ceffac989e18d;p=pdns dnsdist: Support for plaintext KVS lookups, suffix matching limits --- diff --git a/pdns/dnsdist-lua-bindings.cc b/pdns/dnsdist-lua-bindings.cc index 0b005d74e..fc51198b0 100644 --- a/pdns/dnsdist-lua-bindings.cc +++ b/pdns/dnsdist-lua-bindings.cc @@ -719,11 +719,11 @@ void setupLuaBindings(bool client) g_lua.writeFunction("KeyValueLookupKeySourceIP", []() { return std::shared_ptr(new KeyValueLookupKeySourceIP()); }); - g_lua.writeFunction("KeyValueLookupKeyQName", []() { - return std::shared_ptr(new KeyValueLookupKeyQName()); + g_lua.writeFunction("KeyValueLookupKeyQName", [](boost::optional wireFormat) { + return std::shared_ptr(new KeyValueLookupKeyQName(wireFormat ? *wireFormat : true)); }); - g_lua.writeFunction("KeyValueLookupKeySuffix", []() { - return std::shared_ptr(new KeyValueLookupKeySuffix()); + g_lua.writeFunction("KeyValueLookupKeySuffix", [](boost::optional minLabels, boost::optional wireFormat) { + return std::shared_ptr(new KeyValueLookupKeySuffix(minLabels ? *minLabels : 0, wireFormat ? *wireFormat : true)); }); g_lua.writeFunction("KeyValueLookupKeyTag", [](const std::string& tag) { return std::shared_ptr(new KeyValueLookupKeyTag(tag)); @@ -747,7 +747,7 @@ void setupLuaBindings(bool client) }); #endif /* HAVE_CDB */ - g_lua.registerFunction::*)(const boost::variant)>("lookup", [](std::shared_ptr& kvs, const boost::variant keyVar) { + g_lua.registerFunction::*)(const boost::variant, boost::optional wireFormat)>("lookup", [](std::shared_ptr& kvs, const boost::variant keyVar, boost::optional wireFormat) { std::string result; if (!kvs) { return result; @@ -764,7 +764,7 @@ void setupLuaBindings(bool client) } else if (keyVar.type() == typeid(DNSName)) { DNSName dn = *boost::get(&keyVar); - KeyValueLookupKeyQName lookup; + KeyValueLookupKeyQName lookup(wireFormat ? *wireFormat : true); for (const auto& key : lookup.getKeys(dn)) { if (kvs->getValue(key, result)) { return result; @@ -779,13 +779,13 @@ void setupLuaBindings(bool client) return result; }); - g_lua.registerFunction::*)(const DNSName&)>("lookupSuffix", [](std::shared_ptr& kvs, const DNSName& dn) { + g_lua.registerFunction::*)(const DNSName&, boost::optional minLabels, boost::optional wireFormat)>("lookupSuffix", [](std::shared_ptr& kvs, const DNSName& dn, boost::optional minLabels, boost::optional wireFormat) { std::string result; if (!kvs) { return result; } - KeyValueLookupKeySuffix lookup; + KeyValueLookupKeySuffix lookup(minLabels ? *minLabels : 0, wireFormat ? *wireFormat : true); for (const auto& key : lookup.getKeys(dn)) { if (kvs->getValue(key, result)) { return result; diff --git a/pdns/dnsdistdist/dnsdist-kvs.cc b/pdns/dnsdistdist/dnsdist-kvs.cc index a1555f441..b0511140f 100644 --- a/pdns/dnsdistdist/dnsdist-kvs.cc +++ b/pdns/dnsdistdist/dnsdist-kvs.cc @@ -46,12 +46,21 @@ std::vector KeyValueLookupKeySuffix::getKeys(const DNSName& qname) } auto lowerQName = qname.makeLowerCase(); + size_t labelsCount = lowerQName.countLabels(); + if (d_minLabels != 0) { + if (labelsCount < d_minLabels) { + return {}; + } + labelsCount -= (d_minLabels - 1); + } + std::vector result; - result.reserve(lowerQName.countLabels() - 1); + result.reserve(labelsCount); while(!lowerQName.isRoot()) { - result.emplace_back(lowerQName.toDNSString()); - if (!lowerQName.chopOff()) { + result.emplace_back(d_wireFormat ? lowerQName.toDNSString() : lowerQName.toString()); + labelsCount--; + if (!lowerQName.chopOff() || labelsCount == 0) { break; } } diff --git a/pdns/dnsdistdist/dnsdist-kvs.hh b/pdns/dnsdistdist/dnsdist-kvs.hh index 6efb73f77..916bb64c5 100644 --- a/pdns/dnsdistdist/dnsdist-kvs.hh +++ b/pdns/dnsdistdist/dnsdist-kvs.hh @@ -53,9 +53,16 @@ class KeyValueLookupKeyQName: public KeyValueLookupKey { public: + KeyValueLookupKeyQName(bool wireFormat): d_wireFormat(wireFormat) + { + } + std::vector getKeys(const DNSName& qname) { - return {qname.toDNSStringLC()}; + if (d_wireFormat) { + return {qname.toDNSStringLC()}; + } + return {qname.makeLowerCase().toString()}; } std::vector getKeys(const DNSQuestion& dq) override @@ -65,13 +72,23 @@ public: std::string toString() const override { + if (d_wireFormat) { + return "qname in wire format"; + } return "qname"; } + +private: + bool d_wireFormat; }; class KeyValueLookupKeySuffix: public KeyValueLookupKey { public: + KeyValueLookupKeySuffix(size_t minLabels, bool wireFormat): d_minLabels(minLabels), d_wireFormat(wireFormat) + { + } + std::vector getKeys(const DNSName& qname); std::vector getKeys(const DNSQuestion& dq) override @@ -81,8 +98,15 @@ public: std::string toString() const override { - return "suffix"; + if (d_minLabels > 0) { + return "suffix " + std::string(d_wireFormat ? "in wire format " : "") + "at least " + std::to_string(d_minLabels) + " labels)"; + } + return "suffix" + std::string(d_wireFormat ? " in wire format" : ""); } + +private: + size_t d_minLabels; + bool d_wireFormat; }; class KeyValueLookupKeyTag: public KeyValueLookupKey diff --git a/pdns/dnsdistdist/docs/reference/kvs.rst b/pdns/dnsdistdist/docs/reference/kvs.rst index 00585f53b..5617b26de 100644 --- a/pdns/dnsdistdist/docs/reference/kvs.rst +++ b/pdns/dnsdistdist/docs/reference/kvs.rst @@ -43,38 +43,44 @@ If the value found in the LMDB database for the key '\8powerdns\3com\0' was 'thi Represents a Key Value Store - .. method:: KeyValueStore:lookup(key) + .. method:: KeyValueStore:lookup(key [, wireFormat]) Does a lookup into the corresponding key value store, and return the result as a string. The key can be a :class:`ComboAddress` obtained via the :func:`newCA`, a :class:`DNSName` obtained via the :func:`newDNSName` function, or a raw string. :param ComboAddress, DNSName or string key: The key to look up + :param bool wireFormat: If the key is DNSName, whether to use to do the lookup in wire format (default) or in plain text - .. method:: KeyValueStore:lookupSuffix(key) + .. method:: KeyValueStore:lookupSuffix(key [, minLabels [, wireFormat]]) Does a suffix-based lookup into the corresponding key value store, and return the result as a string. The key should be a :class:`DNSName` object obtained via the :func:`newDNSName` function, and several lookups will be done, removing one label from the name at a time until a match has been found or there is no label left. + If ``minLabels`` is set to a value larger than 0 the lookup will only be done as long as there is at least ``minLabels`` remaining. For example if the initial domain is "sub.powerdns.com." and ``minLabels`` is set to 2, lookups will only be done for "sub.powerdns.com." and "powerdns.com.". :param DNSName key: The name to look up + :param int minLabels: The minimum number of labels to do a lookup for. Default is 0 which means unlimited + :param bool wireFormat: Whether to do the lookup in wire format (default) or in plain text .. method:: KeyValueStore:reload() Reload the database if this is supported by the underlying store. As of 1.5.0, only CDB stores can be reloaded, and this method is a no-op for LMDB stores. -.. function:: KeyValueLookupKeyQName() -> KeyValueLookupKey +.. function:: KeyValueLookupKeyQName([wireFormat]) -> KeyValueLookupKey .. versionadded:: 1.5.0 Return a new KeyValueLookupKey object that, when passed to :func:`KeyValueStoreLookupAction`, will return the qname of the query in DNS wire format. + :param bool wireFormat: Whether to do the lookup in wire format (default) or in plain text + .. function:: KeyValueLookupKeySourceIP() -> KeyValueLookupKey .. versionadded:: 1.5.0 Return a new KeyValueLookupKey object that, when passed to :func:`KeyValueStoreLookupAction`, will return the source IP of the client in network byte-order. -.. function:: KeyValueLookupKeySuffix() -> KeyValueLookupKey +.. function:: KeyValueLookupKeySuffix([minLabels [, wireFormat]]) -> KeyValueLookupKey .. versionadded:: 1.5.0 @@ -87,6 +93,15 @@ If the value found in the LMDB database for the key '\8powerdns\3com\0' was 'thi * \3com\0 * \0 + If ``minLabels`` is set to a value larger than 0 the lookup will only be done as long as there is at least ``minLabels`` remaining. Taking back our previous example, it means only the following keys will be returned if ``minLabels`` is set to 2; + + * \3sub\6domain\8powerdns\3com\0 + * \6domain\8powerdns\3com\0 + * \8powerdns\3com\0 + + :param int minLabels: The minimum number of labels to do a lookup for. Default is 0 which means unlimited + :param bool wireFormat: Whether to do the lookup in wire format (default) or in plain text + .. function:: KeyValueLookupKeyTag() -> KeyValueLookupKey .. versionadded:: 1.5.0 diff --git a/pdns/dnsdistdist/test-dnsdistkvs_cc.cc b/pdns/dnsdistdist/test-dnsdistkvs_cc.cc index 7e43eb6a3..a999ee3f3 100644 --- a/pdns/dnsdistdist/test-dnsdistkvs_cc.cc +++ b/pdns/dnsdistdist/test-dnsdistkvs_cc.cc @@ -6,7 +6,7 @@ #include "dnsdist-kvs.hh" -static void doKVSChecks(std::unique_ptr& kvs, const ComboAddress& lc, const ComboAddress& rem, const DNSQuestion& dq) +static void doKVSChecks(std::unique_ptr& kvs, const ComboAddress& lc, const ComboAddress& rem, const DNSQuestion& dq, const DNSName& plaintextDomain) { /* source IP */ { @@ -24,10 +24,10 @@ static void doKVSChecks(std::unique_ptr& kvs, const ComboAddress& const DNSName subdomain = DNSName("sub") + *dq.qname; const DNSName notPDNS("not-powerdns.com."); - /* qname */ + /* qname match, in wire format */ { std::string value; - auto lookupKey = make_unique(); + auto lookupKey = make_unique(true); auto keys = lookupKey->getKeys(dq); BOOST_CHECK_EQUAL(keys.size(), 1); for (const auto& key : keys) { @@ -51,11 +51,56 @@ static void doKVSChecks(std::unique_ptr& kvs, const ComboAddress& value.clear(); BOOST_CHECK_EQUAL(kvs->getValue(key, value), false); } + + /* this domain was inserted in plaintext, the wire format lookup should not match */ + keys = lookupKey->getKeys(plaintextDomain); + BOOST_CHECK_EQUAL(keys.size(), 1); + for (const auto& key : keys) { + value.clear(); + BOOST_CHECK_EQUAL(kvs->getValue(key, value), false); + } } - /* suffix match */ + /* qname match, in plain text */ { - auto lookupKey = make_unique(); + std::string value; + auto lookupKey = make_unique(false); + auto keys = lookupKey->getKeys(dq); + BOOST_CHECK_EQUAL(keys.size(), 1); + for (const auto& key : keys) { + value.clear(); + BOOST_CHECK_EQUAL(kvs->getValue(key, value), false); + } + + /* other domain, should not match */ + keys = lookupKey->getKeys(notPDNS); + BOOST_CHECK_EQUAL(keys.size(), 1); + for (const auto& key : keys) { + value.clear(); + BOOST_CHECK_EQUAL(kvs->getValue(key, value), false); + } + + /* subdomain, should not match */ + keys = lookupKey->getKeys(subdomain); + BOOST_CHECK_EQUAL(keys.size(), 1); + for (const auto& key : keys) { + value.clear(); + BOOST_CHECK_EQUAL(kvs->getValue(key, value), false); + } + + /* this domain was inserted in plaintext, so it should match */ + keys = lookupKey->getKeys(plaintextDomain); + BOOST_CHECK_EQUAL(keys.size(), 1); + for (const auto& key : keys) { + value.clear(); + BOOST_CHECK_EQUAL(kvs->getValue(key, value), true); + BOOST_CHECK_EQUAL(value, "this is the value for the plaintext domain"); + } + } + + /* suffix match in wire format */ + { + auto lookupKey = make_unique(0, true); auto keys = lookupKey->getKeys(dq); BOOST_CHECK_EQUAL(keys.size(), dq.qname->countLabels()); BOOST_REQUIRE(!keys.empty()); @@ -80,6 +125,69 @@ static void doKVSChecks(std::unique_ptr& kvs, const ComboAddress& BOOST_CHECK_EQUAL(kvs->getValue(keys.at(0), value), false); BOOST_CHECK_EQUAL(kvs->getValue(keys.at(1), value), true); BOOST_CHECK_EQUAL(value, "this is the value for the qname"); + + /* this domain was inserted in plaintext, the wire format lookup should not match */ + keys = lookupKey->getKeys(plaintextDomain); + BOOST_CHECK_EQUAL(keys.size(), plaintextDomain.countLabels()); + for (const auto& key : keys) { + value.clear(); + BOOST_CHECK_EQUAL(kvs->getValue(key, value), false); + } + } + + /* suffix match in plain text */ + { + auto lookupKey = make_unique(0, false); + auto keys = lookupKey->getKeys(dq); + BOOST_CHECK_EQUAL(keys.size(), dq.qname->countLabels()); + BOOST_REQUIRE(!keys.empty()); + BOOST_CHECK_EQUAL(keys.at(0), dq.qname->toString()); + std::string value; + BOOST_CHECK_EQUAL(kvs->getValue(keys.at(0), value), false); + value.clear(); + BOOST_CHECK_EQUAL(kvs->getValue(keys.at(1), value), false); + + /* other domain, should not match */ + keys = lookupKey->getKeys(notPDNS); + BOOST_CHECK_EQUAL(keys.size(), notPDNS.countLabels()); + for (const auto& key : keys) { + value.clear(); + BOOST_CHECK_EQUAL(kvs->getValue(key, value), false); + } + + /* subdomain, should not match in plain text */ + keys = lookupKey->getKeys(subdomain); + BOOST_REQUIRE_EQUAL(keys.size(), subdomain.countLabels()); + for (const auto& key : keys) { + value.clear(); + BOOST_CHECK_EQUAL(kvs->getValue(key, value), false); + } + + /* this domain was inserted in plaintext, it should match */ + keys = lookupKey->getKeys(plaintextDomain); + BOOST_REQUIRE_EQUAL(keys.size(), plaintextDomain.countLabels()); + BOOST_CHECK_EQUAL(kvs->getValue(keys.at(0), value), true); + BOOST_CHECK_EQUAL(value, "this is the value for the plaintext domain"); + } + + /* suffix match in wire format, we require at least 2 labels */ + { + auto lookupKey = make_unique(2, true); + auto keys = lookupKey->getKeys(dq); + BOOST_CHECK_EQUAL(keys.size(), 1); + BOOST_REQUIRE(!keys.empty()); + BOOST_CHECK_EQUAL(keys.at(0), dq.qname->toDNSStringLC()); + std::string value; + BOOST_CHECK_EQUAL(kvs->getValue(keys.at(0), value), true); + BOOST_CHECK_EQUAL(value, "this is the value for the qname"); + value.clear(); + + /* subdomain */ + keys = lookupKey->getKeys(subdomain); + BOOST_REQUIRE_EQUAL(keys.size(), 2); + BOOST_CHECK_EQUAL(kvs->getValue(keys.at(0), value), false); + BOOST_CHECK_EQUAL(kvs->getValue(keys.at(1), value), true); + BOOST_CHECK_EQUAL(value, "this is the value for the qname"); } } @@ -89,6 +197,7 @@ BOOST_AUTO_TEST_SUITE(dnsdistkvs_cc) BOOST_AUTO_TEST_CASE(test_LMDB) { DNSName qname("powerdns.com."); + DNSName plaintextDomain("powerdns.org."); uint16_t qtype = QType::A; uint16_t qclass = QClass::IN; ComboAddress lc("192.0.2.1:53"); @@ -113,11 +222,12 @@ BOOST_AUTO_TEST_CASE(test_LMDB) { auto dbi = transaction.openDB("db-name", MDB_CREATE); transaction.put(dbi, MDBInVal(std::string(reinterpret_cast(&rem.sin4.sin_addr.s_addr), sizeof(rem.sin4.sin_addr.s_addr))), MDBInVal("this is the value for the remote addr")); transaction.put(dbi, MDBInVal(qname.toDNSStringLC()), MDBInVal("this is the value for the qname")); + transaction.put(dbi, MDBInVal(plaintextDomain.toString()), MDBInVal("this is the value for the plaintext domain")); transaction.commit(); } auto lmdb = std::unique_ptr(new LMDBKVStore(dbPath, "db-name")); - doKVSChecks(lmdb, lc, rem, dq); + doKVSChecks(lmdb, lc, rem, dq, plaintextDomain); /* std::string value; DTime dt; @@ -139,6 +249,7 @@ BOOST_AUTO_TEST_CASE(test_LMDB) { BOOST_AUTO_TEST_CASE(test_CDB) { DNSName qname("powerdns.com."); + DNSName plaintextDomain("powerdns.org."); uint16_t qtype = QType::A; uint16_t qclass = QClass::IN; ComboAddress lc("192.0.2.1:53"); @@ -163,11 +274,12 @@ BOOST_AUTO_TEST_CASE(test_CDB) { CDBWriter writer(fd); BOOST_REQUIRE(writer.addEntry(std::string(reinterpret_cast(&rem.sin4.sin_addr.s_addr), sizeof(rem.sin4.sin_addr.s_addr)), "this is the value for the remote addr")); BOOST_REQUIRE(writer.addEntry(qname.toDNSStringLC(), "this is the value for the qname")); + BOOST_REQUIRE(writer.addEntry(plaintextDomain.toString(), "this is the value for the plaintext domain")); writer.close(); } auto cdb = std::unique_ptr(new CDBKVStore(db, 0)); - doKVSChecks(cdb, lc, rem, dq); + doKVSChecks(cdb, lc, rem, dq, plaintextDomain); /* std::string value; diff --git a/regression-tests.dnsdist/test_LMDB.py b/regression-tests.dnsdist/test_LMDB.py index 14289e4e9..97ab104cd 100644 --- a/regression-tests.dnsdist/test_LMDB.py +++ b/regression-tests.dnsdist/test_LMDB.py @@ -18,6 +18,11 @@ class TestLMDB(DNSDistTest): -- does a lookup in the LMDB database using the source IP as key, and store the result into the 'kvs-sourceip-result' tag addAction(AllRule(), KeyValueStoreLookupAction(kvs, KeyValueLookupKeySourceIP(), 'kvs-sourceip-result')) + -- does a lookup in the LMDB database using the qname in _plain text_ format as key, and store the result into the 'kvs-plain-text-result' tag + addAction(AllRule(), KeyValueStoreLookupAction(kvs, KeyValueLookupKeyQName(false), 'kvs-plain-text-result')) + -- if the value of the 'kvs-plain-text-result' is set to 'this is the value of the plaintext tag', spoof a response + addAction(TagRule('kvs-plain-text-result', 'this is the value of the plaintext tag'), SpoofAction('9.10.11.12')) + -- does a lookup in the LMDB database using the qname in wire format as key, and store the result into the 'kvs-qname-result' tag addAction(AllRule(), KeyValueStoreLookupAction(kvs, KeyValueLookupKeyQName(), 'kvs-qname-result')) @@ -55,6 +60,7 @@ class TestLMDB(DNSDistTest): txn.put(socket.inet_aton('127.0.0.1'), b'this is the value of the source address tag') txn.put(b'this is the value of the qname tag', b'this is the value of the second tag') txn.put(b'\x06suffix\x04lmdb\x05tests\x08powerdns\x03com\x00', b'this is the value of the suffix tag') + txn.put(b'qname-plaintext.lmdb.tests.powerdns.com.', b'this is the value of the plaintext tag') @classmethod def setUpClass(cls): @@ -135,3 +141,25 @@ class TestLMDB(DNSDistTest): self.assertTrue(receivedResponse) self.assertEquals(expectedResponse, receivedResponse) + def testLMDBQNamePlainText(self): + """ + LMDB: Match on qname in plain text format + """ + name = 'qname-plaintext.lmdb.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + # dnsdist set RA = RD for spoofed responses + query.flags &= ~dns.flags.RD + expectedResponse = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '9.10.11.12') + expectedResponse.answer.append(rrset) + + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + (receivedQuery, receivedResponse) = sender(query, response=None, useQueue=False) + self.assertFalse(receivedQuery) + self.assertTrue(receivedResponse) + self.assertEquals(expectedResponse, receivedResponse)