]> granicus.if.org Git - pdns/commitdiff
Merge work-in-progress Lua policy engine.
authorPeter van Dijk <peter.van.dijk@netherlabs.nl>
Fri, 6 Jun 2014 10:43:27 +0000 (12:43 +0200)
committerPeter van Dijk <peter.van.dijk@netherlabs.nl>
Fri, 13 Feb 2015 14:43:16 +0000 (15:43 +0100)
Some text from the Pull Request at the time of merge:

Should not break anything when not used; should not break anything when used
(assuming the loaded script is free of bugs). Example script may not be
entirely correct. Needs tests (dnsperf QPS is a fine KPI).

Run `git show <thiscommit> | grep FIXME` to see known issues.

Todo/evolution ideas:

Copy reload/unload behaviour from recursor (allow reloading different script,
don't replace running instance when loading fails due to syntax errors etc).
Related, make sure we do PASS when the police() call fails.

Add pdns-side metrics (drops/passes/truncates/lua errors) (probably some
actual breakage in the metrics area right now). Log (sample of) lua errors.

Call metrics() periodically (every second) and merge those into our own,
including carbon submission? Perhaps with incremental (number since last read)
vs. absolute flag (number since startup). If absolute, consider
'checkpointing' on script reload.

Call statsline() periodically (every X minutes) for a summary we can log?

Write wrapper (in Lua?) to allow loading policy scripts into recursor using
the hooks already present there (pre/postresolve).

Expose header/extra flags (RD, DO, etc.).

23 files changed:
.travis.yml
docs/WIP/luapolicy.xml [new file with mode: 0644]
pdns/common_startup.cc
pdns/common_startup.hh
pdns/dnspacket.cc
pdns/dnspacket.hh
pdns/dynhandler.cc
pdns/dynhandler.hh
pdns/lua-auth.cc
pdns/lua-auth.hh
pdns/lua-pdns.cc
pdns/lua-pdns.hh
pdns/packethandler.cc
pdns/pdns.conf-dist
pdns/pdns_recursor.cc
pdns/policy-example-rrl.lua [new file with mode: 0644]
pdns/receiver.cc
pdns/tcpreceiver.cc
regression-tests.nobackend/lua-policy/command [new file with mode: 0755]
regression-tests.nobackend/lua-policy/description [new file with mode: 0644]
regression-tests.nobackend/lua-policy/expected_result [new file with mode: 0644]
regression-tests.nobackend/lua-policy/named.conf [new file with mode: 0644]
regression-tests.nobackend/lua-policy/policy.lua [new file with mode: 0644]

index cd3e85f3ec54942d370cf70ae667b367f16ab6ec..4b960292bf460818a0049f997e859f46f3da2fac 100644 (file)
@@ -9,6 +9,7 @@ before_script:
  - sudo rm -f /etc/apt/sources.list.d/travis_ci_zeromq3-source.list
  - sudo apt-get update --quiet --quiet
  - sudo apt-get install --quiet --quiet --no-install-recommends
+     alien
      authbind
      bc
      bind9utils
@@ -36,6 +37,7 @@ before_script:
      p11-kit
      pkg-config
      python-virtualenv
+     rpm
      ruby-json
      ruby-sqlite3
      ruby1.9.1
@@ -52,6 +54,10 @@ before_script:
  - sudo update-alternatives --set ruby /usr/bin/ruby1.9.1
  - sudo touch /etc/authbind/byport/53
  - sudo chmod 755 /etc/authbind/byport/53
+ - wget ftp://ftp.nominum.com/pub/nominum/dnsperf/2.0.0.0/dnsperf-2.0.0.0-1-rhel-6-x86_64.tar.gz
+ - tar xzvf dnsperf-2.0.0.0-1-rhel-6-x86_64.tar.gz
+ - fakeroot alien --to-deb dnsperf-2.0.0.0-1/dnsperf-2.0.0.0-1.el6.x86_64.rpm
+ - sudo dpkg -i dnsperf_2.0.0.0-2_amd64.deb
  - travis_retry gem install bundler --no-rdoc --no-ri
  - cd regression-tests
  - wget http://s3.amazonaws.com/alexa-static/top-1m.csv.zip
diff --git a/docs/WIP/luapolicy.xml b/docs/WIP/luapolicy.xml
new file mode 100644 (file)
index 0000000..b9608e3
--- /dev/null
@@ -0,0 +1,56 @@
+    <sect1 id="lua-policy-engine"><title>Lua Policy Engine</title>
+    <para>
+      Starting with release 3.4.0, the PowerDNS Authoritative Server has support for Lua scripting to
+      make policy decisions. The most common usage for these hooks is to implement RRL or RRL-like policies.
+    </para>
+    <para>
+      In the source tree, <filename>policy-example-rrl.lua</filename> contains an example RRL script
+      that aims to faithfully implement <ulink url="http://ss.vix.su/~vixie/isc-tn-2012-1.txt">Vixie/Schryver's original draft</ulink>. The script demonstrates most aspects of the policy API. More information about the original (in BIND) and other implementations can be found on
+      <ulink url="http://www.redbarn.org/dns/ratelimits">Vixie's RRL page</ulink>.
+    </para>
+    <para>
+      If you set <command>experimental-lua-policy-script</command> in
+      <filename>pdns.conf</filename>, PowerDNS will load one instance of your
+      script, so your global state is exactly that -- global state. PowerDNS
+      will call the <command>police</command> function with three arguments,
+      up to two times per query. Before talking to the database, PowerDNS
+      will call <command>police</command> with <command>req</command> set, but
+      <command>resp</command> will be <command>nil</command>. For queries answered
+      from cache, this step is skipped. Before sending out a response (either from database
+      or from cache), we will call the function with all arguments filled.
+    </para>
+    <para>
+      The prototype for <command>police</command> is:
+<programlisting>
+function police (req, resp, isTcp)
+</programlisting>
+      <command>req</command> is always set. <command>resp</command> is set when we are about to send out
+      a response. <command>isTcp</command> is a boolean that is set to true when the client is on TCP.
+      You could use this to mark clients as 'real and legit'.
+    </para>
+    <para>
+      <command>req</command> and <command>resp</command> are thin wrappers around the PowerDNS
+      <command>DNSPacket</command> object. The following methods are supported, shown with suggested usage and return values/types:
+<programlisting>
+qname, qtype = resp:getQuestion() -- string, number
+remote = resp:getRemote()         -- string (remote IPv4/v6 address)
+wild = resp:getWild()             -- string (see below)
+zone = resp:getZone()             -- string (see below)
+reqsize = req:getSize()           -- number (in bytes)
+respsize = resp:getSize()         -- number (in bytes)
+rcode = resp:getRcode()           -- number
+an, ns, ar = resp:getRRCounts()   -- number, number, number
+</programlisting>
+      <command>getWild()</command> returns the name of the wildcard that was matched by <command>qname</command>,
+      or the empty string if no wildcard was matched. <command>getZone()</command> yields the name of the
+      authoritative zone holding the returned data. <command>getRRCounts()</command> tells you, respectively, the
+      number of ANSWER, AUTHORITY and ADDITIONAL records in the response.
+    </para>
+    <para>
+      <command>police</command> is expected to return <command>pdns.PASS</command> (tells PowerDNS to proceed
+      as normal), <command>pdns.DROP</command> (tells PowerDNS to silently drop the query/response)
+      or <command>pdns.TRUNCATE</command> (tells PowerDNS to send an empty response with TC=1, which should make
+      legitimate clients switch to TCP for this query). Note that TCP queries/responses (with <command>isTcp</command> set to <command>true</command>) ignore this return value from <command>police</command>.
+    </para>
+
+    </sect1>
index 7d69253dd20f85deac601dc93ac6a4f1090a493b..3eefcb29cba81988b8b2a21812db66f7d315470f 100644 (file)
@@ -40,6 +40,7 @@ UDPNameserver *N;
 int avg_latency;
 TCPNameserver *TN;
 vector<DNSDistributor*> g_distributors;
+AuthLua *LPE;
 
 ArgvMap &arg()
 {
@@ -155,7 +156,8 @@ void declareArguments()
   ::arg().set("max-ent-entries", "Maximum number of empty non-terminals in a zone")="100000";
   ::arg().set("entropy-source", "If set, read entropy from this file")="/dev/urandom";
 
-  ::arg().set("lua-prequery-script", "Lua script with prequery handler")="";
+  ::arg().set("lua-prequery-script", "Lua script with prequery handler (DO NOT USE)")="";
+  ::arg().set("experimental-lua-policy-script", "Lua script for the policy engine")="";
 
   ::arg().setSwitch("traceback-handler","Enable the traceback handler (Linux only)")="yes";
   ::arg().setSwitch("direct-dnskey","Fetch DNSKEY RRs from backend during DNSKEY synthesis")="no";
@@ -388,9 +390,20 @@ void *qthread(void *number)
         cached.d.id=P->d.id;
         cached.commitD(); // commit d to the packet                        inlined
 
-        NS->send(&cached);   // answer it then                              inlined
-        diff=P->d_dt.udiff();
-        avg_latency=(int)(0.999*avg_latency+0.001*diff); // 'EWMA'
+        int policyres = PolicyDecision::PASS;
+        if(LPE)
+        {
+          // FIXME: cached does not have qdomainwild/qdomainzone because packetcache entries
+          // go through tostring/noparse
+          policyres = LPE->police(&question, &cached);
+        }
+
+        if (policyres == PolicyDecision::PASS) {
+          NS->send(&cached);   // answer it then                              inlined
+          diff=P->d_dt.udiff();
+          avg_latency=(int)(0.999*avg_latency+0.001*diff); // 'EWMA'
+        }
+        // FIXME implement truncate
 
         continue;
       }
@@ -479,6 +492,11 @@ void mainthread()
   if(::arg().mustDo("slave") || ::arg().mustDo("master"))
     Communicator.go(); 
 
+  if(!::arg()["experimental-lua-policy-script"].empty()){
+    LPE=new AuthLua(::arg()["experimental-lua-policy-script"]);
+    L<<Logger::Warning<<"Loaded Lua policy script "<<::arg()["experimental-lua-policy-script"]<<endl;
+  }
+
   if(TN)
     TN->go(); // tcp nameserver launch
 
index ea15328a81bbd23f6e34978d3e4d2c6bc06f55b9..9300f8edd88ee4af4de3a10588651d1d4eb5f9ba 100644 (file)
@@ -44,6 +44,7 @@ extern CommunicatorClass Communicator;
 extern UDPNameserver *N;
 extern int avg_latency;
 extern TCPNameserver *TN;
+extern AuthLua *LPE;
 extern ArgvMap & arg( void );
 extern void declareArguments();
 extern void declareStats();
index b1b06a97e24bb2cc5f0525080d8c22283a8e6c09..67c0a85cd81cdd3eaeeb5dea5e924d4a757f1e31 100644 (file)
@@ -88,6 +88,8 @@ DNSPacket::DNSPacket(const DNSPacket &orig)
   qtype=orig.qtype;
   qclass=orig.qclass;
   qdomain=orig.qdomain;
+  qdomainwild=orig.qdomainwild;
+  qdomainzone=orig.qdomainzone;
   d_maxreplylen = orig.d_maxreplylen;
   d_ednsping = orig.d_ednsping;
   d_wantsnsid = orig.d_wantsnsid;
@@ -347,6 +349,12 @@ void DNSPacket::wrapup()
     addTSIG(pw, &d_trc, d_tsigkeyname, d_tsigsecret, d_tsigprevious, d_tsigtimersonly);
   
   d_rawpacket.assign((char*)&packet[0], packet.size());
+
+  // copy RR counts so LPE can read them
+  d.qdcount = pw.getHeader()->qdcount;
+  d.ancount = pw.getHeader()->ancount;
+  d.nscount = pw.getHeader()->nscount;
+  d.arcount = pw.getHeader()->arcount;
 }
 
 void DNSPacket::setQuestion(int op, const string &qd, int newqtype)
