* QType (QTypeRule)
* RegexRule on query name
* RE2Rule on query name (optional)
+ * Response code
* Packet requests DNSSEC processing
* Query received over UDP or TCP
* Opcode (OpcodeRule)
Current response actions are:
+ * Allow (AllowResponseAction)
+ * Delay a response by n milliseconds (DelayResponseAction), over UDP only
+ * Drop (DropResponseAction)
* Log response content to a remote server (RemoteLogResponseAction)
Rules can be added via:
* a QNameLabelsCountRule
* a QNameWireLengthRule
* a QTypeRule
+ * a RCodeRule
* a RegexRule
* a RE2Rule
* a RecordsCountRule
* `QNameLabelsCountRule(min, max)`: matches if the qname has less than `min` or more than `max` labels
* `QNameWireLengthRule(min, max)`: matches if the qname's length on the wire is less than `min` or more than `max` bytes
* `QTypeRule(qtype)`: matches queries with the specified qtype
+ * `RCodeRule(rcode)`: matches queries or responses the specified rcode
* `RegexRule(regex)`: matches the query name against the supplied regex
* `RecordsCountRule(section, minCount, maxCount)`: matches if there is at least `minCount` and at most `maxCount` records in the `section` section
* `RecordsTypeCountRule(section, type, minCount, maxCount)`: matches if there is at least `minCount` and at most `maxCount` records of type `type` in the `section` section
* `topRule()`: move the last rule to the first position
* Built-in Actions for Rules:
* `AllowAction()`: let these packets go through
+ * `AllowResponseAction()`: let these packets go through
* `DelayAction(milliseconds)`: delay the response by the specified amount of milliseconds (UDP-only)
+ * `DelayResponseAction(milliseconds)`: delay the response by the specified amount of milliseconds (UDP-only)
* `DisableValidationAction()`: set the CD bit in the question, let it go through
* `DropAction()`: drop these packets
+ * `DropResponseAction()`: drop these packets
* `LogAction([filename], [binary], [append], [buffered])`: Log a line for each query, to the specified file if any, to the console (require verbose) otherwise. When logging to a file, the `binary` optional parameter specifies whether we log in binary form (default) or in textual form, the `append` optional parameter specifies whether we open the file for appending or truncate each time (default), and the `buffered` optional parameter specifies whether writes to the file are buffered (default) or not.
* `NoRecurseAction()`: strip RD bit from the question, let it go through
* `PoolAction(poolname)`: set the packet into the specified pool
{ "addQPSPoolRule", true, "x, limit, pool", "like `addPoolRule`, but only select at most 'limit' queries/s for this pool, letting the subsequent rules apply otherwise" },
{ "addResponseAction", true, "DNS rule, DNS response action", "add a response rule" },
{ "AllowAction", true, "", "let these packets go through" },
+ { "AllowResponseAction", true, "", "let these packets go through" },
{ "AllRule", true, "", "matches all traffic" },
{ "AndRule", true, "list of DNS rules", "matches if all sub-rules matches" },
{ "benchRule", true, "DNS Rule [, iterations [, suffix]]", "bench the specified DNS rule" },
{ "clearDynBlocks", true, "", "clear all dynamic blocks" },
{ "clearRules", true, "", "remove all current rules" },
{ "DelayAction", true, "milliseconds", "delay the response by the specified amount of milliseconds (UDP-only)" },
+ { "DelayResponseAction", true, "milliseconds", "delay the response by the specified amount of milliseconds (UDP-only)" },
{ "delta", true, "", "shows all commands entered that changed the configuration" },
{ "DisableValidationAction", true, "", "set the CD bit in the question, let it go through" },
{ "DropAction", true, "", "drop these packets" },
+ { "DropResponseAction", true, "", "drop these packets" },
{ "dumpStats", true, "", "print all statistics we gather" },
{ "exceedNXDOMAINs", true, "rate, seconds", "get set of addresses that exceed `rate` NXDOMAIN/s over `seconds` seconds" },
{ "exceedQRate", true, "rate, seconds", "get set of address that exceed `rate` queries/s over `seconds` seconds" },
{ "QNameLabelsCountRule", true, "min, max", "matches if the qname has less than `min` or more than `max` labels" },
{ "QNameWireLengthRule", true, "min, max", "matches if the qname's length on the wire is less than `min` or more than `max` bytes" },
{ "QTypeRule", true, "qtype", "matches queries with the specified qtype" },
+ { "RCodeRule", true, "rcode", "matches responses with the specified rcode" },
{ "setACL", true, "{netmask, netmask}", "replace the ACL set with these netmasks. Use `setACL({})` to reset the list, meaning no one can use us" },
{ "setDNSSECPool", true, "pool name", "move queries requesting DNSSEC processing to this pool" },
{ "setECSOverride", true, "bool", "whether to override an existing EDNS Client Subnet value in the query" },
);
g_lua.writeVariable("DNSResponseAction", std::unordered_map<string,int>{
- {"None",(int)DNSResponseAction::Action::None}}
- );
+ {"Allow", (int)DNSResponseAction::Action::Allow },
+ {"Delay", (int)DNSResponseAction::Action::Delay },
+ {"HeaderModify", (int)DNSResponseAction::Action::HeaderModify },
+ {"None", (int)DNSResponseAction::Action::None }
+ });
g_lua.writeVariable("DNSClass", std::unordered_map<string,int>{
{"IN", QClass::IN },
return std::shared_ptr<DNSRule>(new QNameWireLengthRule(min, max));
});
+ g_lua.writeFunction("RCodeRule", [](int rcode) {
+ return std::shared_ptr<DNSRule>(new RCodeRule(rcode));
+ });
+
g_lua.writeFunction("addAction", [](luadnsrule_t var, std::shared_ptr<DNSAction> ea)
{
setLuaSideEffect();
g_lua.writeFunction("setVerboseHealthChecks", [](bool verbose) { g_verboseHealthChecks=verbose; });
g_lua.writeFunction("setStaleCacheEntriesTTL", [](uint32_t ttl) { g_staleCacheEntriesTTL = ttl; });
+ g_lua.writeFunction("DropResponseAction", []() {
+ return std::shared_ptr<DNSResponseAction>(new DropResponseAction);
+ });
+
+ g_lua.writeFunction("AllowResponseAction", []() {
+ return std::shared_ptr<DNSResponseAction>(new AllowResponseAction);
+ });
+
+ g_lua.writeFunction("DelayResponseAction", [](int msec) {
+ return std::shared_ptr<DNSResponseAction>(new DelayResponseAction(msec));
+ });
+
g_lua.writeFunction("RemoteLogAction", [](std::shared_ptr<RemoteLogger> logger) {
#ifdef HAVE_PROTOBUF
return std::shared_ptr<DNSAction>(new RemoteLogAction(logger));
#ifdef HAVE_PROTOBUF
dr.uniqueId = dq.uniqueId;
#endif
- if (!processResponse(localRespRulactions, dr)) {
+ if (!processResponse(localRespRulactions, dr, &delayMsec)) {
break;
}
#ifdef HAVE_PROTOBUF
dr.uniqueId = ids->uniqueId;
#endif
- if (!processResponse(localRespRulactions, dr)) {
- break;
+ if (!processResponse(localRespRulactions, dr, &ids->delayMsec)) {
+ continue;
}
#ifdef HAVE_DNSCRYPT
return true;
}
-bool processResponse(LocalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSResponseAction> > > >& localRespRulactions, DNSResponse& dr)
+bool processResponse(LocalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSResponseAction> > > >& localRespRulactions, DNSResponse& dr, int* delayMsec)
{
+ DNSResponseAction::Action action=DNSResponseAction::Action::None;
std::string ruleresult;
for(const auto& lr : *localRespRulactions) {
if(lr.first->matches(&dr)) {
lr.first->d_matches++;
- /* for now we only support actions returning None */
- (*lr.second)(&dr, &ruleresult);
+ action=(*lr.second)(&dr, &ruleresult);
+ switch(action) {
+ case DNSResponseAction::Action::Allow:
+ return true;
+ break;
+ case DNSResponseAction::Action::Drop:
+ return false;
+ break;
+ case DNSResponseAction::Action::HeaderModify:
+ return true;
+ break;
+ /* non-terminal actions follow */
+ case DNSResponseAction::Action::Delay:
+ *delayMsec = static_cast<int>(pdns_stou(ruleresult)); // sorry
+ break;
+ case DNSResponseAction::Action::None:
+ break;
+ }
}
}
class DNSResponseAction
{
public:
- enum class Action { None };
+ enum class Action { Allow, Delay, Drop, HeaderModify, None };
virtual Action operator()(DNSResponse*, string* ruleresult) const =0;
virtual string toString() const = 0;
};
bool responseContentMatches(const char* response, const uint16_t responseLen, const DNSName& qname, const uint16_t qtype, const uint16_t qclass, const ComboAddress& remote);
bool processQuery(LocalStateHolder<NetmaskTree<DynBlock> >& localDynBlockNMG,
LocalStateHolder<SuffixMatchTree<DynBlock> >& localDynBlockSMT, LocalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSAction> > > >& localRulactions, blockfilter_t blockFilter, DNSQuestion& dq, string& poolname, int* delayMsec, const struct timespec& now);
-bool processResponse(LocalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSResponseAction> > > >& localRespRulactions, DNSResponse& dr);
+bool processResponse(LocalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSResponseAction> > > >& localRespRulactions, DNSResponse& dr, int* delayMsec);
bool fixUpResponse(char** response, uint16_t* responseLen, size_t* responseSize, const DNSName& qname, uint16_t origFlags, bool ednsAdded, bool ecsAdded, std::vector<uint8_t>& rewrittenResponse, uint16_t addRoom);
void restoreFlags(struct dnsheader* dh, uint16_t origFlags);
size_t d_max;
};
+class RCodeRule : public DNSRule
+{
+public:
+ RCodeRule(int rcode) : d_rcode(rcode)
+ {
+ }
+ bool matches(const DNSQuestion* dq) const override
+ {
+ return d_rcode == dq->dh->rcode;
+ }
+ string toString() const override
+ {
+ return "rcode=="+RCode::to_s(d_rcode);
+ }
+private:
+ int d_rcode;
+};
+
+
class DropAction : public DNSAction
{
public:
private:
std::shared_ptr<RemoteLogger> d_logger;
};
+
+class DropResponseAction : public DNSResponseAction
+{
+public:
+ DNSResponseAction::Action operator()(DNSResponse* dr, string* ruleresult) const override
+ {
+ return Action::Drop;
+ }
+ string toString() const override
+ {
+ return "drop";
+ }
+};
+
+class AllowResponseAction : public DNSResponseAction
+{
+public:
+ DNSResponseAction::Action operator()(DNSResponse* dr, string* ruleresult) const override
+ {
+ return Action::Allow;
+ }
+ string toString() const override
+ {
+ return "allow";
+ }
+};
+
+class DelayResponseAction : public DNSResponseAction
+{
+public:
+ DelayResponseAction(int msec) : d_msec(msec)
+ {}
+ DNSResponseAction::Action operator()(DNSResponse* dr, string* ruleresult) const override
+ {
+ *ruleresult=std::to_string(d_msec);
+ return Action::Delay;
+ }
+ string toString() const override
+ {
+ return "delay by "+std::to_string(d_msec)+ " msec";
+ }
+private:
+ int d_msec;
+};
--- /dev/null
+#!/usr/bin/env python
+from datetime import datetime, timedelta
+import time
+import dns
+from dnsdisttests import DNSDistTest
+
+class TestResponseRuleNXDelayed(DNSDistTest):
+
+ _config_template = """
+ newServer{address="127.0.0.1:%s"}
+ addResponseAction(RCodeRule(dnsdist.NXDOMAIN), DelayResponseAction(1000))
+ """
+
+ def testNXDelayed(self):
+ """
+ Responses: Delayed on NXDomain
+
+ Send an A query to "delayed.responses.tests.powerdns.com.",
+ check that the response delay is longer than 1000 ms
+ for a NXDomain response over UDP, shorter for a NoError one.
+ """
+ name = 'delayed.responses.tests.powerdns.com.'
+ query = dns.message.make_query(name, 'A', 'IN')
+ response = dns.message.make_response(query)
+
+ # NX over UDP
+ response.set_rcode(dns.rcode.NXDOMAIN)
+ begin = datetime.now()
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ end = datetime.now()
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(response, receivedResponse)
+ self.assertTrue((end - begin) > timedelta(0, 1))
+
+ # NoError over UDP
+ response.set_rcode(dns.rcode.NOERROR)
+ begin = datetime.now()
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ end = datetime.now()
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(response, receivedResponse)
+ self.assertTrue((end - begin) < timedelta(0, 1))
+
+ # NX over TCP
+ response.set_rcode(dns.rcode.NXDOMAIN)
+ begin = datetime.now()
+ (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+ end = datetime.now()
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(response, receivedResponse)
+ self.assertTrue((end - begin) < timedelta(0, 1))
+
+class TestResponseRuleQNameDropped(DNSDistTest):
+
+ _config_template = """
+ newServer{address="127.0.0.1:%s"}
+ addResponseAction("drop.responses.tests.powerdns.com.", DropResponseAction())
+ """
+
+ def testDropped(self):
+ """
+ Responses: Dropped on QName
+
+ Send an A query to "drop.responses.tests.powerdns.com.",
+ check that the response (not the query) is dropped.
+ """
+ name = 'drop.responses.tests.powerdns.com.'
+ query = dns.message.make_query(name, 'A', 'IN')
+ response = dns.message.make_response(query)
+
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(receivedResponse, None)
+
+ (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(receivedResponse, None)
+
+ def testNotDropped(self):
+ """
+ Responses: NOT Dropped on QName
+
+ Send an A query to "dontdrop.responses.tests.powerdns.com.",
+ check that the response is not dropped.
+ """
+ name = 'dontdrop.responses.tests.powerdns.com.'
+ query = dns.message.make_query(name, 'A', 'IN')
+ response = dns.message.make_response(query)
+
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(response, receivedResponse)
+
+ (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(response, receivedResponse)
+
+class TestResponseRuleQNameAllowed(DNSDistTest):
+
+ _config_template = """
+ newServer{address="127.0.0.1:%s"}
+ addResponseAction("allow.responses.tests.powerdns.com.", AllowResponseAction())
+ addResponseAction(AllRule(), DropResponseAction())
+ """
+
+ def testAllowed(self):
+ """
+ Responses: Allowed on QName
+
+ Send an A query to "allow.responses.tests.powerdns.com.",
+ check that the response is allowed.
+ """
+ name = 'allow.responses.tests.powerdns.com.'
+ query = dns.message.make_query(name, 'A', 'IN')
+ response = dns.message.make_response(query)
+
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(response, receivedResponse)
+
+ (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(response, receivedResponse)
+
+ def testNotAllowed(self):
+ """
+ Responses: Not allowed on QName
+
+ Send an A query to "dontallow.responses.tests.powerdns.com.",
+ check that the response is dropped.
+ """
+ name = 'dontallow.responses.tests.powerdns.com.'
+ query = dns.message.make_query(name, 'A', 'IN')
+ response = dns.message.make_response(query)
+
+ (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(receivedResponse, None)
+
+ (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+ receivedQuery.id = query.id
+ self.assertEquals(query, receivedQuery)
+ self.assertEquals(receivedResponse, None)