From 73e1f0c55b4dc317df9f1e8960ae9df7af2ca27a Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Wed, 14 Aug 2019 12:14:33 +0200 Subject: [PATCH] dnsdist: Add KeyValueStoreLookupRule to only check if the key exists --- pdns/cdb.cc | 13 ++++++-- pdns/cdb.hh | 1 + pdns/dnsdist-lua-rules.cc | 4 +++ pdns/dnsdistdist/dnsdist-kvs.cc | 44 +++++++++++++++++++++++++ pdns/dnsdistdist/dnsdist-kvs.hh | 3 ++ pdns/dnsdistdist/dnsdist-rules.hh | 30 +++++++++++++++++ pdns/dnsdistdist/docs/reference/kvs.rst | 11 ++++--- pdns/dnsdistdist/docs/rules-actions.rst | 11 +++++++ pdns/dnsdistdist/test-dnsdistkvs_cc.cc | 9 +++-- regression-tests.dnsdist/test_LMDB.py | 27 +++++++++++++++ 10 files changed, 144 insertions(+), 9 deletions(-) diff --git a/pdns/cdb.cc b/pdns/cdb.cc index e2490eff2..5dd391539 100644 --- a/pdns/cdb.cc +++ b/pdns/cdb.cc @@ -162,7 +162,7 @@ vector CDB::findall(string &key) return ret; } -bool CDB::findOne(const string& key, string& value) +bool CDB::keyExists(const string& key) { int ret = cdb_find(&d_cdb, key.c_str(), key.size()); if (ret < 0) { @@ -173,10 +173,19 @@ bool CDB::findOne(const string& key, string& value) return false; } + return true; +} + +bool CDB::findOne(const string& key, string& value) +{ + if (!keyExists(key)) { + return false; + } + unsigned int vpos = cdb_datapos(&d_cdb); unsigned int vlen = cdb_datalen(&d_cdb); value.resize(vlen); - ret = cdb_read(&d_cdb, &value[0], vlen, vpos); + int ret = cdb_read(&d_cdb, &value[0], vlen, vpos); if (ret < 0) { throw std::runtime_error("Error while reading value for key '" + key + "' from CDB database: " + std::to_string(ret)); } diff --git a/pdns/cdb.hh b/pdns/cdb.hh index 8179005b1..8673e3316 100644 --- a/pdns/cdb.hh +++ b/pdns/cdb.hh @@ -41,6 +41,7 @@ public: void searchAll(); bool readNext(pair &value); vector findall(string &key); + bool keyExists(const string& key); bool findOne(const string& key, string& value); private: diff --git a/pdns/dnsdist-lua-rules.cc b/pdns/dnsdist-lua-rules.cc index 3ed911748..07ee3d35e 100644 --- a/pdns/dnsdist-lua-rules.cc +++ b/pdns/dnsdist-lua-rules.cc @@ -482,4 +482,8 @@ void setupLuaRules() g_lua.writeFunction("QNameSetRule", [](const DNSNameSet& names) { return std::shared_ptr(new QNameSetRule(names)); }); + + g_lua.writeFunction("KeyValueStoreLookupRule", [](std::shared_ptr& kvs, std::shared_ptr& lookupKey) { + return std::shared_ptr(new KeyValueStoreLookupRule(kvs, lookupKey)); + }); } diff --git a/pdns/dnsdistdist/dnsdist-kvs.cc b/pdns/dnsdistdist/dnsdist-kvs.cc index b0511140f..d5f743838 100644 --- a/pdns/dnsdistdist/dnsdist-kvs.cc +++ b/pdns/dnsdistdist/dnsdist-kvs.cc @@ -91,6 +91,26 @@ bool LMDBKVStore::getValue(const std::string& key, std::string& value) return false; } +bool LMDBKVStore::keyExists(const std::string& key) +{ + string_view result; + try { + auto transaction = d_env.getROTransaction(); + auto dbi = transaction.openDB(d_dbName, 0); + int rc = transaction.get(dbi, MDBInVal(key), result); + if (rc == 0) { + return true; + } + else if (rc == MDB_NOTFOUND) { + return false; + } + } + catch(const std::exception& e) { + warnlog("Error while looking up key '%s' from LMDB file '%s', database '%s': %s", key, d_fname, d_dbName, e.what()); + } + return false; +} + #endif /* HAVE_LMDB */ #ifdef HAVE_CDB @@ -179,4 +199,28 @@ bool CDBKVStore::getValue(const std::string& key, std::string& value) return false; } +bool CDBKVStore::keyExists(const std::string& key) +{ + time_t now = time(nullptr); + + try { + if (d_nextCheck != 0 && now >= d_nextCheck) { + refreshDBIfNeeded(now); + } + + { + ReadLock rl(&d_lock); + if (!d_cdb) { + return false; + } + + return d_cdb->keyExists(key); + } + } + catch(const std::exception& e) { + warnlog("Error while looking up key '%s' from CDB file '%s': %s", key, d_fname); + } + return false; +} + #endif /* HAVE_CDB */ diff --git a/pdns/dnsdistdist/dnsdist-kvs.hh b/pdns/dnsdistdist/dnsdist-kvs.hh index 916bb64c5..892123f14 100644 --- a/pdns/dnsdistdist/dnsdist-kvs.hh +++ b/pdns/dnsdistdist/dnsdist-kvs.hh @@ -143,6 +143,7 @@ public: { } + virtual bool keyExists(const std::string& key) = 0; virtual bool getValue(const std::string& key, std::string& value) = 0; virtual bool reload() { @@ -161,6 +162,7 @@ public: { } + bool keyExists(const std::string& key) override; bool getValue(const std::string& key, std::string& value) override; private: @@ -180,6 +182,7 @@ class CDBKVStore: public KeyValueStore public: CDBKVStore(const std::string& fname, time_t refreshDelay); + bool keyExists(const std::string& key) override; bool getValue(const std::string& key, std::string& value) override; bool reload() override; diff --git a/pdns/dnsdistdist/dnsdist-rules.hh b/pdns/dnsdistdist/dnsdist-rules.hh index ebe80318e..e443fd342 100644 --- a/pdns/dnsdistdist/dnsdist-rules.hh +++ b/pdns/dnsdistdist/dnsdist-rules.hh @@ -24,6 +24,7 @@ #include "cachecleaner.hh" #include "dnsdist.hh" #include "dnsdist-ecs.hh" +#include "dnsdist-kvs.hh" #include "dnsparser.hh" class MaxQPSIPRule : public DNSRule @@ -1089,3 +1090,32 @@ private: mutable LocalStateHolder d_pools; std::string d_poolname; }; + +class KeyValueStoreLookupRule: public DNSRule +{ +public: + KeyValueStoreLookupRule(std::shared_ptr& kvs, std::shared_ptr& lookupKey): d_kvs(kvs), d_key(lookupKey) + { + } + + bool matches(const DNSQuestion* dq) const override + { + std::vector keys = d_key->getKeys(*dq); + for (const auto& key : keys) { + if (d_kvs->keyExists(key) == true) { + return true; + } + } + + return false; + } + + string toString() const override + { + return "lookup key-value store based on '" + d_key->toString() + "'"; + } + +private: + std::shared_ptr d_kvs; + std::shared_ptr d_key; +}; diff --git a/pdns/dnsdistdist/docs/reference/kvs.rst b/pdns/dnsdistdist/docs/reference/kvs.rst index 5617b26de..6dc92aac0 100644 --- a/pdns/dnsdistdist/docs/reference/kvs.rst +++ b/pdns/dnsdistdist/docs/reference/kvs.rst @@ -3,8 +3,9 @@ Key Value Store functions and objects These are all the functions, objects and methods related to the CDB and LMDB key value stores. -A lookup into a key value store can be done via the :func:`KeyValueStoreLookupAction` action, -using the usual selectors to match the incoming queries for which the lookup should be done. +A lookup into a key value store can be done via the :func:`KeyValueStoreLookupRule` rule or +the :func:`KeyValueStoreLookupAction` action, using the usual selectors to match the incoming +queries for which the lookup should be done. The first step is to get a :ref:`KeyValueStore` object via one of the following functions: @@ -70,7 +71,7 @@ If the value found in the LMDB database for the key '\8powerdns\3com\0' was 'thi .. 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. + Return a new KeyValueLookupKey object that, when passed to :func:`KeyValueStoreLookupAction` or :func:`KeyValueStoreLookupRule`, 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 @@ -78,13 +79,13 @@ If the value found in the LMDB database for the key '\8powerdns\3com\0' was 'thi .. 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. + Return a new KeyValueLookupKey object that, when passed to :func:`KeyValueStoreLookupAction` or :func:`KeyValueStoreLookupRule`, will return the source IP of the client in network byte-order. .. function:: KeyValueLookupKeySuffix([minLabels [, wireFormat]]) -> KeyValueLookupKey .. versionadded:: 1.5.0 - Return a new KeyValueLookupKey object that, when passed to :func:`KeyValueStoreLookupAction`, will return a vector of keys based on the labels of the qname in DNS wire format. + Return a new KeyValueLookupKey object that, when passed to :func:`KeyValueStoreLookupAction` or :func:`KeyValueStoreLookupRule`, will return a vector of keys based on the labels of the qname in DNS wire format or plain text. For example if the qname is sub.domain.powerdns.com. the following keys will be returned: * \3sub\6domain\8powerdns\3com\0 diff --git a/pdns/dnsdistdist/docs/rules-actions.rst b/pdns/dnsdistdist/docs/rules-actions.rst index 6d807f7fd..c1e7519d1 100644 --- a/pdns/dnsdistdist/docs/rules-actions.rst +++ b/pdns/dnsdistdist/docs/rules-actions.rst @@ -597,6 +597,17 @@ These ``DNSRule``\ s be one of the following items: :param str path: The exact HTTP path to match on +.. function:: KeyValueStoreLookupRule(kvs, lookupKey) + .. versionadded:: 1.5.0 + + Return true if the key returned by 'lookupKey' exists in the key value store referenced by 'kvs'. + The store can be a CDB (:func:`newCDBKVStore`) or a LMDB database (:func:`newLMDBKVStore`). + The key can be based on the qname (:func:`KeyValueLookupKeyQName` and :func:`KeyValueLookupKeySuffix`), + source IP (:func:`KeyValueLookupKeySourceIP`) or the value of an existing tag (:func:`KeyValueLookupKeyTag`). + + :param KeyValueStore kvs: The key value store to query + :param KeyValueLookupKey lookupKey: The key to use for the lookup + .. function:: MaxQPSIPRule(qps[, v4Mask[, v6Mask[, burst[, expiration[, cleanupDelay[, scanFraction]]]]]]) .. versionchanged:: 1.3.1 Added the optional parameters ``expiration``, ``cleanupDelay`` and ``scanFraction``. diff --git a/pdns/dnsdistdist/test-dnsdistkvs_cc.cc b/pdns/dnsdistdist/test-dnsdistkvs_cc.cc index a999ee3f3..0b27a31a7 100644 --- a/pdns/dnsdistdist/test-dnsdistkvs_cc.cc +++ b/pdns/dnsdistdist/test-dnsdistkvs_cc.cc @@ -11,11 +11,16 @@ static void doKVSChecks(std::unique_ptr& kvs, const ComboAddress& /* source IP */ { auto lookupKey = make_unique(); + std::string value; + /* local address is not in the db, remote is */ + BOOST_CHECK_EQUAL(kvs->getValue(std::string(reinterpret_cast(&lc.sin4.sin_addr.s_addr), sizeof(lc.sin4.sin_addr.s_addr)), value), false); + BOOST_CHECK_EQUAL(kvs->keyExists(std::string(reinterpret_cast(&lc.sin4.sin_addr.s_addr), sizeof(lc.sin4.sin_addr.s_addr))), false); + BOOST_CHECK(kvs->keyExists(std::string(reinterpret_cast(&dq.remote->sin4.sin_addr.s_addr), sizeof(dq.remote->sin4.sin_addr.s_addr)))); + auto keys = lookupKey->getKeys(dq); BOOST_CHECK_EQUAL(keys.size(), 1); for (const auto& key : keys) { - std::string value; - BOOST_CHECK_EQUAL(kvs->getValue(std::string(reinterpret_cast(&lc.sin4.sin_addr.s_addr), sizeof(lc.sin4.sin_addr.s_addr)), value), false); + value.clear(); BOOST_CHECK_EQUAL(kvs->getValue(key, value), true); BOOST_CHECK_EQUAL(value, "this is the value for the remote addr"); } diff --git a/regression-tests.dnsdist/test_LMDB.py b/regression-tests.dnsdist/test_LMDB.py index 97ab104cd..c074a1f92 100644 --- a/regression-tests.dnsdist/test_LMDB.py +++ b/regression-tests.dnsdist/test_LMDB.py @@ -15,6 +15,9 @@ class TestLMDB(DNSDistTest): kvs = newLMDBKVStore('%s', '%s') -- KVS lookups follow + -- if the qname is 'kvs-rule.lmdb.tests.powerdns.com.', does a lookup in the LMDB database using the qname as key, and spoof an answer if it matches + addAction(AndRule{QNameRule('kvs-rule.lmdb.tests.powerdns.com.'), KeyValueStoreLookupRule(kvs, KeyValueLookupKeyQName(false))}, SpoofAction('13.14.15.16')) + -- 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')) @@ -61,6 +64,7 @@ class TestLMDB(DNSDistTest): 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') + txn.put(b'kvs-rule.lmdb.tests.powerdns.com.', b'the value does not matter') @classmethod def setUpClass(cls): @@ -163,3 +167,26 @@ class TestLMDB(DNSDistTest): self.assertFalse(receivedQuery) self.assertTrue(receivedResponse) self.assertEquals(expectedResponse, receivedResponse) + + def testLMDBKeyValueStoreLookupRule(self): + """ + LMDB: KeyValueStoreLookupRule + """ + name = 'kvs-rule.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, + '13.14.15.16') + 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) -- 2.40.0