index f6c2646d2420717fd99f0950990628d6fdd5aef5..84617a6131e975f439d6986126c7ab7e8e552319 100644 (file)
@@ -143,6 +143,8 @@ public:
   QType qtype;  //!< type of the question 8
 
   string qdomain;  //!< qname of the question 4 - unsure how this is used
+  string qdomainwild;  //!< wildcard matched by qname, used by LuaPolicyEngine
+  string qdomainzone;  //!< zone name for the answer (as reflected in SOA for negative responses), used by LuaPolicyEngine
   bool d_tcp;
   bool d_dnssecOk;
   bool d_havetsig;
index 79c5f42895b18d301a8b4b5bed8b5c8f1eb8ceff..cb10990ac89bd9a470e8eae09affda22283bc19e 100644 (file)
@@ -33,6 +33,7 @@
 #include "nameserver.hh"
 #include "responsestats.hh"
 #include "ueberbackend.hh"
+#include "common_startup.hh"
 
 extern ResponseStats g_rs;
 
@@ -379,3 +380,13 @@ string DLListZones(const vector<string>&parts, Utility::pid_t ppid)
 
   return ret.str();
 }
+
+string DLPolicy(const vector<string>&parts, Utility::pid_t ppid)
+{
+  if(LPE) {
+    return LPE->policycmd(parts);
+  }
+  else {
+    return "no policy script loaded";
+  }
+}
index 88d3c3d113150c4f9f69a66565a2f434f7ef74cd..4573b09ac2da2f90ed8f95c6402e4b433ad7bd74 100644 (file)
@@ -56,5 +56,6 @@ string DLPurgeHandler(const vector<string>&parts, Utility::pid_t ppid);
 string DLNotifyRetrieveHandler(const vector<string>&parts, Utility::pid_t ppid);
 string DLCurrentConfigHandler(const vector<string>&parts, Utility::pid_t ppid);
 string DLListZones(const vector<string>&parts, Utility::pid_t ppid);
