]>
Commit | Line | Data |
---|---|---|
29691be2 OM |
1 | /* |
2 | * This file is part of PowerDNS or dnsdist. | |
3 | * Copyright -- PowerDNS.COM B.V. and its contributors | |
4 | * | |
5 | * This program is free software; you can redistribute it and/or modify | |
6 | * it under the terms of version 2 of the GNU General Public License as | |
7 | * published by the Free Software Foundation. | |
8 | * | |
9 | * In addition, for the avoidance of any doubt, permission is granted to | |
10 | * link this program with OpenSSL and to (re)distribute the binaries | |
11 | * produced as the result of such linking. | |
12 | * | |
13 | * This program is distributed in the hope that it will be useful, | |
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | * GNU General Public License for more details. | |
17 | * | |
18 | * You should have received a copy of the GNU General Public License | |
19 | * along with this program; if not, write to the Free Software | |
20 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
21 | */ | |
22 | #include <condition_variable> | |
39cde30d | 23 | #include "arguments.hh" |
644dd1da | 24 | #include "dnsparser.hh" |
25 | #include "dnsrecords.hh" | |
4ba9d5dc | 26 | #include "ixfr.hh" |
644dd1da | 27 | #include "syncres.hh" |
0dd42dbb | 28 | #include "axfr-retriever.hh" |
0fb64ae1 | 29 | #include "lock.hh" |
39ec5d29 | 30 | #include "logger.hh" |
f3cba034 | 31 | #include "logging.hh" |
ad42489c | 32 | #include "rec-lua-conf.hh" |
4ba9d5dc RG |
33 | #include "rpzloader.hh" |
34 | #include "zoneparser-tng.hh" | |
519f5484 | 35 | #include "threadname.hh" |
20829585 | 36 | #include "query-local-address.hh" |
644dd1da | 37 | |
301148e6 | 38 | Netmask makeNetmaskFromRPZ(const DNSName& name) |
644dd1da | 39 | { |
40 | auto parts = name.getRawLabels(); | |
b8470add PL |
41 | /* |
42 | * why 2?, the minimally valid IPv6 address that can be encoded in an RPZ is | |
43 | * $NETMASK.zz (::/$NETMASK) | |
44 | * Terrible right? | |
45 | */ | |
f8f66c91 OM |
46 | if (parts.size() < 2 || parts.size() > 9) |
47 | throw PDNSException("Invalid IP address in RPZ: " + name.toLogString()); | |
b8470add PL |
48 | |
49 | bool isV6 = (stoi(parts[0]) > 32); | |
50 | bool hadZZ = false; | |
51 | ||
f8f66c91 | 52 | for (auto& part : parts) { |
b8470add PL |
53 | // Check if we have an IPv4 octet |
54 | for (auto c : part) | |
55 | if (!isdigit(c)) | |
56 | isV6 = true; | |
57 | ||
f8f66c91 | 58 | if (pdns_iequals(part, "zz")) { |
b8470add | 59 | if (hadZZ) |
f8f66c91 | 60 | throw PDNSException("more than one 'zz' label found in RPZ name" + name.toLogString()); |
b8470add PL |
61 | part = ""; |
62 | isV6 = true; | |
63 | hadZZ = true; | |
64 | } | |
65 | } | |
66 | ||
67 | if (isV6 && parts.size() < 9 && !hadZZ) | |
f8f66c91 | 68 | throw PDNSException("No 'zz' label found in an IPv6 RPZ name shorter than 9 elements: " + name.toLogString()); |
b8470add PL |
69 | |
70 | if (parts.size() == 5 && !isV6) | |
f8f66c91 | 71 | return Netmask(parts[4] + "." + parts[3] + "." + parts[2] + "." + parts[1] + "/" + parts[0]); |
b8470add PL |
72 | |
73 | string v6; | |
74 | ||
f8f66c91 | 75 | if (parts[parts.size() - 1] == "") { |
301148e6 OM |
76 | v6 += ":"; |
77 | } | |
f8f66c91 | 78 | for (uint8_t i = parts.size() - 1; i > 0; i--) { |
b8470add | 79 | v6 += parts[i]; |
301148e6 | 80 | if (i > 1 || (i == 1 && parts[i] == "")) { |
b8470add | 81 | v6 += ":"; |
301148e6 | 82 | } |
b8470add PL |
83 | } |
84 | v6 += "/" + parts[0]; | |
85 | ||
86 | return Netmask(v6); | |
644dd1da | 87 | } |
88 | ||
62b191dc | 89 | static void RPZRecordToPolicy(const DNSRecord& dr, std::shared_ptr<DNSFilterEngine::Zone> zone, bool addOrRemove, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL, Logr::log_t log) |
644dd1da | 90 | { |
644dd1da | 91 | static const DNSName drop("rpz-drop."), truncate("rpz-tcp-only."), noaction("rpz-passthru."); |
644dd1da | 92 | static const DNSName rpzClientIP("rpz-client-ip"), rpzIP("rpz-ip"), |
93 | rpzNSDname("rpz-nsdname"), rpzNSIP("rpz-nsip."); | |
39c9bef5 | 94 | static const std::string rpzPrefix("rpz-"); |
39ec5d29 | 95 | |
1f1ca368 | 96 | DNSFilterEngine::Policy pol; |
d122dac0 | 97 | bool defpolApplied = false; |
39ec5d29 | 98 | |
f8f66c91 | 99 | if (dr.d_class != QClass::IN) { |
ba3c54cb RG |
100 | return; |
101 | } | |
102 | ||
f8f66c91 | 103 | if (dr.d_type == QType::CNAME) { |
ba3c54cb RG |
104 | auto crc = getRR<CNAMERecordContent>(dr); |
105 | if (!crc) { | |
106 | return; | |
107 | } | |
f8f66c91 OM |
108 | auto crcTarget = crc->getTarget(); |
109 | if (defpol) { | |
110 | pol = *defpol; | |
d122dac0 | 111 | defpolApplied = true; |
ad42489c | 112 | } |
f8f66c91 | 113 | else if (crcTarget.isRoot()) { |
39ec5d29 | 114 | // cerr<<"Wants NXDOMAIN for "<<dr.d_name<<": "; |
115 | pol.d_kind = DNSFilterEngine::PolicyKind::NXDOMAIN; | |
f8f66c91 OM |
116 | } |
117 | else if (crcTarget == g_wildcarddnsname) { | |
39ec5d29 | 118 | // cerr<<"Wants NODATA for "<<dr.d_name<<": "; |
119 | pol.d_kind = DNSFilterEngine::PolicyKind::NODATA; | |
120 | } | |
f8f66c91 | 121 | else if (crcTarget == drop) { |
39ec5d29 | 122 | // cerr<<"Wants DROP for "<<dr.d_name<<": "; |
123 | pol.d_kind = DNSFilterEngine::PolicyKind::Drop; | |
124 | } | |
f8f66c91 | 125 | else if (crcTarget == truncate) { |
39ec5d29 | 126 | // cerr<<"Wants TRUNCATE for "<<dr.d_name<<": "; |
127 | pol.d_kind = DNSFilterEngine::PolicyKind::Truncate; | |
128 | } | |
f8f66c91 | 129 | else if (crcTarget == noaction) { |
39ec5d29 | 130 | // cerr<<"Wants NOACTION for "<<dr.d_name<<": "; |
131 | pol.d_kind = DNSFilterEngine::PolicyKind::NoAction; | |
132 | } | |
39c9bef5 RG |
133 | /* "The special RPZ encodings which are not to be taken as Local Data are |
134 | CNAMEs with targets that are: | |
135 | + "." (NXDOMAIN action), | |
136 | + "*." (NODATA action), | |
137 | + a top level domain starting with "rpz-", | |
138 | + a child of a top level domain starting with "rpz-". | |
139 | */ | |
f8f66c91 | 140 | else if (!crcTarget.empty() && !crcTarget.isRoot() && crcTarget.getRawLabel(crcTarget.countLabels() - 1).compare(0, rpzPrefix.length(), rpzPrefix) == 0) { |
39c9bef5 RG |
141 | /* this is very likely an higher format number or a configuration error, |
142 | let's just ignore it. */ | |
f8f66c91 | 143 | SLOG(g_log << Logger::Info << "Discarding unsupported RPZ entry " << crcTarget << " for " << dr.d_name << endl, |
62b191dc | 144 | log->info(Logr::Info, "Discarding unsupported RPZ entry", "target", Logging::Loggable(crcTarget), "name", Logging::Loggable(dr.d_name))); |
39c9bef5 RG |
145 | return; |
146 | } | |
39ec5d29 | 147 | else { |
148 | pol.d_kind = DNSFilterEngine::PolicyKind::Custom; | |
d06dcda4 | 149 | pol.d_custom.emplace_back(dr.getContent()); |
dd079764 | 150 | // cerr<<"Wants custom "<<crcTarget<<" for "<<dr.d_name<<": "; |
39ec5d29 | 151 | } |
152 | } | |
153 | else { | |
d122dac0 | 154 | if (defpol && defpolOverrideLocal) { |
f8f66c91 | 155 | pol = *defpol; |
d122dac0 | 156 | defpolApplied = true; |
db7dcbb1 RG |
157 | } |
158 | else { | |
159 | pol.d_kind = DNSFilterEngine::PolicyKind::Custom; | |
d06dcda4 | 160 | pol.d_custom.emplace_back(dr.getContent()); |
db7dcbb1 RG |
161 | // cerr<<"Wants custom "<<dr.d_content->getZoneRepresentation()<<" for "<<dr.d_name<<": "; |
162 | } | |
39ec5d29 | 163 | } |
164 | ||
d122dac0 | 165 | if (!defpolApplied || defpol->d_ttl < 0) { |
8f618901 | 166 | pol.d_ttl = static_cast<int32_t>(std::min(maxTTL, dr.d_ttl)); |
f8f66c91 OM |
167 | } |
168 | else { | |
8f618901 RG |
169 | pol.d_ttl = static_cast<int32_t>(std::min(maxTTL, static_cast<uint32_t>(pol.d_ttl))); |
170 | } | |
3876ee44 | 171 | |
39ec5d29 | 172 | // now to DO something with that |
39c9bef5 | 173 | |
f8f66c91 OM |
174 | if (dr.d_name.isPartOf(rpzNSDname)) { |
175 | DNSName filt = dr.d_name.makeRelative(rpzNSDname); | |
176 | if (addOrRemove) | |
d8f1f4e6 | 177 | zone->addNSTrigger(filt, std::move(pol), defpolApplied); |
39ec5d29 | 178 | else |
6da513b2 | 179 | zone->rmNSTrigger(filt, std::move(pol)); |
f8f66c91 OM |
180 | } |
181 | else if (dr.d_name.isPartOf(rpzClientIP)) { | |
182 | DNSName filt = dr.d_name.makeRelative(rpzClientIP); | |
183 | auto nm = makeNetmaskFromRPZ(filt); | |
184 | if (addOrRemove) | |
d8f1f4e6 | 185 | zone->addClientTrigger(nm, std::move(pol), defpolApplied); |
39ec5d29 | 186 | else |
6da513b2 | 187 | zone->rmClientTrigger(nm, std::move(pol)); |
f8f66c91 OM |
188 | } |
189 | else if (dr.d_name.isPartOf(rpzIP)) { | |
39ec5d29 | 190 | // cerr<<"Should apply answer content IP policy: "<<dr.d_name<<endl; |
f8f66c91 OM |
191 | DNSName filt = dr.d_name.makeRelative(rpzIP); |
192 | auto nm = makeNetmaskFromRPZ(filt); | |
193 | if (addOrRemove) | |
d8f1f4e6 | 194 | zone->addResponseTrigger(nm, std::move(pol), defpolApplied); |
39ec5d29 | 195 | else |
6da513b2 | 196 | zone->rmResponseTrigger(nm, std::move(pol)); |
f8f66c91 OM |
197 | } |
198 | else if (dr.d_name.isPartOf(rpzNSIP)) { | |
199 | DNSName filt = dr.d_name.makeRelative(rpzNSIP); | |
200 | auto nm = makeNetmaskFromRPZ(filt); | |
201 | if (addOrRemove) | |
d8f1f4e6 | 202 | zone->addNSIPTrigger(nm, std::move(pol), defpolApplied); |
b8470add | 203 | else |
6da513b2 | 204 | zone->rmNSIPTrigger(nm, std::move(pol)); |
f8f66c91 OM |
205 | } |
206 | else { | |
207 | if (addOrRemove) { | |
d122dac0 RG |
208 | /* if we did override the existing policy with the default policy, |
209 | we might turn two A or AAAA into a CNAME, which would trigger | |
210 | an exception. Let's just ignore it. */ | |
211 | zone->addQNameTrigger(dr.d_name, std::move(pol), defpolApplied); | |
212 | } | |
213 | else { | |
6da513b2 | 214 | zone->rmQNameTrigger(dr.d_name, std::move(pol)); |
d122dac0 | 215 | } |
39ec5d29 | 216 | } |
217 | } | |
218 | ||
d06dcda4 | 219 | static shared_ptr<const SOARecordContent> loadRPZFromServer(Logr::log_t plogger, const ComboAddress& primary, const DNSName& zoneName, std::shared_ptr<DNSFilterEngine::Zone> zone, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL, const TSIGTriplet& tt, size_t maxReceivedBytes, const ComboAddress& localAddress, uint16_t axfrTimeout) |
39ec5d29 | 220 | { |
f3cba034 | 221 | |
49f30160 | 222 | auto logger = plogger->withValues("primary", Logging::Loggable(primary)); |
f8f66c91 | 223 | SLOG(g_log << Logger::Warning << "Loading RPZ zone '" << zoneName << "' from " << primary.toStringWithPort() << endl, |
8a38b7e6 | 224 | logger->info(Logr::Info, "Loading RPZ from nameserver")); |
f8f66c91 OM |
225 | if (!tt.name.empty()) { |
226 | SLOG(g_log << Logger::Warning << "With TSIG key '" << tt.name << "' of algorithm '" << tt.algo << "'" << endl, | |
8a38b7e6 | 227 | logger->info(Logr::Info, "Using TSIG key for authentication", "tsig_key_name", Logging::Loggable(tt.name), "tsig_key_algorithm", Logging::Loggable(tt.algo))); |
f3cba034 | 228 | } |
98c9ec39 | 229 | |
f6a8f7d7 PL |
230 | ComboAddress local(localAddress); |
231 | if (local == ComboAddress()) | |
3d324e00 | 232 | local = pdns::getQueryLocalAddress(primary.sin4.sin_family, 0); |
f6a8f7d7 | 233 | |
3d324e00 | 234 | AXFRRetriever axfr(primary, zoneName, tt, &local, maxReceivedBytes, axfrTimeout); |
f8f66c91 | 235 | unsigned int nrecords = 0; |
39ec5d29 | 236 | Resolver::res_t nop; |
237 | vector<DNSRecord> chunk; | |
f8f66c91 | 238 | time_t last = 0; |
cb6218d2 PL |
239 | time_t axfrStart = time(nullptr); |
240 | time_t axfrNow = time(nullptr); | |
d06dcda4 | 241 | shared_ptr<const SOARecordContent> sr; |
8f67f0c2 | 242 | // coverity[store_truncates_time_t] |
f8f66c91 OM |
243 | while (axfr.getChunk(nop, &chunk, (axfrStart + axfrTimeout - axfrNow))) { |
244 | for (auto& dr : chunk) { | |
245 | if (dr.d_type == QType::NS || dr.d_type == QType::TSIG) { | |
246 | continue; | |
98c9ec39 | 247 | } |
248 | ||
6b972d59 | 249 | dr.d_name.makeUsRelative(zoneName); |
f8f66c91 OM |
250 | if (dr.d_type == QType::SOA) { |
251 | sr = getRR<SOARecordContent>(dr); | |
46572de5 | 252 | zone->setSOA(dr); |
f8f66c91 | 253 | continue; |
39ec5d29 | 254 | } |
39ec5d29 | 255 | |
62b191dc | 256 | RPZRecordToPolicy(dr, zone, true, defpol, defpolOverrideLocal, maxTTL, logger); |
39ec5d29 | 257 | nrecords++; |
f8f66c91 | 258 | } |
ea448a77 | 259 | axfrNow = time(nullptr); |
cb6218d2 | 260 | if (axfrNow < axfrStart || axfrNow - axfrStart > axfrTimeout) { |
ea448a77 PL |
261 | throw PDNSException("Total AXFR time exceeded!"); |
262 | } | |
f8f66c91 OM |
263 | if (last != time(0)) { |
264 | SLOG(g_log << Logger::Info << "Loaded & indexed " << nrecords << " policy records so far for RPZ zone '" << zoneName << "'" << endl, | |
8a38b7e6 | 265 | logger->info(Logr::Info, "RPZ load in progress", "nrecords", Logging::Loggable(nrecords))); |
f8f66c91 | 266 | last = time(0); |
39ec5d29 | 267 | } |
268 | } | |
f8f66c91 | 269 | SLOG(g_log << Logger::Info << "Done: " << nrecords << " policy records active, SOA: " << sr->getZoneRepresentation() << endl, |
8a38b7e6 | 270 | logger->info(Logr::Info, "RPZ load completed", "nrecords", Logging::Loggable(nrecords), "soa", Logging::Loggable(sr->getZoneRepresentation()))); |
39ec5d29 | 271 | return sr; |
272 | } | |
273 | ||
f8f66c91 | 274 | static LockGuarded<std::unordered_map<std::string, shared_ptr<rpzStats>>> s_rpzStats; |
f2c4f873 RG |
275 | |
276 | shared_ptr<rpzStats> getRPZZoneStats(const std::string& zone) | |
277 | { | |
278 | auto stats = s_rpzStats.lock(); | |
279 | auto it = stats->find(zone); | |
280 | if (it == stats->end()) { | |
281 | auto stat = std::make_shared<rpzStats>(); | |
282 | (*stats)[zone] = stat; | |
283 | return stat; | |
284 | } | |
285 | return it->second; | |
286 | } | |
287 | ||
288 | static void incRPZFailedTransfers(const std::string& zone) | |
289 | { | |
290 | auto stats = getRPZZoneStats(zone); | |
291 | if (stats != nullptr) | |
292 | stats->d_failedTransfers++; | |
293 | } | |
294 | ||
295 | static void setRPZZoneNewState(const std::string& zone, uint32_t serial, uint64_t numberOfRecords, bool fromFile, bool wasAXFR) | |
296 | { | |
297 | auto stats = getRPZZoneStats(zone); | |
298 | if (stats == nullptr) { | |
299 | return; | |
300 | } | |
301 | if (!fromFile) { | |
302 | stats->d_successfulTransfers++; | |
303 | if (wasAXFR) { | |
304 | stats->d_fullTransfers++; | |
305 | } | |
306 | } | |
307 | stats->d_lastUpdate = time(nullptr); | |
308 | stats->d_serial = serial; | |
309 | stats->d_numberOfRecords = numberOfRecords; | |
310 | } | |
311 | ||
c823f860 | 312 | // this function is silent - you do the logging |
d06dcda4 | 313 | std::shared_ptr<const SOARecordContent> loadRPZFromFile(const std::string& fname, std::shared_ptr<DNSFilterEngine::Zone> zone, const boost::optional<DNSFilterEngine::Policy>& defpol, bool defpolOverrideLocal, uint32_t maxTTL) |
39ec5d29 | 314 | { |
d06dcda4 | 315 | shared_ptr<const SOARecordContent> sr = nullptr; |
39ec5d29 | 316 | ZoneParserTNG zpt(fname); |
39cde30d | 317 | zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps")); |
e3fc3ebd | 318 | zpt.setMaxIncludes(::arg().asNum("max-include-depth")); |
39ec5d29 | 319 | DNSResourceRecord drr; |
46572de5 | 320 | DNSRecord soaRecord; |
644dd1da | 321 | DNSName domain; |
62b191dc | 322 | auto log = g_slog->withName("rpz")->withValues("file", Logging::Loggable(fname), "zone", Logging::Loggable(zone->getName())); |
f8f66c91 | 323 | while (zpt.get(drr)) { |
644dd1da | 324 | try { |
f8f66c91 OM |
325 | if (drr.qtype.getCode() == QType::CNAME && drr.content.empty()) |
326 | drr.content = "."; | |
644dd1da | 327 | DNSRecord dr(drr); |
f8f66c91 | 328 | if (dr.d_type == QType::SOA) { |
a66c5cfa | 329 | sr = getRR<SOARecordContent>(dr); |
6791663c RG |
330 | domain = dr.d_name; |
331 | zone->setDomain(domain); | |
50bd111e | 332 | soaRecord = std::move(dr); |
644dd1da | 333 | } |
f8f66c91 OM |
334 | else if (dr.d_type == QType::NS) { |
335 | continue; | |
39ec5d29 | 336 | } |
337 | else { | |
f8f66c91 OM |
338 | dr.d_name = dr.d_name.makeRelative(domain); |
339 | RPZRecordToPolicy(dr, zone, true, defpol, defpolOverrideLocal, maxTTL, log); | |
644dd1da | 340 | } |
341 | } | |
f8f66c91 OM |
342 | catch (const PDNSException& pe) { |
343 | throw PDNSException("Issue parsing '" + drr.qname.toLogString() + "' '" + drr.content + "' at " + zpt.getLineOfFile() + ": " + pe.reason); | |
644dd1da | 344 | } |
345 | } | |
a66c5cfa | 346 | |
d70a7627 OM |
347 | if (sr != nullptr) { |
348 | zone->setRefresh(sr->d_st.refresh); | |
50bd111e | 349 | zone->setSOA(std::move(soaRecord)); |
f2c4f873 | 350 | setRPZZoneNewState(zone->getName(), sr->d_st.serial, zone->size(), true, false); |
d70a7627 | 351 | } |
a66c5cfa | 352 | return sr; |
644dd1da | 353 | } |
4ba9d5dc | 354 | |
62b191dc | 355 | static bool dumpZoneToDisk(Logr::log_t logger, const DNSName& zoneName, const std::shared_ptr<DNSFilterEngine::Zone>& newZone, const std::string& dumpZoneFileName) |
23f886c0 | 356 | { |
62b191dc | 357 | logger->info(Logr::Debug, "Dumping zone to disk", "destination_file", Logging::Loggable(dumpZoneFileName)); |
23f886c0 RG |
358 | std::string temp = dumpZoneFileName + "XXXXXX"; |
359 | int fd = mkstemp(&temp.at(0)); | |
360 | if (fd < 0) { | |
f8f66c91 | 361 | SLOG(g_log << Logger::Warning << "Unable to open a file to dump the content of the RPZ zone " << zoneName << endl, |
d197c31c | 362 | logger->error(Logr::Error, errno, "Unable to create temporary file")); |
23f886c0 RG |
363 | return false; |
364 | } | |
365 | ||
f8f66c91 | 366 | auto fp = std::unique_ptr<FILE, int (*)(FILE*)>(fdopen(fd, "w+"), fclose); |
5e1f23ca | 367 | if (!fp) { |
f3cba034 | 368 | int err = errno; |
23f886c0 | 369 | close(fd); |
f8f66c91 | 370 | SLOG(g_log << Logger::Warning << "Unable to open a file pointer to dump the content of the RPZ zone " << zoneName << endl, |
d197c31c | 371 | logger->error(Logr::Error, err, "Unable to open file pointer")); |
23f886c0 RG |
372 | return false; |
373 | } | |
23f886c0 RG |
374 | |
375 | try { | |
5e1f23ca | 376 | newZone->dump(fp.get()); |
23f886c0 | 377 | } |
f8f66c91 OM |
378 | catch (const std::exception& e) { |
379 | SLOG(g_log << Logger::Warning << "Error while dumping the content of the RPZ zone " << zoneName << ": " << e.what() << endl, | |
d197c31c | 380 | logger->error(Logr::Error, e.what(), "Error while dumping the content of the RPZ")); |
23f886c0 RG |
381 | return false; |
382 | } | |
383 | ||
5e1f23ca | 384 | if (fflush(fp.get()) != 0) { |
f8f66c91 | 385 | SLOG(g_log << Logger::Warning << "Error while flushing the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl, |
8a38b7e6 | 386 | logger->error(Logr::Warning, errno, "Error while flushing the content of the RPZ")); |
23f886c0 RG |
387 | return false; |
388 | } | |
389 | ||
5e1f23ca | 390 | if (fsync(fileno(fp.get())) != 0) { |
f8f66c91 | 391 | SLOG(g_log << Logger::Warning << "Error while syncing the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl, |
d197c31c | 392 | logger->error(Logr::Error, errno, "Error while syncing the content of the RPZ")); |
23f886c0 RG |
393 | return false; |
394 | } | |
395 | ||
5e1f23ca | 396 | if (fclose(fp.release()) != 0) { |
f8f66c91 | 397 | SLOG(g_log << Logger::Warning << "Error while writing the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl, |
d197c31c | 398 | logger->error(Logr::Error, errno, "Error while writing the content of the RPZ")); |
23f886c0 RG |
399 | return false; |
400 | } | |
401 | ||
402 | if (rename(temp.c_str(), dumpZoneFileName.c_str()) != 0) { | |
f8f66c91 | 403 | SLOG(g_log << Logger::Warning << "Error while moving the content of the RPZ zone " << zoneName << " to the dump file: " << stringerror() << endl, |
d197c31c | 404 | logger->error(Logr::Error, errno, "Error while moving the content of the RPZ", "destination_file", Logging::Loggable(dumpZoneFileName))); |
23f886c0 RG |
405 | return false; |
406 | } | |
407 | ||
408 | return true; | |
409 | } | |
410 | ||
b59c7fc3 OM |
411 | // A struct that holds the condition var and related stuff to allow notifies to be sent to the tread owning |
412 | // the struct. | |
29691be2 OM |
413 | struct RPZWaiter |
414 | { | |
415 | RPZWaiter(std::thread::id arg) : | |
416 | id(arg) {} | |
b59c7fc3 OM |
417 | std::thread::id id; |
418 | std::mutex mutex; | |
419 | std::condition_variable condVar; | |
420 | std::atomic<bool> stop{false}; | |
421 | }; | |
422 | ||
423 | static void preloadRPZFIle(RPZTrackerParams& params, const DNSName& zoneName, std::shared_ptr<DNSFilterEngine::Zone>& oldZone, uint32_t& refresh, const string& polName, RPZWaiter& rpzwaiter, Logr::log_t logger) | |
4ba9d5dc | 424 | { |
4fc57a40 | 425 | while (!params.soaRecordContent) { |
a66c5cfa RG |
426 | /* if we received an empty sr, the zone was not really preloaded */ |
427 | ||
1bf8d12a RG |
428 | /* full copy, as promised */ |
429 | std::shared_ptr<DNSFilterEngine::Zone> newZone = std::make_shared<DNSFilterEngine::Zone>(*oldZone); | |
4fc57a40 | 430 | for (const auto& primary : params.primaries) { |
5f311886 | 431 | try { |
4fc57a40 OM |
432 | params.soaRecordContent = loadRPZFromServer(logger, primary, zoneName, newZone, params.defpol, params.defpolOverrideLocal, params.maxTTL, params.tsigtriplet, params.maxReceivedBytes, params.localAddress, params.xfrTimeout); |
433 | newZone->setSerial(params.soaRecordContent->d_st.serial); | |
434 | newZone->setRefresh(params.soaRecordContent->d_st.refresh); | |
435 | refresh = std::max(params.refreshFromConf != 0 ? params.refreshFromConf : newZone->getRefresh(), 1U); | |
436 | setRPZZoneNewState(polName, params.soaRecordContent->d_st.serial, newZone->size(), false, true); | |
5f311886 | 437 | |
4fc57a40 | 438 | g_luaconfs.modify([zoneIdx = params.zoneIdx, &newZone](LuaConfigItems& lci) { |
f8f66c91 OM |
439 | lci.dfe.setZone(zoneIdx, newZone); |
440 | }); | |
5f311886 | 441 | |
4fc57a40 OM |
442 | if (!params.dumpZoneFileName.empty()) { |
443 | dumpZoneToDisk(logger, zoneName, newZone, params.dumpZoneFileName); | |
5f311886 RG |
444 | } |
445 | ||
3d324e00 | 446 | /* no need to try another primary */ |
5f311886 | 447 | break; |
4ba9d5dc | 448 | } |
f8f66c91 OM |
449 | catch (const std::exception& e) { |
450 | SLOG(g_log << Logger::Warning << "Unable to load RPZ zone '" << zoneName << "' from '" << primary << "': '" << e.what() << "'. (Will try again in " << refresh << " seconds...)" << endl, | |
77d23f79 | 451 | logger->error(Logr::Warning, e.what(), "Unable to load RPZ zone, will retry", "from", Logging::Loggable(primary), "exception", Logging::Loggable("std::exception"), "refresh", Logging::Loggable(refresh))); |
5f311886 RG |
452 | incRPZFailedTransfers(polName); |
453 | } | |
f8f66c91 OM |
454 | catch (const PDNSException& e) { |
455 | SLOG(g_log << Logger::Warning << "Unable to load RPZ zone '" << zoneName << "' from '" << primary << "': '" << e.reason << "'. (Will try again in " << refresh << " seconds...)" << endl, | |
77d23f79 | 456 | logger->error(Logr::Warning, e.reason, "Unable to load RPZ zone, will retry", "from", Logging::Loggable(primary), "exception", Logging::Loggable("PDNSException"), "refresh", Logging::Loggable(refresh))); |
5f311886 | 457 | incRPZFailedTransfers(polName); |
23f886c0 | 458 | } |
4ba9d5dc | 459 | } |
60d691fb OM |
460 | // Release newZone before (long) sleep to reduce memory usage |
461 | newZone = nullptr; | |
4fc57a40 | 462 | if (!params.soaRecordContent) { |
b59c7fc3 OM |
463 | std::unique_lock lock(rpzwaiter.mutex); |
464 | rpzwaiter.condVar.wait_for(lock, std::chrono::seconds(refresh), | |
29691be2 | 465 | [&stop = rpzwaiter.stop] { return stop.load(); }); |
4ba9d5dc | 466 | } |
b59c7fc3 | 467 | rpzwaiter.stop = false; |
4ba9d5dc | 468 | } |
4fc57a40 | 469 | } |
4ba9d5dc | 470 | |
b59c7fc3 | 471 | static bool RPZTrackerIteration(RPZTrackerParams& params, const DNSName& zoneName, std::shared_ptr<DNSFilterEngine::Zone>& oldZone, uint32_t& refresh, const string& polName, bool& skipRefreshDelay, uint64_t configGeneration, RPZWaiter& rpzwaiter, Logr::log_t logger) |
4fc57a40 OM |
472 | { |
473 | // Don't hold on to oldZone, it well be re-assigned after sleep in the try block | |
474 | oldZone = nullptr; | |
b59c7fc3 OM |
475 | DNSRecord dnsRecord; |
476 | dnsRecord.setContent(params.soaRecordContent); | |
4ba9d5dc | 477 | |
4fc57a40 OM |
478 | if (skipRefreshDelay) { |
479 | skipRefreshDelay = false; | |
480 | } | |
481 | else { | |
b59c7fc3 OM |
482 | std::unique_lock lock(rpzwaiter.mutex); |
483 | rpzwaiter.condVar.wait_for(lock, std::chrono::seconds(refresh), | |
29691be2 | 484 | [&stop = rpzwaiter.stop] { return stop.load(); }); |
b59c7fc3 | 485 | rpzwaiter.stop = false; |
4fc57a40 OM |
486 | } |
487 | auto luaconfsLocal = g_luaconfs.getLocal(); | |
4ba9d5dc | 488 | |
4fc57a40 OM |
489 | if (luaconfsLocal->generation != configGeneration) { |
490 | /* the configuration has been reloaded, meaning that a new thread | |
491 | has been started to handle that zone and we are now obsolete. | |
492 | */ | |
493 | SLOG(g_log << Logger::Info << "A more recent configuration has been found, stopping the existing RPZ update thread for " << zoneName << endl, | |
494 | logger->info(Logr::Info, "A more recent configuration has been found, stopping the existing RPZ update thread")); | |
b59c7fc3 | 495 | return false; |
4fc57a40 | 496 | } |
13dacc77 | 497 | |
4fc57a40 OM |
498 | vector<pair<vector<DNSRecord>, vector<DNSRecord>>> deltas; |
499 | for (const auto& primary : params.primaries) { | |
b59c7fc3 | 500 | auto soa = getRR<SOARecordContent>(dnsRecord); |
4fc57a40 OM |
501 | auto serial = soa ? soa->d_st.serial : 0; |
502 | SLOG(g_log << Logger::Info << "Getting IXFR deltas for " << zoneName << " from " << primary.toStringWithPort() << ", our serial: " << serial << endl, | |
503 | logger->info(Logr::Info, "Getting IXFR deltas", "address", Logging::Loggable(primary), "ourserial", Logging::Loggable(serial))); | |
5f311886 | 504 | |
4fc57a40 OM |
505 | ComboAddress local(params.localAddress); |
506 | if (local == ComboAddress()) { | |
507 | local = pdns::getQueryLocalAddress(primary.sin4.sin_family, 0); | |
508 | } | |
4ba9d5dc | 509 | |
4fc57a40 | 510 | try { |
b59c7fc3 | 511 | deltas = getIXFRDeltas(primary, zoneName, dnsRecord, params.xfrTimeout, true, params.tsigtriplet, &local, params.maxReceivedBytes); |
4ba9d5dc | 512 | |
4fc57a40 OM |
513 | /* no need to try another primary */ |
514 | break; | |
4ba9d5dc | 515 | } |
4fc57a40 OM |
516 | catch (const std::runtime_error& e) { |
517 | SLOG(g_log << Logger::Warning << e.what() << endl, | |
518 | logger->error(Logr::Warning, e.what(), "Exception during retrieval of delta", "exception", Logging::Loggable("std::runtime_error"))); | |
519 | incRPZFailedTransfers(polName); | |
4ba9d5dc | 520 | continue; |
5f311886 | 521 | } |
4fc57a40 | 522 | } |
5f311886 | 523 | |
4fc57a40 | 524 | if (deltas.empty()) { |
b59c7fc3 | 525 | return true; |
4fc57a40 | 526 | } |
7d3cb915 | 527 | |
4fc57a40 OM |
528 | try { |
529 | SLOG(g_log << Logger::Info << "Processing " << deltas.size() << " delta" << addS(deltas) << " for RPZ " << zoneName << endl, | |
530 | logger->info(Logr::Info, "Processing deltas", "size", Logging::Loggable(deltas.size()))); | |
531 | ||
532 | if (luaconfsLocal->generation != configGeneration) { | |
533 | SLOG(g_log << Logger::Info << "A more recent configuration has been found, stopping the existing RPZ update thread for " << zoneName << endl, | |
534 | logger->info(Logr::Info, "A more recent configuration has been found, stopping the existing RPZ update thread")); | |
b59c7fc3 | 535 | return false; |
4fc57a40 OM |
536 | } |
537 | oldZone = luaconfsLocal->dfe.getZone(params.zoneIdx); | |
538 | if (!oldZone || oldZone->getDomain() != zoneName) { | |
539 | SLOG(g_log << Logger::Info << "This policy is no more, stopping the existing RPZ update thread for " << zoneName << endl, | |
540 | logger->info(Logr::Info, "This policy is no more, stopping the existing RPZ update thread")); | |
b59c7fc3 | 541 | return false; |
4fc57a40 OM |
542 | } |
543 | /* we need to make a _full copy_ of the zone we are going to work on */ | |
544 | std::shared_ptr<DNSFilterEngine::Zone> newZone = std::make_shared<DNSFilterEngine::Zone>(*oldZone); | |
545 | /* initialize the current serial to the last one */ | |
546 | std::shared_ptr<const SOARecordContent> currentSR = params.soaRecordContent; | |
547 | ||
548 | int totremove = 0; | |
549 | int totadd = 0; | |
550 | bool fullUpdate = false; | |
551 | for (const auto& delta : deltas) { | |
552 | const auto& remove = delta.first; | |
553 | const auto& add = delta.second; | |
554 | if (remove.empty()) { | |
555 | SLOG(g_log << Logger::Warning << "IXFR update is a whole new zone" << endl, | |
556 | logger->info(Logr::Warning, "IXFR update is a whole new zone")); | |
557 | newZone->clear(); | |
558 | fullUpdate = true; | |
0c98e3de | 559 | } |
4fc57a40 OM |
560 | for (const auto& resourceRecord : remove) { // should always contain the SOA |
561 | if (resourceRecord.d_type == QType::NS) { | |
562 | continue; | |
7d3cb915 | 563 | } |
4fc57a40 OM |
564 | if (resourceRecord.d_type == QType::SOA) { |
565 | auto oldsr = getRR<SOARecordContent>(resourceRecord); | |
566 | if (oldsr && oldsr->d_st.serial == currentSR->d_st.serial) { | |
046b69a5 | 567 | // Got good removal of SOA serial, no work to be done |
7d3cb915 RG |
568 | } |
569 | else { | |
4fc57a40 OM |
570 | if (!oldsr) { |
571 | throw std::runtime_error("Unable to extract serial from SOA record while processing the removal part of an update"); | |
7d3cb915 | 572 | } |
4fc57a40 | 573 | throw std::runtime_error("Received an unexpected serial (" + std::to_string(oldsr->d_st.serial) + ", expecting " + std::to_string(currentSR->d_st.serial) + ") from SOA record while processing the removal part of an update"); |
7d3cb915 RG |
574 | } |
575 | } | |
4fc57a40 OM |
576 | else { |
577 | totremove++; | |
578 | SLOG(g_log << (g_logRPZChanges ? Logger::Info : Logger::Debug) << "Had removal of " << resourceRecord.d_name << " from RPZ zone " << zoneName << endl, | |
579 | logger->info(g_logRPZChanges ? Logr::Info : Logr::Debug, "Remove from RPZ zone", "name", Logging::Loggable(resourceRecord.d_name))); | |
580 | RPZRecordToPolicy(resourceRecord, newZone, false, params.defpol, params.defpolOverrideLocal, params.maxTTL, logger); | |
581 | } | |
4ba9d5dc | 582 | } |
7d3cb915 | 583 | |
4fc57a40 OM |
584 | for (const auto& resourceRecord : add) { // should always contain the new SOA |
585 | if (resourceRecord.d_type == QType::NS) { | |
586 | continue; | |
587 | } | |
588 | if (resourceRecord.d_type == QType::SOA) { | |
589 | auto tempSR = getRR<SOARecordContent>(resourceRecord); | |
4fc57a40 OM |
590 | if (tempSR) { |
591 | currentSR = std::move(tempSR); | |
592 | } | |
593 | } | |
594 | else { | |
595 | totadd++; | |
596 | SLOG(g_log << (g_logRPZChanges ? Logger::Info : Logger::Debug) << "Had addition of " << resourceRecord.d_name << " to RPZ zone " << zoneName << endl, | |
597 | logger->info(g_logRPZChanges ? Logr::Info : Logr::Debug, "Addition to RPZ zone", "name", Logging::Loggable(resourceRecord.d_name))); | |
598 | RPZRecordToPolicy(resourceRecord, newZone, true, params.defpol, params.defpolOverrideLocal, params.maxTTL, logger); | |
599 | } | |
0c98e3de | 600 | } |
4fc57a40 | 601 | } |
4ba9d5dc | 602 | |
4fc57a40 OM |
603 | /* only update sr now that all changes have been converted */ |
604 | if (currentSR) { | |
605 | params.soaRecordContent = std::move(currentSR); | |
4ba9d5dc | 606 | } |
4fc57a40 OM |
607 | SLOG(g_log << Logger::Info << "Had " << totremove << " RPZ removal" << addS(totremove) << ", " << totadd << " addition" << addS(totadd) << " for " << zoneName << " New serial: " << params.soaRecordContent->d_st.serial << endl, |
608 | logger->info(Logr::Info, "RPZ mutations", "removals", Logging::Loggable(totremove), "additions", Logging::Loggable(totadd), "newserial", Logging::Loggable(params.soaRecordContent->d_st.serial))); | |
609 | newZone->setSerial(params.soaRecordContent->d_st.serial); | |
610 | newZone->setRefresh(params.soaRecordContent->d_st.refresh); | |
611 | setRPZZoneNewState(polName, params.soaRecordContent->d_st.serial, newZone->size(), false, fullUpdate); | |
612 | ||
613 | /* we need to replace the existing zone with the new one, | |
614 | but we don't want to touch anything else, especially other zones, | |
615 | since they might have been updated by another RPZ IXFR tracker thread. | |
616 | */ | |
617 | if (luaconfsLocal->generation != configGeneration) { | |
618 | SLOG(g_log << Logger::Info << "A more recent configuration has been found, stopping the existing RPZ update thread for " << zoneName << endl, | |
619 | logger->info(Logr::Info, "A more recent configuration has been found, stopping the existing RPZ update thread")); | |
b59c7fc3 | 620 | return false; |
23f886c0 | 621 | } |
4fc57a40 OM |
622 | g_luaconfs.modify([zoneIdx = params.zoneIdx, &newZone](LuaConfigItems& lci) { |
623 | lci.dfe.setZone(zoneIdx, newZone); | |
624 | }); | |
625 | ||
626 | if (!params.dumpZoneFileName.empty()) { | |
627 | dumpZoneToDisk(logger, zoneName, newZone, params.dumpZoneFileName); | |
55a99233 | 628 | } |
4fc57a40 OM |
629 | refresh = std::max(params.refreshFromConf != 0 ? params.refreshFromConf : newZone->getRefresh(), 1U); |
630 | } | |
631 | catch (const std::exception& e) { | |
632 | SLOG(g_log << Logger::Error << "Error while applying the update received over XFR for " << zoneName << ", skipping the update: " << e.what() << endl, | |
633 | logger->error(Logr::Error, e.what(), "Exception while applying the update received over XFR, skipping", "exception", Logging::Loggable("std::exception"))); | |
634 | } | |
635 | catch (const PDNSException& e) { | |
636 | SLOG(g_log << Logger::Error << "Error while applying the update received over XFR for " << zoneName << ", skipping the update: " << e.reason << endl, | |
637 | logger->error(Logr::Error, e.reason, "Exception while applying the update received over XFR, skipping", "exception", Logging::Loggable("PDNSException"))); | |
638 | } | |
b59c7fc3 OM |
639 | return true; |
640 | } | |
641 | ||
642 | // As there can be multiple threads doing updates (due to config reloads), we use a multimap. | |
643 | // The value contains the actual thread id that owns the struct. | |
644 | ||
645 | static LockGuarded<std::multimap<DNSName, RPZWaiter&>> condVars; | |
646 | ||
647 | // Notify all threads trakcing the RPZ name | |
648 | bool notifyRPZTracker(const DNSName& name) | |
649 | { | |
650 | auto lock = condVars.lock(); | |
651 | auto [start, end] = lock->equal_range(name); | |
652 | if (start == end) { | |
653 | // Did not find any thread tracking that RPZ name | |
654 | return false; | |
655 | } | |
656 | while (start != end) { | |
657 | start->second.stop = true; | |
658 | start->second.condVar.notify_one(); | |
659 | ++start; | |
660 | } | |
661 | return true; | |
4fc57a40 OM |
662 | } |
663 | ||
664 | void RPZIXFRTracker(RPZTrackerParams params, uint64_t configGeneration) | |
665 | { | |
666 | setThreadName("rec/rpzixfr"); | |
667 | bool isPreloaded = params.soaRecordContent != nullptr; | |
668 | auto logger = g_slog->withName("rpz"); | |
b59c7fc3 | 669 | RPZWaiter waiter(std::this_thread::get_id()); |
4fc57a40 OM |
670 | |
671 | /* we can _never_ modify this zone directly, we need to do a full copy then replace the existing zone */ | |
672 | std::shared_ptr<DNSFilterEngine::Zone> oldZone = g_luaconfs.getLocal()->dfe.getZone(params.zoneIdx); | |
673 | if (!oldZone) { | |
674 | SLOG(g_log << Logger::Error << "Unable to retrieve RPZ zone with index " << params.zoneIdx << " from the configuration, exiting" << endl, | |
675 | logger->error(Logr::Error, "Unable to retrieve RPZ zone from configuration", "index", Logging::Loggable(params.zoneIdx))); | |
676 | return; | |
677 | } | |
678 | ||
679 | // If oldZone failed to load its getRefresh() returns 0, protect against that | |
680 | uint32_t refresh = std::max(params.refreshFromConf != 0 ? params.refreshFromConf : oldZone->getRefresh(), 10U); | |
681 | DNSName zoneName = oldZone->getDomain(); | |
682 | std::string polName = !oldZone->getName().empty() ? oldZone->getName() : zoneName.toStringNoDot(); | |
683 | ||
684 | // Now that we know the name, set it in the logger | |
685 | logger = logger->withValues("zone", Logging::Loggable(zoneName)); | |
686 | ||
b59c7fc3 OM |
687 | { |
688 | auto lock = condVars.lock(); | |
689 | lock->emplace(zoneName, waiter); | |
690 | } | |
691 | preloadRPZFIle(params, zoneName, oldZone, refresh, polName, waiter, logger); | |
4fc57a40 OM |
692 | |
693 | bool skipRefreshDelay = isPreloaded; | |
694 | ||
b59c7fc3 OM |
695 | while (RPZTrackerIteration(params, zoneName, oldZone, refresh, polName, skipRefreshDelay, configGeneration, waiter, logger)) { |
696 | // empty | |
697 | } | |
698 | ||
699 | // Zap our (and only our) RPZWaiter struct out of the multimap | |
700 | auto lock = condVars.lock(); | |
701 | auto [start, end] = lock->equal_range(zoneName); | |
702 | while (start != end) { | |
703 | if (start->second.id == std::this_thread::get_id()) { | |
704 | start = lock->erase(start); | |
705 | } | |
706 | else { | |
707 | ++start; | |
708 | } | |
4ba9d5dc RG |
709 | } |
710 | } |