From: Charles-Henri Bruyand Date: Thu, 5 Apr 2018 12:20:21 +0000 (+0200) Subject: luarec: add basic tests X-Git-Tag: dnsdist-1.3.1~136^2~14 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1bc56192750169019bb95c5ec36a12fb2dac7624;p=thirdparty%2Fpdns.git luarec: add basic tests --- diff --git a/docs/lua-records.rst b/docs/lua-records.rst index cc92b6044a..f2e1f24115 100644 --- a/docs/lua-records.rst +++ b/docs/lua-records.rst @@ -50,13 +50,13 @@ addresses mentioned in availability rules are, in fact, available. Another example:: - www IN LUA A "closest({'192.0.2.1','192.0.2.2','198.51.100.1'})" + www IN LUA A "pickclosest({'192.0.2.1','192.0.2.2','198.51.100.1'})" This uses the GeoIP backend to find indications of the geographical location of the requestor and the listed IP addresses. It will return with one of the closest addresses. -``closest`` and ifportup can be combined as follows:: +``pickclosest`` and ifportup can be combined as follows:: www IN LUA A ("ifportup(443, {'192.0.2.1', '192.0.2.2', '198.51.100.1'}" ", {selector='closest'}) ") @@ -221,7 +221,7 @@ Record creation functions :param addresses: A list of strings with the possible IP addresses. -.. function:: closest(addresses) +.. function:: pickclosest(addresses) Returns IP address deemed closest to the ``bestwho`` IP address. @@ -275,7 +275,7 @@ Record creation functions This function also works for CNAME or TXT records. -.. function:: whashed(weightparams) +.. function:: pickwhashed(weightparams) Based on the hash of ``bestwho``, returns an IP address from the list supplied, as weighted by the various ``weight`` parameters. @@ -289,20 +289,20 @@ Record creation functions An example:: - mydomain.example.com IN LUA A ("whashed( " - " {15, {"192.0.2.1", "203.0.113.2"}, " - " {100, {"198.51.100.5"} " - ") ") + mydomain.example.com IN LUA A ("pickwhashed({ " + " {15, "192.0.2.1"}, " + " {100, "198.51.100.5"} " + "}) ") -.. function:: wrandom(weightparams) +.. function:: pickwrandom(weightparams) Returns a random IP address from the list supplied, as weighted by the various ``weight`` parameters. Performs no uptime checking. :param weightparams: table of weight, IP addresses. - See :func:`whashed` for an example. + See :func:`pickwhashed` for an example. Helper functions ~~~~~~~~~~~~~~~~ diff --git a/pdns/lua-record.cc b/pdns/lua-record.cc index 1b8870dd26..8725cf8cee 100644 --- a/pdns/lua-record.cc +++ b/pdns/lua-record.cc @@ -26,12 +26,18 @@ expire them too? pool of UeberBackends? + + Pool checks ? */ +using iplist_t = vector >; +using wiplist_t = std::unordered_map; +using ipunitlist_t = vector >; +using opts_t = std::unordered_map; + class IsUpOracle { private: - typedef std::unordered_map opts_t; struct CheckDesc { ComboAddress rem; @@ -44,19 +50,19 @@ private: oopts[m.first]=m.second; for(const auto& m : rhs.opts) rhsoopts[m.first]=m.second; - + return std::make_tuple(rem, url, oopts) < std::make_tuple(rhs.rem, rhs.url, rhsoopts); } }; public: - bool isUp(const ComboAddress& remote, opts_t opts); - bool isUp(const ComboAddress& remote, const std::string& url, opts_t opts=opts_t()); + bool isUp(const ComboAddress& remote, const opts_t& opts); + bool isUp(const ComboAddress& remote, const std::string& url, const opts_t& opts); bool isUp(const CheckDesc& cd); - + private: - void checkURLThread(ComboAddress rem, std::string url, opts_t opts); - void checkTCPThread(ComboAddress rem, opts_t opts); + void checkURLThread(ComboAddress rem, std::string url, const opts_t& opts); + void checkTCPThread(ComboAddress rem, const opts_t& opts); struct Checker { @@ -66,24 +72,25 @@ private: typedef map statuses_t; statuses_t d_statuses; - + std::mutex d_mutex; - void setStatus(const CheckDesc& cd, bool status) + void setStatus(const CheckDesc& cd, bool status) { std::lock_guard l(d_mutex); d_statuses[cd].status=status; } - void setDown(const ComboAddress& rem, const std::string& url=std::string(), opts_t opts=opts_t()) + void setDown(const ComboAddress& rem, const std::string& url=std::string(), const opts_t& opts = opts_t()) { CheckDesc cd{rem, url, opts}; setStatus(cd, false); } - void setUp(const ComboAddress& rem, const std::string& url=std::string(), opts_t opts=opts_t()) + void setUp(const ComboAddress& rem, const std::string& url=std::string(), const opts_t& opts = opts_t()) { CheckDesc cd{rem, url, opts}; + setStatus(cd, true); } @@ -97,7 +104,7 @@ private: setStatus(cd, true); } - bool upStatus(const ComboAddress& rem, const std::string& url=std::string(), opts_t opts=opts_t()) + bool upStatus(const ComboAddress& rem, const std::string& url=std::string(), const opts_t& opts = opts_t()) { CheckDesc cd{rem, url, opts}; std::lock_guard l(d_mutex); @@ -117,7 +124,6 @@ bool IsUpOracle::isUp(const CheckDesc& cd) std::lock_guard l(d_mutex); auto iter = d_statuses.find(cd); if(iter == d_statuses.end()) { -// L< opts) +bool IsUpOracle::isUp(const ComboAddress& remote, const std::string& url, const opts_t& opts) { CheckDesc cd{remote, url, opts}; std::lock_guard l(d_mutex); @@ -143,21 +149,21 @@ bool IsUpOracle::isUp(const ComboAddress& remote, const std::string& url, std::u d_statuses[cd]=Checker{checker, false}; return false; } - + return iter->second.status; } -void IsUpOracle::checkTCPThread(ComboAddress rem, opts_t opts) +void IsUpOracle::checkTCPThread(ComboAddress rem, const opts_t& opts) { CheckDesc cd{rem, "", opts}; setDown(cd); for(bool first=true;;first=false) { try { Socket s(rem.sin4.sin_family, SOCK_STREAM); - s.setNonBlocking(); ComboAddress src; + s.setNonBlocking(); if(opts.count("source")) { - src=ComboAddress(opts["source"]); + src=ComboAddress(opts.at("source")); s.bind(src); } s.connect(rem, 1); @@ -179,25 +185,23 @@ void IsUpOracle::checkTCPThread(ComboAddress rem, opts_t opts) } -void IsUpOracle::checkURLThread(ComboAddress rem, std::string url, opts_t opts) +void IsUpOracle::checkURLThread(ComboAddress rem, std::string url, const opts_t& opts) { setDown(rem, url, opts); for(bool first=true;;first=false) { try { MiniCurl mc; - // cout<<"Checking URL "< bool doCompare(const T& var, const std::string& res, const C& cmp) { - if(auto country = boost::get(&var)) + if(auto country = boost::get(&var)) return cmp(*country, res); auto countries=boost::get > >(&var); @@ -259,7 +262,7 @@ static ComboAddress hashed(const ComboAddress& who, const vector& } -static ComboAddress wrandom(const vector >& wips) +static ComboAddress pickwrandom(const vector >& wips) { int sum=0; vector > pick; @@ -272,7 +275,7 @@ static ComboAddress wrandom(const vector >& wips) return p->second; } -static ComboAddress whashed(const ComboAddress& bestwho, vector >& wips) +static ComboAddress pickwhashed(const ComboAddress& bestwho, vector >& wips) { int sum=0; vector > pick; @@ -303,7 +306,7 @@ static bool getLatLon(const std::string& ip, string& loc) int latdeg, latmin, londeg, lonmin; double latsec, lonsec; char lathem='X', lonhem='X'; - + double lat, lon; if(!getLatLon(ip, lat, lon)) return false; @@ -331,7 +334,7 @@ static bool getLatLon(const std::string& ip, string& loc) >>> print("{}º {}' {}\"".format(deg, min, sec)) */ - + latdeg = lat; latmin = (lat - latdeg)*60.0; latsec = (((lat - latdeg)*60.0) - latmin)*60.0; @@ -347,10 +350,8 @@ static bool getLatLon(const std::string& ip, string& loc) loc= (fmt % latdeg % latmin % latsec % lathem % londeg % lonmin % lonsec % lonhem ).str(); return true; } - - -static ComboAddress closest(const ComboAddress& bestwho, const vector& wips) +static ComboAddress pickclosest(const ComboAddress& bestwho, const vector& wips) { map > ranked; double wlat=0, wlon=0; @@ -364,7 +365,7 @@ static ComboAddress closest(const ComboAddress& bestwho, const vector 180) - londiff = 360 - londiff; + londiff = 360 - londiff; double dist2=latdiff*latdiff + londiff*londiff; // cout<<" distance: "<> luaSynth(const std::string& code, const DNSName& query, const DNSName& zone, int zoneid, const DNSPacket& dnsp, uint16_t qtype) +static vector convIplist(const iplist_t& src) +{ + vector ret; + + for(const auto& ip : src) + ret.emplace_back(ip.second); + + return ret; +} + +static vector > convWIplist(std::unordered_map src) +{ + vector > ret; + + for(const auto& i : src) + ret.emplace_back(atoi(i.second.at(1).c_str()), ComboAddress(i.second.at(2))); + + return ret; +} + +std::vector> luaSynth(const std::string& code, const DNSName& query, const DNSName& zone, int zoneid, const DNSPacket& dnsp, uint16_t qtype) { // cerr<<"Called for "<> ret; - + LuaContext& lua = *alua.getLua(); lua.writeVariable("qname", query); lua.writeVariable("who", dnsp.getRemote()); @@ -445,7 +466,7 @@ std::vector> luaSynth(const std::string& code, cons return loc; }); - + lua.writeFunction("closestMagic", [&bestwho,&query](){ vector candidates; for(auto l : query.getRawLabels()) { @@ -457,10 +478,10 @@ std::vector> luaSynth(const std::string& code, cons break; } } - - return closest(bestwho, candidates).toString(); + + return pickclosest(bestwho, candidates).toString(); }); - + lua.writeFunction("latlonMagic", [&query](){ auto labels= query.getRawLabels(); if(labels.size()<4) @@ -470,7 +491,7 @@ std::vector> luaSynth(const std::string& code, cons return std::to_string(lat)+" "+std::to_string(lon); }); - + lua.writeFunction("createReverse", [&bestwho,&query,&zone](string suffix, boost::optional> e){ try { auto labels= query.getRawLabels(); @@ -482,7 +503,7 @@ std::vector> luaSynth(const std::string& code, cons // exceptions are relative to zone // so, query comes in for 4.3.2.1.in-addr.arpa, zone is called 2.1.in-addr.arpa // e["1.2.3.4"]="bert.powerdns.com" - should match, easy enough to do - // the issue is with classless delegation.. + // the issue is with classless delegation.. if(e) { ComboAddress req(labels[3]+"."+labels[2]+"."+labels[1]+"."+labels[0], 0); const auto& uom = *e; @@ -490,7 +511,7 @@ std::vector> luaSynth(const std::string& code, cons if(ComboAddress(c.first, 0) == req) return c.second; } - + boost::format fmt(suffix); fmt.exceptions( boost::io::all_error_bits ^ ( boost::io::too_many_args_bit | boost::io::too_few_args_bit ) ); @@ -524,8 +545,8 @@ std::vector> luaSynth(const std::string& code, cons if(sscanf(parts[0].c_str()+2, "%02x%02x%02x%02x", &x1, &x2, &x3, &x4)==4) { return std::to_string(x1)+"."+std::to_string(x2)+"."+std::to_string(x3)+"."+std::to_string(x4); } - - + + } return std::string("0.0.0.0"); }); @@ -552,7 +573,7 @@ std::vector> luaSynth(const std::string& code, cons return std::string("::"); }); - + lua.writeFunction("createReverse6", [&bestwho,&query,&zone](string suffix, boost::optional> e){ vector candidates; @@ -562,7 +583,7 @@ std::vector> luaSynth(const std::string& code, cons return std::string("unknown"); boost::format fmt(suffix); fmt.exceptions( boost::io::all_error_bits ^ ( boost::io::too_many_args_bit | boost::io::too_few_args_bit ) ); - + string together; vector quads; @@ -586,17 +607,17 @@ std::vector> luaSynth(const std::string& code, cons return addr.second; } } - + string dashed=ip6.toString(); boost::replace_all(dashed, ":", "-"); - + for(int i=31; i>=0; --i) fmt % labels[i]; fmt % dashed; for(const auto& quad : quads) fmt % quad; - + return fmt.str(); } catch(std::exception& e) { @@ -608,126 +629,132 @@ std::vector> luaSynth(const std::string& code, cons return std::string("unknown"); }); - + + /* + * Simplistic test to see if an IP address listens on a certain port + * Will return a single IP address from the set of available IP addresses. If + * no IP address is available, will return a random element of the set of + * addresses suppplied for testing. + * + * @example ifportup(443, { '1.2.3.4', '5.4.3.2' })" + */ lua.writeFunction("ifportup", [&bestwho](int port, const vector >& ips, const boost::optional> options) { - vector candidates; - std::unordered_map opts; + vector candidates, unavailables; + opts_t opts; + vector conv; + if(options) opts = *options; - for(const auto& i : ips) { ComboAddress rem(i.second, port); - if(g_up.isUp(rem, opts)) + if(g_up.isUp(rem, opts)) { candidates.push_back(rem); + } + else { + unavailables.push_back(rem); + } } - vector ret; if(candidates.empty()) { - // cout<<"Everything is down. Returning all of them"< >, - vector > > > - > & ips, boost::optional> options) { + const boost::variant& ips, + boost::optional options) { vector > candidates; - std::unordered_map opts; + opts_t opts; if(options) opts = *options; - if(auto simple = boost::get>>(&ips)) { - vector unit; - for(const auto& i : *simple) { - ComboAddress rem(i.second, 80); - unit.push_back(rem); - } + if(auto simple = boost::get(&ips)) { + vector unit = convIplist(*simple); candidates.push_back(unit); } else { - auto units = boost::get > > >>(ips); + auto units = boost::get(ips); for(const auto& u : units) { - vector unit; - for(const auto& c : u.second) { - ComboAddress rem(c.second, 80); - unit.push_back(rem); - } + vector unit = convIplist(u.second); candidates.push_back(unit); } } - // - // cout<<"Have "< ret; for(const auto& unit : candidates) { vector available; - for(const auto& c : unit) - if(g_up.isUp(c, url, opts)) + for(const auto& c : unit) { + if(g_up.isUp(c, url, opts)) { available.push_back(c); - if(available.empty()) { - // cerr<<"Entire unit is down, trying next one if available"< ret{}; for(const auto& unit : candidates) { - for(const auto& c : unit) - ret.push_back(c.toString()); + ret.insert(ret.end(), unit.begin(), unit.end()); } - return ret; + return pickrandom(ret).toString(); }); - /* idea: we have policies on vectors of ComboAddresses, like - random, wrandom, whashed, closest. In C++ this is ComboAddress in, + random, pickwrandom, pickwhashed, pickclosest. In C++ this is ComboAddress in, ComboAddress out. In Lua, vector string in, string out */ - - lua.writeFunction("pickrandom", [](const vector >& ips) { - return ips[random()%ips.size()].second; + + /* + * Returns a random IP address from the supplied list + * @example pickrandom({ '1.2.3.4', '5.4.3.2' })" + */ + lua.writeFunction("pickrandom", [](const iplist_t& ips) { + vector conv = convIplist(ips); + + return pickrandom(conv).toString(); }); - // wrandom({ {100, '1.2.3.4'}, {50, '5.4.3.2'}, {1, '192.168.1.0'}})" - lua.writeFunction("wrandom", [](std::unordered_map > ips) { - vector > conv; - for(auto& i : ips) - conv.emplace_back(atoi(i.second[1].c_str()), ComboAddress(i.second[2])); - - return wrandom(conv).toString(); + /* + * Returns a random IP address from the supplied list, as weighted by the + * various ``weight`` parameters + * @example pickwrandom({ {100, '1.2.3.4'}, {50, '5.4.3.2'}, {1, '192.168.1.0'} }) + */ + lua.writeFunction("pickwrandom", [](std::unordered_map ips) { + vector > conv = convWIplist(ips); + + return pickwrandom(conv).toString(); }); - lua.writeFunction("whashed", [&bestwho](std::unordered_map > ips) { + /* + * Based on the hash of `bestwho`, returns an IP address from the list + * supplied, as weighted by the various `weight` parameters + * @example pickwhashed({ {15, '1.2.3.4'}, {50, '5.4.3.2'} }) + */ + lua.writeFunction("pickwhashed", [&bestwho](std::unordered_map ips) { vector > conv; - for(auto& i : ips) + + for(auto& i : ips) conv.emplace_back(atoi(i.second[1].c_str()), ComboAddress(i.second[2])); - - return whashed(bestwho, conv).toString(); - + + return pickwhashed(bestwho, conv).toString(); }); - lua.writeFunction("closest", [&bestwho](std::unordered_map ips) { - vector conv; - for(auto& i : ips) - conv.emplace_back(i.second); - - return closest(bestwho, conv).toString(); - + lua.writeFunction("pickclosest", [&bestwho](const iplist_t& ips) { + vector conv = convIplist(ips); + + return pickclosest(bestwho, conv).toString(); + }); - + int counter=0; lua.writeFunction("report", [&counter](string event, boost::optional line){ throw std::runtime_error("Script took too long"); @@ -753,16 +780,16 @@ std::vector> luaSynth(const std::string& code, cons return !strcasecmp(a.c_str(), b.c_str()); }); }); - + lua.writeFunction("country", [&bestwho](const combovar_t& var) { string res = getGeo(bestwho.toString(), GeoIPInterface::Country2); return doCompare(var, res, [](const std::string& a, const std::string& b) { return !strcasecmp(a.c_str(), b.c_str()); }); - + }); - lua.writeFunction("netmask", [bestwho](const vector>& ips) { + lua.writeFunction("netmask", [bestwho](const iplist_t& ips) { for(const auto& i :ips) { Netmask nm(i.second); if(nm.match(bestwho)) @@ -773,15 +800,15 @@ std::vector> luaSynth(const std::string& code, cons /* { { - {'192.168.0.0/16', '10.0.0.0/8'}, + {'192.168.0.0/16', '10.0.0.0/8'}, {'192.168.20.20', '192.168.20.21'} }, { {'0.0.0.0/0'}, {'192.0.2.1'} } } - */ - lua.writeFunction("view", [bestwho](const vector > > > > >& in) { + */ + lua.writeFunction("view", [bestwho](const vector > > >& in) { for(const auto& rule : in) { const auto& netmasks=rule.second[0].second; const auto& destinations=rule.second[1].second; @@ -795,8 +822,8 @@ std::vector> luaSynth(const std::string& code, cons return std::string(); } ); - - + + lua.writeFunction("include", [&lua,zone,zoneid](string record) { try { vector drs = lookup(DNSName(record) +zone, QType::LUA, zoneid); @@ -810,7 +837,6 @@ std::vector> luaSynth(const std::string& code, cons } }); - try { string actual; if(!code.empty() && code[0]!=';') @@ -826,15 +852,16 @@ std::vector> luaSynth(const std::string& code, cons else for(const auto& c : boost::get>>(content)) contents.push_back(c.second); - + for(const auto& content: contents) { if(qtype==QType::TXT) ret.push_back(std::shared_ptr(DNSRecordContent::mastermake(qtype, 1, '"'+content+'"' ))); else ret.push_back(std::shared_ptr(DNSRecordContent::mastermake(qtype, 1, content ))); } - }catch(std::exception &e) { + } catch(std::exception &e) { L<toString()); - //cout<<"Setting hardcoded IP: "<toString()); - // cout<<"Setting hardcoded IP: "< ports{80, 443}; + if (found != std::string::npos) { + int port = std::stoi(host4.substr(found + 1)); + if (port <= 0 || port > 65535) + throw std::overflow_error("Invalid port number"); + ports = {(uint16_t)port}; + host4 = host4.substr(0, found); + } + + for (const auto& port : ports) { + string hcode = boost::str(boost::format("%s:%u:%s") % host4 % port % rem->toString()); + hostlist = curl_slist_append(hostlist, hcode.c_str()); + } curl_easy_setopt(d_curl, CURLOPT_RESOLVE, hostlist); } diff --git a/pdns/packethandler.cc b/pdns/packethandler.cc index c374e2833f..4388c9a025 100644 --- a/pdns/packethandler.cc +++ b/pdns/packethandler.cc @@ -384,12 +384,17 @@ bool PacketHandler::getBestWildcard(DNSPacket *p, SOAData& sd, const DNSName &ta if(rec->d_type == QType::CNAME || rec->d_type == p->qtype.getCode()) { // noCache=true; DLOG(L<<"Executing Lua: '"<getCode()<<"'"<getCode(), target, sd.qname, sd.domain_id, *p, rec->d_type); - for(const auto& r : recvec) { - rr.dr.d_type = rec->d_type; // might be CNAME - rr.dr.d_content = r; - rr.scopeMask = p->getRealRemote().getBits(); // this makes sure answer is a specific as your question - ret->push_back(rr); + try { + auto recvec=luaSynth(rec->getCode(), target, sd.qname, sd.domain_id, *p, rec->d_type); + for(const auto& r : recvec) { + rr.dr.d_type = rec->d_type; // might be CNAME + rr.dr.d_content = r; + rr.scopeMask = p->getRealRemote().getBits(); // this makes sure answer is a specific as your question + ret->push_back(rr); + } + } + catch(std::exception &e) { + ; } } } @@ -1336,20 +1341,26 @@ DNSPacket *PacketHandler::doQuestion(DNSPacket *p) auto rec=getRR(rr.dr); if(rec->d_type == QType::CNAME || rec->d_type == p->qtype.getCode()) { noCache=true; - auto recvec=luaSynth(rec->getCode(), target, sd.qname, sd.domain_id, *p, rec->d_type); - if(!recvec.empty()) { - - for(const auto& r : recvec) { - rr.dr.d_type = rec->d_type; // might be CNAME - rr.dr.d_content = r; - rr.scopeMask = p->getRealRemote().getBits(); // this makes sure answer is a specific as your question - - rrset.push_back(rr); + try { + auto recvec=luaSynth(rec->getCode(), target, sd.qname, sd.domain_id, *p, rec->d_type); + if(!recvec.empty()) { + for(const auto& r : recvec) { + rr.dr.d_type = rec->d_type; // might be CNAME + rr.dr.d_content = r; + rr.scopeMask = p->getRealRemote().getBits(); // this makes sure answer is a specific as your question + rrset.push_back(rr); + } + if(rec->d_type == QType::CNAME && p->qtype.getCode() != QType::CNAME) + weRedirected = 1; + else + weDone = 1; } - if(rec->d_type == QType::CNAME && p->qtype.getCode() != QType::CNAME) - weRedirected = 1; - else - weDone = 1; + } + catch(std::exception &e) { + r=p->replyPacket(); + r->setRcode(RCode::ServFail); + + return r; } } } diff --git a/regression-tests.auth-py/.gitignore b/regression-tests.auth-py/.gitignore new file mode 100644 index 0000000000..7103d74161 --- /dev/null +++ b/regression-tests.auth-py/.gitignore @@ -0,0 +1,5 @@ +/*.pyc +/*.xml +/.venv +/configs +/vars diff --git a/regression-tests.auth-py/authtests.py b/regression-tests.auth-py/authtests.py new file mode 100644 index 0000000000..9e55a32ced --- /dev/null +++ b/regression-tests.auth-py/authtests.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python2 + +import errno +import shutil +import os +import socket +import struct +import subprocess +import sys +import time +import unittest +import dns +import dns.message + +from pprint import pprint + +class AuthTest(unittest.TestCase): + """ + Setup auth required for the tests + """ + + _confdir = 'auth' + _authPort = 5300 + + _root_DS = "63149 13 1 a59da3f5c1b97fcd5fa2b3b2b0ac91d38a60d33a" + + # The default SOA for zones in the authoritative servers + _SOA = "ns1.example.net. hostmaster.example.net. 1 3600 1800 1209600 300" + + # The definitions of the zones on the authoritative servers, the key is the + # zonename and the value is the zonefile content. several strings are replaced: + # - {soa} => value of _SOA + # - {prefix} value of _PREFIX + _zones = { + 'example.org': """ +example.org. 3600 IN SOA {soa} +example.org. 3600 IN NS ns1.example.org. +example.org. 3600 IN NS ns2.example.org. +ns1.example.org. 3600 IN A {prefix}.10 +ns2.example.org. 3600 IN A {prefix}.11 + """, + } + + _zone_keys = { + 'example.org': """ +Private-key-format: v1.2 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: Lt0v0Gol3pRUFM7fDdcy0IWN0O/MnEmVPA+VylL8Y4U= + """, + } + + _auth_cmd = ['authbind', + os.environ['PDNS']] + _auth_env = {} + _auths = {} + + _PREFIX = os.environ['PREFIX'] + + + @classmethod + def createConfigDir(cls, confdir): + try: + shutil.rmtree(confdir) + except OSError as e: + if e.errno != errno.ENOENT: + raise + os.mkdir(confdir, 0755) + + @classmethod + def generateAuthZone(cls, confdir, zonename, zonecontent): + with open(os.path.join(confdir, '%s.zone' % zonename), 'w') as zonefile: + zonefile.write(zonecontent.format(prefix=cls._PREFIX, soa=cls._SOA)) + + @classmethod + def generateAuthNamedConf(cls, confdir, zones): + with open(os.path.join(confdir, 'named.conf'), 'w') as namedconf: + namedconf.write(""" +options { + directory "%s"; +};""" % confdir) + for zonename in zones: + zone = '.' if zonename == 'ROOT' else zonename + + namedconf.write(""" + zone "%s" { + type master; + file "%s.zone"; + };""" % (zone, zonename)) + + @classmethod + def generateAuthConfig(cls, confdir): + bind_dnssec_db = os.path.join(confdir, 'bind-dnssec.sqlite3') + + with open(os.path.join(confdir, 'pdns.conf'), 'w') as pdnsconf: + pdnsconf.write(""" +module-dir=../regression-tests/modules +launch=bind geoip +daemon=no +local-ipv6= +bind-config={confdir}/named.conf +bind-dnssec-db={bind_dnssec_db} +socket-dir={confdir} +cache-ttl=0 +negquery-cache-ttl=0 +query-cache-ttl=0 +log-dns-queries=yes +log-dns-details=yes +loglevel=9 +geoip-zones-file=../modules/geoipbackend/regression-tests/geo.yaml +geoip-database-files=../modules/geoipbackend/regression-tests/GeoLiteCity.dat +distributor-threads=1""".format(confdir=confdir, + bind_dnssec_db=bind_dnssec_db)) + + pdnsutilCmd = [os.environ['PDNSUTIL'], + '--config-dir=%s' % confdir, + 'create-bind-db', + bind_dnssec_db] + + print ' '.join(pdnsutilCmd) + try: + subprocess.check_output(pdnsutilCmd, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + print e.output + raise + + @classmethod + def secureZone(cls, confdir, zonename, key=None): + zone = '.' if zonename == 'ROOT' else zonename + if not key: + pdnsutilCmd = [os.environ['PDNSUTIL'], + '--config-dir=%s' % confdir, + 'secure-zone', + zone] + else: + keyfile = os.path.join(confdir, 'dnssec.key') + with open(keyfile, 'w') as fdKeyfile: + fdKeyfile.write(key) + + pdnsutilCmd = [os.environ['PDNSUTIL'], + '--config-dir=%s' % confdir, + 'import-zone-key', + zone, + keyfile, + 'active', + 'ksk'] + + print ' '.join(pdnsutilCmd) + try: + subprocess.check_output(pdnsutilCmd, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + print e.output + raise + + @classmethod + def generateAllAuthConfig(cls, confdir): + if cls._zones: + cls.generateAuthConfig(confdir) + cls.generateAuthNamedConf(confdir, cls._zones.keys()) + + for zonename, zonecontent in cls._zones.items(): + cls.generateAuthZone(confdir, + zonename, + zonecontent) + if cls._zone_keys.get(zonename, None): + cls.secureZone(confdir, zonename, cls._zone_keys.get(zonename)) + + @classmethod + def startAuth(cls, confdir, ipaddress): + + print("Launching pdns_server..") + authcmd = list(cls._auth_cmd) + authcmd.append('--config-dir=%s' % confdir) + authcmd.append('--local-address=%s' % ipaddress) + authcmd.append('--local-port=%s' % cls._authPort) + authcmd.append('--loglevel=9') + authcmd.append('--enable-lua-record') + print(' '.join(authcmd)) + + logFile = os.path.join(confdir, 'pdns.log') + with open(logFile, 'w') as fdLog: + cls._auths[ipaddress] = subprocess.Popen(authcmd, close_fds=True, + stdout=fdLog, stderr=fdLog, + env=cls._auth_env) + + time.sleep(2) + + if cls._auths[ipaddress].poll() is not None: + try: + cls._auths[ipaddress].kill() + except OSError as e: + if e.errno != errno.ESRCH: + raise + with open(logFile, 'r') as fdLog: + print fdLog.read() + sys.exit(cls._auths[ipaddress].returncode) + + @classmethod + def setUpSockets(cls): + print("Setting up UDP socket..") + cls._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + cls._sock.settimeout(2.0) + cls._sock.connect((cls._PREFIX + ".1", cls._authPort)) + + @classmethod + def startResponders(cls): + pass + + @classmethod + def setUpClass(cls): + cls.setUpSockets() + + cls.startResponders() + + confdir = os.path.join('configs', cls._confdir) + cls.createConfigDir(confdir) + + cls.generateAllAuthConfig(confdir) + cls.startAuth(confdir, cls._PREFIX + ".1") + + print("Launching tests..") + + @classmethod + def tearDownClass(cls): + cls.tearDownAuth() + cls.tearDownResponders() + + @classmethod + def tearDownResponders(cls): + pass + + @classmethod + def tearDownClass(cls): + cls.tearDownAuth() + + @classmethod + def tearDownAuth(cls): + if 'PDNSRECURSOR_FAST_TESTS' in os.environ: + delay = 0.1 + else: + delay = 1.0 + + for _, auth in cls._auths.items(): + try: + auth.terminate() + if auth.poll() is None: + time.sleep(delay) + if auth.poll() is None: + auth.kill() + auth.wait() + except OSError as e: + if e.errno != errno.ESRCH: + raise + + @classmethod + def sendUDPQuery(cls, query, timeout=2.0, decode=True, fwparams=dict()): + if timeout: + cls._sock.settimeout(timeout) + + try: + cls._sock.send(query.to_wire()) + data = cls._sock.recv(4096) + except socket.timeout: + data = None + finally: + if timeout: + cls._sock.settimeout(None) + + message = None + if data: + if not decode: + return data + message = dns.message.from_wire(data, **fwparams) + return message + + @classmethod + def sendTCPQuery(cls, query, timeout=2.0): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if timeout: + sock.settimeout(timeout) + + sock.connect(("127.0.0.1", cls._recursorPort)) + + try: + wire = query.to_wire() + sock.send(struct.pack("!H", len(wire))) + sock.send(wire) + data = sock.recv(2) + if data: + (datalen,) = struct.unpack("!H", data) + data = sock.recv(datalen) + except socket.timeout as e: + print("Timeout: %s" % (str(e))) + data = None + except socket.error as e: + print("Network error: %s" % (str(e))) + data = None + finally: + sock.close() + + message = None + if data: + message = dns.message.from_wire(data) + return message + + + @classmethod + def sendTCPQuery(cls, query, timeout=2.0): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if timeout: + sock.settimeout(timeout) + + sock.connect(("127.0.0.1", cls._authPort)) + + try: + wire = query.to_wire() + sock.send(struct.pack("!H", len(wire))) + sock.send(wire) + data = sock.recv(2) + if data: + (datalen,) = struct.unpack("!H", data) + data = sock.recv(datalen) + except socket.timeout as e: + print("Timeout: %s" % (str(e))) + data = None + except socket.error as e: + print("Network error: %s" % (str(e))) + data = None + finally: + sock.close() + + message = None + if data: + message = dns.message.from_wire(data) + return message + + def setUp(self): + # This function is called before every tests + return + + ## Functions for comparisons + def assertMessageHasFlags(self, msg, flags, ednsflags=[]): + """Asserts that msg has all the flags from flags set + + @param msg: the dns.message.Message to check + @param flags: a list of strings with flag mnemonics (like ['RD', 'RA']) + @param ednsflags: a list of strings with edns-flag mnemonics (like ['DO'])""" + + if not isinstance(msg, dns.message.Message): + raise TypeError("msg is not a dns.message.Message") + + if isinstance(flags, list): + for elem in flags: + if not isinstance(elem, str): + raise TypeError("flags is not a list of strings") + else: + raise TypeError("flags is not a list of strings") + + if isinstance(ednsflags, list): + for elem in ednsflags: + if not isinstance(elem, str): + raise TypeError("ednsflags is not a list of strings") + else: + raise TypeError("ednsflags is not a list of strings") + + msgFlags = dns.flags.to_text(msg.flags).split() + missingFlags = [flag for flag in flags if flag not in msgFlags] + + msgEdnsFlags = dns.flags.edns_to_text(msg.ednsflags).split() + missingEdnsFlags = [ednsflag for ednsflag in ednsflags if ednsflag not in msgEdnsFlags] + + if len(missingFlags) or len(missingEdnsFlags) or len(msgFlags) > len(flags): + raise AssertionError("Expected flags '%s' (EDNS: '%s'), found '%s' (EDNS: '%s') in query %s" % + (' '.join(flags), ' '.join(ednsflags), + ' '.join(msgFlags), ' '.join(msgEdnsFlags), + msg.question[0])) + + def assertMessageIsAuthenticated(self, msg): + """Asserts that the message has the AD bit set + + @param msg: the dns.message.Message to check""" + + if not isinstance(msg, dns.message.Message): + raise TypeError("msg is not a dns.message.Message") + + msgFlags = dns.flags.to_text(msg.flags) + self.assertTrue('AD' in msgFlags, "No AD flag found in the message for %s" % msg.question[0].name) + + def assertRRsetInAnswer(self, msg, rrset): + """Asserts the rrset (without comparing TTL) exists in the + answer section of msg + + @param msg: the dns.message.Message to check + @param rrset: a dns.rrset.RRset object""" + + ret = '' + if not isinstance(msg, dns.message.Message): + raise TypeError("msg is not a dns.message.Message") + + if not isinstance(rrset, dns.rrset.RRset): + raise TypeError("rrset is not a dns.rrset.RRset") + + found = False + for ans in msg.answer: + ret += "%s\n" % ans.to_text() + if ans.match(rrset.name, rrset.rdclass, rrset.rdtype, 0, None): + self.assertEqual(ans, rrset, "'%s' != '%s'" % (ans.to_text(), rrset.to_text())) + found = True + + if not found : + raise AssertionError("RRset not found in answer\n\n%s" % ret) + + def assertAnyRRsetInAnswer(self, msg, rrsets): + """Asserts that any of the supplied rrsets exists (without comparing TTL) + in the answer section of msg + + @param msg: the dns.message.Message to check + @param rrsets: an array of dns.rrset.RRset object""" + + if not isinstance(msg, dns.message.Message): + raise TypeError("msg is not a dns.message.Message") + + found = False + for rrset in rrsets: + if not isinstance(rrset, dns.rrset.RRset): + raise TypeError("rrset is not a dns.rrset.RRset") + for ans in msg.answer: + if ans.match(rrset.name, rrset.rdclass, rrset.rdtype, 0, None): + if ans == rrset: + found = True + + if not found: + raise AssertionError("RRset not found in answer\n%s" % + "\n".join(([ans.to_text() for ans in msg.answer]))) + + def assertMatchingRRSIGInAnswer(self, msg, coveredRRset, keys=None): + """Looks for coveredRRset in the answer section and if there is an RRSIG RRset + that covers that RRset. If keys is not None, this function will also try to + validate the RRset against the RRSIG + + @param msg: The dns.message.Message to check + @param coveredRRset: The RRSet to check for + @param keys: a dictionary keyed by dns.name.Name with node or rdataset values to use for validation""" + + if not isinstance(msg, dns.message.Message): + raise TypeError("msg is not a dns.message.Message") + + if not isinstance(coveredRRset, dns.rrset.RRset): + raise TypeError("coveredRRset is not a dns.rrset.RRset") + + msgRRsigRRSet = None + msgRRSet = None + + ret = '' + for ans in msg.answer: + ret += ans.to_text() + "\n" + + if ans.match(coveredRRset.name, coveredRRset.rdclass, coveredRRset.rdtype, 0, None): + msgRRSet = ans + if ans.match(coveredRRset.name, dns.rdataclass.IN, dns.rdatatype.RRSIG, coveredRRset.rdtype, None): + msgRRsigRRSet = ans + if msgRRSet and msgRRsigRRSet: + break + + if not msgRRSet: + raise AssertionError("RRset for '%s' not found in answer" % msg.question[0].to_text()) + + if not msgRRsigRRSet: + raise AssertionError("No RRSIGs found in answer for %s:\nFull answer:\n%s" % (msg.question[0].to_text(), ret)) + + if keys: + try: + dns.dnssec.validate(msgRRSet, msgRRsigRRSet.to_rdataset(), keys) + except dns.dnssec.ValidationFailure as e: + raise AssertionError("Signature validation failed for %s:\n%s" % (msg.question[0].to_text(), e)) + + def assertNoRRSIGsInAnswer(self, msg): + """Checks if there are _no_ RRSIGs in the answer section of msg""" + + if not isinstance(msg, dns.message.Message): + raise TypeError("msg is not a dns.message.Message") + + ret = "" + for ans in msg.answer: + if ans.rdtype == dns.rdatatype.RRSIG: + ret += ans.name.to_text() + "\n" + + if len(ret): + raise AssertionError("RRSIG found in answers for:\n%s" % ret) + + def assertAnswerEmpty(self, msg): + self.assertTrue(len(msg.answer) == 0, "Data found in the the answer section for %s:\n%s" % (msg.question[0].to_text(), '\n'.join([i.to_text() for i in msg.answer]))) + + def assertAnswerNotEmpty(self, msg): + self.assertTrue(len(msg.answer) > 0, "Answer is empty") + + def assertRcodeEqual(self, msg, rcode): + if not isinstance(msg, dns.message.Message): + raise TypeError("msg is not a dns.message.Message but a %s" % type(msg)) + + if not isinstance(rcode, int): + if isinstance(rcode, str): + rcode = dns.rcode.from_text(rcode) + else: + raise TypeError("rcode is neither a str nor int") + + if msg.rcode() != rcode: + msgRcode = dns.rcode._by_value[msg.rcode()] + wantedRcode = dns.rcode._by_value[rcode] + + raise AssertionError("Rcode for %s is %s, expected %s." % (msg.question[0].to_text(), msgRcode, wantedRcode)) + + def assertAuthorityHasSOA(self, msg): + if not isinstance(msg, dns.message.Message): + raise TypeError("msg is not a dns.message.Message but a %s" % type(msg)) + + found = False + for rrset in msg.authority: + if rrset.rdtype == dns.rdatatype.SOA: + found = True + break + + if not found: + raise AssertionError("No SOA record found in the authority section:\n%s" % msg.to_text()) diff --git a/regression-tests.auth-py/pylintrc b/regression-tests.auth-py/pylintrc new file mode 100644 index 0000000000..c5f98b1a79 --- /dev/null +++ b/regression-tests.auth-py/pylintrc @@ -0,0 +1,2 @@ +[MESSAGES CONTROL] +disable=invalid-name, missing-docstring, line-too-long, superfluous-parens diff --git a/regression-tests.auth-py/requirements.txt b/regression-tests.auth-py/requirements.txt new file mode 100644 index 0000000000..67690fba37 --- /dev/null +++ b/regression-tests.auth-py/requirements.txt @@ -0,0 +1,3 @@ +dnspython>=1.11 +nose>=1.3.7 +Twisted>0.15.0 diff --git a/regression-tests.auth-py/runtests b/regression-tests.auth-py/runtests new file mode 100755 index 0000000000..6c3fb67d4b --- /dev/null +++ b/regression-tests.auth-py/runtests @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -e + +readonly PYTHON=${PYTHON:-python2} + +if [ ! -d .venv ]; then + virtualenv -p ${PYTHON} .venv +fi + +. .venv/bin/activate +python -V +pip install -q -r requirements.txt + +mkdir -p configs + +[ -f ./vars ] && . ./vars + +export PDNS=${PDNS:-${PWD}/../pdns/pdns_server} +export PDNSUTIL=${PDNSUTIL:-${PWD}/../pdns/pdnsutil} +export PDNSRECURSOR=${PDNSRECURSOR:-${PWD}/../pdns/recursordist/pdns_recursor} +export RECCONTROL=${RECCONTROL:-${PWD}/../pdns/recursordist/rec_control} + +export PREFIX=127.0.0 + +for bin in "$PDNS" "$PDNSUTIL" "$PDNSRECURSOR" "$RECCONTROL"; do + if [ -n "$bin" -a ! -e "$bin" ]; then + echo "E: Required binary $bin not found. Please install the binary and/or edit ./vars." + exit 1 + fi +done + +set -e +if [ "${PDNS_DEBUG}" = "YES" ]; then + set -x +fi + +nosetests --with-xunit $@ diff --git a/regression-tests.auth-py/test_LuaRecords.py b/regression-tests.auth-py/test_LuaRecords.py new file mode 100644 index 0000000000..e9ffdb7017 --- /dev/null +++ b/regression-tests.auth-py/test_LuaRecords.py @@ -0,0 +1,459 @@ +#!/usr/bin/env python +import unittest +import requests +import threading +import dns +import time + +from authtests import AuthTest + +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + +class FakeHTTPServer(BaseHTTPRequestHandler): + def _set_headers(self): + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + + def do_GET(self): + self._set_headers() + if (self.path == '/ping.json'): + self.wfile.write('{"ping":"pong"}') + else: + self.wfile.write("

hi!

Programming in Lua !

") + + def log_message(self, format, *args): + return + + def do_HEAD(self): + self._set_headers() + +class TestLuaRecords(AuthTest): + """ + * ifurlup supports multiple groups of IP addresses, why not ifportup ? + + * pickrandom() can be used with a set of IPs or CNAMES whereas pickwrandom cannot + maybe unifying this would be nice + Note: there is a comment about that "In C++ this is ComboAddress in, + ComboAddress out. In Lua, vector string in, string out" + + * first query to a ifportup/ifurlup looks like returning all records + + * ifurlup with a different port ? + + TODO + ---- + * [x] test pickrandom() + * [x] test pickwrandom() + * [x] test pickwhashed() + * [x] test ifportup() + * [ ] test ifportup() with other selectors + * [x] test ifurlup() + * [x] test latlon() + * [x] test latlonloc() + * [x] test netmask() + + * [ ] test pickclosest() + * [ ] test country() + * [ ] test continent() + * [ ] test closestMagic() + * [x] test view() + * [ ] test asnum() + * [x] rename pickwhashed() and pickwrandom() ? + * [x] unify pickrandom() pickwhashed() and pickwrandom() parameters (ComboAddress vs string) + * [x] make lua errors SERVFAIL + * [ ] Feature Request: allow both list of ips and string as argument of `pick*()` to return multiple records + * [ ] What to do with cases like "LUA AAAA pickrandom('::1', '127.0.0.1')" that will fail only if "127.0.0.1" is returned ? + * [ ] ifurlup supports multiple groups of IP addresses, why not ifportup ? (ie: "{{ip1g1, ip2g1}, {ip1g2}}" vs "{ip1, ip2, ip3}") + """ + _zones = { + 'example.org': """ +example.org. 3600 IN SOA {soa} +example.org. 3600 IN NS ns1.example.org. +example.org. 3600 IN NS ns2.example.org. +ns1.example.org. 3600 IN A {prefix}.10 +ns2.example.org. 3600 IN A {prefix}.11 + +web1.example.org. 3600 IN A {prefix}.101 +web2.example.org. 3600 IN A {prefix}.102 +web3.example.org. 3600 IN A {prefix}.103 + +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'}})" +none.ifportup 3600 IN LUA A "ifportup(8080, {{'192.168.42.21', '192.168.21.42'}})" + +whashed.example.org. 3600 IN LUA A "pickwhashed({{ {{15, '1.2.3.4'}}, {{42, '4.3.2.1'}} }})" +rand.example.org. 3600 IN LUA A "pickrandom({{'{prefix}.101', '{prefix}.102'}})" +v6-bogus.rand.example.org. 3600 IN LUA AAAA "pickrandom({{'{prefix}.101', '{prefix}.102'}})" +v6.rand.example.org. 3600 IN LUA AAAA "pickrandom({{'2001:db8:a0b:12f0::1', 'fe80::2a1:9bff:fe9b:f268'}})" +closest 3600 IN LUA A "pickclosest({{'192.0.2.1','192.0.2.2','{prefix}.102', '198.51.100.1'}})" +empty.rand.example.org. 3600 IN LUA A "pickrandom()" +wrand.example.org. 3600 IN LUA A "pickwrandom({{ {{30, '{prefix}.102'}}, {{15, '{prefix}.103'}} }})" + +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'}} ") + +usa IN LUA A ( ";include('config') " + "return ifurlup('http://www.lua.org:8080/', " + "{{USAips, EUEips}}, 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) ") + +nl IN LUA A ( ";include('config') " + "return ifportup(8081, NLips) ") +latlon.geo IN LUA TXT "latlon()" +latlonloc.geo IN LUA TXT "latlonloc()" + +true.netmask IN LUA TXT ( ";if(netmask({{ '{prefix}.0/24' }})) " + "then return 'true' " + "else return 'false' end " ) +false.netmask IN LUA TXT ( ";if(netmask({{ '1.2.3.4/8' }})) " + "then return 'true' " + "else return 'false' end " ) + +view IN LUA A ("view({{ " + "{{ {{'192.168.0.0/16'}}, {{'192.168.1.54'}}}}," + "{{ {{'{prefix}.0/16'}}, {{'{prefix}.54'}}}}, " + "{{ {{'0.0.0.0/0'}}, {{'192.0.2.1'}}}} " + " }}) " ) +txt.view IN LUA TXT ("view({{ " + "{{ {{'192.168.0.0/16'}}, {{'txt'}}}}, " + "{{ {{'0.0.0.0/0'}}, {{'else'}}}} " + " }}) " ) +none.view IN LUA A ("view({{ " + "{{ {{'192.168.0.0/16'}}, {{'192.168.1.54'}}}}," + "{{ {{'1.2.0.0/16'}}, {{'1.2.3.4'}}}}, " + " }}) " ) + """, + } + _web_rrsets = [] + + @classmethod + def startResponders(cls): + webserver = threading.Thread(name='HTTP Listener', + target=cls.HTTPResponder, + args=[8080] + ) + webserver.setDaemon(True) + webserver.start() + + @classmethod + def HTTPResponder(cls, port): + server_address = ('', port) + httpd = HTTPServer(server_address, FakeHTTPServer) + httpd.serve_forever() + + @classmethod + def setUpClass(cls): + + super(TestLuaRecords, cls).setUpClass() + + cls._web_rrsets = [dns.rrset.from_text('web1.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.101'.format(prefix=cls._PREFIX)), + dns.rrset.from_text('web2.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.102'.format(prefix=cls._PREFIX)), + dns.rrset.from_text('web3.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.103'.format(prefix=cls._PREFIX)) + ] + + def testPickRandom(self): + """ + Basic pickrandom() test with a set of A records + """ + expected = [dns.rrset.from_text('rand.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.101'.format(prefix=self._PREFIX)), + dns.rrset.from_text('rand.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.102'.format(prefix=self._PREFIX))] + query = dns.message.make_query('rand.example.org', 'A') + + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, expected) + + def testBogusV6PickRandom(self): + """ + Test a bogus AAAA pickrandom() record with a set of v4 addr + """ + query = dns.message.make_query('v6-bogus.rand.example.org', 'AAAA') + + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.SERVFAIL) + + def testV6PickRandom(self): + """ + Test pickrandom() AAAA record + """ + expected = [dns.rrset.from_text('v6.rand.example.org.', 0, dns.rdataclass.IN, 'AAAA', + '2001:db8:a0b:12f0::1'), + dns.rrset.from_text('v6.rand.example.org.', 0, dns.rdataclass.IN, 'AAAA', + 'fe80::2a1:9bff:fe9b:f268')] + query = dns.message.make_query('v6.rand.example.org', 'AAAA') + + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, expected) + + def testEmptyRandom(self): + """ + Basic pickrandom() test with an empty set + """ + query = dns.message.make_query('empty.rand.example.org', 'A') + + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.SERVFAIL) + + def testWRandom(self): + """ + Basic pickwrandom() test with a set of A records + """ + expected = [dns.rrset.from_text('wrand.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.103'.format(prefix=self._PREFIX)), + dns.rrset.from_text('wrand.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.102'.format(prefix=self._PREFIX))] + query = dns.message.make_query('wrand.example.org', 'A') + + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, expected) + + @unittest.skip + def testClosest(self): + """ + Basic pickClosest() test with a set of A records + """ + expected = [dns.rrset.from_text('wrand.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.103'.format(prefix=self._PREFIX)), + dns.rrset.from_text('wrand.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.102'.format(prefix=self._PREFIX))] + query = dns.message.make_query('closest.example.org', 'A') + + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, expected) + + def testIfportup(self): + """ + Basic ifportup() test + """ + query = dns.message.make_query('all.ifportup.example.org', 'A') + expected = [ + dns.rrset.from_text('all.ifportup.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.101'.format(prefix=self._PREFIX)), + dns.rrset.from_text('all.ifportup.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.102'.format(prefix=self._PREFIX))] + + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, expected) + + def testIfportupWithSomeDown(self): + """ + Basic ifportup() test with some ports DOWN + """ + query = dns.message.make_query('some.ifportup.example.org', 'A') + expected = [ + dns.rrset.from_text('some.ifportup.example.org.', 0, dns.rdataclass.IN, 'A', + '192.168.42.21'), + dns.rrset.from_text('some.ifportup.example.org.', 0, dns.rdataclass.IN, 'A', + '{prefix}.102'.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) + + # the first IP should not be up so only second shoud be returned + expected = [expected[1]] + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, expected) + + def testIfportupWithAllDown(self): + """ + Basic ifportup() test with all ports DOWN + """ + query = dns.message.make_query('none.ifportup.example.org', 'A') + expected = [ + dns.rrset.from_text('none.ifportup.example.org.', 0, dns.rdataclass.IN, 'A', + '192.168.42.21'), + dns.rrset.from_text('none.ifportup.example.org.', 0, dns.rdataclass.IN, 'A', + '192.168.21.42'.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) + + # no port should be up so we expect any + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, expected) + + def testIfurlup(self): + """ + Basic ifurlup() test + """ + reachable = [ + '{prefix}.103'.format(prefix=self._PREFIX) + ] + unreachable = ['192.168.42.101', '192.168.42.102'] + ips = reachable + unreachable + all_rrs = [] + reachable_rrs = [] + for ip in ips: + rr = dns.rrset.from_text('usa.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.example.org', 'A') + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, all_rrs) + + time.sleep(1) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, reachable_rrs) + + def testIfurlupSimplified(self): + """ + Basic ifurlup() test with the simplified list of ips + Also ensures the correct path is queried + """ + reachable = [ + '{prefix}.101'.format(prefix=self._PREFIX) + ] + unreachable = ['192.168.42.101'] + ips = reachable + unreachable + all_rrs = [] + reachable_rrs = [] + for ip in ips: + rr = dns.rrset.from_text('mix.ifurlup.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('mix.ifurlup.example.org', 'A') + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, all_rrs) + + time.sleep(1) + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(res, reachable_rrs) + + def testLatlon(self): + """ + Basic latlon() test + """ + expected = dns.rrset.from_text('latlon.geo.example.org.', 0, + dns.rdataclass.IN, 'TXT', + '"0.000000 0.000000"') + query = dns.message.make_query('latlon.geo.example.org', 'TXT') + + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, expected) + + def testLatlonloc(self): + """ + Basic latlonloc() test + """ + expected = dns.rrset.from_text('latlonloc.geo.example.org.', 0, + dns.rdataclass.IN, 'TXT', + '"0 0 -0 S 0 0 -0 W 0.00m 1.00m 10000.00m 10.00m"') + query = dns.message.make_query('latlonloc.geo.example.org', 'TXT') + + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, expected) + + def testNetmask(self): + """ + Basic netmask() test + """ + queries = [ + { + 'expected': dns.rrset.from_text('true.netmask.example.org.', 0, + dns.rdataclass.IN, 'TXT', + '"true"'), + 'query': dns.message.make_query('true.netmask.example.org', 'TXT') + }, + { + 'expected': dns.rrset.from_text('false.netmask.example.org.', 0, + dns.rdataclass.IN, 'TXT', + '"false"'), + 'query': dns.message.make_query('false.netmask.example.org', 'TXT') + } + ] + for query in queries : + res = self.sendUDPQuery(query['query']) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, query['expected']) + + def testView(self): + """ + Basic view() test + """ + queries = [ + { + 'expected': dns.rrset.from_text('view.example.org.', 0, + dns.rdataclass.IN, 'A', + '{prefix}.54'.format(prefix=self._PREFIX)), + 'query': dns.message.make_query('view.example.org', 'A') + }, + { + 'expected': dns.rrset.from_text('txt.view.example.org.', 0, + dns.rdataclass.IN, 'TXT', + '"else"'), + 'query': dns.message.make_query('txt.view.example.org', 'TXT') + } + ] + for query in queries : + res = self.sendUDPQuery(query['query']) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, query['expected']) + + def testViewNoMatch(self): + """ + view() test where no netmask match + """ + expected = dns.rrset.from_text('none.view.example.org.', 0, + dns.rdataclass.IN, 'A') + query = dns.message.make_query('none.view.example.org', 'A') + + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.SERVFAIL) + self.assertAnswerEmpty(res) + + def testWHashed(self): + """ + Basic pickwhashed() test with a set of A records + As the `bestwho` is hashed, we should always get the same answer + """ + expected = [dns.rrset.from_text('whashed.example.org.', 0, dns.rdataclass.IN, 'A', '1.2.3.4'), + dns.rrset.from_text('whashed.example.org.', 0, dns.rdataclass.IN, 'A', '4.3.2.1')] + query = dns.message.make_query('whashed.example.org', 'A') + + first = self.sendUDPQuery(query) + self.assertRcodeEqual(first, dns.rcode.NOERROR) + self.assertAnyRRsetInAnswer(first, expected) + for _ in range(5): + res = self.sendUDPQuery(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, first.answer[0]) + +if __name__ == '__main__': + unittest.main() + exit(0)