]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add spoofRawAction() to craft answers from raw bytes 8722/head
authorRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 14 Nov 2019 14:51:23 +0000 (15:51 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 20 Jan 2020 16:40:19 +0000 (17:40 +0100)
pdns/dnsdist-console.cc
pdns/dnsdist-lua-actions.cc
pdns/dnsdist-lua-bindings.cc
pdns/dnsdist-lua-vars.cc
pdns/dnsdist-lua.hh
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsdistdist/docs/reference/constants.rst
pdns/dnsdistdist/docs/rules-actions.rst
regression-tests.dnsdist/test_Spoofing.py

index 6c0f78205dac755d1d325aa8fc42a4621b4620f3..8baa664653c68e0f37c1f72ad07bdb43b8f7f971 100644 (file)
@@ -571,6 +571,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "SNMPTrapResponseAction", true, "[reason]", "send an SNMP trap, adding the optional `reason` string as the response description"},
   { "SpoofAction", true, "ip|list of ips [, options]", "forge a response with the specified IPv4 (for an A query) or IPv6 (for an AAAA). If you specify multiple addresses, all that match the query type (A, AAAA or ANY) will get spoofed in" },
   { "SpoofCNAMEAction", true, "cname [, options]", "Forge a response with the specified CNAME value" },
+  { "SpoofRawAction", true, "raw [, options]", "Forge a response with the specified record data as raw bytes" },
   { "SuffixMatchNodeRule", true, "smn[, quiet]", "Matches based on a group of domain suffixes for rapid testing of membership. Pass true as second parameter to prevent listing of all domains matched" },
   { "TagAction", true, "name, value", "set the tag named 'name' to the given value" },
   { "TagResponseAction", true, "name, value", "set the tag named 'name' to the given value" },
