]> granicus.if.org Git - pdns/commitdiff
rec: Add a new Lua FFI hook, gettag-ffi
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 12 Mar 2018 10:39:45 +0000 (11:39 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 14 Mar 2018 10:01:34 +0000 (11:01 +0100)
pdns/lua-recursor4-ffi.hh [new file with mode: 0644]
pdns/lua-recursor4.cc
pdns/lua-recursor4.hh
pdns/pdns_recursor.cc
pdns/recursordist/Makefile.am
pdns/recursordist/lua-recursor4-ffi.hh [new symlink]

diff --git a/pdns/lua-recursor4-ffi.hh b/pdns/lua-recursor4-ffi.hh
new file mode 100644 (file)
index 0000000..64e5a43
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+extern "C" {
+  typedef struct pdns_ffi_param pdns_ffi_param_t;
+
+  typedef struct pdns_ednsoption {
+    uint16_t    optionCode;
+    uint16_t    len;
+    const void* data;
+  } pdns_ednsoption_t;
+
+  const char* pdns_ffi_param_get_qname(pdns_ffi_param_t* ref);
+  uint16_t pdns_ffi_param_get_qtype(const pdns_ffi_param_t* ref);
+  const char* pdns_ffi_param_get_remote(pdns_ffi_param_t* ref);
+  uint16_t pdns_ffi_param_get_remote_port(const pdns_ffi_param_t* ref);
+  const char* pdns_ffi_param_get_local(pdns_ffi_param_t* ref);
+  uint16_t pdns_ffi_param_get_local_port(const pdns_ffi_param_t* ref);
+  const char* pdns_ffi_param_get_edns_cs(pdns_ffi_param_t* ref);
+  uint8_t pdns_ffi_param_get_edns_cs_source_mask(const pdns_ffi_param_t* ref);
+
+  // returns the length of the resulting 'out' array. 'out' is not set if the length is 0
+  size_t pdns_ffi_param_get_edns_options(pdns_ffi_param_t* ref, const pdns_ednsoption_t** out);
+  size_t pdns_ffi_param_get_edns_options_by_code(pdns_ffi_param_t* ref, uint16_t optionCode, const pdns_ednsoption_t** out);
+
+  void pdns_ffi_param_set_tag(pdns_ffi_param_t* ref, unsigned int tag);
+  void pdns_ffi_param_add_policytag(pdns_ffi_param_t *ref, const char* name);
+  void pdns_ffi_param_set_requestorid(pdns_ffi_param_t* ref, const char* name);
+  void pdns_ffi_param_set_devicename(pdns_ffi_param_t* ref, const char* name);
+  void pdns_ffi_param_set_deviceid(pdns_ffi_param_t* ref, size_t len, const void* name);
+  void pdns_ffi_param_set_variable(pdns_ffi_param_t* ref, bool variable);
+  void pdns_ffi_param_set_ttl_cap(pdns_ffi_param_t* ref, uint32_t ttl);
+}
index e05e80e3267dcb8dbd6b44c5cdaa95cae401da39..5c9a4cbe6323c9f2d8c5ebb173f6da8400d8319d 100644 (file)
@@ -405,6 +405,7 @@ void RecursorLua4::postLoad() {
 
   d_ipfilter = d_lw->readVariable<boost::optional<ipfilter_t>>("ipfilter").get_value_or(0);
   d_gettag = d_lw->readVariable<boost::optional<gettag_t>>("gettag").get_value_or(0);
+  d_gettag_ffi = d_lw->readVariable<boost::optional<gettag_ffi_t>>("gettag_ffi").get_value_or(0);
 }
 
 bool RecursorLua4::prerpz(DNSQuestion& dq, int& ret)
@@ -479,6 +480,50 @@ unsigned int RecursorLua4::gettag(const ComboAddress& remote, const Netmask& edn
   return 0;
 }
 
+struct pdns_ffi_param
+{
+public:
+  pdns_ffi_param(const DNSName& qname_, uint16_t qtype_, const ComboAddress& local_, const ComboAddress& remote_, const Netmask& ednssubnet_, std::vector<std::string>& policyTags_, const std::map<uint16_t, EDNSOptionView>& ednsOptions_, std::string& requestorId_, std::string& deviceId_, uint32_t& ttlCap_, bool& variable_, bool tcp_): qname(qname_), local(local_), remote(remote_), ednssubnet(ednssubnet_), policyTags(policyTags_), ednsOptions(ednsOptions_), requestorId(requestorId_), deviceId(deviceId_), ttlCap(ttlCap_), variable(variable_), qtype(qtype_), tcp(tcp_)
+  {
+  }
+
+  std::unique_ptr<std::string> qnameStr{nullptr};
+  std::unique_ptr<std::string> localStr{nullptr};
+  std::unique_ptr<std::string> remoteStr{nullptr};
+  std::unique_ptr<std::string> ednssubnetStr{nullptr};
+  std::vector<pdns_ednsoption_t> ednsOptionsVect;
+
+  const DNSName& qname;
+  const ComboAddress& local;
+  const ComboAddress& remote;
+  const Netmask& ednssubnet;
+  std::vector<std::string>& policyTags;
+  const std::map<uint16_t, EDNSOptionView>& ednsOptions;
+  std::string& requestorId;
+  std::string& deviceId;
+  uint32_t& ttlCap;
+  bool& variable;
+
+  unsigned int tag{0};
+  uint16_t qtype;
+  bool tcp;
+};
+
+unsigned int RecursorLua4::gettag_ffi(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data, const std::map<uint16_t, EDNSOptionView>& ednsOptions, bool tcp, std::string& requestorId, std::string& deviceId, uint32_t& ttlCap, bool& variable)
+{
+  if (d_gettag_ffi) {
+    pdns_ffi_param_t param(qname, qtype, local, remote, ednssubnet, *policyTags, ednsOptions, requestorId, deviceId, ttlCap, variable, tcp);
+
+    auto ret = d_gettag_ffi(&param);
+    if (ret) {
+      data = *ret;
+    }
+
+    return param.tag;
+  }
+  return 0;
+}
+
 bool RecursorLua4::genhook(luacall_t& func, DNSQuestion& dq, int& ret)
 {
   if(!func)
@@ -538,3 +583,146 @@ loop:;
 }
 
 RecursorLua4::~RecursorLua4(){}
+
+const char* pdns_ffi_param_get_qname(pdns_ffi_param_t* ref)
+{
+  if (!ref->qnameStr) {
+    ref->qnameStr = std::unique_ptr<std::string>(new std::string(ref->qname.toStringNoDot()));
+  }
+
+  return ref->qnameStr->c_str();
+}
+
+uint16_t pdns_ffi_param_get_qtype(const pdns_ffi_param_t* ref)
+{
+  return ref->qtype;
+}
+
+const char* pdns_ffi_param_get_remote(pdns_ffi_param_t* ref)
+{
+  if (!ref->remoteStr) {
+    ref->remoteStr = std::unique_ptr<std::string>(new std::string(ref->remote.toString()));
+  }
+
+  return ref->remoteStr->c_str();
+}
+
+uint16_t pdns_ffi_param_get_remote_port(const pdns_ffi_param_t* ref)
+{
+  return ref->remote.getPort();
+}
+
+const char* pdns_ffi_param_get_local(pdns_ffi_param_t* ref)
+{
+  if (!ref->localStr) {
+    ref->localStr = std::unique_ptr<std::string>(new std::string(ref->local.toString()));
+  }
+
+  return ref->localStr->c_str();
+}
+
+uint16_t pdns_ffi_param_get_local_port(const pdns_ffi_param_t* ref)
+{
+  return ref->local.getPort();
+}
+
+const char* pdns_ffi_param_get_edns_cs(pdns_ffi_param_t* ref)
+{
+  if (ref->ednssubnet.empty()) {
+    return nullptr;
+  }
+
+  if (!ref->ednssubnetStr) {
+    ref->ednssubnetStr = std::unique_ptr<std::string>(new std::string(ref->ednssubnet.toStringNoMask()));
+  }
+
+  return ref->ednssubnetStr->c_str();
+}
+
+uint8_t pdns_ffi_param_get_edns_cs_source_mask(const pdns_ffi_param_t* ref)
+{
+  return ref->ednssubnet.getBits();
+}
+
+static void fill_edns_option(const EDNSOptionView& view, pdns_ednsoption_t& option)
+{
+  option.len = view.size;
+  option.data = nullptr;
+
+  if (view.size > 0) {
+    option.data = view.content;
+  }
+}
+
+size_t pdns_ffi_param_get_edns_options(pdns_ffi_param_t* ref, const pdns_ednsoption_t** out)
+{
+  if (ref->ednsOptions.empty()) {
+    return 0;
+  }
+
+  size_t count = ref->ednsOptions.size();
+  ref->ednsOptionsVect.resize(count);
+
+  size_t pos = 0;
+  for (const auto& entry : ref->ednsOptions) {
+    fill_edns_option(entry.second, ref->ednsOptionsVect.at(pos));
+    ref->ednsOptionsVect.at(pos).optionCode = entry.first;
+    pos++;
+  }
+
+  *out = ref->ednsOptionsVect.data();
+
+  return count;
+}
+
+size_t pdns_ffi_param_get_edns_options_by_code(pdns_ffi_param_t* ref, uint16_t optionCode, const pdns_ednsoption_t** out)
+{
+  const auto& it = ref->ednsOptions.find(optionCode);
+  if (it == ref->ednsOptions.cend()) {
+    return 0;
+  }
+
+  /* the current code deals with only one entry per code, but we will fix that */
+  ref->ednsOptionsVect.resize(1);
+  fill_edns_option(it->second, ref->ednsOptionsVect.at(0));
+  ref->ednsOptionsVect.at(0).optionCode = it->first;
+
+  *out = ref->ednsOptionsVect.data();
+
+  return 1;
+}
+
+void pdns_ffi_param_set_tag(pdns_ffi_param_t* ref, unsigned int tag)
+{
+  ref->tag = tag;
+}
+
+void pdns_ffi_param_add_policytag(pdns_ffi_param_t *ref, const char* name)
+{
+  ref->policyTags.push_back(std::string(name));
+}
+
+void pdns_ffi_param_set_requestorid(pdns_ffi_param_t* ref, const char* name)
+{
+  ref->requestorId = std::string(name);
+}
+
+void pdns_ffi_param_set_devicename(pdns_ffi_param_t* ref, const char* name)
+{
+  ref->deviceId = std::string(name);
+}
+
+void pdns_ffi_param_set_deviceid(pdns_ffi_param_t* ref, size_t len, const void* name)
+{
+  ref->deviceId = std::string(reinterpret_cast<const char*>(name), len);
+}
+
+void pdns_ffi_param_set_variable(pdns_ffi_param_t* ref, bool variable)
+{
+  ref->variable = variable;
+}
+
+void pdns_ffi_param_set_ttl_cap(pdns_ffi_param_t* ref, uint32_t ttl)
+{
+  ref->ttlCap = ttl;
+}
index 44451f5f18adf5ffd60f5b89f371eca0b6f3ffc2..6f630831456f03e28dd4a3e3be3f23881d8ae6c5 100644 (file)
@@ -35,6 +35,8 @@
 #include "lua-base4.hh"
 #include <unordered_map>
 
+#include "lua-recursor4-ffi.hh"
+
 string GenUDPQueryResponse(const ComboAddress& dest, const string& query);
 unsigned int getRecursorThreadId();
 
@@ -96,6 +98,7 @@ public:
   };
 
   unsigned int gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data, const std::map<uint16_t, EDNSOptionView>&, bool tcp, std::string& requestorId, std::string& deviceId);
