]>
Commit | Line | Data |
---|---|---|
12471842 PL |
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 | */ | |
39ec5d29 | 22 | #include "ixfr.hh" |
23 | #include "sstuff.hh" | |
24 | #include "dns_random.hh" | |
25 | #include "dnsrecords.hh" | |
189f9579 | 26 | #include "dnssecinfra.hh" |
60a1c204 | 27 | #include "tsigverifier.hh" |
7eafc52f | 28 | |
3d324e00 | 29 | vector<pair<vector<DNSRecord>, vector<DNSRecord> > > processIXFRRecords(const ComboAddress& primary, const DNSName& zone, |
d06dcda4 | 30 | const vector<DNSRecord>& records, const std::shared_ptr<const SOARecordContent>& primarySOA) |
d67ae3b4 RG |
31 | { |
32 | vector<pair<vector<DNSRecord>, vector<DNSRecord> > > ret; | |
33 | ||
3d324e00 | 34 | if (records.size() == 0 || primarySOA == nullptr) { |
d67ae3b4 RG |
35 | return ret; |
36 | } | |
37 | ||
38 | // we start at 1 to skip the first SOA record | |
39 | // we don't increase pos because the final SOA | |
40 | // of the previous sequence is also the first SOA | |
41 | // of this one | |
42 | for(unsigned int pos = 1; pos < records.size(); ) { | |
43 | vector<DNSRecord> remove, add; | |
44 | ||
45 | // cerr<<"Looking at record in position "<<pos<<" of type "<<QType(records[pos].d_type).getName()<<endl; | |
46 | ||
47 | if (records[pos].d_type != QType::SOA) { | |
48 | // this is an actual AXFR! | |
49 | return {{remove, records}}; | |
50 | } | |
51 | ||
52 | auto sr = getRR<SOARecordContent>(records[pos]); | |
53 | if (!sr) { | |
3d324e00 | 54 | throw std::runtime_error("Error getting the content of the first SOA record of this IXFR sequence for zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort()+"'"); |
d67ae3b4 RG |
55 | } |
56 | ||
3d324e00 | 57 | // cerr<<"Serial is "<<sr->d_st.serial<<", final serial is "<<primarySOA->d_st.serial<<endl; |
d67ae3b4 RG |
58 | |
59 | // the serial of this SOA record is the serial of the | |
60 | // zone before the removals and updates of this sequence | |
3d324e00 | 61 | if (sr->d_st.serial == primarySOA->d_st.serial) { |
98b33176 RG |
62 | if (records.size() == 2) { |
63 | // if the entire update is two SOAs records with the same | |
64 | // serial, this is actually an empty AXFR! | |
65 | return {{remove, records}}; | |
66 | } | |
67 | ||
d67ae3b4 RG |
68 | // if it's the final SOA, there is nothing for us to see |
69 | break; | |
70 | } | |
71 | ||
72 | remove.push_back(records[pos]); // this adds the SOA | |
73 | ||
74 | // process removals | |
75 | for(pos++; pos < records.size() && records[pos].d_type != QType::SOA; ++pos) { | |
76 | remove.push_back(records[pos]); | |
77 | } | |
78 | ||
79 | if (pos >= records.size()) { | |
3d324e00 | 80 | throw std::runtime_error("No SOA record to finish the removals part of the IXFR sequence of zone '" + zone.toLogString() + "' from " + primary.toStringWithPort()); |
d67ae3b4 RG |
81 | } |
82 | ||
83 | sr = getRR<SOARecordContent>(records[pos]); | |
84 | if (!sr) { | |
3d324e00 | 85 | throw std::runtime_error("Invalid SOA record to finish the removals part of the IXFR sequence of zone '" + zone.toLogString() + "' from " + primary.toStringWithPort()); |
d67ae3b4 RG |
86 | } |
87 | ||
88 | // this is the serial of the zone after the removals | |
89 | // and updates, but that might not be the final serial | |
90 | // because there might be several sequences | |
91 | uint32_t newSerial = sr->d_st.serial; | |
92 | add.push_back(records[pos]); // this adds the new SOA | |
93 | ||
94 | // process additions | |
95 | for(pos++; pos < records.size() && records[pos].d_type != QType::SOA; ++pos) { | |
96 | add.push_back(records[pos]); | |
97 | } | |
98 | ||
99 | if (pos >= records.size()) { | |
3d324e00 | 100 | throw std::runtime_error("No SOA record to finish the additions part of the IXFR sequence of zone '" + zone.toLogString() + "' from " + primary.toStringWithPort()); |
d67ae3b4 RG |
101 | } |
102 | ||
103 | sr = getRR<SOARecordContent>(records[pos]); | |
104 | if (!sr) { | |
3d324e00 | 105 | throw std::runtime_error("Invalid SOA record to finish the additions part of the IXFR sequence of zone '" + zone.toLogString() + "' from " + primary.toStringWithPort()); |
d67ae3b4 RG |
106 | } |
107 | ||
108 | if (sr->d_st.serial != newSerial) { | |
3d324e00 | 109 | throw std::runtime_error("Invalid serial (" + std::to_string(sr->d_st.serial) + ", expecting " + std::to_string(newSerial) + ") in the SOA record finishing the additions part of the IXFR sequence of zone '" + zone.toLogString() + "' from " + primary.toStringWithPort()); |
d67ae3b4 RG |
110 | } |
111 | ||
3d324e00 | 112 | if (newSerial == primarySOA->d_st.serial) { |
d67ae3b4 RG |
113 | // this was the last sequence |
114 | if (pos != (records.size() - 1)) { | |
3d324e00 | 115 | throw std::runtime_error("Trailing records after the last IXFR sequence of zone '" + zone.toLogString() + "' from " + primary.toStringWithPort()); |
d67ae3b4 RG |
116 | } |
117 | } | |
118 | ||
e32a8d46 | 119 | ret.emplace_back(remove, add); |
d67ae3b4 RG |
120 | } |
121 | ||
122 | return ret; | |
123 | } | |
124 | ||
3e7dcee6 | 125 | // Returns pairs of "remove & add" vectors. If you get an empty remove, it means you got an AXFR! |
8faf5a90 | 126 | // NOLINTNEXTLINE(readability-function-cognitive-complexity): https://github.com/PowerDNS/pdns/issues/12791 |
fee334ae OM |
127 | vector<pair<vector<DNSRecord>, vector<DNSRecord>>> getIXFRDeltas(const ComboAddress& primary, const DNSName& zone, const DNSRecord& oursr, |
128 | uint16_t xfrTimeout, bool totalTimeout, | |
129 | const TSIGTriplet& tt, const ComboAddress* laddr, size_t maxReceivedBytes) | |
39ec5d29 | 130 | { |
240460d7 OM |
131 | // Auth documents xfrTimeout to be a max idle time (sets totalTimeout=false) |
132 | // Rec documents it to be a total XFR time (sets totalTimeout=true) | |
fee334ae | 133 | // |
39ec5d29 | 134 | vector<pair<vector<DNSRecord>, vector<DNSRecord> > > ret; |
135 | vector<uint8_t> packet; | |
136 | DNSPacketWriter pw(packet, zone, QType::IXFR); | |
137 | pw.getHeader()->qr=0; | |
138 | pw.getHeader()->rd=0; | |
a410b176 | 139 | pw.getHeader()->id=dns_random_uint16(); |
189f9579 | 140 | pw.startRecord(zone, QType::SOA, 0, QClass::IN, DNSResourceRecord::AUTHORITY); |
d06dcda4 | 141 | oursr.getContent()->toPacket(pw); |
189f9579 | 142 | |
39ec5d29 | 143 | pw.commit(); |
60a1c204 | 144 | TSIGRecordContent trc; |
3d324e00 | 145 | TSIGTCPVerifier tsigVerifier(tt, primary, trc); |
98c9ec39 | 146 | if(!tt.algo.empty()) { |
bd051ad6 PL |
147 | TSIGHashEnum the; |
148 | getTSIGHashEnum(tt.algo, the); | |
bd051ad6 PL |
149 | try { |
150 | trc.d_algoName = getTSIGAlgoName(the); | |
151 | } catch(PDNSException& pe) { | |
86f1af1c | 152 | throw std::runtime_error("TSIG algorithm '"+tt.algo.toLogString()+"' is unknown."); |
bd051ad6 | 153 | } |
4646277d | 154 | trc.d_time = time((time_t*)nullptr); |
189f9579 | 155 | trc.d_fudge = 300; |
156 | trc.d_origID=ntohs(pw.getHeader()->id); | |
157 | trc.d_eRcode=0; | |
ea3816cf | 158 | addTSIG(pw, trc, tt.name, tt.secret, "", false); |
189f9579 | 159 | } |
39ec5d29 | 160 | uint16_t len=htons(packet.size()); |
161 | string msg((const char*)&len, 2); | |
162 | msg.append((const char*)&packet[0], packet.size()); | |
163 | ||
3d324e00 | 164 | Socket s(primary.sin4.sin_family, SOCK_STREAM); |
fee334ae | 165 | if (laddr != nullptr) { |
3e7dcee6 | 166 | s.bind(*laddr); |
fee334ae OM |
167 | } |
168 | s.setNonBlocking(); | |
169 | ||
170 | const time_t xfrStart = time(nullptr); | |
171 | ||
172 | // Helper function: if we have a total timeout, check it and set elapsed to the total time taken sofar, | |
173 | // otherwise set elapsed to 0, making the total time limit ineffective | |
174 | const auto timeoutChecker = [=] () -> time_t { | |
175 | time_t elapsed = 0; | |
176 | if (totalTimeout) { | |
177 | elapsed = time(nullptr) - xfrStart; | |
178 | if (elapsed >= xfrTimeout) { | |
179 | throw std::runtime_error("Reached the maximum elapsed time in an IXFR delta for zone '" + zone.toLogString() + "' from primary " + primary.toStringWithPort()); | |
180 | } | |
181 | } | |
182 | return elapsed; | |
183 | }; | |
184 | ||
185 | s.connect(primary, xfrTimeout); | |
186 | ||
187 | time_t elapsed = timeoutChecker(); | |
8f67f0c2 | 188 | // coverity[store_truncates_time_t] |
fee334ae | 189 | s.writenWithTimeout(msg.data(), msg.size(), xfrTimeout - elapsed); |
39ec5d29 | 190 | |
3d324e00 | 191 | // CURRENT PRIMARY SOA |
39ec5d29 | 192 | // REPEAT: |
193 | // SOA WHERE THIS DELTA STARTS | |
194 | // RECORDS TO REMOVE | |
195 | // SOA WHERE THIS DELTA GOES | |
196 | // RECORDS TO ADD | |
fee334ae | 197 | // CURRENT PRIMARY SOA |
d06dcda4 | 198 | std::shared_ptr<const SOARecordContent> primarySOA = nullptr; |
39ec5d29 | 199 | vector<DNSRecord> records; |
db8f9152 | 200 | size_t receivedBytes = 0; |
32fc3b03 | 201 | std::string reply; |
60a1c204 | 202 | |
8bed4b38 OM |
203 | enum transferStyle { Unknown, AXFR, IXFR } style = Unknown; |
204 | const unsigned int expectedSOAForAXFR = 2; | |
205 | const unsigned int expectedSOAForIXFR = 3; | |
206 | unsigned int primarySOACount = 0; | |
207 | ||
8faf5a90 | 208 | std::string state; |
fee334ae | 209 | for (;;) { |
8bed4b38 OM |
210 | // IXFR or AXFR style end reached? We don't want to process trailing data after the closing SOA |
211 | if (style == AXFR && primarySOACount == expectedSOAForAXFR) { | |
8faf5a90 | 212 | state = "AXFRdone"; |
8bed4b38 OM |
213 | break; |
214 | } | |
8faf5a90 PD |
215 | if (style == IXFR && primarySOACount == expectedSOAForIXFR) { |
216 | state = "IXFRdone"; | |
397ed71d | 217 | break; |
8bed4b38 | 218 | } |
397ed71d | 219 | |
fee334ae | 220 | elapsed = timeoutChecker(); |
8faf5a90 PD |
221 | try { |
222 | const struct timeval remainingTime = { .tv_sec = xfrTimeout - elapsed, .tv_usec = 0 }; | |
223 | const struct timeval idleTime = remainingTime; | |
224 | readn2WithTimeout(s.getHandle(), &len, sizeof(len), idleTime, remainingTime, false); | |
225 | } | |
226 | catch (const runtime_error& ex) { | |
227 | state = ex.what(); | |
39ec5d29 | 228 | break; |
fee334ae | 229 | } |
d67ae3b4 | 230 | |
fee334ae OM |
231 | len = ntohs(len); |
232 | if (len == 0) { | |
8faf5a90 | 233 | state = "zeroLen"; |
39ec5d29 | 234 | break; |
fee334ae | 235 | } |
8faf5a90 | 236 | // Currently no more break statements after this |
db8f9152 | 237 | |
fee334ae | 238 | if (maxReceivedBytes > 0 && (maxReceivedBytes - receivedBytes) < (size_t) len) { |
3d324e00 | 239 | throw std::runtime_error("Reached the maximum number of received bytes in an IXFR delta for zone '"+zone.toLogString()+"' from primary "+primary.toStringWithPort()); |
fee334ae | 240 | } |
db8f9152 | 241 | |
32fc3b03 | 242 | reply.resize(len); |
fee334ae OM |
243 | |
244 | elapsed = timeoutChecker(); | |
8faf5a90 | 245 | const struct timeval remainingTime = { .tv_sec = xfrTimeout - elapsed, .tv_usec = 0 }; |
fee334ae | 246 | const struct timeval idleTime = remainingTime; |
8faf5a90 | 247 | readn2WithTimeout(s.getHandle(), reply.data(), len, idleTime, remainingTime, false); |
db8f9152 | 248 | receivedBytes += len; |
d67ae3b4 | 249 | |
32fc3b03 | 250 | MOADNSParser mdp(false, reply); |
fee334ae | 251 | if (mdp.d_header.rcode) { |
3d324e00 | 252 | throw std::runtime_error("Got an error trying to IXFR zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort()+"': "+RCode::to_s(mdp.d_header.rcode)); |
fee334ae | 253 | } |
189f9579 | 254 | |
fee334ae | 255 | if (!tt.algo.empty()) { // TSIG verify message |
32fc3b03 | 256 | tsigVerifier.check(reply, mdp); |
60a1c204 RG |
257 | } |
258 | ||
fee334ae | 259 | for (auto& r: mdp.d_answers) { |
3d324e00 | 260 | if(!primarySOA) { |
d67ae3b4 RG |
261 | // we have not seen the first SOA record yet |
262 | if (r.first.d_type != QType::SOA) { | |
d5fcd583 | 263 | throw std::runtime_error("The first record of the IXFR answer for zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort()+"' is not a SOA ("+QType(r.first.d_type).toString()+")"); |
d67ae3b4 RG |
264 | } |
265 | ||
397ed71d LX |
266 | auto sr = getRR<SOARecordContent>(r.first); |
267 | if (!sr) { | |
3d324e00 | 268 | throw std::runtime_error("Error getting the content of the first SOA record of the IXFR answer for zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort()+"'"); |
d67ae3b4 RG |
269 | } |
270 | ||
d06dcda4 | 271 | if(sr->d_st.serial == getRR<SOARecordContent>(oursr)->d_st.serial) { |
d67ae3b4 RG |
272 | // we are up to date |
273 | return ret; | |
274 | } | |
4c5a50dc | 275 | primarySOA = std::move(sr); |
8bed4b38 | 276 | ++primarySOACount; |
4f5cde49 LX |
277 | } else if (r.first.d_type == QType::SOA) { |
278 | auto sr = getRR<SOARecordContent>(r.first); | |
279 | if (!sr) { | |
3d324e00 | 280 | throw std::runtime_error("Error getting the content of SOA record of IXFR answer for zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort()+"'"); |
4f5cde49 LX |
281 | } |
282 | ||
8bed4b38 | 283 | // we hit a marker SOA record |
3d324e00 | 284 | if (primarySOA->d_st.serial == sr->d_st.serial) { |
8bed4b38 OM |
285 | ++primarySOACount; |
286 | } | |
287 | } | |
288 | // When we see the 2nd record, we can decide what the style is | |
289 | if (records.size() == 1 && style == Unknown) { | |
290 | if (r.first.d_type != QType::SOA) { | |
291 | // Non-empty AXFR style has a non-SOA record following the first SOA | |
292 | style = AXFR; | |
293 | } | |
294 | else if (primarySOACount == expectedSOAForAXFR) { | |
295 | // Empty zone AXFR style: start SOA is immediately followed by end marker SOA | |
296 | style = AXFR; | |
297 | } | |
298 | else { | |
299 | // IXFR has a 2nd SOA (with different serial) following the first | |
300 | style = IXFR; | |
397ed71d | 301 | } |
39ec5d29 | 302 | } |
7eafc52f | 303 | |
d67ae3b4 RG |
304 | if(r.first.d_place != DNSResourceRecord::ANSWER) { |
305 | if(r.first.d_type == QType::TSIG) | |
306 | continue; | |
39ec5d29 | 307 | |
d67ae3b4 RG |
308 | if(r.first.d_type == QType::OPT) |
309 | continue; | |
310 | ||
8fb5bba0 | 311 | throw std::runtime_error("Unexpected record (" +QType(r.first.d_type).toString()+") in non-answer section ("+std::to_string(r.first.d_place)+") in IXFR response for zone '"+zone.toLogString()+"' from primary '"+primary.toStringWithPort()); |
d67ae3b4 RG |
312 | } |
313 | ||
314 | r.first.d_name.makeUsRelative(zone); | |
315 | records.push_back(r.first); | |
39ec5d29 | 316 | } |
39ec5d29 | 317 | } |
d67ae3b4 | 318 | |
8bed4b38 OM |
319 | switch (style) { |
320 | case IXFR: | |
321 | if (primarySOACount != expectedSOAForIXFR) { | |
8faf5a90 | 322 | throw std::runtime_error("Incomplete IXFR transfer (primarySOACount=" + std::to_string(primarySOACount) + ") for '" + zone.toLogString() + "' from primary '" + primary.toStringWithPort() + " state=" + state); |
8bed4b38 OM |
323 | } |
324 | break; | |
325 | case AXFR: | |
326 | if (primarySOACount != expectedSOAForAXFR){ | |
8faf5a90 | 327 | throw std::runtime_error("Incomplete AXFR style transfer (primarySOACount=" + std::to_string(primarySOACount) + ") for '" + zone.toLogString() + "' from primary '" + primary.toStringWithPort() + " state=" + state); |
8bed4b38 OM |
328 | } |
329 | break; | |
330 | case Unknown: | |
8faf5a90 | 331 | throw std::runtime_error("Incomplete XFR (primarySOACount=" + std::to_string(primarySOACount) + ") for '" + zone.toLogString() + "' from primary '" + primary.toStringWithPort() + " state=" + state); |
8bed4b38 OM |
332 | break; |
333 | } | |
d67ae3b4 | 334 | |
3d324e00 | 335 | return processIXFRRecords(primary, zone, records, primarySOA); |
39ec5d29 | 336 | } |