index cc714d23d36e1bc31dfe578eed317419f7c9d426..54fe8385906405366e9dc19f63b70ff5a21126bb 100644 (file)
@@ -404,19 +404,24 @@ DNSAction::Action SpoofAction::operator()(DNSQuestion* dq, std::string* ruleresu
 {
   uint16_t qtype = dq->qtype;
   // do we even have a response?
-  if(d_cname.empty() && !std::count_if(d_addrs.begin(), d_addrs.end(), [qtype](const ComboAddress& a)
-                                       {
-                                         return (qtype == QType::ANY || ((a.sin4.sin_family == AF_INET && qtype == QType::A) ||
-                                                                         (a.sin4.sin_family == AF_INET6 && qtype == QType::AAAA)));
-                                       }))
+  if (d_cname.empty() &&
+      d_rawResponse.empty() &&
+      d_types.count(qtype) == 0) {
     return Action::None;
+  }
 
   vector<ComboAddress> addrs;
-  unsigned int totrdatalen=0;
+  unsigned int totrdatalen = 0;
+  uint16_t numberOfRecords = 0;
   if (!d_cname.empty()) {
     qtype = QType::CNAME;
     totrdatalen += d_cname.toDNSString().size();
-  } else {
+    numberOfRecords = 1;
+  } else if (!d_rawResponse.empty()) {
+    totrdatalen += d_rawResponse.size();
+    numberOfRecords = 1;
+  }
+  else {
     for(const auto& addr : d_addrs) {
       if(qtype != QType::ANY && ((addr.sin4.sin_family == AF_INET && qtype != QType::A) ||
                                  (addr.sin4.sin_family == AF_INET6 && qtype != QType::AAAA))) {
@@ -424,6 +429,7 @@ DNSAction::Action SpoofAction::operator()(DNSQuestion* dq, std::string* ruleresu
       }
       totrdatalen += addr.sin4.sin_family == AF_INET ? sizeof(addr.sin4.sin_addr.s_addr) : sizeof(addr.sin6.sin6_addr.s6_addr);
       addrs.push_back(addr);
+      ++numberOfRecords;
     }
   }
 
@@ -433,7 +439,7 @@ DNSAction::Action SpoofAction::operator()(DNSQuestion* dq, std::string* ruleresu
   unsigned int consumed=0;
   DNSName ignore((char*)dq->dh, dq->len, sizeof(dnsheader), false, 0, 0, &consumed);
 
-  if (dq->size < (sizeof(dnsheader) + consumed + 4 + ((d_cname.empty() ? 0 : 1) + addrs.size())*12 /* recordstart */ + totrdatalen)) {
+  if (dq->size < (sizeof(dnsheader) + consumed + 4 + numberOfRecords*12 /* recordstart */ + totrdatalen)) {
     return Action::None;
   }
 
@@ -452,14 +458,21 @@ DNSAction::Action SpoofAction::operator()(DNSQuestion* dq, std::string* ruleresu
   dq->dh->ancount = 0;
   dq->dh->arcount = 0; // for now, forget about your EDNS, we're marching over it
 
-  if(qtype == QType::CNAME) {
-    std::string wireData = d_cname.toDNSString(); // Note! This doesn't do compression!
-    const unsigned char recordstart[]={0xc0, 0x0c,    // compressed name
-                                       0, (unsigned char) qtype,
-                                       0, QClass::IN, // IN
-                                       0, 0, 0, 60,   // TTL
-                                       0, (unsigned char)wireData.length()};
-    static_assert(sizeof(recordstart) == 12, "sizeof(recordstart) must be equal to 12, otherwise the above check is invalid");
+  uint32_t ttl = htonl(d_responseConfig.ttl);
+  unsigned char recordstart[] = {0xc0, 0x0c,    // compressed name
+                                 0, 0,          // QTYPE
+                                 0, QClass::IN,
+                                 0, 0, 0, 0,    // TTL
+                                 0, 0 };        // rdata length
+  static_assert(sizeof(recordstart) == 12, "sizeof(recordstart) must be equal to 12, otherwise the above check is invalid");
+  memcpy(&recordstart[6], &ttl, sizeof(ttl));
+
+  if (qtype == QType::CNAME) {
+    const std::string wireData = d_cname.toDNSString(); // Note! This doesn't do compression!
+    uint16_t rdataLen = htons(wireData.length());
+    qtype = htons(qtype);
+    memcpy(&recordstart[2], &qtype, sizeof(qtype));
+    memcpy(&recordstart[10], &rdataLen, sizeof(rdataLen));
 
     memcpy(dest, recordstart, sizeof(recordstart));
     dest += sizeof(recordstart);
@@ -467,24 +480,33 @@ DNSAction::Action SpoofAction::operator()(DNSQuestion* dq, std::string* ruleresu
     dq->len += wireData.length() + sizeof(recordstart);
     dq->dh->ancount++;
   }
+  else if (!d_rawResponse.empty()) {
+    uint16_t rdataLen = htons(d_rawResponse.size());
+    qtype = htons(qtype);
+    memcpy(&recordstart[2], &qtype, sizeof(qtype));
+    memcpy(&recordstart[10], &rdataLen, sizeof(rdataLen));
+
+    memcpy(dest, recordstart, sizeof(recordstart));
+    dest += sizeof(recordstart);
+    memcpy(dest, d_rawResponse.c_str(), d_rawResponse.size());
+    dq->len += d_rawResponse.size() + sizeof(recordstart);
+    dq->dh->ancount++;
+  }
   else {
     for(const auto& addr : addrs) {
-      unsigned char rdatalen = addr.sin4.sin_family == AF_INET ? sizeof(addr.sin4.sin_addr.s_addr) : sizeof(addr.sin6.sin6_addr.s6_addr);
-      const unsigned char recordstart[]={0xc0, 0x0c,    // compressed name
-                                         0, (unsigned char) (addr.sin4.sin_family == AF_INET ? QType::A : QType::AAAA),
-                                         0, QClass::IN, // IN
-                                         0, 0, 0, 60,   // TTL
-                                         0, rdatalen};
-      static_assert(sizeof(recordstart) == 12, "sizeof(recordstart) must be equal to 12, otherwise the above check is invalid");
+      uint16_t rdataLen = htons(addr.sin4.sin_family == AF_INET ? sizeof(addr.sin4.sin_addr.s_addr) : sizeof(addr.sin6.sin6_addr.s6_addr));
+      qtype = htons(addr.sin4.sin_family == AF_INET ? QType::A : QType::AAAA);
+      memcpy(&recordstart[2], &qtype, sizeof(qtype));
+      memcpy(&recordstart[10], &rdataLen, sizeof(rdataLen));
 
       memcpy(dest, recordstart, sizeof(recordstart));
       dest += sizeof(recordstart);
 
       memcpy(dest,
              addr.sin4.sin_family == AF_INET ? (void*)&addr.sin4.sin_addr.s_addr : (void*)&addr.sin6.sin6_addr.s6_addr,
-             rdatalen);
-      dest += rdatalen;
-      dq->len += rdatalen + sizeof(recordstart);
+             addr.sin4.sin_family == AF_INET ? sizeof(addr.sin4.sin_addr.s_addr) : sizeof(addr.sin6.sin6_addr.s6_addr));
+      dest += (addr.sin4.sin_family == AF_INET ? sizeof(addr.sin4.sin_addr.s_addr) : sizeof(addr.sin6.sin6_addr.s6_addr));
+      dq->len += (addr.sin4.sin_family == AF_INET ? sizeof(addr.sin4.sin_addr.s_addr) : sizeof(addr.sin6.sin6_addr.s6_addr)) + sizeof(recordstart);
       dq->dh->ancount++;
     }
   }
@@ -1250,11 +1272,14 @@ static void addAction(GlobalStateHolder<vector<T> > *someRulActions, const luadn
     });
 }
 
-typedef std::unordered_map<std::string, boost::variant<bool> > responseParams_t;
+typedef std::unordered_map<std::string, boost::variant<bool, uint32_t> > responseParams_t;
 
 static void parseResponseConfig(boost::optional<responseParams_t> vars, ResponseConfig& config)
 {
   if (vars) {
+    if (vars->count("ttl")) {
+      config.ttl = boost::get<uint32_t>((*vars)["ttl"]);
+    }
     if (vars->count("aa")) {
       config.setAA = boost::get<bool>((*vars)["aa"]);
     }
@@ -1398,11 +1423,16 @@ void setupLuaActions()
     });
 
   g_lua.writeFunction("SpoofCNAMEAction", [](const std::string& a, boost::optional<responseParams_t> vars) {
-      auto ret = std::shared_ptr<DNSAction>(new SpoofAction(a));
-      ResponseConfig responseConfig;
-      parseResponseConfig(vars, responseConfig);
+      auto ret = std::shared_ptr<DNSAction>(new SpoofAction(DNSName(a)));
       auto sa = std::dynamic_pointer_cast<SpoofAction>(ret);
-      sa->d_responseConfig = responseConfig;
+      parseResponseConfig(vars, sa->d_responseConfig);
+      return ret;
+    });
+
+  g_lua.writeFunction("SpoofRawAction", [](const std::string& raw, boost::optional<responseParams_t> vars) {
+      auto ret = std::shared_ptr<DNSAction>(new SpoofAction(raw));
+      auto sa = std::dynamic_pointer_cast<SpoofAction>(ret);
+      parseResponseConfig(vars, sa->d_responseConfig);
       return ret;
     });
 
index 2cfb6f61e800cd58800e220f69271dbb6cdca220..06cc42f9da88f566e31916730b729f93a40998cf 100644 (file)
@@ -147,7 +147,7 @@ void setupLuaBindings(bool client)
 
   g_lua.registerFunction<void(dnsheader::*)(bool)>("setAA", [](dnsheader& dh, bool v) {
       dh.aa=v;
-     });
+    });
 
   g_lua.registerFunction<bool(dnsheader::*)()>("getAA", [](dnsheader& dh) {
       return (bool)dh.aa;
index f10c6c3d37ba72890329202404e5bdcdb389494d..b76a05cfee59bb4f8b37788d1cca600416dfbc28 100644 (file)
@@ -31,6 +31,7 @@ void setupLuaVars()
       {"Nxdomain", (int)DNSAction::Action::Nxdomain},
       {"Refused", (int)DNSAction::Action::Refused},
       {"Spoof", (int)DNSAction::Action::Spoof},
+      {"SpoofRaw", (int)DNSAction::Action::SpoofRaw},
       {"Allow", (int)DNSAction::Action::Allow},
       {"HeaderModify", (int)DNSAction::Action::HeaderModify},
       {"Pool", (int)DNSAction::Action::Pool},
index aa33859c8001f47c956ab50137893c1f70af6f90..021e1248195a259ec67a585c48917eb7e6188a16 100644 (file)
@@ -26,6 +26,7 @@ struct ResponseConfig
   boost::optional<bool> setAA{boost::none};
   boost::optional<bool> setAD{boost::none};
   boost::optional<bool> setRA{boost::none};
+  uint32_t ttl{60};
 };
 void setResponseHeadersFromConfig(dnsheader& dh, const ResponseConfig& config);
 
@@ -64,17 +65,40 @@ class SpoofAction : public DNSAction
 public:
   SpoofAction(const vector<ComboAddress>& addrs): d_addrs(addrs)
   {
+    for (const auto& addr : d_addrs) {
+      if (addr.isIPv4()) {
+        d_types.insert(QType::A);
+      }
+      else if (addr.isIPv6()) {
+        d_types.insert(QType::AAAA);
+      }
+    }
+
+    if (!d_addrs.empty()) {
+      d_types.insert(QType::ANY);
+    }
   }
-  SpoofAction(const string& cname): d_cname(cname)
+
+  SpoofAction(const DNSName& cname): d_cname(cname)
   {
   }
+
+  SpoofAction(const std::string& raw): d_rawResponse(raw)
+  {
+  }
+
   DNSAction::Action operator()(DNSQuestion* dq, string* ruleresult) const override;
+
   string toString() const override
   {
     string ret = "spoof in ";
-    if(!d_cname.empty()) {
-      ret+=d_cname.toString()+ " ";
-    } else {
+    if (!d_cname.empty()) {
+      ret += d_cname.toString() + " ";
+    }
+    else if (!d_rawResponse.empty()) {
+      ret += "raw bytes ";
+    }
+    else {
       for(const auto& a : d_addrs)
         ret += a.toString()+" ";
     }
@@ -85,6 +109,8 @@ public:
   ResponseConfig d_responseConfig;
 private:
   std::vector<ComboAddress> d_addrs;
+  std::set<uint16_t> d_types;
+  std::string d_rawResponse;
   DNSName d_cname;
 };
 
index 96af451cf7f52905ac9b89ef937312c923e6f37f..7c584c0e993b32e2a1607192d0bedccf40a815e6 100644 (file)
@@ -1031,34 +1031,41 @@ NumberedServerVector getDownstreamCandidates(const pools_t& pools, const std::st
   return pool->getServers();
 }
 
-static void spoofResponseFromString(DNSQuestion& dq, const string& spoofContent)
+static void spoofResponseFromString(DNSQuestion& dq, const string& spoofContent, bool raw)
 {
   string result;
 
-  std::vector<std::string> addrs;
-  stringtok(addrs, spoofContent, " ,");
+  if (raw) {
+    SpoofAction sa(spoofContent);
+    sa(&dq, &result);
+  }
+  else {
+    std::vector<std::string> addrs;
+    stringtok(addrs, spoofContent, " ,");
 
-  if (addrs.size() == 1) {
-    try {
-      ComboAddress spoofAddr(spoofContent);
-      SpoofAction sa({spoofAddr});
-      sa(&dq, &result);
-    }
-    catch(const PDNSException &e) {
-      SpoofAction sa(spoofContent); // CNAME then
-      sa(&dq, &result);
-    }
-  } else {
-    std::vector<ComboAddress> cas;
-    for (const auto& addr : addrs) {
+    if (addrs.size() == 1) {
       try {
-        cas.push_back(ComboAddress(addr));
+        ComboAddress spoofAddr(spoofContent);
+        SpoofAction sa({spoofAddr});
+        sa(&dq, &result);
       }
-      catch (...) {
+      catch(const PDNSException &e) {
+        DNSName cname(spoofContent);
+        SpoofAction sa(cname); // CNAME then
+        sa(&dq, &result);
       }
+    } else {
+      std::vector<ComboAddress> cas;
+      for (const auto& addr : addrs) {
+        try {
+          cas.push_back(ComboAddress(addr));
+        }
+        catch (...) {
+        }
+      }
+      SpoofAction sa(cas);
+      sa(&dq, &result);
     }
-    SpoofAction sa(cas);
-    sa(&dq, &result);
   }
 }
 
@@ -1092,7 +1099,11 @@ bool processRulesResult(const DNSAction::Action& action, DNSQuestion& dq, std::s
     return true;
     break;
   case DNSAction::Action::Spoof:
-    spoofResponseFromString(dq, ruleresult);
+    spoofResponseFromString(dq, ruleresult, false);
+    return true;
+    break;
+  case DNSAction::Action::SpoofRaw:
+    spoofResponseFromString(dq, ruleresult, true);
     return true;
     break;
   case DNSAction::Action::Truncate:
index 9c65edc8740b24c9abbb6118e483f9316d4df376..791b99a194212f9ebd39e53d2e8b7fc8001969c0 100644 (file)
@@ -132,7 +132,7 @@ struct DNSResponse : DNSQuestion
 class DNSAction
 {
 public:
-  enum class Action { Drop, Nxdomain, Refused, Spoof, Allow, HeaderModify, Pool, Delay, Truncate, ServFail, None, NoOp, NoRecurse };
+  enum class Action { Drop, Nxdomain, Refused, Spoof, Allow, HeaderModify, Pool, Delay, Truncate, ServFail, None, NoOp, NoRecurse, SpoofRaw };
   static std::string typeToString(const Action& action)
   {
     switch(action) {
@@ -144,6 +144,8 @@ public:
       return "Send Refused";
     case Action::Spoof:
       return "Spoof an answer";
+    case Action::SpoofRaw:
+      return "Spoof an answer from raw bytes";
     case Action::Allow:
       return "Allow";
     case Action::HeaderModify:
index 1ead1ec82e0a3235239443ece6da9dd2c9f50c9a..aba032b706af5eafc63d83ad647b6cdb1f7a4ba4 100644 (file)
@@ -102,6 +102,9 @@ These constants represent the section in the DNS Packet.
 DNSAction
 ---------
 
+.. versionchanged:: 1.5.0
+  ``DNSAction.SpoofRaw`` has been added.
+
 These constants represent an Action that can be returned from :func:`LuaAction` functions.
 
  * ``DNSAction.Allow``: let the query pass, skipping other rules
@@ -115,6 +118,7 @@ These constants represent an Action that can be returned from :func:`LuaAction`
  * ``DNSAction.Refused``: return a response with a Refused rcode
  * ``DNSAction.ServFail``: return a response with a ServFail rcode
  * ``DNSAction.Spoof``: spoof the response using the supplied IPv4 (A), IPv6 (AAAA) or string (CNAME) value
+ * ``DNSAction.SpoofRaw``: spoof the response using the supplied raw value as record data
  * ``DNSAction.Truncate``: truncate the response
  * ``DNSAction.NoRecurse``: set rd=0 on the query
 
index 1d698b69d5fc59466b646ebcc9fd8d1c0abc5658..d1e18c5703ab64f609d1c029b890410d577f078a 100644 (file)
@@ -1223,6 +1223,7 @@ The following actions exist.
   * ``aa``: bool - Set the AA bit to this value (true means the bit is set, false means it's cleared). Default is to clear it.
   * ``ad``: bool - Set the AD bit to this value (true means the bit is set, false means it's cleared). Default is to clear it.
   * ``ra``: bool - Set the RA bit to this value (true means the bit is set, false means it's cleared). Default is to copy the value of the RD bit from the incoming query.
+  * ``ttl``: int - The TTL of the record.
 
 .. function:: SpoofCNAMEAction(cname [, options])
 
@@ -1239,6 +1240,24 @@ The following actions exist.
   * ``aa``: bool - Set the AA bit to this value (true means the bit is set, false means it's cleared). Default is to clear it.
   * ``ad``: bool - Set the AD bit to this value (true means the bit is set, false means it's cleared). Default is to clear it.
   * ``ra``: bool - Set the RA bit to this value (true means the bit is set, false means it's cleared). Default is to copy the value of the RD bit from the incoming query.
+  * ``ttl``: int - The TTL of the record.
+
+.. function:: SpoofRawAction(rawAnswer [, options])
+
+  .. versionadded:: 1.5.0
+
+  Forge a response with the specified raw bytes as record data.
+  For example, for a TXT record of "aaa" "bbbb": SpoofRawAction("\003aaa\004bbbb")
+
+  :param string rawAnswer: The raw record data
+  :param table options: A table with key: value pairs with options.
+
+  Options:
+
+  * ``aa``: bool - Set the AA bit to this value (true means the bit is set, false means it's cleared). Default is to clear it.
+  * ``ad``: bool - Set the AD bit to this value (true means the bit is set, false means it's cleared). Default is to clear it.
+  * ``ra``: bool - Set the RA bit to this value (true means the bit is set, false means it's cleared). Default is to copy the value of the RD bit from the incoming query.
+  * ``ttl``: int - The TTL of the record.
 
 .. function:: TagAction(name, value)
 
index 2ae03bf8668a178a0b846bdd8c9d5ab3b6102567..fa81ccf263a8c1edb2196914e2fa9a0dfcf7a3c1 100644 (file)
@@ -12,6 +12,9 @@ class TestSpoofingSpoof(DNSDistTest):
     addAction(makeRule("spoofaction-nora.spoofing.tests.powerdns.com."), SpoofAction("192.0.2.1", "2001:DB8::1", {ra=false}))
     addAction(makeRule("cnamespoofaction.spoofing.tests.powerdns.com."), SpoofCNAMEAction("cnameaction.spoofing.tests.powerdns.com."))
     addAction("multispoof.spoofing.tests.powerdns.com", SpoofAction({"192.0.2.1", "192.0.2.2", "2001:DB8::1", "2001:DB8::2"}))
+    addAction(AndRule{makeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.A)}, SpoofRawAction("\\192\\000\\002\\001"))
+    addAction(AndRule{makeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.TXT)}, SpoofRawAction("\\003aaa\\004bbbb\\011ccccccccccc"))
+    addAction(AndRule{makeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.SRV)}, SpoofRawAction("\\000\\000\\000\\000\\255\\255\\003srv\\008powerdns\\003com\\000", { aa=true, ttl=3600 }))
     newServer{address="127.0.0.1:%s"}
     """
 
@@ -195,6 +198,7 @@ class TestSpoofingSpoof(DNSDistTest):
             (_, receivedResponse) = sender(query, response=None, useQueue=False)
             self.assertTrue(receivedResponse)
             self.assertEquals(expectedResponse, receivedResponse)
+            self.assertEquals(receivedResponse.answer[0].ttl, 60)
 
     def testSpoofActionSetAD(self):
         """
@@ -218,6 +222,7 @@ class TestSpoofingSpoof(DNSDistTest):
             (_, receivedResponse) = sender(query, response=None, useQueue=False)
             self.assertTrue(receivedResponse)
             self.assertEquals(expectedResponse, receivedResponse)
+            self.assertEquals(receivedResponse.answer[0].ttl, 60)
 
     def testSpoofActionSetRA(self):
         """
@@ -241,6 +246,7 @@ class TestSpoofingSpoof(DNSDistTest):
             (_, receivedResponse) = sender(query, response=None, useQueue=False)
             self.assertTrue(receivedResponse)
             self.assertEquals(expectedResponse, receivedResponse)
+            self.assertEquals(receivedResponse.answer[0].ttl, 60)
 
     def testSpoofActionSetNoRA(self):
         """
@@ -262,6 +268,71 @@ class TestSpoofingSpoof(DNSDistTest):
             (_, receivedResponse) = sender(query, response=None, useQueue=False)
             self.assertTrue(receivedResponse)
             self.assertEquals(expectedResponse, receivedResponse)
+            self.assertEquals(receivedResponse.answer[0].ttl, 60)
+
+    def testSpoofRawAction(self):
+        """
+        Spoofing: Spoof a response from raw bytes
+        """
+        name = 'raw.spoofing.tests.powerdns.com.'
+
+        # A
+        query = dns.message.make_query(name, 'A', 'IN')
+        query.flags &= ~dns.flags.RD
+        expectedResponse = dns.message.make_response(query)
+        expectedResponse.flags &= ~dns.flags.AA
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        expectedResponse.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertTrue(receivedResponse)
+            self.assertEquals(expectedResponse, receivedResponse)
+            self.assertEquals(receivedResponse.answer[0].ttl, 60)
+
+        # TXT
+        query = dns.message.make_query(name, 'TXT', 'IN')
+        query.flags &= ~dns.flags.RD
+        expectedResponse = dns.message.make_response(query)
+        expectedResponse.flags &= ~dns.flags.AA
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.TXT,
+                                    '"aaa" "bbbb" "ccccccccccc"')
+        expectedResponse.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertTrue(receivedResponse)
+            self.assertEquals(expectedResponse, receivedResponse)
+            self.assertEquals(receivedResponse.answer[0].ttl, 60)
+
+        # SRV
+        query = dns.message.make_query(name, 'SRV', 'IN')
+        query.flags &= ~dns.flags.RD
+        expectedResponse = dns.message.make_response(query)
+        # this one should have the AA flag set
+        expectedResponse.flags |= dns.flags.AA
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.SRV,
+                                    '0 0 65535 srv.powerdns.com.')
+        expectedResponse.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertTrue(receivedResponse)
+            self.assertEquals(expectedResponse, receivedResponse)
+            self.assertEquals(receivedResponse.answer[0].ttl, 3600)
 
 class TestSpoofingLuaSpoof(DNSDistTest):
 
@@ -277,11 +348,29 @@ class TestSpoofingLuaSpoof(DNSDistTest):
                 return DNSAction.None, ""
         end
     end
+
     function spoof2rule(dq)
         return DNSAction.Spoof, "spoofedcname.spoofing.tests.powerdns.com."
     end
+
+    addAction(AndRule{makeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.TXT)}, SpoofRawAction("\\003aaa\\004bbbb\\011ccccccccccc"))
+    addAction(AndRule{makeRule("raw.spoofing.tests.powerdns.com"), QTypeRule(DNSQType.SRV)}, SpoofRawAction("\\000\\000\\000\\000\\255\\255\\003srv\\008powerdns\\003com\\000", { aa=true, ttl=3600 }))
+
+    function spoofrawrule(dq)
+        if dq.qtype == DNSQType.A then
+             return DNSAction.SpoofRaw, "\\192\\000\\002\\001"
+        elseif dq.qtype == DNSQType.TXT then
+             return DNSAction.SpoofRaw, "\\003aaa\\004bbbb\\011ccccccccccc"
+        elseif dq.qtype == DNSQType.SRV then
+            dq.dh:setAA(true)
+            return DNSAction.SpoofRaw, "\\000\\000\\000\\000\\255\\255\\003srv\\008powerdns\\003com\\000"
+        end
+        return DNSAction.None, ""
+    end
+
     addAction("luaspoof1.spoofing.tests.powerdns.com.", LuaAction(spoof1rule))
     addAction("luaspoof2.spoofing.tests.powerdns.com.", LuaAction(spoof2rule))
+    addAction("lua-raw.spoofing.tests.powerdns.com.", LuaAction(spoofrawrule))
     newServer{address="127.0.0.1:%s"}
     """
 
@@ -385,6 +474,71 @@ class TestSpoofingLuaSpoof(DNSDistTest):
             self.assertTrue(receivedResponse)
             self.assertEquals(expectedResponse, receivedResponse)
 
+    def testLuaSpoofRawAction(self):
+        """
+        Spoofing: Spoof a response from raw bytes via Lua
+        """
+        name = 'lua-raw.spoofing.tests.powerdns.com.'
+
+        # A
+        query = dns.message.make_query(name, 'A', 'IN')
+        query.flags &= ~dns.flags.RD
+        expectedResponse = dns.message.make_response(query)
+        expectedResponse.flags &= ~dns.flags.AA
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '192.0.2.1')
+        expectedResponse.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertTrue(receivedResponse)
+            self.assertEquals(expectedResponse, receivedResponse)
+            self.assertEquals(receivedResponse.answer[0].ttl, 60)
+
+        # TXT
+        query = dns.message.make_query(name, 'TXT', 'IN')
+        query.flags &= ~dns.flags.RD
+        expectedResponse = dns.message.make_response(query)
+        expectedResponse.flags &= ~dns.flags.AA
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.TXT,
+                                    '"aaa" "bbbb" "ccccccccccc"')
+        expectedResponse.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertTrue(receivedResponse)
+            self.assertEquals(expectedResponse, receivedResponse)
+            self.assertEquals(receivedResponse.answer[0].ttl, 60)
+
+        # SRV
+        query = dns.message.make_query(name, 'SRV', 'IN')
+        query.flags &= ~dns.flags.RD
+        expectedResponse = dns.message.make_response(query)
+        # this one should have the AA flag set
+        expectedResponse.flags |= dns.flags.AA
+        rrset = dns.rrset.from_text(name,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.SRV,
+                                    '0 0 65535 srv.powerdns.com.')
+        expectedResponse.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertTrue(receivedResponse)
+            self.assertEquals(expectedResponse, receivedResponse)
+            # sorry, we can't set the TTL from the Lua API right now
+            #self.assertEquals(receivedResponse.answer[0].ttl, 3600)
+
 class TestSpoofingLuaWithStatistics(DNSDistTest):
 
     _config_template = """