+  unsigned int gettag_ffi(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data, const std::map<uint16_t, EDNSOptionView>&, bool tcp, std::string& requestorId, std::string& deviceId, uint32_t& ttlCap, bool& variable);
 
   bool prerpz(DNSQuestion& dq, int& ret);
   bool preresolve(DNSQuestion& dq, int& ret);
@@ -117,6 +120,9 @@ public:
 
   typedef std::function<std::tuple<unsigned int,boost::optional<std::unordered_map<int,string> >,boost::optional<LuaContext::LuaObject>,boost::optional<std::string>,boost::optional<std::string> >(ComboAddress, Netmask, ComboAddress, DNSName, uint16_t, const std::map<uint16_t, EDNSOptionView>&, bool)> gettag_t;
   gettag_t d_gettag; // public so you can query if we have this hooked
+  typedef std::function<boost::optional<LuaContext::LuaObject>(pdns_ffi_param_t*)> gettag_ffi_t;
+  gettag_ffi_t d_gettag_ffi;
+
 protected:
   virtual void postPrepareContext() override;
   virtual void postLoad() override;
index 60fc1ccab78fb7bc76aefa28c10e8d2da54ad67b..15486f3e096bc64e8f1efc1b27ea223bccd9013f 100644 (file)
@@ -250,6 +250,8 @@ struct DNSComboWriter {
   vector<pair<uint16_t, string> > d_ednsOpts;
   std::vector<std::string> d_policyTags;
   LuaContext::LuaObject d_data;
+  uint32_t d_ttlCap{std::numeric_limits<uint32_t>::max()};
+  bool d_variable{false};
 };
 
 MT_t* getMT()
