1 /*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
2 
3     This program is free software: you can redistribute it and/or modify
4     it under the terms of the GNU General Public License as published by
5     the Free Software Foundation, either version 3 of the License, or
6     (at your option) any later version.
7 
8     This program is distributed in the hope that it will be useful,
9     but WITHOUT ANY WARRANTY; without even the implied warranty of
10     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11     GNU General Public License for more details.
12 
13     You should have received a copy of the GNU General Public License
14     along with this program.  If not, see <https://www.gnu.org/licenses/>.
15  */
16 
17 #include <assert.h>
18 
19 #include "knot/catalog/generate.h"
20 #include "knot/common/log.h"
21 #include "knot/conf/conf.h"
22 #include "knot/dnssec/key-events.h"
23 #include "knot/dnssec/zone-events.h"
24 #include "knot/events/handlers.h"
25 #include "knot/events/replan.h"
26 #include "knot/zone/digest.h"
27 #include "knot/zone/serial.h"
28 #include "knot/zone/zone-diff.h"
29 #include "knot/zone/zone-load.h"
30 #include "knot/zone/zone.h"
31 #include "knot/zone/zonefile.h"
32 #include "knot/updates/acl.h"
33 
dontcare_load_error(conf_t * conf,const zone_t * zone)34 static bool dontcare_load_error(conf_t *conf, const zone_t *zone)
35 {
36 	return (zone->contents == NULL && zone_load_can_bootstrap(conf, zone->name));
37 }
38 
allowed_xfr(conf_t * conf,const zone_t * zone)39 static bool allowed_xfr(conf_t *conf, const zone_t *zone)
40 {
41 	conf_val_t acl = conf_zone_get(conf, C_ACL, zone->name);
42 	while (acl.code == KNOT_EOK) {
43 		conf_val_t action = conf_id_get(conf, C_ACL, C_ACTION, &acl);
44 		while (action.code == KNOT_EOK) {
45 			if (conf_opt(&action) == ACL_ACTION_TRANSFER) {
46 				return true;
47 			}
48 			conf_val_next(&action);
49 		}
50 		conf_val_next(&acl);
51 	}
52 
53 	return false;
54 }
55 
event_load(conf_t * conf,zone_t * zone)56 int event_load(conf_t *conf, zone_t *zone)
57 {
58 	zone_update_t up = { 0 };
59 	zone_contents_t *journal_conts = NULL, *zf_conts = NULL;
60 	bool old_contents_exist = (zone->contents != NULL), zone_in_journal_exists = false;
61 
62 	conf_val_t val = conf_zone_get(conf, C_JOURNAL_CONTENT, zone->name);
63 	unsigned load_from = conf_opt(&val);
64 
65 	val = conf_zone_get(conf, C_ZONEFILE_LOAD, zone->name);
66 	unsigned zf_from = conf_opt(&val);
67 
68 	int ret = KNOT_EOK;
69 
70 	// If configured, load journal contents.
71 	if (!old_contents_exist &&
72 	    ((load_from == JOURNAL_CONTENT_ALL && zf_from != ZONEFILE_LOAD_WHOLE) ||
73 	     zone->cat_members != NULL)) {
74 		ret = zone_load_from_journal(conf, zone, &journal_conts);
75 		switch (ret) {
76 		case KNOT_EOK:
77 			zone_in_journal_exists = true;
78 			break;
79 		case KNOT_ENOENT:
80 			zone_in_journal_exists = false;
81 			break;
82 		default:
83 			goto cleanup;
84 		}
85 	} else {
86 		zone_in_journal_exists = zone_journal_has_zij(zone);
87 	}
88 
89 	// If configured, attempt to load zonefile.
90 	if (zf_from != ZONEFILE_LOAD_NONE && zone->cat_members == NULL) {
91 		struct timespec mtime;
92 		char *filename = conf_zonefile(conf, zone->name);
93 		ret = zonefile_exists(filename, &mtime);
94 		bool zonefile_unchanged = (zone->zonefile.exists &&
95 					   zone->zonefile.mtime.tv_sec == mtime.tv_sec &&
96 					   zone->zonefile.mtime.tv_nsec == mtime.tv_nsec);
97 		if (ret == KNOT_EOK) {
98 			ret = zone_load_contents(conf, zone->name, &zf_conts, false);
99 		}
100 		if (ret != KNOT_EOK) {
101 			zf_conts = NULL;
102 			if (dontcare_load_error(conf, zone)) {
103 				log_zone_info(zone->name, "failed to parse zone file '%s' (%s)",
104 				              filename, knot_strerror(ret));
105 			} else {
106 				log_zone_error(zone->name, "failed to parse zone file '%s' (%s)",
107 				               filename, knot_strerror(ret));
108 			}
109 			free(filename);
110 			goto load_end;
111 		}
112 		free(filename);
113 
114 		// Save zonefile information.
115 		zone->zonefile.serial = zone_contents_serial(zf_conts);
116 		zone->zonefile.exists = (zf_conts != NULL);
117 		zone->zonefile.mtime = mtime;
118 
119 		// If configured and possible, fix the SOA serial of zonefile.
120 		zone_contents_t *relevant = (zone->contents != NULL ? zone->contents : journal_conts);
121 		if (zf_conts != NULL && zf_from == ZONEFILE_LOAD_DIFSE && relevant != NULL) {
122 			uint32_t serial = zone_contents_serial(relevant);
123 			conf_val_t policy = conf_zone_get(conf, C_SERIAL_POLICY, zone->name);
124 			uint32_t set = serial_next(serial, conf_opt(&policy), 1);
125 			zone_contents_set_soa_serial(zf_conts, set);
126 			log_zone_info(zone->name, "zone file parsed, serial corrected %u -> %u",
127 			              zone->zonefile.serial, set);
128 			zone->zonefile.serial = set;
129 		} else {
130 			log_zone_info(zone->name, "zone file parsed, serial %u",
131 			              zone->zonefile.serial);
132 		}
133 
134 		// If configured and appliable to zonefile, load journal changes.
135 		bool journal_load_configured1 = (load_from == JOURNAL_CONTENT_CHANGES);
136 		bool journal_load_configured2 = (load_from == JOURNAL_CONTENT_ALL);
137 
138 		if ((journal_load_configured1 || journal_load_configured2) &&
139 		    (!old_contents_exist || zonefile_unchanged)) {
140 			ret = zone_load_journal(conf, zone, zf_conts);
141 			if (ret != KNOT_EOK) {
142 				zone_contents_deep_free(zf_conts);
143 				zf_conts = NULL;
144 				log_zone_warning(zone->name, "failed to load journal (%s)",
145 				                 knot_strerror(ret));
146 			}
147 		}
148 	}
149 	if (zone->cat_members != NULL && !old_contents_exist) {
150 		uint32_t serial = journal_conts == NULL ? 1 : zone_contents_serial(journal_conts);
151 		serial = serial_next(serial, SERIAL_POLICY_UNIXTIME, 1); // unixtime hardcoded
152 		zf_conts = catalog_update_to_zone(zone->cat_members, zone->name, serial);
153 		if (zf_conts == NULL) {
154 			ret = zone->cat_members->error == KNOT_EOK ? KNOT_ENOMEM : zone->cat_members->error;
155 			goto cleanup;
156 		}
157 	}
158 
159 	// If configured contents=all, but not present, store zonefile.
160 	if ((load_from == JOURNAL_CONTENT_ALL || zone->cat_members != NULL) &&
161 	    !zone_in_journal_exists && (zf_conts != NULL || old_contents_exist)) {
162 		zone_contents_t *store_c = old_contents_exist ? zone->contents : zf_conts;
163 		ret = zone_in_journal_store(conf, zone, store_c);
164 		if (ret != KNOT_EOK) {
165 			log_zone_warning(zone->name, "failed to write zone-in-journal (%s)",
166 			                 knot_strerror(ret));
167 		} else {
168 			zone_in_journal_exists = true;
169 		}
170 	}
171 
172 	val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name);
173 	bool dnssec_enable = (conf_bool(&val) && zone->cat_members == NULL), zu_from_zf_conts = false;
174 	bool do_diff = (zf_from == ZONEFILE_LOAD_DIFF || zf_from == ZONEFILE_LOAD_DIFSE || zone->cat_members != NULL);
175 	bool ignore_dnssec = (do_diff && dnssec_enable);
176 
177 	// Create zone_update structure according to current state.
178 	if (old_contents_exist) {
179 		if (zone->cat_members != NULL) {
180 			ret = zone_update_init(&up, zone, UPDATE_INCREMENTAL);
181 			if (ret == KNOT_EOK) {
182 				ret = catalog_update_to_update(zone->cat_members, &up);
183 			}
184 			if (ret == KNOT_EOK) {
185 				ret = zone_update_increment_soa(&up, conf);
186 			}
187 		} else if (zf_conts == NULL) {
188 			// nothing to be re-loaded
189 			ret = KNOT_EOK;
190 			goto cleanup;
191 		} else if (zf_from == ZONEFILE_LOAD_WHOLE) {
192 			// throw old zone contents and load new from ZF
193 			ret = zone_update_from_contents(&up, zone, zf_conts,
194 			                                (load_from == JOURNAL_CONTENT_NONE ?
195 			                                 UPDATE_FULL : UPDATE_HYBRID));
196 			zu_from_zf_conts = true;
197 		} else {
198 			// compute ZF diff and if success, apply it
199 			ret = zone_update_from_differences(&up, zone, NULL, zf_conts, UPDATE_INCREMENTAL, ignore_dnssec);
200 		}
201 	} else {
202 		if (journal_conts != NULL && (zf_from != ZONEFILE_LOAD_WHOLE || zone->cat_members != NULL)) {
203 			if (zf_conts == NULL) {
204 				// load zone-in-journal
205 				ret = zone_update_from_contents(&up, zone, journal_conts, UPDATE_HYBRID);
206 			} else {
207 				// load zone-in-journal, compute ZF diff and if success, apply it
208 				ret = zone_update_from_differences(&up, zone, journal_conts, zf_conts,
209 				                                   UPDATE_HYBRID, ignore_dnssec);
210 				if (ret == KNOT_ESEMCHECK || ret == KNOT_ERANGE) {
211 					log_zone_warning(zone->name,
212 					                 "zone file changed with SOA serial %s, "
213 					                 "ignoring zone file and loading from journal",
214 					                 (ret == KNOT_ESEMCHECK ? "unupdated" : "decreased"));
215 					zone_contents_deep_free(zf_conts);
216 					zf_conts = NULL;
217 					ret = zone_update_from_contents(&up, zone, journal_conts, UPDATE_HYBRID);
218 				}
219 			}
220 		} else {
221 			if (zf_conts == NULL) {
222 				// nothing to be loaded
223 				ret = KNOT_ENOENT;
224 			} else {
225 				// load from ZF
226 				ret = zone_update_from_contents(&up, zone, zf_conts,
227 				                                (load_from == JOURNAL_CONTENT_NONE ?
228 				                                 UPDATE_FULL : UPDATE_HYBRID));
229 				if (zf_from == ZONEFILE_LOAD_WHOLE) {
230 					zu_from_zf_conts = true;
231 				}
232 			}
233 		}
234 	}
235 
236 load_end:
237 	if (ret != KNOT_EOK) {
238 		switch (ret) {
239 		case KNOT_ENOENT:
240 			if (zone_load_can_bootstrap(conf, zone->name)) {
241 				log_zone_info(zone->name, "zone will be bootstrapped");
242 			} else {
243 				log_zone_info(zone->name, "zone not found");
244 			}
245 			break;
246 		case KNOT_ESEMCHECK:
247 			log_zone_warning(zone->name, "zone file changed without SOA serial update");
248 			break;
249 		case KNOT_ERANGE:
250 			log_zone_warning(zone->name, "zone file changed, but SOA serial decreased");
251 			break;
252 		}
253 		goto cleanup;
254 	}
255 
256 	bool zf_serial_updated = (zf_conts != NULL && zone_contents_serial(zf_conts) != zone_contents_serial(zone->contents));
257 
258 	// The contents are already part of zone_update.
259 	zf_conts = NULL;
260 	journal_conts = NULL;
261 
262 	ret = zone_update_verify_digest(conf, &up);
263 	if (ret != KNOT_EOK) {
264 		goto cleanup;
265 	}
266 
267 	uint32_t middle_serial = zone_contents_serial(up.new_cont);
268 
269 	if (do_diff && old_contents_exist && dnssec_enable && zf_serial_updated &&
270 	    !zone_in_journal_exists) {
271 		ret = zone_update_start_extra(&up, conf);
272 		if (ret != KNOT_EOK) {
273 			goto cleanup;
274 		}
275 	}
276 
277 	val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name);
278 	unsigned digest_alg = conf_opt(&val);
279 
280 	// Sign zone using DNSSEC if configured.
281 	zone_sign_reschedule_t dnssec_refresh = { 0 };
282 	if (dnssec_enable) {
283 		ret = knot_dnssec_zone_sign(&up, conf, 0, KEY_ROLL_ALLOW_ALL, 0, &dnssec_refresh);
284 		if (ret != KNOT_EOK) {
285 			goto cleanup;
286 		}
287 		if (zu_from_zf_conts && (up.flags & UPDATE_HYBRID) && allowed_xfr(conf, zone)) {
288 			log_zone_warning(zone->name,
289 			                 "with automatic DNSSEC signing and outgoing transfers enabled, "
290 			                 "'zonefile-load: difference' should be set to avoid malformed "
291 			                 "IXFR after manual zone file update");
292 		}
293 	} else if (digest_alg != ZONE_DIGEST_NONE) {
294 		if (zone_update_to(&up) == NULL || middle_serial == zone->zonefile.serial) {
295 			ret = zone_update_increment_soa(&up, conf);
296 		}
297 		if (ret == KNOT_EOK) {
298 			ret = zone_update_add_digest(&up, digest_alg, false);
299 		}
300 		if (ret != KNOT_EOK) {
301 			goto cleanup;
302 		}
303 	}
304 
305 	// If the change is only automatically incremented SOA serial, make it no change.
306 	if ((zf_from == ZONEFILE_LOAD_DIFSE || zone->cat_members != NULL) &&
307 	    (up.flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) &&
308 	    changeset_differs_just_serial(&up.change)) {
309 		changeset_t *cpy = changeset_clone(&up.change);
310 		if (cpy == NULL) {
311 			ret = KNOT_ENOMEM;
312 			goto cleanup;
313 		}
314 		ret = zone_update_apply_changeset_reverse(&up, cpy);
315 		changeset_free(cpy);
316 		if (ret != KNOT_EOK) {
317 			goto cleanup;
318 		}
319 	}
320 
321 	uint32_t old_serial = 0, new_serial = zone_contents_serial(up.new_cont);
322 	char old_serial_str[11] = "none", new_serial_str[15] = "";
323 	if (old_contents_exist) {
324 		old_serial = zone_contents_serial(zone->contents);
325 		(void)snprintf(old_serial_str, sizeof(old_serial_str), "%u", old_serial);
326 	}
327 	if (new_serial != middle_serial) {
328 		(void)snprintf(new_serial_str, sizeof(new_serial_str), " -> %u", new_serial);
329 	}
330 
331 	// Commit zone_update back to zone (including journal update, rcu,...).
332 	ret = zone_update_commit(conf, &up);
333 	if (ret != KNOT_EOK) {
334 		goto cleanup;
335 	}
336 
337 	log_zone_info(zone->name, "loaded, serial %s -> %u%s, %zu bytes",
338 	              old_serial_str, middle_serial, new_serial_str, zone->contents->size);
339 
340 	if (zone->cat_members != NULL) {
341 		catalog_update_clear(zone->cat_members);
342 	}
343 
344 	// Schedule depedent events.
345 	const knot_rdataset_t *soa = zone_soa(zone);
346 	zone->timers.soa_expire = knot_soa_expire(soa->rdata);
347 
348 	if (dnssec_enable) {
349 		event_dnssec_reschedule(conf, zone, &dnssec_refresh, false); // false since we handle NOTIFY below
350 	}
351 
352 	replan_from_timers(conf, zone);
353 
354 	if (!zone_timers_serial_notified(&zone->timers, new_serial)) {
355 		zone_events_schedule_now(zone, ZONE_EVENT_NOTIFY);
356 	}
357 
358 	return KNOT_EOK;
359 
360 cleanup:
361 	// Try to bootstrap the zone if local error.
362 	replan_from_timers(conf, zone);
363 
364 	zone_update_clear(&up);
365 	zone_contents_deep_free(zf_conts);
366 	zone_contents_deep_free(journal_conts);
367 
368 	return (dontcare_load_error(conf, zone) ? KNOT_EOK : ret);
369 }
370