+string DLPolicy(const vector<string>&parts, Utility::pid_t ppid);
 uint64_t udpErrorStats(const std::string& str);
 #endif /* PDNS_DYNHANDLER_HH */
index 4e977ae30c6cea93fa224ac795f117eda3011133..c85e73e968583a461f50dbf5df71cb2eede8c3e4 100644 (file)
@@ -42,6 +42,7 @@ AuthLua::AuthLua(const std::string &fname)
   : PowerDNSLua(fname)
 {
   registerLuaDNSPacket();
+  pthread_mutex_init(&d_lock,0);
 }
 
 bool AuthLua::axfrfilter(const ComboAddress& remote, const string& zone, const DNSResourceRecord& in, vector<DNSResourceRecord>& out)
@@ -147,6 +148,18 @@ static int ldp_getQuestion(lua_State *L) {
   return 2;
 }
 
+static int ldp_getWild(lua_State *L) {
+  DNSPacket *p=ldp_checkDNSPacket(L);
+  lua_pushstring(L, p->qdomainwild.c_str());
+  return 1;
+}
+
+static int ldp_getZone(lua_State *L) {
+  DNSPacket *p=ldp_checkDNSPacket(L);
+  lua_pushstring(L, p->qdomainzone.c_str());
+  return 1;
+}
+
 static int ldp_addRecords(lua_State *L) {
   DNSPacket *p=ldp_checkDNSPacket(L);
   vector<DNSResourceRecord> rrs;
@@ -163,16 +176,42 @@ static int ldp_getRemote(lua_State *L) {
   return 1;
 }
 
-// these functions are used for PowerDNS recursor regresseion testing against auth. The Lua 5.2 implementation is most likely broken.
-#if LUA_VERSION_NUM < 502
-static const struct luaL_reg ldp_methods [] = {
+static int ldp_getRcode(lua_State *L) {
+  DNSPacket *p=ldp_checkDNSPacket(L);
+  lua_pushnumber(L, p->d.rcode);
+  return 1;
+}
+
+static int ldp_getSize(lua_State *L) {
+  DNSPacket *p=ldp_checkDNSPacket(L);
+  lua_pushnumber(L, p->getString().size());
+  return 1;
+}
+
+static int ldp_getRRCounts(lua_State *L) {
+  DNSPacket *p=ldp_checkDNSPacket(L);
+  lua_pushnumber(L, ntohs(p->d.ancount));
+  lua_pushnumber(L, ntohs(p->d.nscount));
+  lua_pushnumber(L, ntohs(p->d.arcount));
+  return 3;
+}
+
+// these functions are used for PowerDNS recursor regression testing against auth,
+// and for the Lua Policy Engine. The Lua 5.2 implementation is untested.
+static const struct luaL_Reg ldp_methods [] = {
       {"setRcode", ldp_setRcode},
       {"getQuestion", ldp_getQuestion},
+      {"getWild", ldp_getWild},
+      {"getZone", ldp_getZone},
       {"addRecords", ldp_addRecords},
       {"getRemote", ldp_getRemote},
+      {"getSize", ldp_getSize},
+      {"getRRCounts", ldp_getRRCounts},
+      {"getRcode", ldp_getRcode},
       {NULL, NULL}
     };
 
+#if LUA_VERSION_NUM < 502
 void AuthLua::registerLuaDNSPacket(void) {
 
   luaL_newmetatable(d_lua, "LuaDNSPacket");
@@ -186,13 +225,6 @@ void AuthLua::registerLuaDNSPacket(void) {
   lua_pop(d_lua, 1);
 }
 #else
-static const struct luaL_Reg ldp_methods [] = {
-      {"setRcode", ldp_setRcode},
-      {"getQuestion", ldp_getQuestion},
-      {"addRecords", ldp_addRecords},
-      {"getRemote", ldp_getRemote},
-      {NULL, NULL}
-    };
 
 void AuthLua::registerLuaDNSPacket(void) {
 
@@ -251,5 +283,78 @@ DNSPacket* AuthLua::prequery(DNSPacket *p)
   }
 }
 
+int AuthLua::police(DNSPacket *req, DNSPacket *resp, bool isTcp)
+{
+  Lock l(&d_lock);
+
+  lua_getglobal(d_lua,  "police");
+  if(!lua_isfunction(d_lua, -1)) {
+    // cerr<<"No such function 'police'\n"; FIXME: raise Exception? check this beforehand so we can log it once?
+    lua_pop(d_lua, 1);
+    return PolicyDecision::PASS;
+  }
+
+  /* wrap request */
+  LuaDNSPacket* lreq = (LuaDNSPacket *)lua_newuserdata(d_lua, sizeof(LuaDNSPacket));
+  lreq->d_p=req;
+  luaL_getmetatable(d_lua, "LuaDNSPacket");
+  lua_setmetatable(d_lua, -2);
+
+  /* wrap response */
+  if(resp) {
+    LuaDNSPacket* lresp = (LuaDNSPacket *)lua_newuserdata(d_lua, sizeof(LuaDNSPacket));
+    lresp->d_p=resp;
+    luaL_getmetatable(d_lua, "LuaDNSPacket");
+    lua_setmetatable(d_lua, -2);
+  }
+  else
+  {
+    lua_pushnil(d_lua);
+  }
+
+  lua_pushboolean(d_lua, isTcp);
+
+  if(lua_pcall(d_lua, 3, 1, 0)) {
+    string error=string("lua error in police: ")+lua_tostring(d_lua, -1);
+    lua_pop(d_lua, 1);
+    theL()<<Logger::Error<<"police error: "<<error<<endl;
+
+    throw runtime_error(error);
+  }
+
+  int res = (int) lua_tonumber(d_lua, 1);
+  lua_pop(d_lua, 1);
+
+  return res;
+}
+
+string AuthLua::policycmd(const vector<string>&parts) {
+  Lock l(&d_lock);
+
+  lua_getglobal(d_lua, "policycmd");
+  if(!lua_isfunction(d_lua, -1)) {
+    // cerr<<"No such function 'police'\n"; FIXME: raise Exception? check this beforehand so we can log it once?
+    lua_pop(d_lua, 1);
+    return "no policycmd function in policy script";
+  }
+
+  for(int i=1; i<parts.size(); i++)
+    lua_pushstring(d_lua, parts[i].c_str());
+
+  if(lua_pcall(d_lua, parts.size()-1, 1, 0)) {
+    string error = string("lua error in policycmd: ")+lua_tostring(d_lua, -1);
+    lua_pop(d_lua, 1);
+    return error;
+  }
+
+  const char *ret = lua_tostring(d_lua, 1);
+  string rets;
+  if(ret)
+    rets = ret;
+
+  lua_pop(d_lua, 1);
+
+  return rets;
+}
 
 #endif
index 35a0e9069f459676ee9ee604b69c5b9d0ece4870..633dfb831ef221561a7c8908f208991376dcbdb8 100644 (file)
@@ -4,6 +4,7 @@
 #include "iputils.hh"
 #include "dnspacket.hh"
 #include "lua-pdns.hh"
+#include "lock.hh"
 
 class AuthLua : public PowerDNSLua
 {
@@ -12,9 +13,13 @@ public:
   // ~AuthLua();
   bool axfrfilter(const ComboAddress& remote, const string& zone, const DNSResourceRecord& in, vector<DNSResourceRecord>& out);
   DNSPacket* prequery(DNSPacket *p);
+  int police(DNSPacket *req, DNSPacket *resp, bool isTcp=false);
+  string policycmd(const vector<string>&parts);
 
 private:
   void registerLuaDNSPacket(void);
+
+  pthread_mutex_t d_lock;
 };
 
 #endif
index 4dbde69c2a935528b27feaac232eb611bf93ebef..c8bcefb02640216c41d6f2b7b556e128c983e9ba 100644 (file)
@@ -312,10 +312,12 @@ PowerDNSLua::PowerDNSLua(const std::string& fname)
   // set syslog codes used by Logger/enum Urgency
   pushSyslogSecurityLevelTable(d_lua);
   lua_setfield(d_lua, -2, "loglevels");
-  lua_pushnumber(d_lua, RecursorBehaviour::PASS);
+  lua_pushnumber(d_lua, PolicyDecision::PASS);
   lua_setfield(d_lua, -2, "PASS");
-  lua_pushnumber(d_lua, RecursorBehaviour::DROP);
+  lua_pushnumber(d_lua, PolicyDecision::DROP);
   lua_setfield(d_lua, -2, "DROP");
+  lua_pushnumber(d_lua, PolicyDecision::TRUNCATE);
+  lua_setfield(d_lua, -2, "TRUNCATE");
 
   lua_setglobal(d_lua, "pdns");
 
index bdc0ed42cca551fbbdbf685020d7b07b068cf330..beac27762d093954d8c5f7057ec4236ee9ef7772 100644 (file)
@@ -30,8 +30,8 @@ protected: // FIXME?
   bool d_variable;  
   ComboAddress d_local;
 };
-// this enum creates constants to track the pdns_recursor behaviour when returned from the Lua call 
-namespace RecursorBehaviour { enum returnTypes { PASS=-1, DROP=-2 }; };
+// enum for policy decisions, used by both auth and recursor. Not all values supported everywhere.
+namespace PolicyDecision { enum returnTypes { PASS=-1, DROP=-2, TRUNCATE=-3 }; };
 void pushResourceRecordsTable(lua_State* lua, const vector<DNSResourceRecord>& records);
 void popResourceRecordsTable(lua_State *lua, const string &query, vector<DNSResourceRecord>& ret);
 void pushSyslogSecurityLevelTable(lua_State *lua);
index e6441b0d2efa5bb34e2be9b1d1836121ac2f1371..2bae19e7cc08c492e7a4dcc345ebb0494cd5bab6 100644 (file)
@@ -819,6 +819,7 @@ bool validDNSName(const string &name)
 DNSPacket *PacketHandler::question(DNSPacket *p)
 {
   DNSPacket *ret;
+  int policyres = PolicyDecision::PASS;
 
   if(d_pdl)
   {
@@ -827,17 +828,45 @@ DNSPacket *PacketHandler::question(DNSPacket *p)
       return ret;
   }
 
-
   if(p->d.rd) {
     static AtomicCounter &rdqueries=*S.getPointer("rd-queries");  
     rdqueries++;
   }
 
+  if(LPE)
+  {
+    policyres = LPE->police(p, NULL);
+  }
+
+  if (policyres == PolicyDecision::DROP)
+    return NULL;
+
+  if (policyres == PolicyDecision::TRUNCATE) {
+    ret=p->replyPacket();  // generate an empty reply packet
+    ret->d.tc = 1;
+    ret->commitD();
+    return ret;
+  }
+
   bool shouldRecurse=false;
   ret=questionOrRecurse(p, &shouldRecurse);
   if(shouldRecurse) {
     DP->sendPacket(p);
   }
+  if(LPE) {
+    int policyres=LPE->police(p, ret);
+    if(policyres == PolicyDecision::DROP) {
+      delete ret;
+      return NULL;
+    }
+    if (policyres == PolicyDecision::TRUNCATE) {
+      delete ret;
+      ret=p->replyPacket();  // generate an empty reply packet
+      ret->d.tc = 1;
+      ret->commitD();
+    }
+
+  }
   return ret;
 }
 
@@ -1141,6 +1170,9 @@ DNSPacket *PacketHandler::questionOrRecurse(DNSPacket *p, bool *shouldRecurse)
     DLOG(L<<Logger::Error<<"We have authority, zone='"<<sd.qname<<"', id="<<sd.domain_id<<endl);
     authSet.insert(sd.qname); 
 
+    if(!retargetcount) r->qdomainzone=sd.qname;
+
+
     if(pdns_iequals(sd.qname, p->qdomain)) {
       if(p->qtype.getCode() == QType::DNSKEY)
       {
@@ -1263,6 +1295,7 @@ DNSPacket *PacketHandler::questionOrRecurse(DNSPacket *p, bool *shouldRecurse)
       string wildcard;
       if(tryWildcard(p, r, sd, target, wildcard, wereRetargeted, nodata)) {
         if(wereRetargeted) {
+          if(!retargetcount) r->qdomainwild=wildcard;
           retargetcount++;
           goto retargeted;
         }
index 39f7b37aa4f549e9d7b436e986c0a976a084d228..1119dccf8fe9b20aadfb436267b173357a9ddb7c 100644 (file)
 #
 # experimental-logfile=/var/log/pdns.log
 
+#################################
+# experimental-lua-policy-script       Lua script for the policy engine
+#
+# experimental-lua-policy-script=
+
 #################################
 # forward-dnsupdate    A global setting to allow DNS update packages that are for a Slave domain, to be forwarded to the master.
 #
 # loglevel=4
 
 #################################
-# lua-prequery-script  Lua script with prequery handler
+# lua-prequery-script  Lua script with prequery handler (DO NOT USE)
 #
 # lua-prequery-script=
 
index 23fca87bed28ce99777ea8b5a2e8d8217fc15039..72f46837c26f3c8f50ba881fa4a59f4b91dc0946 100644 (file)
@@ -623,13 +623,13 @@ void startDoResolve(void *p)
       }
     }
     
-    if(res == RecursorBehaviour::DROP) {
+    if(res == PolicyDecision::DROP) {
       g_stats.policyDrops++;
       delete dc;
       dc=0;
       return;
     }  
-    if(tracedQuery || res == RecursorBehaviour::PASS || res == RCode::ServFail || pw.getHeader()->rcode == RCode::ServFail)
+    if(tracedQuery || res == PolicyDecision::PASS || res == RCode::ServFail || pw.getHeader()->rcode == RCode::ServFail)
     {
       string trace(sr.getTrace());
       if(!trace.empty()) {
@@ -642,7 +642,7 @@ void startDoResolve(void *p)
       }
     }
     
-    if(res == RecursorBehaviour::PASS) {
+    if(res == PolicyDecision::PASS) {
       pw.getHeader()->rcode=RCode::ServFail;
       // no commit here, because no record
       g_stats.servFails++;
diff --git a/pdns/policy-example-rrl.lua b/pdns/policy-example-rrl.lua
new file mode 100644 (file)
index 0000000..79cb04b
--- /dev/null
@@ -0,0 +1,165 @@
+-- Lua policy engine example
+--
+-- intended to be a faithful implementation of http://ss.vix.su/~vixie/isc-tn-2012-1.txt
+
+conf = {}
+conf.rps = 5
+conf.eps = 5
+conf.logonly = false
+conf.window = 5
+conf.v4len = 24
+conf.v6len = 56
+conf.leakrate = 3
+conf.tcrate = 2
+
+window = {}
+timechanged = false
+
+function getslot (ts)
+       idx = (ts % conf.window) + 1
+       if window[idx]
+       then
+               if window[idx][1] == ts
+               then
+                       return window[idx][2]
+               end
+       end
+
+       newslot = {}
+       window[idx] = {ts, newslot}
+       timechanged = true
+       return newslot
+end
+
+function getwindow ()
+       mywindow = {}
+       now = os.time()
+       for i = now, now-conf.window+1, -1
+       do
+               table.insert(mywindow, getslot(i))
+       end
+
+       return mywindow
+end
+
+function mask (host)
+       -- assumes /24 and ipv4
+       f = host:gmatch('%d+')
+       return f().."."..f().."."..f()
+end
+
+function submit (slot, token)
+       if slot[token]
+       then
+               slot[token] = slot[token] + 1
+       else
+               slot[token] = 1
+       end
+       print("submit: count for "..token.." now "..slot[token])
+end
+
+function count (window, token)
+       total = 0
+       for i,v in ipairs(window)
+       do
+               if v[token]
+               then
+                       total = total + v[token]
+               end
+       end
+
+       return total / conf.window
+end
+
+function police (req, resp, isTcp)
+
+       timechanged = false
+       mywindow = getwindow()
+
+       if resp
+       then
+               qname, qtype = resp:getQuestion()
+               remote = resp:getRemote()
+               wild = resp:getWild()
+               zone = resp:getZone()
+               reqsize = req:getSize()
+               respsize = resp:getSize()
+               rcode = resp:getRcode()
+               print ("< ", qname, qtype, remote, "wild: "..wild, "zone: "..zone, reqsize.."/"..respsize, rcode, isTcp )
+               if isTcp then return pdns.PASS end
+
+               -- mywindow[1][1] = mywindow[1][1]+1
+               -- mywindow[1][2] = mywindow[1][2]+req:getSize()
+               -- mywindow[1][3] = mywindow[1][3]+resp:getSize()
+               an, ns, ar = resp:getRRCounts()
+               imputedname = qname
+               errorstatus = (rcode == pdns.REFUSED or rcode == pdns.FORMERR or rcode == pdns.SERVFAIL or rcode == pdns.NOTIMP)
+
+               if wild:len() > 0
+               then
+                       imputedname = wild
+               elseif rcode == pdns.NXDOMAIN or errorstatus
+               then
+                       imputedname = zone
+               end
+               token = mask(remote).."/"..imputedname.."/"..tostring(errorstatus)
+               submit(mywindow[1], token) -- FIXME: only submit when doing PASS/TRUNCATE?
+               qps = count(mywindow, token)
+               print("qps for token "..token.." is "..qps)
+
+               limit = conf.rps
+               if errorstatus then limit = conf.eps end
+
+               if qps > limit
+               then
+                       print( "considering a drop")
+
+                       -- LEAK-RATE's intention is to give the victim (real owner of spoofed IP)
+                       -- some kind of chance to receive a reply. When the leakrate is set to
+                       -- 5, effectively 1 out of 5 queries probably get an answer. The lucky
+                       -- query has to draw a 1 from our pseudo-random uniformly distributed lottery.
+                       -- Note: the higher leakrate is set, the more queries will be dropped to the floor!
+                       if conf.leakrate > 0 and math.random(conf.leakrate) == 1
+                       then
+                           print ("leaking instead")
+                           return pdns.PASS
+                       end
+                       if conf.tcrate > 0 and math.random(conf.tcrate) == 1
+                       then
+                               print ("truncating instead")
+                               return pdns.TRUNCATE
+                       end
+                       return pdns.DROP
+               end
+               -- token = { mask(resp:getRemote()), }
+       else
+               qname, qtype = req:getQuestion()
+               remote = req:getRemote()
+               print ("> ", qname, qtype, remote)
+               if isTcp then return pdns.PASS end
+       end
+       if timechanged
+       then
+               print("lua memory usage is "..collectgarbage("count"))
+       end
+       -- then
+       --      print("qps stats last", conf.window, "seconds: ")
+       --      for i = 1, conf.window
+       --      do
+       --              print(mywindow[i][1], mywindow[i][2], mywindow[i][3])
+       --      end
+       -- end
+
+       -- print("--")
+       return pdns.PASS
+end
+
+function policycmd(cmd, arg)
+       if cmd ~= "get" then return "unknown command "..cmd end
+
+       mywindow = getwindow()
+       qps = count(mywindow, arg)
+
+       -- return "qps for token "..arg.." is "..qps
+       return qps
+end
index 763ca8f36d96d186c3d23da7e9347eb82539d5cc..9452edf1846b118833f12f62a68f9e5e8893c228 100644 (file)
@@ -556,6 +556,7 @@ int main(int argc, char **argv)
     DynListener::registerFunc("RETRIEVE",&DLNotifyRetrieveHandler, "retrieve slave domain", "<domain>");
     DynListener::registerFunc("CURRENT-CONFIG",&DLCurrentConfigHandler, "retrieve the current configuration");
     DynListener::registerFunc("LIST-ZONES",&DLListZones, "show list of zones", "[master|slave|native]");
+    DynListener::registerFunc("POLICY",&DLPolicy, "interact with policy engine", "[policy command]");
 
     if(!::arg()["tcp-control-address"].empty()) {
       DynListener* dlTCP=new DynListener(ComboAddress(::arg()["tcp-control-address"], ::arg().asNum("tcp-control-port")));
index 6356d343b8432ae146172e7a4ca7285d0784380b..d94142afacba4d4f8e862a5c5cb0991c760215f7 100644 (file)
@@ -44,6 +44,7 @@
 #include "logger.hh"
 #include "arguments.hh"
 
+#include "common_startup.hh"
 #include "packethandler.hh"
 #include "statbag.hh"
 #include "resolver.hh"
@@ -327,8 +328,11 @@ void *TCPNameserver::doConnection(void *data)
         cached->d.rd=packet->d.rd; // copy in recursion desired bit 
         cached->commitD(); // commit d to the packet                        inlined
 
+        if(LPE) LPE->police(&(*packet), &(*cached), true);
+
         sendPacket(cached, fd); // presigned, don't do it again
         S.inc("tcp-answers");
+
         continue;
       }
       if(logDNSQueries)
@@ -343,6 +347,8 @@ void *TCPNameserver::doConnection(void *data)
 
         reply=shared_ptr<DNSPacket>(s_P->questionOrRecurse(packet.get(), &shouldRecurse)); // we really need to ask the backend :-)
 
+        if(LPE) LPE->police(&(*packet), &(*reply), true);
+
         if(shouldRecurse) {
           proxyQuestion(packet);
           continue;
diff --git a/regression-tests.nobackend/lua-policy/command b/regression-tests.nobackend/lua-policy/command
new file mode 100755 (executable)
index 0000000..db4fcfe
--- /dev/null
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+set -e
+set -x
+
+bindwait ()
+{
+       configname=$1
+       domcount=1
+       loopcount=0
+       while [ $loopcount -lt 20 ]; do
+               sleep 1
+               done=$( (../pdns/pdns_control --config-name=$configname --socket-dir=. --no-config bind-domain-status || true) | grep -c 'parsed into memory' || true )
+               if [ $done = $domcount ]
+                       then
+                       return
+               fi
+               let loopcount=loopcount+1
+       done
+       if [ $done != $domcount ]; then
+               echo "Domain parsing failed" >> failed_tests
+       fi
+}
+
+port=5501
+rm -f pdns*.pid
+
+../pdns/pdns_server --daemon=no --local-port=$port --socket-dir=./          \
+       --no-shuffle --launch=bind --bind-config=lua-policy/named.conf   \
+       --experimental-lua-policy-script=lua-policy/policy.lua \
+       --send-root-referral --cache-ttl=60 --no-config --module-dir=../regression-tests/modules &
+bindwait
+
+# plain SOA query
+../pdns/sdig 127.0.0.1 5501 minimal.com SOA | LC_ALL=C sort
+# expect DROP, so timeout
+timeout 3 ../pdns/sdig 127.0.0.1 5501 drop.minimal.com SOA || ret=$?
+echo timeout/sdig return value: $ret
+# expect TRUNCATE
+../pdns/sdig 127.0.0.1 5501 truncate.minimal.com SOA
+
+kill $(cat pdns*.pid)
+rm pdns*.pid
diff --git a/regression-tests.nobackend/lua-policy/description b/regression-tests.nobackend/lua-policy/description
new file mode 100644 (file)
index 0000000..89252d9
--- /dev/null
@@ -0,0 +1 @@
+Test the Lua policy engine.
\ No newline at end of file
diff --git a/regression-tests.nobackend/lua-policy/expected_result b/regression-tests.nobackend/lua-policy/expected_result
new file mode 100644 (file)
index 0000000..547be0e
--- /dev/null
@@ -0,0 +1,9 @@
+policy.lua loaded
+0      minimal.com.    IN      SOA     120     ns1.example.com. ahu.example.com. 2000081501 28800 7200 604800 86400
+Rcode: 0, RD: 0, QR: 1, TC: 0, AA: 1, opcode: 0
+Reply to question for qname='minimal.com.', qtype=SOA
+dropping!
+timeout/sdig return value: 124
+truncating!
+Reply to question for qname='truncate.minimal.com.', qtype=SOA
+Rcode: 0, RD: 0, QR: 1, TC: 1, AA: 1, opcode: 0
diff --git a/regression-tests.nobackend/lua-policy/named.conf b/regression-tests.nobackend/lua-policy/named.conf
new file mode 100644 (file)
index 0000000..e94fe49
--- /dev/null
@@ -0,0 +1,14 @@
+options {
+       directory "../regression-tests/zones/";
+       recursion no;
+       listen-on port 5300 {
+               127.0.0.1;
+       };
+       version "Meow!Meow!";
+       minimal-responses yes;
+};
+
+zone "minimal.com"{
+       type master;
+       file "./minimal.com";
+};
diff --git a/regression-tests.nobackend/lua-policy/policy.lua b/regression-tests.nobackend/lua-policy/policy.lua
new file mode 100644 (file)
index 0000000..8f4b9d5
--- /dev/null
@@ -0,0 +1,10 @@
+print("policy.lua loaded")
+io.flush()
+function police (req, resp, isTcp)
+       qname, qtype = req:getQuestion()
+
+       if qname == 'drop.minimal.com' then print 'dropping!' io.flush() return pdns.DROP end
+       if qname == 'truncate.minimal.com' then print 'truncating!' io.flush() return pdns.TRUNCATE end
+
+       return pdns.PASS
+end