@@ -741,9 +743,9 @@ static void handleRPZCustom(const DNSRecord& spoofed, const QType& qtype, SyncRe
   }
 }
 
-static bool addRecordToPacket(DNSPacketWriter& pw, const DNSRecord& rec, uint32_t& minTTL, const uint16_t maxAnswerSize)
+static bool addRecordToPacket(DNSPacketWriter& pw, const DNSRecord& rec, uint32_t& minTTL, uint32_t ttlCap, const uint16_t maxAnswerSize)
 {
-  pw.startRecord(rec.d_name, rec.d_type, rec.d_ttl, rec.d_class, rec.d_place);
+  pw.startRecord(rec.d_name, rec.d_type, (rec.d_ttl > ttlCap ? ttlCap : rec.d_ttl), rec.d_class, rec.d_place);
 
   if(rec.d_type != QType::OPT) // their TTL ain't real
     minTTL = min(minTTL, rec.d_ttl);
@@ -819,7 +821,11 @@ static void startDoResolve(void *p)
     pw.getHeader()->rd=dc->d_mdp.d_header.rd;
     pw.getHeader()->cd=dc->d_mdp.d_header.cd;
 
-    uint32_t minTTL=std::numeric_limits<uint32_t>::max();
+    /* This is the lowest TTL seen in the records of the response,
+       so we can't cache it for longer than this value.
+       If we have a TTL cap, this value can't be larger than the
+       cap no matter what. */
+    uint32_t minTTL = dc->d_ttlCap;
 
     SyncRes sr(dc->d_now);
 
@@ -848,7 +854,7 @@ static void startDoResolve(void *p)
     sr.setQuerySource(dc->d_remote, g_useIncomingECS && !dc->d_ednssubnet.source.empty() ? boost::optional<const EDNSSubnetOpts&>(dc->d_ednssubnet) : boost::none);
 
     bool tracedQuery=false; // we could consider letting Lua know about this too
-    bool variableAnswer = false;
+    bool variableAnswer = dc->d_variable;
     bool shouldNotValidate = false;
 
     /* preresolve expects res (dq.rcode) to be set to RCode::NoError by default */
@@ -1164,7 +1170,7 @@ static void startDoResolve(void *p)
           continue;
         }
 
