]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/rpzloader.cc
rec: ensure correct service user on debian
[thirdparty/pdns.git] / pdns / rpzloader.cc
1 #include "dnsparser.hh"
2 #include "dnsrecords.hh"
3 #include "ixfr.hh"
4 #include "syncres.hh"
5 #include "resolver.hh"
6 #include "logger.hh"
7 #include "rec-lua-conf.hh"
8 #include "rpzloader.hh"
9 #include "zoneparser-tng.hh"
10 #include "threadname.hh"
11
12 static Netmask makeNetmaskFromRPZ(const DNSName& name)
13 {
14 auto parts = name.getRawLabels();
15 /*
16 * why 2?, the minimally valid IPv6 address that can be encoded in an RPZ is
17 * $NETMASK.zz (::/$NETMASK)
18 * Terrible right?
19 */
20 if(parts.size() < 2 || parts.size() > 9)
21 throw PDNSException("Invalid IP address in RPZ: "+name.toLogString());
22
23 bool isV6 = (stoi(parts[0]) > 32);
24 bool hadZZ = false;
25
26 for (auto &part : parts) {
27 // Check if we have an IPv4 octet
28 for (auto c : part)
29 if (!isdigit(c))
30 isV6 = true;
31
32 if (pdns_iequals(part,"zz")) {
33 if (hadZZ)
34 throw PDNSException("more than one 'zz' label found in RPZ name"+name.toLogString());
35 part = "";
36 isV6 = true;
37 hadZZ = true;
38 }
39 }
40
41 if (isV6 && parts.size() < 9 && !hadZZ)
42 throw PDNSException("No 'zz' label found in an IPv6 RPZ name shorter than 9 elements: "+name.toLogString());
43
44 if (parts.size() == 5 && !isV6)
45 return Netmask(parts[4]+"."+parts[3]+"."+parts[2]+"."+parts[1]+"/"+parts[0]);
46
47 string v6;
48
49 for (uint8_t i = parts.size()-1 ; i > 0; i--) {
50 v6 += parts[i];
51 if (parts[i] == "" && i == 1 && i == parts.size()-1)
52 v6+= "::";
53 if (parts[i] == "" && i != parts.size()-1)
54 v6+= ":";
55 if (parts[i] != "" && i != 1)
56 v6 += ":";
57 }
58 v6 += "/" + parts[0];
59
60 return Netmask(v6);
61 }
62
63 static void RPZRecordToPolicy(const DNSRecord& dr, std::shared_ptr<DNSFilterEngine::Zone> zone, bool addOrRemove, boost::optional<DNSFilterEngine::Policy> defpol, bool defpolOverrideLocal, uint32_t maxTTL)
64 {
65 static const DNSName drop("rpz-drop."), truncate("rpz-tcp-only."), noaction("rpz-passthru.");
66 static const DNSName rpzClientIP("rpz-client-ip"), rpzIP("rpz-ip"),
67 rpzNSDname("rpz-nsdname"), rpzNSIP("rpz-nsip.");
68 static const std::string rpzPrefix("rpz-");
69
70 DNSFilterEngine::Policy pol;
71 bool defpolApplied = false;
72
73 if(dr.d_class != QClass::IN) {
74 return;
75 }
76
77 if(dr.d_type == QType::CNAME) {
78 auto crc = getRR<CNAMERecordContent>(dr);
79 if (!crc) {
80 return;
81 }
82 auto crcTarget=crc->getTarget();
83 if(defpol) {
84 pol=*defpol;
85 defpolApplied = true;
86 }
87 else if(crcTarget.isRoot()) {
88 // cerr<<"Wants NXDOMAIN for "<<dr.d_name<<": ";
89 pol.d_kind = DNSFilterEngine::PolicyKind::NXDOMAIN;
90 } else if(crcTarget==g_wildcarddnsname) {
91 // cerr<<"Wants NODATA for "<<dr.d_name<<": ";
92 pol.d_kind = DNSFilterEngine::PolicyKind::NODATA;
93 }
94 else if(crcTarget==drop) {
95 // cerr<<"Wants DROP for "<<dr.d_name<<": ";
96 pol.d_kind = DNSFilterEngine::PolicyKind::Drop;
97 }
98 else if(crcTarget==truncate) {
99 // cerr<<"Wants TRUNCATE for "<<dr.d_name<<": ";
100 pol.d_kind = DNSFilterEngine::PolicyKind::Truncate;
101 }
102 else if(crcTarget==noaction) {
103 // cerr<<"Wants NOACTION for "<<dr.d_name<<": ";
104 pol.d_kind = DNSFilterEngine::PolicyKind::NoAction;
105 }
106 /* "The special RPZ encodings which are not to be taken as Local Data are
107 CNAMEs with targets that are:
108 + "." (NXDOMAIN action),
109 + "*." (NODATA action),
110 + a top level domain starting with "rpz-",
111 + a child of a top level domain starting with "rpz-".
112 */
113 else if(!crcTarget.empty() && !crcTarget.isRoot() && crcTarget.getRawLabel(crcTarget.countLabels() - 1).compare(0, rpzPrefix.length(), rpzPrefix) == 0) {
114 /* this is very likely an higher format number or a configuration error,
115 let's just ignore it. */
116 g_log<<Logger::Info<<"Discarding unsupported RPZ entry "<<crcTarget<<" for "<<dr.d_name<<endl;
117 return;
118 }
119 else {
120 pol.d_kind = DNSFilterEngine::PolicyKind::Custom;
121 pol.d_custom.emplace_back(dr.d_content);
122 // cerr<<"Wants custom "<<crcTarget<<" for "<<dr.d_name<<": ";
123 }
124 }
125 else {
126 if (defpol && defpolOverrideLocal) {
127 pol=*defpol;
128 defpolApplied = true;
129 }
130 else {
131 pol.d_kind = DNSFilterEngine::PolicyKind::Custom;
132 pol.d_custom.emplace_back(dr.d_content);
133 // cerr<<"Wants custom "<<dr.d_content->getZoneRepresentation()<<" for "<<dr.d_name<<": ";
134 }
135 }
136
137 if (!defpolApplied || defpol->d_ttl < 0) {
138 pol.d_ttl = static_cast<int32_t>(std::min(maxTTL, dr.d_ttl));
139 } else {
140 pol.d_ttl = static_cast<int32_t>(std::min(maxTTL, static_cast<uint32_t>(pol.d_ttl)));
141 }
142
143 // now to DO something with that
144
145 if(dr.d_name.isPartOf(rpzNSDname)) {
146 DNSName filt=dr.d_name.makeRelative(rpzNSDname);
147 if(addOrRemove)
148 zone->addNSTrigger(filt, std::move(pol));
149 else
150 zone->rmNSTrigger(filt, std::move(pol));
151 } else if(dr.d_name.isPartOf(rpzClientIP)) {
152 DNSName filt=dr.d_name.makeRelative(rpzClientIP);
153 auto nm=makeNetmaskFromRPZ(filt);
154 if(addOrRemove)
155 zone->addClientTrigger(nm, std::move(pol));
156 else
157 zone->rmClientTrigger(nm, std::move(pol));
158
159 } else if(dr.d_name.isPartOf(rpzIP)) {
160 // cerr<<"Should apply answer content IP policy: "<<dr.d_name<<endl;
161 DNSName filt=dr.d_name.makeRelative(rpzIP);
162 auto nm=makeNetmaskFromRPZ(filt);
163 if(addOrRemove)
164 zone->addResponseTrigger(nm, std::move(pol));
165 else
166 zone->rmResponseTrigger(nm, std::move(pol));
167 } else if(dr.d_name.isPartOf(rpzNSIP)) {
168 DNSName filt=dr.d_name.makeRelative(rpzNSIP);
169 auto nm=makeNetmaskFromRPZ(filt);
170 if(addOrRemove)
171 zone->addNSIPTrigger(nm, std::move(pol));
172 else
173 zone->rmNSIPTrigger(nm, std::move(pol));
174 } else {
175 if(addOrRemove) {
176 /* if we did override the existing policy with the default policy,
177 we might turn two A or AAAA into a CNAME, which would trigger
178 an exception. Let's just ignore it. */
179 zone->addQNameTrigger(dr.d_name, std::move(pol), defpolApplied);
180 }
181 else {
182 zone->rmQNameTrigger(dr.d_name, std::move(pol));
183 }
184 }
185 }
186
187 static shared_ptr<SOARecordContent> loadRPZFromServer(const ComboAddress& master, const DNSName& zoneName, std::shared_ptr<DNSFilterEngine::Zone> zone, boost::optional<DNSFilterEngine::Policy> defpol, bool defpolOverrideLocal, uint32_t maxTTL, const TSIGTriplet& tt, size_t maxReceivedBytes, const ComboAddress& localAddress, uint16_t axfrTimeout)
188 {
189 g_log<<Logger::Warning<<"Loading RPZ zone '"<<zoneName<<"' from "<<master.toStringWithPort()<<endl;
190 if(!tt.name.empty())
191 g_log<<Logger::Warning<<"With TSIG key '"<<tt.name<<"' of algorithm '"<<tt.algo<<"'"<<endl;
192
193 ComboAddress local(localAddress);
194 if (local == ComboAddress())
195 local = getQueryLocalAddress(master.sin4.sin_family, 0);
196
197 AXFRRetriever axfr(master, zoneName, tt, &local, maxReceivedBytes, axfrTimeout);
198 unsigned int nrecords=0;
199 Resolver::res_t nop;
200 vector<DNSRecord> chunk;
201 time_t last=0;
202 time_t axfrStart = time(nullptr);
203 time_t axfrNow = time(nullptr);
204 shared_ptr<SOARecordContent> sr;
205 while(axfr.getChunk(nop, &chunk, (axfrStart + axfrTimeout - axfrNow))) {
206 for(auto& dr : chunk) {
207 if(dr.d_type==QType::NS || dr.d_type==QType::TSIG) {
208 continue;
209 }
210
211 dr.d_name.makeUsRelative(zoneName);
212 if(dr.d_type==QType::SOA) {
213 sr = getRR<SOARecordContent>(dr);
214 continue;
215 }
216
217 RPZRecordToPolicy(dr, zone, true, defpol, defpolOverrideLocal, maxTTL);
218 nrecords++;
219 }
220 axfrNow = time(nullptr);
221 if (axfrNow < axfrStart || axfrNow - axfrStart > axfrTimeout) {
222 throw PDNSException("Total AXFR time exceeded!");
223 }
224 if(last != time(0)) {
225 g_log<<Logger::Info<<"Loaded & indexed "<<nrecords<<" policy records so far for RPZ zone '"<<zoneName<<"'"<<endl;
226 last=time(0);
227 }
228 }
229 g_log<<Logger::Info<<"Done: "<<nrecords<<" policy records active, SOA: "<<sr->getZoneRepresentation()<<endl;
230 return sr;
231 }
232
233 // this function is silent - you do the logging
234 std::shared_ptr<SOARecordContent> loadRPZFromFile(const std::string& fname, std::shared_ptr<DNSFilterEngine::Zone> zone, boost::optional<DNSFilterEngine::Policy> defpol, bool defpolOverrideLocal, uint32_t maxTTL)
235 {
236 shared_ptr<SOARecordContent> sr = nullptr;
237 ZoneParserTNG zpt(fname);
238 DNSResourceRecord drr;
239 DNSName domain;
240 while(zpt.get(drr)) {
241 try {
242 if(drr.qtype.getCode() == QType::CNAME && drr.content.empty())
243 drr.content=".";
244 DNSRecord dr(drr);
245 if(dr.d_type == QType::SOA) {
246 sr = getRR<SOARecordContent>(dr);
247 domain = dr.d_name;
248 zone->setDomain(domain);
249 }
250 else if(dr.d_type == QType::NS) {
251 continue;
252 }
253 else {
254 dr.d_name=dr.d_name.makeRelative(domain);
255 RPZRecordToPolicy(dr, zone, true, defpol, defpolOverrideLocal, maxTTL);
256 }
257 }
258 catch(const PDNSException& pe) {
259 throw PDNSException("Issue parsing '"+drr.qname.toLogString()+"' '"+drr.content+"' at "+zpt.getLineOfFile()+": "+pe.reason);
260 }
261 }
262
263 return sr;
264 }
265
266 static std::unordered_map<std::string, shared_ptr<rpzStats> > s_rpzStats;
267 static std::mutex s_rpzStatsMutex;
268
269 shared_ptr<rpzStats> getRPZZoneStats(const std::string& zone)
270 {
271 std::lock_guard<std::mutex> l(s_rpzStatsMutex);
272 if (s_rpzStats.find(zone) == s_rpzStats.end()) {
273 s_rpzStats[zone] = std::make_shared<rpzStats>();
274 }
275 return s_rpzStats[zone];
276 }
277
278 static void incRPZFailedTransfers(const std::string& zone)
279 {
280 auto stats = getRPZZoneStats(zone);
281 if (stats != nullptr)
282 stats->d_failedTransfers++;
283 }
284
285 static void setRPZZoneNewState(const std::string& zone, uint32_t serial, uint64_t numberOfRecords, bool wasAXFR)
286 {
287 auto stats = getRPZZoneStats(zone);
288 if (stats == nullptr)
289 return;
290 stats->d_successfulTransfers++;
291 if (wasAXFR) {
292 stats->d_fullTransfers++;
293 }
294 stats->d_lastUpdate = time(nullptr);
295 stats->d_serial = serial;
296 stats->d_numberOfRecords = numberOfRecords;
297 }
298
299 static bool dumpZoneToDisk(const DNSName& zoneName, const std::shared_ptr<DNSFilterEngine::Zone>& newZone, const std::string& dumpZoneFileName)
300 {
301 std::string temp = dumpZoneFileName + "XXXXXX";
302 int fd = mkstemp(&temp.at(0));
303 if (fd < 0) {
304 g_log<<Logger::Warning<<"Unable to open a file to dump the content of the RPZ zone "<<zoneName.toLogString()<<endl;
305 return false;
306 }
307
308 auto fp = std::unique_ptr<FILE, int(*)(FILE*)>(fdopen(fd, "w+"), fclose);
309 if (!fp) {
310 close(fd);
311 g_log<<Logger::Warning<<"Unable to open a file pointer to dump the content of the RPZ zone "<<zoneName.toLogString()<<endl;
312 return false;
313 }
314 fd = -1;
315
316 try {
317 newZone->dump(fp.get());
318 }
319 catch(const std::exception& e) {
320 g_log<<Logger::Warning<<"Error while dumping the content of the RPZ zone "<<zoneName.toLogString()<<": "<<e.what()<<endl;
321 return false;
322 }
323
324 if (fflush(fp.get()) != 0) {
325 g_log<<Logger::Warning<<"Error while flushing the content of the RPZ zone "<<zoneName.toLogString()<<" to the dump file: "<<strerror(errno)<<endl;
326 return false;
327 }
328
329 if (fsync(fileno(fp.get())) != 0) {
330 g_log<<Logger::Warning<<"Error while syncing the content of the RPZ zone "<<zoneName.toLogString()<<" to the dump file: "<<strerror(errno)<<endl;
331 return false;
332 }
333
334 if (fclose(fp.release()) != 0) {
335 g_log<<Logger::Warning<<"Error while writing the content of the RPZ zone "<<zoneName.toLogString()<<" to the dump file: "<<strerror(errno)<<endl;
336 return false;
337 }
338
339 if (rename(temp.c_str(), dumpZoneFileName.c_str()) != 0) {
340 g_log<<Logger::Warning<<"Error while moving the content of the RPZ zone "<<zoneName.toLogString()<<" to the dump file: "<<strerror(errno)<<endl;
341 return false;
342 }
343
344 return true;
345 }
346
347 void RPZIXFRTracker(const std::vector<ComboAddress>& masters, boost::optional<DNSFilterEngine::Policy> defpol, bool defpolOverrideLocal, uint32_t maxTTL, size_t zoneIdx, const TSIGTriplet& tt, size_t maxReceivedBytes, const ComboAddress& localAddress, const uint16_t axfrTimeout, std::shared_ptr<SOARecordContent> sr, std::string dumpZoneFileName, uint64_t configGeneration)
348 {
349 setThreadName("pdns-r/RPZIXFR");
350 bool isPreloaded = sr != nullptr;
351 auto luaconfsLocal = g_luaconfs.getLocal();
352 /* we can _never_ modify this zone directly, we need to do a full copy then replace the existing zone */
353 std::shared_ptr<DNSFilterEngine::Zone> oldZone = luaconfsLocal->dfe.getZone(zoneIdx);
354 if (!oldZone) {
355 g_log<<Logger::Error<<"Unable to retrieve RPZ zone with index "<<zoneIdx<<" from the configuration, exiting"<<endl;
356 return;
357 }
358 uint32_t refresh = oldZone->getRefresh();
359 DNSName zoneName = oldZone->getDomain();
360 std::string polName = oldZone->getName() ? *(oldZone->getName()) : zoneName.toString();
361
362 while (!sr) {
363 /* if we received an empty sr, the zone was not really preloaded */
364
365 /* full copy, as promised */
366 std::shared_ptr<DNSFilterEngine::Zone> newZone = std::make_shared<DNSFilterEngine::Zone>(*oldZone);
367 for (const auto& master : masters) {
368 try {
369 sr = loadRPZFromServer(master, zoneName, newZone, defpol, defpolOverrideLocal, maxTTL, tt, maxReceivedBytes, localAddress, axfrTimeout);
370 if(refresh == 0) {
371 refresh = sr->d_st.refresh;
372 }
373 newZone->setSerial(sr->d_st.serial);
374 setRPZZoneNewState(polName, sr->d_st.serial, newZone->size(), true);
375
376 g_luaconfs.modify([zoneIdx, &newZone](LuaConfigItems& lci) {
377 lci.dfe.setZone(zoneIdx, newZone);
378 });
379
380 if (!dumpZoneFileName.empty()) {
381 dumpZoneToDisk(zoneName, newZone, dumpZoneFileName);
382 }
383
384 /* no need to try another master */
385 break;
386 }
387 catch(const std::exception& e) {
388 g_log<<Logger::Warning<<"Unable to load RPZ zone '"<<zoneName<<"' from '"<<master<<"': '"<<e.what()<<"'. (Will try again in "<<(refresh > 0 ? refresh : 10)<<" seconds...)"<<endl;
389 incRPZFailedTransfers(polName);
390 }
391 catch(const PDNSException& e) {
392 g_log<<Logger::Warning<<"Unable to load RPZ zone '"<<zoneName<<"' from '"<<master<<"': '"<<e.reason<<"'. (Will try again in "<<(refresh > 0 ? refresh : 10)<<" seconds...)"<<endl;
393 incRPZFailedTransfers(polName);
394 }
395 }
396
397 if (!sr) {
398 if (refresh == 0) {
399 sleep(10);
400 } else {
401 sleep(refresh);
402 }
403 }
404 }
405
406 bool skipRefreshDelay = isPreloaded;
407
408 for(;;) {
409 DNSRecord dr;
410 dr.d_content=sr;
411
412 if (skipRefreshDelay) {
413 skipRefreshDelay = false;
414 }
415 else {
416 sleep(refresh);
417 }
418
419 if (luaconfsLocal->generation != configGeneration) {
420 /* the configuration has been reloaded, meaning that a new thread
421 has been started to handle that zone and we are now obsolete.
422 */
423 g_log<<Logger::Info<<"A more recent configuration has been found, stopping the existing RPZ update thread for "<<zoneName<<endl;
424 return;
425 }
426
427 vector<pair<vector<DNSRecord>, vector<DNSRecord> > > deltas;
428 for (const auto& master : masters) {
429 g_log<<Logger::Info<<"Getting IXFR deltas for "<<zoneName<<" from "<<master.toStringWithPort()<<", our serial: "<<getRR<SOARecordContent>(dr)->d_st.serial<<endl;
430
431 ComboAddress local(localAddress);
432 if (local == ComboAddress()) {
433 local = getQueryLocalAddress(master.sin4.sin_family, 0);
434 }
435
436 try {
437 deltas = getIXFRDeltas(master, zoneName, dr, tt, &local, maxReceivedBytes);
438
439 /* no need to try another master */
440 break;
441 } catch(const std::runtime_error& e ){
442 g_log<<Logger::Warning<<e.what()<<endl;
443 incRPZFailedTransfers(polName);
444 continue;
445 }
446 }
447
448 if(deltas.empty()) {
449 continue;
450 }
451
452 g_log<<Logger::Info<<"Processing "<<deltas.size()<<" delta"<<addS(deltas)<<" for RPZ "<<zoneName<<endl;
453
454 oldZone = luaconfsLocal->dfe.getZone(zoneIdx);
455 /* we need to make a _full copy_ of the zone we are going to work on */
456 std::shared_ptr<DNSFilterEngine::Zone> newZone = std::make_shared<DNSFilterEngine::Zone>(*oldZone);
457
458 int totremove=0, totadd=0;
459 bool fullUpdate = false;
460 for(const auto& delta : deltas) {
461 const auto& remove = delta.first;
462 const auto& add = delta.second;
463 if(remove.empty()) {
464 g_log<<Logger::Warning<<"IXFR update is a whole new zone"<<endl;
465 newZone->clear();
466 fullUpdate = true;
467 }
468 for(const auto& rr : remove) { // should always contain the SOA
469 if(rr.d_type == QType::NS)
470 continue;
471 if(rr.d_type == QType::SOA) {
472 auto oldsr = getRR<SOARecordContent>(rr);
473 if(oldsr && oldsr->d_st.serial == sr->d_st.serial) {
474 // cout<<"Got good removal of SOA serial "<<oldsr->d_st.serial<<endl;
475 }
476 else
477 g_log<<Logger::Error<<"GOT WRONG SOA SERIAL REMOVAL, SHOULD TRIGGER WHOLE RELOAD"<<endl;
478 }
479 else {
480 totremove++;
481 g_log<<(g_logRPZChanges ? Logger::Info : Logger::Debug)<<"Had removal of "<<rr.d_name<<" from RPZ zone "<<zoneName<<endl;
482 RPZRecordToPolicy(rr, newZone, false, defpol, defpolOverrideLocal, maxTTL);
483 }
484 }
485
486 for(const auto& rr : add) { // should always contain the new SOA
487 if(rr.d_type == QType::NS)
488 continue;
489 if(rr.d_type == QType::SOA) {
490 auto newsr = getRR<SOARecordContent>(rr);
491 // g_log<<Logger::Info<<"New SOA serial for "<<zoneName<<": "<<newsr->d_st.serial<<endl;
492 if (newsr) {
493 sr = newsr;
494 }
495 }
496 else {
497 totadd++;
498 g_log<<(g_logRPZChanges ? Logger::Info : Logger::Debug)<<"Had addition of "<<rr.d_name<<" to RPZ zone "<<zoneName<<endl;
499 RPZRecordToPolicy(rr, newZone, true, defpol, defpolOverrideLocal, maxTTL);
500 }
501 }
502 }
503 g_log<<Logger::Info<<"Had "<<totremove<<" RPZ removal"<<addS(totremove)<<", "<<totadd<<" addition"<<addS(totadd)<<" for "<<zoneName<<" New serial: "<<sr->d_st.serial<<endl;
504 newZone->setSerial(sr->d_st.serial);
505 setRPZZoneNewState(polName, sr->d_st.serial, newZone->size(), fullUpdate);
506
507 /* we need to replace the existing zone with the new one,
508 but we don't want to touch anything else, especially other zones,
509 since they might have been updated by another RPZ IXFR tracker thread.
510 */
511 g_luaconfs.modify([zoneIdx, &newZone](LuaConfigItems& lci) {
512 lci.dfe.setZone(zoneIdx, newZone);
513 });
514
515 if (!dumpZoneFileName.empty()) {
516 dumpZoneToDisk(zoneName, newZone, dumpZoneFileName);
517 }
518 }
519 }