From 6c8714d2a73180eb0ea08a39edf3b7c95dde29dc Mon Sep 17 00:00:00 2001 From: Charles-Henri Bruyand Date: Mon, 20 Mar 2023 14:43:49 +0100 Subject: [PATCH 1/4] auth: allow multiple blocks of ips in ifportup() --- pdns/lua-record.cc | 56 ++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/pdns/lua-record.cc b/pdns/lua-record.cc index 5665bdbbd04c..b4810b4cf5f6 100644 --- a/pdns/lua-record.cc +++ b/pdns/lua-record.cc @@ -548,13 +548,13 @@ static vector convComboAddressListToString(const vector& i return result; } -static vector convComboAddressList(const iplist_t& items) +static vector convComboAddressList(const iplist_t& items, uint16_t port=0) { vector result; result.reserve(items.size()); for(const auto& item : items) { - result.emplace_back(ComboAddress(item.second)); + result.emplace_back(ComboAddress(item.second, port)); } return result; @@ -832,33 +832,51 @@ static void setupLuaRecords(LuaContext& lua) * * @example ifportup(443, { '1.2.3.4', '5.4.3.2' })" */ - lua.writeFunction("ifportup", [](int port, const vector >& ips, const boost::optional> options) { - vector candidates, unavailables; + lua.writeFunction("ifportup", [](int port, const boost::variant& ips, const boost::optional> options) { + vector> candidates; opts_t opts; - vector conv; std::string selector; + if (port < 0) { + port = 0; + } + if (port > std::numeric_limits::max()) { + port = std::numeric_limits::max(); + } if(options) opts = *options; - for(const auto& i : ips) { - ComboAddress rem(i.second, port); - if(g_up.isUp(rem, opts)) { - candidates.push_back(rem); + + if(auto simple = boost::get(&ips)) { + vector unit = convComboAddressList(*simple, port); + candidates.push_back(unit); + } else { + auto units = boost::get(ips); + for(const auto& u : units) { + vector unit = convComboAddressList(u.second, port); + candidates.push_back(unit); + } + } + + for(const auto& unit : candidates) { + vector available; + for(const auto& c : unit) { + if(g_up.isUp(c, opts)) { + available.push_back(c); + } } - else { - unavailables.push_back(rem); + if(!available.empty()) { + vector res = useSelector(getOptionValue(options, "selector", "random"), s_lua_record_ctx->bestwho, available); + return convComboAddressListToString(res); } } - if(!candidates.empty()) { - // use regular selector - selector = getOptionValue(options, "selector", "random"); - } else { - // All units are down, apply backupSelector on all candidates - candidates = std::move(unavailables); - selector = getOptionValue(options, "backupSelector", "random"); + + // All units down, apply backupSelector on all candidates + vector unavailable{}; + for(const auto& unit : candidates) { + unavailable.insert(unavailable.end(), unit.begin(), unit.end()); } - vector res = useSelector(selector, s_lua_record_ctx->bestwho, candidates); + vector res = useSelector(getOptionValue(options, "backupSelector", "random"), s_lua_record_ctx->bestwho, unavailable); return convComboAddressListToString(res); }); From 04c537836d9dbc27540ad4653c3450239cd302ab Mon Sep 17 00:00:00 2001 From: Charles-Henri Bruyand Date: Tue, 21 Mar 2023 14:58:19 +0100 Subject: [PATCH 2/4] auth: refactor if{url,port}up functions --- pdns/lua-record.cc | 136 +++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 74 deletions(-) diff --git a/pdns/lua-record.cc b/pdns/lua-record.cc index b4810b4cf5f6..639653fede22 100644 --- a/pdns/lua-record.cc +++ b/pdns/lua-record.cc @@ -301,7 +301,6 @@ bool doCompare(const T& var, const std::string& res, const C& cmp) } } - static std::string getGeo(const std::string& ip, GeoIPInterface::GeoIPQueryAttribute qa) { static bool initialized; @@ -560,6 +559,29 @@ static vector convComboAddressList(const iplist_t& items, uint16_t return result; } +/** + * Reads and unify single or multiple sets of ips : + * - {'192.0.2.1', '192.0.2.2'} + * - {{'192.0.2.1', '192.0.2.2'}, {'198.51.100.1'}} + */ + +static vector> convMultiComboAddressList(const boost::variant& items, uint16_t port = 0) +{ + vector> candidates; + + if(auto simple = boost::get(&items)) { + vector unit = convComboAddressList(*simple, port); + candidates.push_back(unit); + } else { + auto units = boost::get(items); + for(const auto& u : units) { + vector unit = convComboAddressList(u.second, port); + candidates.push_back(unit); + } + } + return candidates; +} + static vector convStringList(const iplist_t& items) { vector result; @@ -596,6 +618,37 @@ typedef struct AuthLuaRecordContext static thread_local unique_ptr s_lua_record_ctx; +static vector genericIfUp(const boost::variant& ips, boost::optional options, std::function upcheckf, uint16_t port = 0) { + vector > candidates; + opts_t opts; + if(options) + opts = *options; + + candidates = convMultiComboAddressList(ips, port); + + for(const auto& unit : candidates) { + vector available; + for(const auto& c : unit) { + if (upcheckf(c, opts)) { + available.push_back(c); + } + } + if(!available.empty()) { + vector res = useSelector(getOptionValue(options, "selector", "random"), s_lua_record_ctx->bestwho, available); + return convComboAddressListToString(res); + } + } + + // All units down, apply backupSelector on all candidates + vector ret{}; + for(const auto& unit : candidates) { + ret.insert(ret.end(), unit.begin(), unit.end()); + } + + vector res = useSelector(getOptionValue(options, "backupSelector", "random"), s_lua_record_ctx->bestwho, ret); + return convComboAddressListToString(res); +} + static void setupLuaRecords(LuaContext& lua) { lua.writeFunction("latlon", []() { @@ -833,51 +886,17 @@ static void setupLuaRecords(LuaContext& lua) * @example ifportup(443, { '1.2.3.4', '5.4.3.2' })" */ lua.writeFunction("ifportup", [](int port, const boost::variant& ips, const boost::optional> options) { - vector> candidates; - opts_t opts; - std::string selector; - if (port < 0) { port = 0; } if (port > std::numeric_limits::max()) { port = std::numeric_limits::max(); } - if(options) - opts = *options; - - if(auto simple = boost::get(&ips)) { - vector unit = convComboAddressList(*simple, port); - candidates.push_back(unit); - } else { - auto units = boost::get(ips); - for(const auto& u : units) { - vector unit = convComboAddressList(u.second, port); - candidates.push_back(unit); - } - } - - for(const auto& unit : candidates) { - vector available; - for(const auto& c : unit) { - if(g_up.isUp(c, opts)) { - available.push_back(c); - } - } - if(!available.empty()) { - vector res = useSelector(getOptionValue(options, "selector", "random"), s_lua_record_ctx->bestwho, available); - return convComboAddressListToString(res); - } - } - // All units down, apply backupSelector on all candidates - vector unavailable{}; - for(const auto& unit : candidates) { - unavailable.insert(unavailable.end(), unit.begin(), unit.end()); - } - - vector res = useSelector(getOptionValue(options, "backupSelector", "random"), s_lua_record_ctx->bestwho, unavailable); - return convComboAddressListToString(res); + auto checker = [](const ComboAddress& addr, const opts_t& opts) { + return g_up.isUp(addr, opts); + }; + return genericIfUp(ips, options, checker, port); }); lua.writeFunction("ifurlextup", [](const vector >& ipurls, boost::optional options) { @@ -916,42 +935,11 @@ static void setupLuaRecords(LuaContext& lua) lua.writeFunction("ifurlup", [](const std::string& url, const boost::variant& ips, boost::optional options) { - vector > candidates; - opts_t opts; - if(options) - opts = *options; - if(auto simple = boost::get(&ips)) { - vector unit = convComboAddressList(*simple); - candidates.push_back(unit); - } else { - auto units = boost::get(ips); - for(const auto& u : units) { - vector unit = convComboAddressList(u.second); - candidates.push_back(unit); - } - } - for(const auto& unit : candidates) { - vector available; - for(const auto& c : unit) { - if(g_up.isUp(c, url, opts)) { - available.push_back(c); - } - } - if(!available.empty()) { - vector res = useSelector(getOptionValue(options, "selector", "random"), s_lua_record_ctx->bestwho, available); - return convComboAddressListToString(res); - } - } - - // All units down, apply backupSelector on all candidates - vector ret{}; - for(const auto& unit : candidates) { - ret.insert(ret.end(), unit.begin(), unit.end()); - } - - vector res = useSelector(getOptionValue(options, "backupSelector", "random"), s_lua_record_ctx->bestwho, ret); - return convComboAddressListToString(res); + auto checker = [&url](const ComboAddress& addr, const opts_t& opts) { + return g_up.isUp(addr, url, opts); + }; + return genericIfUp(ips, options, checker); }); /* * Returns a random IP address from the supplied list From 05d6f9ed2d4c0940c6fff6deef31e0e76745aa8b Mon Sep 17 00:00:00 2001 From: Charles-Henri Bruyand Date: Tue, 21 Mar 2023 17:56:40 +0100 Subject: [PATCH 3/4] auth: add regression tests for ip sets arguments to ifup() functions --- regression-tests.auth-py/test_LuaRecords.py | 72 ++++++++++++++++++--- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/regression-tests.auth-py/test_LuaRecords.py b/regression-tests.auth-py/test_LuaRecords.py index bbcdb103e117..e9df7c8ba7f4 100644 --- a/regression-tests.auth-py/test_LuaRecords.py +++ b/regression-tests.auth-py/test_LuaRecords.py @@ -60,6 +60,7 @@ class TestLuaRecords(AuthTest): all.ifportup 3600 IN LUA A "ifportup(8080, {{'{prefix}.101', '{prefix}.102'}})" some.ifportup 3600 IN LUA A "ifportup(8080, {{'192.168.42.21', '{prefix}.102'}})" +multi.ifportup 3600 IN LUA A "ifportup(8080, {{ {{'192.168.42.23'}}, {{'192.168.42.21', '{prefix}.102'}}, {{'{prefix}.101'}} }})" none.ifportup 3600 IN LUA A "ifportup(8080, {{'192.168.42.21', '192.168.21.42'}})" all.noneup.ifportup 3600 IN LUA A "ifportup(8080, {{'192.168.42.21', '192.168.21.42'}}, {{ backupSelector='all' }})" @@ -83,21 +84,21 @@ class TestLuaRecords(AuthTest): config IN LUA LUA ("settings={{stringmatch='Programming in Lua'}} " "EUWips={{'{prefix}.101','{prefix}.102'}} " "EUEips={{'192.168.42.101','192.168.42.102'}} " - "NLips={{'{prefix}.111', '{prefix}.112'}} " - "USAips={{'{prefix}.103'}} ") + "NLips={{'{prefix}.111', '{prefix}.112'}} " + "USAips={{'{prefix}.103', '192.168.42.105'}} ") usa IN LUA A ( ";include('config') " "return ifurlup('http://www.lua.org:8080/', " - "{{USAips, EUEips}}, settings) ") + "USAips, settings) ") + +usa-ext IN LUA A ( ";include('config') " + "return ifurlup('http://www.lua.org:8080/', " + "{{EUEips, USAips}}, settings) ") mix.ifurlup IN LUA A ("ifurlup('http://www.other.org:8080/ping.json', " "{{ '192.168.42.101', '{prefix}.101' }}, " "{{ stringmatch='pong' }}) ") -eu-west IN LUA A ( ";include('config') " - "return ifurlup('http://www.lua.org:8080/', " - "{{EUWips, EUEips, USAips}}, settings) ") - ifurlextup IN LUA A "ifurlextup({{{{['192.168.0.1']='http://{prefix}.101:8080/404',['192.168.0.2']='http://{prefix}.102:8080/404'}}, {{['192.168.0.3']='http://{prefix}.101:8080/'}}}})" nl IN LUA A ( ";include('config') " @@ -327,6 +328,33 @@ def testIfportupWithSomeDown(self): self.assertRcodeEqual(res, dns.rcode.NOERROR) self.assertAnyRRsetInAnswer(res, expected) + def testIfportupWithSomeDownMultiset(self): + """ + Basic ifportup() test with some ports DOWN from multiple sets + """ + query = dns.message.make_query('multi.ifportup.example.org', 'A') + expected = [ + dns.rrset.from_text('multi.ifportup.example.org.', 0, dns.rdataclass.IN, 'A', + '192.168.42.21'), + dns.rrset.from_text('multi.ifportup.example.org.', 0, dns.rdataclass.IN, 'A', + '192.168.42.23'), + dns.rrset.from_text('multi.ifportup.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.102'.format(prefix=self._PREFIX)), + dns.rrset.from_text('multi.ifportup.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.101'.format(prefix=self._PREFIX)) + ] + + # we first expect any of the IPs as no check has been performed yet + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, expected) + + # An ip is up in 2 sets, but we expect only the one from the middle set + expected = [expected[2]] + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, expected) + def testIfportupWithAllDown(self): """ Basic ifportup() test with all ports DOWN @@ -371,7 +399,7 @@ def testIfurlup(self): reachable = [ '{prefix}.103'.format(prefix=self._PREFIX) ] - unreachable = ['192.168.42.101', '192.168.42.102'] + unreachable = ['192.168.42.105'] ips = reachable + unreachable all_rrs = [] reachable_rrs = [] @@ -392,6 +420,34 @@ def testIfurlup(self): self.assertRcodeEqual(res, dns.rcode.NOERROR) self.assertAnyRRsetInAnswer(res, reachable_rrs) + def testIfurlupMultiSet(self): + """ + Basic ifurlup() test with mutiple sets + """ + reachable = [ + '{prefix}.103'.format(prefix=self._PREFIX) + ] + unreachable = ['192.168.42.101', '192.168.42.102', '192.168.42.105'] + ips = reachable + unreachable + all_rrs = [] + reachable_rrs = [] + for ip in ips: + rr = dns.rrset.from_text('usa-ext.example.org.', 0, dns.rdataclass.IN, 'A', ip) + all_rrs.append(rr) + if ip in reachable: + reachable_rrs.append(rr) + + query = dns.message.make_query('usa-ext.example.org', 'A') + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, all_rrs) + + # the timeout in the LUA health checker is 2 second, so we make sure to wait slightly longer here + time.sleep(3) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, reachable_rrs) + def testIfurlextup(self): expected = [dns.rrset.from_text('ifurlextup.example.org.', 0, dns.rdataclass.IN, dns.rdatatype.A, '192.168.0.3')] From fe8e05d6240b6f1e0ccf35d530b0d65824d55610 Mon Sep 17 00:00:00 2001 From: Charles-Henri Bruyand Date: Tue, 21 Mar 2023 18:07:55 +0100 Subject: [PATCH 4/4] auth: explicit documentation about sets for ifportup() --- docs/lua-records/index.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/lua-records/index.rst b/docs/lua-records/index.rst index c3e08efbcde2..66d0b82876ca 100644 --- a/docs/lua-records/index.rst +++ b/docs/lua-records/index.rst @@ -49,6 +49,14 @@ addresses listen on port 443. If either IP address stops listening, only the other address will be returned. If all IP addresses are down, all candidates are returned. +You can also provide multiple sets of IP addresses to prioritize a set over the +rest. If an IP address from the first set is available, it will be returned. If +no addresses work in the first set, the second set is tried. + +For example:: + + www IN LUA A "ifportup(443, {{'192.0.2.1', '192.0.2.2'}, {'192.0.3.1'}})" + Because DNS queries require rapid answers, server availability is not checked synchronously. In the background, a process periodically determines if IP addresses mentioned in availability rules are, in fact, available.