dnsparser.cc \
dnsrecords.cc dnsrecords.hh \
dnswriter.cc dnswriter.hh \
+ filterpo.cc filterpo.hh \
iputils.cc \
json.cc json.hh \
logger.cc \
reczones.cc \
resolver.hh \
responsestats.cc \
+ rpzloader.cc rpzloader.hh \
secpoll-recursor.cc \
secpoll-recursor.hh \
selectmplexer.cc \
--- /dev/null
+$TTL 2h;
+$ORIGIN domain.example.com.
+@ SOA powerdns.example.net. hostmaster.example.com ( 1 12h 15m 3w 2h)
+ NS powerdns.example.net. // out-of-zone no A/AAAA RR required
+; begin RPZ RR definitions
+
+;; QNAME Trigger
+
+; QNAME Trigger NXDOMAIN Action
+; kills whole domain
+nxdomain.org CNAME .
+*.nxdomain-apex.org CNAME .
+
+; QNAME Trigger PASSTHRU Action
+; typically only used for bypass
+mail.nxdomain-apix.org CNAME rpz-passthru.
+
+; QNAME Trigger DROP Action
+; kills whole domain
+example.net CNAME rpz-drop.
+*.example.net CNAME rpz-drop.
+
+; QNAME Trigger Truncate Action
+; kills whole domain
+truncate.org CNAME rpz-tcp-only.
+*.truncate-apex.org CNAME rpz-tcp-only.
+
+; QNAME Trigger Local-Data Action
+; sends to a local website
+; kills whole domain
+local.org CNAME explanation.example.com.
+*.local.org CNAME explanation.example.com.
+
+local-a.org A 192.168.2.5
+*.local-a-apex.org A 192.168.2.5
+
+; CLIENT-IP Trigger DROP Action
+; kills all DNS activity from this client
+24.0.0.0.127.rpz-client-ip CNAME rpz-drop.
+
+; CLIENT-IP Trigger TCP-ONLY Action
+; slows-up all DNS activity from this client
+32.1.0.0.10.rpz-client-ip CNAME rpz-tcp-only.
+
+; IP Trigger NXDOMAIN Action
+; any answer containing IP range
+32.2.0.0.10.rpz-ip CNAME .
+
+;; NSDNAME Trigger
+;; if ns1.example.org appears in the authority section
+;; of any answer
+
+; NSDNAME Trigger NXDOMAIN Action
+; kills specific name server
+dns-eu1.powerdns.net.rpz-nsdname CNAME .
+; this will kill any name servers from example.org
+*.powerdns.net.rpz-nsdname CNAME .
+
+; NSDNAME Trigger TCP-ONLY Action
+; kills specific name server
+*.gtld-servers.net.rpz-nsdname CNAME rpz-tcp-only.
+
--- /dev/null
+#include "filterpo.hh"
+#include <iostream>
+#include "namespaces.hh"
+#include "dnsrecords.hh"
+
+DNSFilterEngine::DNSFilterEngine()
+{
+}
+
+bool findNamedPolicy(const map<DNSName, DNSFilterEngine::Policy>& polmap, const DNSName& qname, DNSFilterEngine::Policy& pol)
+{
+ DNSName s(qname);
+
+ /* for www.powerdns.com, we need to check:
+ www.powerdns.com.
+ *.powerdns.com.
+ powerdns.com.
+ *.com.
+ com.
+ *.
+ . */
+
+ bool first=true;
+ do {
+ auto iter = polmap.find(s);
+ if(iter != polmap.end()) {
+ pol=iter->second;
+ return true;
+ }
+ if(!first) {
+ iter = polmap.find(DNSName("*")+s);
+ if(iter != polmap.end()) {
+ pol=iter->second;
+ return true;
+ }
+ }
+ first=false;
+ } while(s.chopOff());
+ return false;
+}
+
+DNSFilterEngine::Policy DNSFilterEngine::getProcessingPolicy(const DNSName& qname) const
+{
+ cout<<"Got question for nameserver name "<<qname<<endl;
+ Policy pol = Policy::NoAction;
+ for(const auto& z : d_zones) {
+ if(findNamedPolicy(z.propolName, qname, pol)) {
+ cerr<<"Had a hit on the nameserver used to process the query"<<endl;
+ return pol;
+ }
+ }
+ return pol;
+}
+
+
+DNSFilterEngine::Policy DNSFilterEngine::getQueryPolicy(const DNSName& qname, const ComboAddress& ca) const
+{
+ cout<<"Got question for "<<qname<<" from "<<ca.toString()<<endl;
+
+ Policy pol = Policy::NoAction;
+ for(const auto& z : d_zones) {
+ if(findNamedPolicy(z.qpolName, qname, pol)) {
+ cerr<<"Had a hit on the name of the query"<<endl;
+ return pol;
+ }
+
+ for(const auto& qa : z.qpolAddr) {
+ if(qa.first.match(ca)) {
+ cerr<<"Had a hit on the IP address of the client"<<endl;
+ return qa.second;
+ }
+ }
+ }
+
+ return Policy::NoAction;
+}
+
+DNSFilterEngine::Policy DNSFilterEngine::getPostPolicy(const vector<DNSRecord>& records) const
+{
+ ComboAddress ca;
+
+ for(const auto& r : records) {
+ if(r.d_place != DNSRecord::Answer)
+ continue;
+ if(r.d_type == QType::A)
+ ca = std::dynamic_pointer_cast<ARecordContent>(r.d_content)->getCA();
+ else if(r.d_type == QType::AAAA)
+ ca = std::dynamic_pointer_cast<AAAARecordContent>(r.d_content)->getCA();
+ else
+ continue;
+
+ for(const auto& z : d_zones) {
+ for(const auto& qa : z.postpolAddr) {
+ if(qa.first.match(ca)) {
+ cerr<<"Had a hit on IP address in answer"<<endl;
+ return qa.second;
+ }
+ }
+ }
+ }
+ return Policy::NoAction;
+}
+
+void DNSFilterEngine::assureZones(int zone)
+{
+ if((int)d_zones.size() <= zone)
+ d_zones.resize(zone+1);
+
+}
+
+void DNSFilterEngine::addClientTrigger(const Netmask& nm, Policy pol, int zone)
+{
+ assureZones(zone);
+ d_zones[zone].qpolAddr.push_back({nm,pol});
+}
+
+void DNSFilterEngine::addResponseTrigger(const Netmask& nm, Policy pol, int zone)
+{
+ assureZones(zone);
+ d_zones[zone].postpolAddr.push_back({nm,pol});
+}
+
+void DNSFilterEngine::addQNameTrigger(const DNSName& n, Policy pol, int zone)
+{
+ assureZones(zone);
+ d_zones[zone].qpolName[n]=pol;
+}
+
+void DNSFilterEngine::addNSTrigger(const DNSName& n, Policy pol, int zone)
+{
+ assureZones(zone);
+ d_zones[zone].propolName[n]=pol;
+}
--- /dev/null
+#pragma once
+#include "iputils.hh"
+#include "dns.hh"
+#include <map>
+
+/* This class implements a filtering policy that is able to fully implement RPZ, but is not bound to it.
+ In other words, it is generic enough to support RPZ, but could get its data from other places.
+
+
+ We know the following actions:
+
+ No action - just pass it on
+ Drop - drop a query, no response
+ NXDOMAIN - fake up an NXDOMAIN for the query
+ NODATA - just return no data for this qtype
+ Truncate - set TC bit
+ Modified - "we fake an answer for you"
+
+ These actions can be caused by the following triggers:
+
+ qname - the query name
+ client-ip - the IP address of the requestor
+ response-ip - an IP address in the response
+ ns-name - the name of a server used in the delegation
+ ns-ip - the IP address of a server used in the delegation
+
+ This means we get several hook points:
+ 1) when the query comes in: qname & client-ip
+ 2) during processing: ns-name & ns-ip
+ 3) after processing: response-ip
+
+ Triggers meanwhile can apply to:
+ Verbatim domain names
+ Wildcard versions (*.domain.com does NOT match domain.com)
+ Netmasks (IPv4 and IPv6)
+
+ Finally, triggers are grouped in different zones. The "first" zone that has a match
+ is consulted. Then within that zone, rules again have precedences.
+*/
+
+
+class DNSFilterEngine
+{
+public:
+ enum class Policy { NoAction, Drop, NXDOMAIN, NODATA, Truncate};
+
+ DNSFilterEngine();
+ void clear();
+ void clear(int zone);
+ void addClientTrigger(const Netmask& nm, Policy pol, int zone=0);
+ void addQNameTrigger(const DNSName& nm, Policy pol, int zone=0);
+ void addNSTrigger(const DNSName& dn, Policy pol, int zone=0);
+ void addResponseTrigger(const Netmask& nm, Policy pol, int zone=0);
+
+ Policy getQueryPolicy(const DNSName& qname, const ComboAddress& nm) const;
+ Policy getProcessingPolicy(const DNSName& qname) const;
+ Policy getPostPolicy(const vector<DNSRecord>& records) const;
+
+private:
+ void assureZones(int zone);
+ struct Zone {
+ std::map<DNSName, Policy> qpolName;
+ std::vector<pair<Netmask, Policy>> qpolAddr;
+ std::map<DNSName, Policy> propolName;
+ std::vector<pair<Netmask, Policy>> postpolAddr;
+ };
+ vector<Zone> d_zones;
+
+};
{
return d_network.sin4.sin_family == AF_INET;
}
+
+ bool operator<(const Netmask& rhs) const
+ {
+ return tie(d_network, d_bits) < tie(rhs.d_network, rhs.d_bits);
+ }
private:
ComboAddress d_network;
uint32_t d_mask;
#include "responsestats.hh"
#include "secpoll-recursor.hh"
#include "dnsname.hh"
+#include "filterpo.hh"
+#include "rpzloader.hh"
#ifndef RECURSOR
#include "statbag.hh"
StatBag S;
__thread boost::circular_buffer<pair<DNSName, uint16_t> >* t_queryring, *t_servfailqueryring;
__thread shared_ptr<Regex>* t_traceRegex;
+DNSFilterEngine g_dfe;
+
RecursorControlChannel s_rcc; // only active in thread 0
// for communicating with our threads
// if there is a RecursorLua active, and it 'took' the query in preResolve, we don't launch beginResolve
+ switch(g_dfe.getQueryPolicy(dc->d_mdp.d_qname, dc->d_remote)) {
+ case DNSFilterEngine::Policy::NoAction:
+ break;
+ case DNSFilterEngine::Policy::Drop:
+ g_stats.policyDrops++;
+ delete dc;
+ dc=0;
+ return;
+ case DNSFilterEngine::Policy::NXDOMAIN:
+ res=RCode::NXDomain;
+ goto haveAnswer;
+
+ case DNSFilterEngine::Policy::NODATA:
+ res=RCode::NoError;
+ goto haveAnswer;
+
+ case DNSFilterEngine::Policy::Truncate:
+ if(!dc->d_tcp) {
+ res=RCode::NoError;
+ pw.getHeader()->tc=1;
+ goto haveAnswer;
+ }
+ break;
+ }
+
if(!t_pdl->get() || !(*t_pdl)->preresolve(dc->d_remote, local, dc->d_mdp.d_qname, QType(dc->d_mdp.d_qtype), ret, res, &variableAnswer)) {
try {
res = sr.beginResolve(dc->d_mdp.d_qname, QType(dc->d_mdp.d_qtype), dc->d_mdp.d_qclass, ret);
res = RCode::ServFail;
}
+ switch(g_dfe.getPostPolicy(ret)) {
+ case DNSFilterEngine::Policy::NoAction:
+ break;
+ case DNSFilterEngine::Policy::Drop:
+ g_stats.policyDrops++;
+ delete dc;
+ dc=0;
+ return;
+ case DNSFilterEngine::Policy::NXDOMAIN:
+ ret.clear();
+ res=RCode::NXDomain;
+ goto haveAnswer;
+
+ case DNSFilterEngine::Policy::NODATA:
+ ret.clear();
+ res=RCode::NoError;
+ goto haveAnswer;
+
+ case DNSFilterEngine::Policy::Truncate:
+ if(!dc->d_tcp) {
+ ret.clear();
+ res=RCode::NoError;
+ pw.getHeader()->tc=1;
+ goto haveAnswer;
+ }
+ break;
+ }
+
if(t_pdl->get()) {
if(res == RCode::NoError) {
auto i=ret.cbegin();
(*t_pdl)->nodata(dc->d_remote,local, dc->d_mdp.d_qname, QType(dc->d_mdp.d_qtype), ret, res, &variableAnswer);
}
else if(res == RCode::NXDomain)
- (*t_pdl)->nxdomain(dc->d_remote,local, dc->d_mdp.d_qname, QType(dc->d_mdp.d_qtype), ret, res, &variableAnswer);
-
- (*t_pdl)->postresolve(dc->d_remote,local, dc->d_mdp.d_qname, QType(dc->d_mdp.d_qtype), ret, res, &variableAnswer);
+ (*t_pdl)->nxdomain(dc->d_remote,local, dc->d_mdp.d_qname, QType(dc->d_mdp.d_qtype), ret, res, &variableAnswer);
+
+
+ (*t_pdl)->postresolve(dc->d_remote,local, dc->d_mdp.d_qname, QType(dc->d_mdp.d_qtype), ret, res, &variableAnswer);
}
}
-
+ haveAnswer:;
if(res == PolicyDecision::DROP) {
g_stats.policyDrops++;
delete dc;
int serviceMain(int argc, char*argv[])
{
+
L.setName(s_programname);
L.setLoglevel((Logger::Urgency)(6)); // info and up
if(!s_pidfname.empty())
unlink(s_pidfname.c_str()); // remove possible old pid file
+ loadRPZFiles();
+
if(::arg().mustDo("daemon")) {
L<<Logger::Warning<<"Calling daemonize, going to background"<<endl;
L.toConsole(Logger::Critical);
g_maxMThreads = ::arg().asNum("max-mthreads");
checkOrFixFDS();
+
+
int newgid=0;
if(!::arg()["setgid"].empty())
newgid=Utility::makeGidNumeric(::arg()["setgid"]);
::arg().set("spoof-nearmiss-max", "If non-zero, assume spoofing after this many near misses")="20";
::arg().set("single-socket", "If set, only use a single socket for outgoing queries")="off";
::arg().set("auth-zones", "Zones for which we have authoritative data, comma separated domain=file pairs ")="";
+ ::arg().set("rpz-files", "RPZ files to load in order, domain or domain=policy pairs separated by commas")="";
+
::arg().set("forward-zones", "Zones for which we forward queries, comma separated domain=ip pairs")="";
::arg().set("forward-zones-recurse", "Zones for which we forward queries with recursion bit, comma separated domain=ip pairs")="";
::arg().set("forward-zones-file", "File with (+)domain=ip pairs for forwarding")="";
#include "logger.hh"
#include "dnsrecords.hh"
#include <boost/foreach.hpp>
+#include "rpzloader.hh"
extern int g_argc;
extern char** g_argv;
return "reloading failed, see log\n";
}
+void loadRPZFiles()
+{
+ vector<string> fnames;
+ stringtok(fnames, ::arg()["rpz-files"],",");
+ int count=0;
+ for(const auto& f : fnames) {
+ loadRPZFromFile(f, g_dfe, count++);
+ }
+}
+
SyncRes::domainmap_t* parseAuthAndForwards()
{
TXTRecordContent::report();
--- /dev/null
+#include "rpzloader.hh"
+#include "zoneparser-tng.hh"
+#include "dnsparser.hh"
+#include "dnsrecords.hh"
+#include "syncres.hh"
+
+static Netmask makeNetmaskFromRPZ(const DNSName& name)
+{
+ auto parts = name.getRawLabels();
+ if(parts.size() < 5)
+ throw PDNSException("Invalid IP address in RPZ: "+name.toString());
+ return Netmask(parts[4]+"."+parts[3]+"."+parts[2]+"."+parts[1]+"/"+parts[0]);
+}
+
+int loadRPZFromFile(const std::string& fname, DNSFilterEngine& target, int place)
+{
+ ZoneParserTNG zpt(fname);
+ DNSResourceRecord drr;
+
+ static const DNSName drop("rpz-drop."), truncate("rpz-tcp-only."), noaction("rpz-passthru.");
+
+ static const DNSName rpzClientIP("rpz-client-ip"), rpzIP("rpz-ip"),
+ rpzNSDname("rpz-nsdname"), rpzNSIP("rpz-nsip.");
+
+
+
+ DNSName domain;
+ while(zpt.get(drr)) {
+ DNSFilterEngine::Policy pol=DNSFilterEngine::Policy::NoAction;
+
+ try {
+ if(drr.qtype.getCode() == QType::CNAME && drr.content.empty())
+ drr.content=".";
+ DNSRecord dr(drr);
+ if(dr.d_type == QType::SOA) {
+ domain = dr.d_name;
+ cerr<<"Origin is "<<domain<<endl;
+ }
+ if(dr.d_type == QType::CNAME) {
+ dr.d_name=dr.d_name.makeRelative(domain);
+ auto target=std::dynamic_pointer_cast<CNAMERecordContent>(dr.d_content)->getTarget();
+ if(target.isRoot()) {
+ cerr<<"Wants NXDOMAIN for "<<dr.d_name<<": ";
+ pol = DNSFilterEngine::Policy::NXDOMAIN;
+ } else if(target==DNSName("*")) {
+ cerr<<"Wants NODATA for "<<dr.d_name<<": ";
+ pol = DNSFilterEngine::Policy::NODATA;
+ }
+ else if(target==drop) {
+ cerr<<"Wants DROP for "<<dr.d_name<<": ";
+ pol = DNSFilterEngine::Policy::Drop;
+ }
+ else if(target==truncate) {
+ cerr<<"Wants TRUNCATE for "<<dr.d_name<<": ";
+ pol = DNSFilterEngine::Policy::Truncate;
+ }
+ else if(target==noaction) {
+ cerr<<"Wants NOACTION for "<<dr.d_name<<": ";
+ pol = DNSFilterEngine::Policy::NoAction;
+ }
+ else
+ cerr<<"Wants custom "<<target<<" for "<<dr.d_name<<": ";
+
+ if(dr.d_name.isPartOf(rpzNSDname)) {
+ DNSName filt=dr.d_name.makeRelative(rpzNSDname);
+ cerr<<"Should apply '"<<filt<<"' to nameserver policy"<<endl;
+ g_dfe.addNSTrigger(filt, pol);
+ } else if(dr.d_name.isPartOf(rpzClientIP)) {
+ cerr<<"Should apply to client IP policy"<<endl;
+ auto nm=makeNetmaskFromRPZ(dr.d_name);
+ cout<<"Parsed as "<<nm.toString()<<endl;
+ g_dfe.addClientTrigger(nm, pol);
+
+ } else if(dr.d_name.isPartOf(rpzIP)) {
+ cerr<<"Should apply answer content IP policy: "<<dr.d_name<<endl;
+ auto nm=makeNetmaskFromRPZ(dr.d_name);
+ cout<<"Parsed as "<<nm.toString()<<endl;
+ g_dfe.addResponseTrigger(nm, pol);
+ } else if(dr.d_name.isPartOf(rpzNSIP)) {
+ cerr<<"Should apply to nameserver IP address policy"<<endl;
+ } else {
+ cerr<<"Should apply to query names"<<endl;
+ g_dfe.addQNameTrigger(dr.d_name, pol);
+ }
+
+ }
+ }
+ catch(PDNSException& pe) {
+ cerr<<"Issue parsing '"<<drr.qname<<"' '"<<drr.content<<"' at "<<zpt.getLineOfFile()<<endl;
+ cerr<<pe.reason<<endl;
+ }
+ }
+
+ return place;
+}
--- /dev/null
+#pragma once
+#include "filterpo.hh"
+#include <string>
+
+int loadRPZFromFile(const std::string& fname, DNSFilterEngine& target, int place);
LOG(prefix<<qname.toString()<<": Trying to resolve NS '"<<tns->toString()<< "' ("<<1+tns-rnameservers.begin()<<"/"<<(unsigned int)rnameservers.size()<<")"<<endl);
;
+ if(g_dfe.getProcessingPolicy(*tns) != DNSFilterEngine::Policy::NoAction)
+ throw ImmediateServFailException("Dropped because of policy");
+
if(!isCanonical(*tns)) {
LOG(prefix<<qname.toString()<<": Domain has hardcoded nameserver(s)"<<endl);
#include <boost/tuple/tuple_comparison.hpp>
#include "mtasker.hh"
#include "iputils.hh"
+#include "filterpo.hh"
+
+extern DNSFilterEngine g_dfe;
void primeHints(void);
class RecursorLua;
template<class T> T broadcastAccFunction(const boost::function<T*()>& func, bool skipSelf=false);
SyncRes::domainmap_t* parseAuthAndForwards();
+void loadRPZFiles();
uint64_t* pleaseGetNsSpeedsSize();
uint64_t* pleaseGetCacheSize();
bool get(DNSResourceRecord& rr, std::string* comment=0);
typedef runtime_error exception;
typedef deque<pair<string::size_type, string::size_type> > parts_t;
+ string getLineOfFile();
private:
bool getLine();
bool getTemplateLine();
void stackFile(const std::string& fname);
unsigned makeTTLFromZone(const std::string& str);
- string getLineOfFile();
+
struct filestate {
filestate(FILE* fp, string filename) : d_fp(fp), d_filename(filename), d_lineno(0){}
FILE *d_fp;