Skip to content

Commit

Permalink
Merge pull request #12669 from chbruyand/auth-ifurlup-ips
Browse files Browse the repository at this point in the history
auth: luarecords, enhance ifportup() with lists of sets of addresses like ifurlup()
  • Loading branch information
Habbie committed Apr 21, 2023
2 parents 0d64d9a + fe8e05d commit 277508d
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 71 deletions.
8 changes: 8 additions & 0 deletions docs/lua-records/index.rst
Expand Up @@ -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.
Expand Down
132 changes: 69 additions & 63 deletions pdns/lua-record.cc
Expand Up @@ -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;
Expand Down Expand Up @@ -548,18 +547,41 @@ static vector<string> convComboAddressListToString(const vector<ComboAddress>& i
return result;
}

static vector<ComboAddress> convComboAddressList(const iplist_t& items)
static vector<ComboAddress> convComboAddressList(const iplist_t& items, uint16_t port=0)
{
vector<ComboAddress> 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;
}

/**
* 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<vector<ComboAddress>> convMultiComboAddressList(const boost::variant<iplist_t, ipunitlist_t>& items, uint16_t port = 0)
{
vector<vector<ComboAddress>> candidates;

if(auto simple = boost::get<iplist_t>(&items)) {
vector<ComboAddress> unit = convComboAddressList(*simple, port);
candidates.push_back(unit);
} else {
auto units = boost::get<ipunitlist_t>(items);
for(const auto& u : units) {
vector<ComboAddress> unit = convComboAddressList(u.second, port);
candidates.push_back(unit);
}
}
return candidates;
}

static vector<string> convStringList(const iplist_t& items)
{
vector<string> result;
Expand Down Expand Up @@ -596,6 +618,37 @@ typedef struct AuthLuaRecordContext

static thread_local unique_ptr<lua_record_ctx_t> s_lua_record_ctx;

static vector<string> genericIfUp(const boost::variant<iplist_t, ipunitlist_t>& ips, boost::optional<opts_t> options, std::function<bool(const ComboAddress&, const opts_t&)> upcheckf, uint16_t port = 0) {
vector<vector<ComboAddress> > candidates;
opts_t opts;
if(options)
opts = *options;

candidates = convMultiComboAddressList(ips, port);

for(const auto& unit : candidates) {
vector<ComboAddress> available;
for(const auto& c : unit) {
if (upcheckf(c, opts)) {
available.push_back(c);
}
}
if(!available.empty()) {
vector<ComboAddress> res = useSelector(getOptionValue(options, "selector", "random"), s_lua_record_ctx->bestwho, available);
return convComboAddressListToString(res);
}
}

// All units down, apply backupSelector on all candidates
vector<ComboAddress> ret{};
for(const auto& unit : candidates) {
ret.insert(ret.end(), unit.begin(), unit.end());
}

vector<ComboAddress> res = useSelector(getOptionValue(options, "backupSelector", "random"), s_lua_record_ctx->bestwho, ret);
return convComboAddressListToString(res);
}

static void setupLuaRecords(LuaContext& lua)
{
lua.writeFunction("latlon", []() {
Expand Down Expand Up @@ -832,34 +885,18 @@ static void setupLuaRecords(LuaContext& lua)
*
* @example ifportup(443, { '1.2.3.4', '5.4.3.2' })"
*/
lua.writeFunction("ifportup", [](int port, const vector<pair<int, string> >& ips, const boost::optional<std::unordered_map<string,string>> options) {
vector<ComboAddress> candidates, unavailables;
opts_t opts;
vector<ComboAddress > conv;
std::string selector;

if(options)
opts = *options;
for(const auto& i : ips) {
ComboAddress rem(i.second, port);
if(g_up.isUp(rem, opts)) {
candidates.push_back(rem);
}
else {
unavailables.push_back(rem);
}
lua.writeFunction("ifportup", [](int port, const boost::variant<iplist_t, ipunitlist_t>& ips, const boost::optional<std::unordered_map<string,string>> options) {
if (port < 0) {
port = 0;
}
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");
if (port > std::numeric_limits<uint16_t>::max()) {
port = std::numeric_limits<uint16_t>::max();
}

vector<ComboAddress> res = useSelector(selector, s_lua_record_ctx->bestwho, candidates);
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<pair<int, opts_t> >& ipurls, boost::optional<opts_t> options) {
Expand Down Expand Up @@ -898,42 +935,11 @@ static void setupLuaRecords(LuaContext& lua)
lua.writeFunction("ifurlup", [](const std::string& url,
const boost::variant<iplist_t, ipunitlist_t>& ips,
boost::optional<opts_t> options) {
vector<vector<ComboAddress> > candidates;
opts_t opts;
if(options)
opts = *options;
if(auto simple = boost::get<iplist_t>(&ips)) {
vector<ComboAddress> unit = convComboAddressList(*simple);
candidates.push_back(unit);
} else {
auto units = boost::get<ipunitlist_t>(ips);
for(const auto& u : units) {
vector<ComboAddress> unit = convComboAddressList(u.second);
candidates.push_back(unit);
}
}

for(const auto& unit : candidates) {
vector<ComboAddress> available;
for(const auto& c : unit) {
if(g_up.isUp(c, url, opts)) {
available.push_back(c);
}
}
if(!available.empty()) {
vector<ComboAddress> res = useSelector(getOptionValue(options, "selector", "random"), s_lua_record_ctx->bestwho, available);
return convComboAddressListToString(res);
}
}

// All units down, apply backupSelector on all candidates
vector<ComboAddress> ret{};
for(const auto& unit : candidates) {
ret.insert(ret.end(), unit.begin(), unit.end());
}

vector<ComboAddress> 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
Expand Down
72 changes: 64 additions & 8 deletions regression-tests.auth-py/test_LuaRecords.py
Expand Up @@ -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' }})"
Expand 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') "
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = []
Expand All @@ -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')]

Expand Down

0 comments on commit 277508d

Please sign in to comment.