From: bert hubert Date: Sun, 18 Oct 2015 17:03:29 +0000 (+0200) Subject: very first RPZ work. Can load RPZ from disk, most policies work. What doesn't: IPv6... X-Git-Tag: dnsdist-1.0.0-alpha1~252^2~6^2~23 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=644dd1da2641ccc7ffb8110219741d7d5fdf0417;p=thirdparty%2Fpdns.git very first RPZ work. Can load RPZ from disk, most policies work. What doesn't: IPv6 addresses as triggers, nameserver name policies other than 'drop', "custom" data, default policies for RPZ files, slaving RPZ data, reloading RPZ files, performance. To test, try 'rpz-files=basic.rpz'. --- diff --git a/pdns/Makefile.am b/pdns/Makefile.am index 8203f0419b..d31fe56ad2 100644 --- a/pdns/Makefile.am +++ b/pdns/Makefile.am @@ -1071,6 +1071,7 @@ pdns_recursor_SOURCES = \ dnsparser.cc \ dnsrecords.cc dnsrecords.hh \ dnswriter.cc dnswriter.hh \ + filterpo.cc filterpo.hh \ iputils.cc \ json.cc json.hh \ logger.cc \ @@ -1094,6 +1095,7 @@ pdns_recursor_SOURCES = \ reczones.cc \ resolver.hh \ responsestats.cc \ + rpzloader.cc rpzloader.hh \ secpoll-recursor.cc \ secpoll-recursor.hh \ selectmplexer.cc \ diff --git a/pdns/basic.rpz b/pdns/basic.rpz new file mode 100644 index 0000000000..60f8d5e62e --- /dev/null +++ b/pdns/basic.rpz @@ -0,0 +1,62 @@ +$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. + diff --git a/pdns/filterpo.cc b/pdns/filterpo.cc new file mode 100644 index 0000000000..771e1ff277 --- /dev/null +++ b/pdns/filterpo.cc @@ -0,0 +1,133 @@ +#include "filterpo.hh" +#include +#include "namespaces.hh" +#include "dnsrecords.hh" + +DNSFilterEngine::DNSFilterEngine() +{ +} + +bool findNamedPolicy(const map& 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 "<& 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(r.d_content)->getCA(); + else if(r.d_type == QType::AAAA) + ca = std::dynamic_pointer_cast(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"< + +/* 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& records) const; + +private: + void assureZones(int zone); + struct Zone { + std::map qpolName; + std::vector> qpolAddr; + std::map propolName; + std::vector> postpolAddr; + }; + vector d_zones; + +}; diff --git a/pdns/iputils.hh b/pdns/iputils.hh index 52e6fe3d8d..672a9ff9db 100644 --- a/pdns/iputils.hh +++ b/pdns/iputils.hh @@ -347,6 +347,11 @@ public: { 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; diff --git a/pdns/pdns_recursor.cc b/pdns/pdns_recursor.cc index a0632736ad..f3c7d9220a 100644 --- a/pdns/pdns_recursor.cc +++ b/pdns/pdns_recursor.cc @@ -71,6 +71,8 @@ #include "responsestats.hh" #include "secpoll-recursor.hh" #include "dnsname.hh" +#include "filterpo.hh" +#include "rpzloader.hh" #ifndef RECURSOR #include "statbag.hh" StatBag S; @@ -91,6 +93,8 @@ __thread addrringbuf_t* t_remotes, *t_servfailremotes, *t_largeanswerremotes; __thread boost::circular_buffer >* t_queryring, *t_servfailqueryring; __thread shared_ptr* t_traceRegex; +DNSFilterEngine g_dfe; + RecursorControlChannel s_rcc; // only active in thread 0 // for communicating with our threads @@ -608,6 +612,31 @@ void startDoResolve(void *p) // 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); @@ -618,6 +647,34 @@ void startDoResolve(void *p) 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(); @@ -628,12 +685,13 @@ void startDoResolve(void *p) (*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; @@ -1929,6 +1987,7 @@ void parseACLs() int serviceMain(int argc, char*argv[]) { + L.setName(s_programname); L.setLoglevel((Logger::Urgency)(6)); // info and up @@ -2050,6 +2109,8 @@ int serviceMain(int argc, char*argv[]) if(!s_pidfname.empty()) unlink(s_pidfname.c_str()); // remove possible old pid file + loadRPZFiles(); + if(::arg().mustDo("daemon")) { L< +#include "rpzloader.hh" extern int g_argc; extern char** g_argv; @@ -313,6 +314,16 @@ string reloadAuthAndForwards() return "reloading failed, see log\n"; } +void loadRPZFiles() +{ + vector 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(); diff --git a/pdns/rpzloader.cc b/pdns/rpzloader.cc new file mode 100644 index 0000000000..088b263c38 --- /dev/null +++ b/pdns/rpzloader.cc @@ -0,0 +1,95 @@ +#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 "<(dr.d_content)->getTarget(); + if(target.isRoot()) { + cerr<<"Wants NXDOMAIN for "< + +int loadRPZFromFile(const std::string& fname, DNSFilterEngine& target, int place); diff --git a/pdns/syncres.cc b/pdns/syncres.cc index e967d76bdc..df6c66fa78 100644 --- a/pdns/syncres.cc +++ b/pdns/syncres.cc @@ -919,6 +919,9 @@ int SyncRes::doResolveAt(set nameservers, DNSName auth, bool flawedNSSe LOG(prefix<toString()<< "' ("<<1+tns-rnameservers.begin()<<"/"<<(unsigned int)rnameservers.size()<<")"< #include "mtasker.hh" #include "iputils.hh" +#include "filterpo.hh" + +extern DNSFilterEngine g_dfe; void primeHints(void); class RecursorLua; @@ -637,6 +640,7 @@ int directResolve(const DNSName& qname, const QType& qtype, int qclass, vector T broadcastAccFunction(const boost::function& func, bool skipSelf=false); SyncRes::domainmap_t* parseAuthAndForwards(); +void loadRPZFiles(); uint64_t* pleaseGetNsSpeedsSize(); uint64_t* pleaseGetCacheSize(); diff --git a/pdns/zoneparser-tng.hh b/pdns/zoneparser-tng.hh index 0691634259..0631516765 100644 --- a/pdns/zoneparser-tng.hh +++ b/pdns/zoneparser-tng.hh @@ -39,12 +39,13 @@ public: bool get(DNSResourceRecord& rr, std::string* comment=0); typedef runtime_error exception; typedef deque > 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;