]> granicus.if.org Git - pdns/commitdiff
luarec: add basic tests
authorCharles-Henri Bruyand <charles-henri.bruyand@open-xchange.com>
Thu, 5 Apr 2018 12:20:21 +0000 (14:20 +0200)
committerCharles-Henri Bruyand <charles-henri.bruyand@open-xchange.com>
Fri, 13 Apr 2018 17:52:21 +0000 (19:52 +0200)
docs/lua-records.rst
pdns/lua-record.cc
pdns/minicurl.cc
pdns/packethandler.cc
regression-tests.auth-py/.gitignore [new file with mode: 0644]
regression-tests.auth-py/authtests.py [new file with mode: 0644]
regression-tests.auth-py/pylintrc [new file with mode: 0644]
regression-tests.auth-py/requirements.txt [new file with mode: 0644]
regression-tests.auth-py/runtests [new file with mode: 0755]
regression-tests.auth-py/test_LuaRecords.py [new file with mode: 0644]

index cc92b6044a8b18e34e53be60ddd195d8acb1ca2c..f2e1f241155cbdb6bd6c844ba22153fd3353aff4 100644 (file)
@@ -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
 ~~~~~~~~~~~~~~~~
index 1b8870dd26a0bb01f1655f9c0eddaa5daff7d3dc..8725cf8cee7a6c44df701baeb5c94305a239c4a5 100644 (file)
       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;
@@ -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<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);
   }
 
@@ -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<std::mutex> l(d_mutex);
@@ -117,7 +124,6 @@ bool IsUpOracle::isUp(const CheckDesc& cd)
   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;
@@ -126,13 +132,13 @@ bool IsUpOracle::isUp(const CheckDesc& cd)
 
 }
 
-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);
@@ -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 "<<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;
@@ -208,7 +212,6 @@ void IsUpOracle::checkURLThread(ComboAddress rem, std::string url, opts_t opts)
         L<<Logger::Warning<<"LUA record monitoring declaring "<<rem.toString()<<" DOWN for URL "<<url<<", error: "<<ne.what()<<endl;
       setDown(rem,url,opts);
     }
-  loop:;
     sleep(5);
   }
 }
@@ -219,7 +222,7 @@ namespace {
 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);
@@ -259,7 +262,7 @@ static ComboAddress hashed(const ComboAddress& who, const vector<ComboAddress>&
 }
 
 
-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;
@@ -272,7 +275,7 @@ static ComboAddress wrandom(const vector<pair<int,ComboAddress> >& wips)
   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;
@@ -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<ComboAddress>& wips)
+static ComboAddress pickclosest(const ComboAddress& bestwho, const vector<ComboAddress>& wips)
 {
   map<double,vector<ComboAddress> > ranked;
   double wlat=0, wlon=0;
@@ -364,7 +365,7 @@ static ComboAddress closest(const ComboAddress& bestwho, const vector<ComboAddre
     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);
@@ -396,8 +397,8 @@ static ComboAddress useSelector(const boost::optional<std::unordered_map<string,
 
   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);
 
@@ -405,16 +406,36 @@ static ComboAddress useSelector(const boost::optional<std::unordered_map<string,
   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());
@@ -445,7 +466,7 @@ std::vector<shared_ptr<DNSRecordContent>> luaSynth(const std::string& code, cons
       return loc;
   });
 
-   
+
   lua.writeFunction("closestMagic", [&bestwho,&query](){
       vector<ComboAddress> candidates;
       for(auto l : query.getRawLabels()) {
@@ -457,10 +478,10 @@ std::vector<shared_ptr<DNSRecordContent>> 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<shared_ptr<DNSRecordContent>> 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<std::unordered_map<string,string>> e){
       try {
       auto labels= query.getRawLabels();
@@ -482,7 +503,7 @@ std::vector<shared_ptr<DNSRecordContent>> 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<shared_ptr<DNSRecordContent>> 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<shared_ptr<DNSRecordContent>> 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<shared_ptr<DNSRecordContent>> luaSynth(const std::string& code, cons
       return std::string("::");
     });
 
-  
+
   lua.writeFunction("createReverse6", [&bestwho,&query,&zone](string suffix, boost::optional<std::unordered_map<string,string>> e){
       vector<ComboAddress> candidates;
 
@@ -562,7 +583,7 @@ std::vector<shared_ptr<DNSRecordContent>> 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<string> quads;
@@ -586,17 +607,17 @@ std::vector<shared_ptr<DNSRecordContent>> 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<shared_ptr<DNSRecordContent>> 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<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");
@@ -753,16 +780,16 @@ std::vector<shared_ptr<DNSRecordContent>> 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<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))
@@ -773,15 +800,15 @@ std::vector<shared_ptr<DNSRecordContent>> 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<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;
@@ -795,8 +822,8 @@ std::vector<shared_ptr<DNSRecordContent>> luaSynth(const std::string& code, cons
       return std::string();
     }
     );
-  
-  
+
+
   lua.writeFunction("include", [&lua,zone,zoneid](string record) {
       try {
         vector<DNSZoneRecord> drs = lookup(DNSName(record) +zone, QType::LUA, zoneid);
@@ -810,7 +837,6 @@ std::vector<shared_ptr<DNSRecordContent>> luaSynth(const std::string& code, cons
       }
     });
 
-  
   try {
     string actual;
     if(!code.empty() && code[0]!=';')
@@ -826,15 +852,16 @@ std::vector<shared_ptr<DNSRecordContent>> luaSynth(const std::string& code, cons
     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;
index 99b6dfe40a306b40324fc9172c8160fd334f3b07..034957783def2573f7a62b8b7cc3b000d222856c 100644 (file)
@@ -36,17 +36,25 @@ static string extractHostFromURL(const std::string& url)
 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);
   }
index c374e2833ff657cd4a5049b149b40ae3f33bff35..4388c9a025e9adfaf9b7ce49dc80252de3e32d8a 100644 (file)
@@ -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: '"<<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) {
+            ;
           }
         }
       }
@@ -1336,20 +1341,26 @@ DNSPacket *PacketHandler::doQuestion(DNSPacket *p)
         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;
           }
         }
       }
diff --git a/regression-tests.auth-py/.gitignore b/regression-tests.auth-py/.gitignore
new file mode 100644 (file)
index 0000000..7103d74
--- /dev/null
@@ -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 (file)
index 0000000..9e55a32
--- /dev/null
@@ -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 (file)
index 0000000..c5f98b1
--- /dev/null
@@ -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 (file)
index 0000000..67690fb
--- /dev/null
@@ -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 (executable)
index 0000000..6c3fb67
--- /dev/null
@@ -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 (file)
index 0000000..e9ffdb7
--- /dev/null
@@ -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("<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)