-        if (!addRecordToPacket(pw, *i, minTTL, maxanswersize)) {
+        if (!addRecordToPacket(pw, *i, minTTL, dc->d_ttlCap, maxanswersize)) {
           needCommit = false;
           break;
         }
@@ -1188,7 +1194,7 @@ static void startDoResolve(void *p)
          OPT record.  This MUST also occur when a truncated response (using
          the DNS header's TC bit) is returned."
       */
-      if (addRecordToPacket(pw, makeOpt(edo.d_packetsize, 0, edo.d_Z), minTTL, maxanswersize)) {
+      if (addRecordToPacket(pw, makeOpt(edo.d_packetsize, 0, edo.d_Z), minTTL, dc->d_ttlCap, maxanswersize)) {
         pw.commit();
       }
     }
@@ -1222,6 +1228,7 @@ static void startDoResolve(void *p)
       }
       if(sendmsg(dc->d_socket, &msgh, 0) < 0 && g_logCommonErrors) 
         L<<Logger::Warning<<"Sending UDP reply to client "<<dc->getRemote()<<" failed with: "<<strerror(errno)<<endl;
+
       if(!SyncRes::s_nopacketcache && !variableAnswer && !sr.wasVariable() ) {
         t_packetCache->insertResponsePacket(dc->d_tag, dc->d_qhash, dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_mdp.d_qclass,
                                             string((const char*)&*packet.begin(), packet.size()),
@@ -1545,7 +1552,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
       }
 #endif
 
-      if(needECS || needXPF || (t_pdl && t_pdl->d_gettag)) {
+      if(needECS || needXPF || (t_pdl && (t_pdl->d_gettag_ffi || t_pdl->d_gettag))) {
 
         try {
           std::map<uint16_t, EDNSOptionView> ednsOptions;
@@ -1556,17 +1563,22 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
                             dc->d_ecsFound, &dc->d_ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr,
                             xpfFound, needXPF ? &dc->d_source : nullptr, needXPF ? &dc->d_destination : nullptr);
 
-          if(t_pdl && t_pdl->d_gettag) {
+          if(t_pdl) {
             try {
-              dc->d_tag = t_pdl->gettag(dc->d_source, dc->d_ednssubnet.source, dc->d_destination, qname, qtype, &dc->d_policyTags, dc->d_data, ednsOptions, true, requestorId, deviceId);
+              if (t_pdl->d_gettag_ffi) {
+                dc->d_tag = t_pdl->gettag_ffi(dc->d_source, dc->d_ednssubnet.source, dc->d_destination, qname, qtype, &dc->d_policyTags, dc->d_data, ednsOptions, true, requestorId, deviceId, dc->d_ttlCap, dc->d_variable);
+              }
+              else if (t_pdl->d_gettag) {
+                dc->d_tag = t_pdl->gettag(dc->d_source, dc->d_ednssubnet.source, dc->d_destination, qname, qtype, &dc->d_policyTags, dc->d_data, ednsOptions, true, requestorId, deviceId);
+              }
             }
-            catch(std::exception& e)  {
+            catch(const std::exception& e)  {
               if(g_logCommonErrors)
                 L<<Logger::Warning<<"Error parsing a query packet qname='"<<qname<<"' for tag determination, setting tag=0: "<<e.what()<<endl;
             }
           }
         }
-        catch(std::exception& e)
+        catch(const std::exception& e)
         {
           if(g_logCommonErrors)
             L<<Logger::Warning<<"Error parsing a query packet for tag determination, setting tag=0: "<<e.what()<<endl;
@@ -1711,6 +1723,8 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
   EDNSSubnetOpts ednssubnet;
   bool ecsFound = false;
   bool ecsParsed = false;
+  uint32_t ttlCap = std::numeric_limits<uint32_t>::max();
+  bool variable = false;
   try {
     DNSName qname;
     uint16_t qtype=0;
@@ -1729,7 +1743,7 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
     */
 #endif
 
-    if(needECS || needXPF || (t_pdl && t_pdl->d_gettag)) {
+    if(needECS || needXPF || (t_pdl && (t_pdl->d_gettag || t_pdl->d_gettag_ffi))) {
       try {
         std::map<uint16_t, EDNSOptionView> ednsOptions;
         bool xpfFound = false;
@@ -1743,17 +1757,22 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
         qnameParsed = true;
         ecsParsed = true;
 
-        if(t_pdl && t_pdl->d_gettag) {
+        if(t_pdl) {
           try {
-            ctag=t_pdl->gettag(source, ednssubnet.source, destination, qname, qtype, &policyTags, data, ednsOptions, false, requestorId, deviceId);
+            if (t_pdl->d_gettag_ffi) {
+              ctag = t_pdl->gettag_ffi(source, ednssubnet.source, destination, qname, qtype, &policyTags, data, ednsOptions, false, requestorId, deviceId, ttlCap, variable);
+            }
+            else if (t_pdl->d_gettag) {
+              ctag = t_pdl->gettag(source, ednssubnet.source, destination, qname, qtype, &policyTags, data, ednsOptions, false, requestorId, deviceId);
+            }
           }
-          catch(std::exception& e)  {
+          catch(const std::exception& e)  {
             if(g_logCommonErrors)
               L<<Logger::Warning<<"Error parsing a query packet qname='"<<qname<<"' for tag determination, setting tag=0: "<<e.what()<<endl;
           }
         }
       }
-      catch(std::exception& e)
+      catch(const std::exception& e)
       {
         if(g_logCommonErrors)
           L<<Logger::Warning<<"Error parsing a query packet for tag determination, setting tag=0: "<<e.what()<<endl;
@@ -1770,6 +1789,9 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
     }
 #endif /* HAVE_PROTOBUF */
 
+    /* It might seem like a good idea to skip the packet cache lookup if we know that the answer is not cacheable,
+       but it means that the hash would not be computed. If some script decides at a later time to mark back the answer
+       as cacheable we would cache it with a wrong tag, so better safe than sorry. */
     if (qnameParsed) {
       cacheHit = (!SyncRes::s_nopacketcache && t_packetCache->getResponsePacket(ctag, question, qname, qtype, qclass, g_now.tv_sec, &response, &age, &qhash, &pbMessage));
     }
@@ -1855,6 +1877,8 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
   dc->d_ecsFound = ecsFound;
   dc->d_ecsParsed = ecsParsed;
   dc->d_ednssubnet = ednssubnet;
+  dc->d_ttlCap = ttlCap;
+  dc->d_variable = variable;
 #ifdef HAVE_PROTOBUF
   if (luaconfsLocal->protobufServer || luaconfsLocal->outgoingProtobufServer) {
     dc->d_uuid = uniqueId;
index 1cce535d896dfcbf3830e435bec598f8d4820559..e9ec0ee57e65341119e76ed5e85ef66611f7d542 100644 (file)
@@ -112,6 +112,7 @@ pdns_recursor_SOURCES = \
        logger.hh logger.cc \
        lua-base4.cc lua-base4.hh \
        lua-recursor4.cc lua-recursor4.hh \
+       lua-recursor4-ffi.hh \
        lwres.cc lwres.hh \
        misc.hh misc.cc \
        mplexer.hh \
diff --git a/pdns/recursordist/lua-recursor4-ffi.hh b/pdns/recursordist/lua-recursor4-ffi.hh
new file mode 120000 (symlink)
index 0000000..5e23460
--- /dev/null
@@ -0,0 +1 @@
+../lua-recursor4-ffi.hh
\ No newline at end of file