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'}) ")
: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.
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.
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
~~~~~~~~~~~~~~~~
expire them too?
pool of UeberBackends?
+
+ Pool checks ?
*/
+using iplist_t = vector<pair<int, string> >;
+using wiplist_t = std::unordered_map<int, string>;
+using ipunitlist_t = vector<pair<int, iplist_t> >;
+using opts_t = std::unordered_map<string,string>;
+
class IsUpOracle
{
private:
- typedef std::unordered_map<string,string> opts_t;
struct CheckDesc
{
ComboAddress rem;
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
{
typedef map<CheckDesc, Checker> 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<std::mutex> 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);
}
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<std::mutex> l(d_mutex);
std::lock_guard<std::mutex> l(d_mutex);
auto iter = d_statuses.find(cd);
if(iter == d_statuses.end()) {
-// L<<Logger::Warning<<"Launching TCP/IP status checker for "<<remote.toStringWithPort()<<endl;
std::thread* checker = new std::thread(&IsUpOracle::checkTCPThread, this, cd.rem, cd.opts);
d_statuses[cd]=Checker{checker, false};
return false;
}
-bool IsUpOracle::isUp(const ComboAddress& remote, opts_t opts)
+bool IsUpOracle::isUp(const ComboAddress& remote, const opts_t& opts)
{
CheckDesc cd{remote, "", opts};
return isUp(cd);
}
-bool IsUpOracle::isUp(const ComboAddress& remote, const std::string& url, std::unordered_map<string,string> opts)
+bool IsUpOracle::isUp(const ComboAddress& remote, const std::string& url, const opts_t& opts)
{
CheckDesc cd{remote, url, opts};
std::lock_guard<std::mutex> l(d_mutex);
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);
}
-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 "<<url<<" at "<<rem.toString()<<endl;
string content;
if(opts.count("source")) {
- ComboAddress src(opts["source"]);
+ ComboAddress src(opts.at("source"));
content=mc.getURL(url, &rem, &src);
}
- else
+ else {
content=mc.getURL(url, &rem);
- if(opts.count("stringmatch") && content.find(opts["stringmatch"]) == string::npos) {
- // cout<<"URL "<<url<<" is up at "<<rem.toString()<<", but could not find stringmatch "<<opts["stringmatch"]<<" in page content, setting DOWN"<<endl;
- setDown(rem, url, opts);
- goto loop;
+ }
+ if(opts.count("stringmatch") && content.find(opts.at("stringmatch")) == string::npos) {
+ throw std::runtime_error(boost::str(boost::format("unable to match content with `%s`") % opts.at("stringmatch")));
}
if(!upStatus(rem,url,opts))
L<<Logger::Warning<<"LUA record monitoring declaring "<<rem.toString()<<" UP for URL "<<url<<"!"<<endl;
L<<Logger::Warning<<"LUA record monitoring declaring "<<rem.toString()<<" DOWN for URL "<<url<<", error: "<<ne.what()<<endl;
setDown(rem,url,opts);
}
- loop:;
sleep(5);
}
}
template<typename T, typename C>
bool doCompare(const T& var, const std::string& res, const C& cmp)
{
- if(auto country = boost::get<string>(&var))
+ if(auto country = boost::get<string>(&var))
return cmp(*country, res);
auto countries=boost::get<vector<pair<int,string> > >(&var);
}
-static ComboAddress wrandom(const vector<pair<int,ComboAddress> >& wips)
+static ComboAddress pickwrandom(const vector<pair<int,ComboAddress> >& wips)
{
int sum=0;
vector<pair<int, ComboAddress> > pick;
return p->second;
}
-static ComboAddress whashed(const ComboAddress& bestwho, vector<pair<int,ComboAddress> >& wips)
+static ComboAddress pickwhashed(const ComboAddress& bestwho, vector<pair<int,ComboAddress> >& wips)
{
int sum=0;
vector<pair<int, ComboAddress> > pick;
int latdeg, latmin, londeg, lonmin;
double latsec, lonsec;
char lathem='X', lonhem='X';
-
+
double lat, lon;
if(!getLatLon(ip, lat, lon))
return false;
>>> print("{}ยบ {}' {}\"".format(deg, min, sec))
*/
-
+
latdeg = lat;
latmin = (lat - latdeg)*60.0;
latsec = (((lat - latdeg)*60.0) - latmin)*60.0;
loc= (fmt % latdeg % latmin % latsec % lathem % londeg % lonmin % lonsec % lonhem ).str();
return true;
}
-
-
-static ComboAddress closest(const ComboAddress& bestwho, const vector<ComboAddress>& wips)
+static ComboAddress pickclosest(const ComboAddress& bestwho, const vector<ComboAddress>& wips)
{
map<double,vector<ComboAddress> > ranked;
double wlat=0, wlon=0;
double latdiff = wlat-lat;
double londiff = wlon-lon;
if(londiff > 180)
- londiff = 360 - londiff;
+ londiff = 360 - londiff;
double dist2=latdiff*latdiff + londiff*londiff;
// cout<<" distance: "<<sqrt(dist2) * 40000.0/360<<" km"<<endl; // length of a degree
ranked[dist2].push_back(c);
if(selector=="random")
return pickrandom(candidates);
- else if(selector=="closest")
- return closest(bestwho, candidates);
+ else if(selector=="pickclosest")
+ return pickclosest(bestwho, candidates);
else if(selector=="hashed")
return hashed(bestwho, candidates);
return pickrandom(candidates);
}
-std::vector<shared_ptr<DNSRecordContent>> luaSynth(const std::string& code, const DNSName& query, const DNSName& zone, int zoneid, const DNSPacket& dnsp, uint16_t qtype)
+static vector<ComboAddress> convIplist(const iplist_t& src)
+{
+ vector<ComboAddress> ret;
+
+ for(const auto& ip : src)
+ ret.emplace_back(ip.second);
+
+ return ret;
+}
+
+static vector<pair<int, ComboAddress> > convWIplist(std::unordered_map<int, wiplist_t > src)
+{
+ vector<pair<int,ComboAddress> > 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<shared_ptr<DNSRecordContent>> luaSynth(const std::string& code, const DNSName& query, const DNSName& zone, int zoneid, const DNSPacket& dnsp, uint16_t qtype)
{
// cerr<<"Called for "<<query<<", in zone "<<zone<<" for type "<<qtype<<endl;
// cerr<<"Code: '"<<code<<"'"<<endl;
-
+
AuthLua4 alua;
//
-
+
std::vector<shared_ptr<DNSRecordContent>> ret;
-
+
LuaContext& lua = *alua.getLua();
lua.writeVariable("qname", query);
lua.writeVariable("who", dnsp.getRemote());
return loc;
});
-
+
lua.writeFunction("closestMagic", [&bestwho,&query](){
vector<ComboAddress> candidates;
for(auto l : query.getRawLabels()) {
break;
}
}
-
- return closest(bestwho, candidates).toString();
+
+ return pickclosest(bestwho, candidates).toString();
});
-
+
lua.writeFunction("latlonMagic", [&query](){
auto labels= query.getRawLabels();
if(labels.size()<4)
return std::to_string(lat)+" "+std::to_string(lon);
});
-
+
lua.writeFunction("createReverse", [&bestwho,&query,&zone](string suffix, boost::optional<std::unordered_map<string,string>> e){
try {
auto labels= query.getRawLabels();
// 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;
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 ) );
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");
});
return std::string("::");
});
-
+
lua.writeFunction("createReverse6", [&bestwho,&query,&zone](string suffix, boost::optional<std::unordered_map<string,string>> e){
vector<ComboAddress> candidates;
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<string> quads;
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) {
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<pair<int, string> >& ips, const boost::optional<std::unordered_map<string,string>> options) {
- vector<ComboAddress> candidates;
- std::unordered_map<string, string> opts;
+ vector<ComboAddress> candidates, unavailables;
+ opts_t opts;
+ vector<ComboAddress > 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<string> ret;
if(candidates.empty()) {
- // cout<<"Everything is down. Returning all of them"<<endl;
- for(const auto& i : ips)
- ret.push_back(i.second);
- }
- else {
- ComboAddress res=useSelector(options, bestwho, candidates);
- ret.push_back(res.toString());
+ // if no IP is available, use selector on the whole set
+ candidates = std::move(unavailables);
}
- return ret;
- });
+ ComboAddress res=useSelector(options, bestwho, candidates);
+ return res.toString();
+ });
lua.writeFunction("ifurlup", [&bestwho](const std::string& url,
- const boost::variant<
- vector<pair<int, string> >,
- vector<pair<int, vector<pair<int, string> > > >
- > & ips, boost::optional<std::unordered_map<string,string>> options) {
+ const boost::variant<iplist_t, ipunitlist_t>& ips,
+ boost::optional<opts_t> options) {
vector<vector<ComboAddress> > candidates;
- std::unordered_map<string,string> opts;
+ opts_t opts;
if(options)
opts = *options;
- if(auto simple = boost::get<vector<pair<int,string>>>(&ips)) {
- vector<ComboAddress> unit;
- for(const auto& i : *simple) {
- ComboAddress rem(i.second, 80);
- unit.push_back(rem);
- }
+ if(auto simple = boost::get<iplist_t>(&ips)) {
+ vector<ComboAddress> unit = convIplist(*simple);
candidates.push_back(unit);
} else {
- auto units = boost::get<vector<pair<int, vector<pair<int, string> > > >>(ips);
+ auto units = boost::get<ipunitlist_t>(ips);
for(const auto& u : units) {
- vector<ComboAddress> unit;
- for(const auto& c : u.second) {
- ComboAddress rem(c.second, 80);
- unit.push_back(rem);
- }
+ vector<ComboAddress> unit = convIplist(u.second);
candidates.push_back(unit);
}
}
- //
- // cout<<"Have "<<candidates.size()<<" units of IP addresses: "<<endl;
- vector<string> ret;
for(const auto& unit : candidates) {
vector<ComboAddress> 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"<<endl;
- continue;
+ }
}
- ComboAddress res=useSelector(options, bestwho, available);
- ret.push_back(res.toString());
- return ret;
- }
- // cerr<<"ALL units are down, returning all IP addresses"<<endl;
+ if(!available.empty()) {
+ ComboAddress res=useSelector(options, bestwho, available);
+
+ return res.toString();
+ }
+ }
+
+ // All units down, return a single, random record
+ vector<ComboAddress> 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<pair<int, string> >& 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<ComboAddress > 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<int, std::unordered_map<int, string> > ips) {
- vector<pair<int,ComboAddress> > 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<int, wiplist_t> ips) {
+ vector<pair<int,ComboAddress> > conv = convWIplist(ips);
+
+ return pickwrandom(conv).toString();
});
- lua.writeFunction("whashed", [&bestwho](std::unordered_map<int, std::unordered_map<int, string> > 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<int, wiplist_t > ips) {
vector<pair<int,ComboAddress> > 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<int, string> ips) {
- vector<ComboAddress > conv;
- for(auto& i : ips)
- conv.emplace_back(i.second);
-
- return closest(bestwho, conv).toString();
-
+ lua.writeFunction("pickclosest", [&bestwho](const iplist_t& ips) {
+ vector<ComboAddress > conv = convIplist(ips);
+
+ return pickclosest(bestwho, conv).toString();
+
});
-
+
int counter=0;
lua.writeFunction("report", [&counter](string event, boost::optional<string> line){
throw std::runtime_error("Script took too long");
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<pair<int,string>>& ips) {
+ lua.writeFunction("netmask", [bestwho](const iplist_t& ips) {
for(const auto& i :ips) {
Netmask nm(i.second);
if(nm.match(bestwho))
/* {
{
- {'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<pair<int, vector<pair<int, vector<pair<int, string> > > > > >& in) {
+ */
+ lua.writeFunction("view", [bestwho](const vector<pair<int, vector<pair<int, iplist_t> > > >& in) {
for(const auto& rule : in) {
const auto& netmasks=rule.second[0].second;
const auto& destinations=rule.second[1].second;
return std::string();
}
);
-
-
+
+
lua.writeFunction("include", [&lua,zone,zoneid](string record) {
try {
vector<DNSZoneRecord> drs = lookup(DNSName(record) +zone, QType::LUA, zoneid);
}
});
-
try {
string actual;
if(!code.empty() && code[0]!=';')
else
for(const auto& c : boost::get<vector<pair<int,string>>>(content))
contents.push_back(c.second);
-
+
for(const auto& content: contents) {
if(qtype==QType::TXT)
ret.push_back(std::shared_ptr<DNSRecordContent>(DNSRecordContent::mastermake(qtype, 1, '"'+content+'"' )));
else
ret.push_back(std::shared_ptr<DNSRecordContent>(DNSRecordContent::mastermake(qtype, 1, content )));
}
- }catch(std::exception &e) {
+ } catch(std::exception &e) {
L<<Logger::Error<<"Lua record reported: "<<e.what()<<endl;
+ throw ;
}
return ret;
void MiniCurl::setupURL(const std::string& str, const ComboAddress* rem, const ComboAddress* src)
{
if(rem) {
- struct curl_slist *hostlist = NULL; // THIS SHOULD BE FREED
+ struct curl_slist *hostlist = nullptr; // THIS SHOULD BE FREED
// url = http://hostname.enzo/url
-
string host4=extractHostFromURL(str);
- string hcode=(host4+":80:"+rem->toString());
- //cout<<"Setting hardcoded IP: "<<hcode<<endl;
- hostlist = curl_slist_append(NULL, hcode.c_str());
- hcode=(host4+":443:"+rem->toString());
- // cout<<"Setting hardcoded IP: "<<hcode<<endl;;
- hostlist = curl_slist_append(hostlist, hcode.c_str());
+ // doest the host contain port indication
+ std::size_t found = host4.find(':');
+ vector<uint16_t> 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);
}
if(rec->d_type == QType::CNAME || rec->d_type == p->qtype.getCode()) {
// noCache=true;
DLOG(L<<"Executing Lua: '"<<rec->getCode()<<"'"<<endl);
- 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);
+ 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) {
+ ;
}
}
}
auto rec=getRR<LUARecordContent>(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;
}
}
}
--- /dev/null
+/*.pyc
+/*.xml
+/.venv
+/configs
+/vars
--- /dev/null
+#!/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())
--- /dev/null
+[MESSAGES CONTROL]
+disable=invalid-name, missing-docstring, line-too-long, superfluous-parens
--- /dev/null
+dnspython>=1.11
+nose>=1.3.7
+Twisted>0.15.0
--- /dev/null
+#!/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 $@
--- /dev/null
+#!/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("<html><body><h1>hi!</h1><h2>Programming in Lua !</h2></body></html>")
+
+ 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)