1#!/usr/bin/perl
2
3# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
4#
5# SPDX-License-Identifier: MPL-2.0
6#
7# This Source Code Form is subject to the terms of the Mozilla Public
8# License, v. 2.0.  If a copy of the MPL was not distributed with this
9# file, you can obtain one at https://mozilla.org/MPL/2.0/.
10#
11# See the COPYRIGHT file distributed with this work for additional
12# information regarding copyright ownership.
13
14#
15# Dynamic update test suite.
16#
17# Usage:
18#
19#   perl update_test.pl [-s server] [-p port] zone
20#
21# The server defaults to 127.0.0.1.
22# The port defaults to 53.
23#
24# The "Special NS rules" tests will only work correctly if the
25# zone has no NS records to begin with, or alternatively has a
26# single NS record pointing at the name "ns1" (relative to
27# the zone name).
28#
29# Installation notes:
30#
31# This program uses the Net::DNS::Resolver module.
32# You can install it by saying
33#
34#    perl -MCPAN -e "install Net::DNS"
35#
36
37use Getopt::Std;
38use Net::DNS;
39use Net::DNS::Update;
40use Net::DNS::Resolver;
41
42$opt_s = "127.0.0.1";
43$opt_p = 53;
44
45getopt('s:p:');
46
47$res = new Net::DNS::Resolver;
48$res->nameservers($opt_s);
49$res->port($opt_p);
50$res->defnames(0); # Do not append default domain.
51
52@ARGV == 1 or die
53    "usage: perl update_test.pl [-s server] [-p port] zone\n";
54
55$zone = shift @ARGV;
56
57my $failures = 0;
58
59sub assert {
60    my ($cond, $explanation) = @_;
61    if (!$cond) {
62	print "Test Failed: $explanation ***\n";
63	$failures++;
64    }
65}
66
67sub test {
68    my ($expected, @records) = @_;
69
70    my $update = new Net::DNS::Update("$zone");
71
72    foreach $rec (@records) {
73	$update->push(@$rec);
74    }
75
76    $reply = $res->send($update);
77
78    # Did it work?
79    if (defined $reply) {
80	my $rcode = $reply->header->rcode;
81        assert($rcode eq $expected, "expected $expected, got $rcode");
82    } else {
83	print "Update failed: ", $res->errorstring, "\n";
84	$failures++;
85    }
86}
87
88sub section {
89    my ($msg) = @_;
90    print "$msg\n";
91}
92
93section("Delete any leftovers from previous tests");
94test("NOERROR", ["update", rr_del("a.$zone")]);
95test("NOERROR", ["update", rr_del("b.$zone")]);
96test("NOERROR", ["update", rr_del("c.$zone")]);
97test("NOERROR", ["update", rr_del("d.$zone")]);
98test("NOERROR", ["update", rr_del("e.$zone")]);
99test("NOERROR", ["update", rr_del("f.$zone")]);
100test("NOERROR", ["update", rr_del("ns.s.$zone")]);
101test("NOERROR", ["update", rr_del("s.$zone")]);
102test("NOERROR", ["update", rr_del("t.$zone")]);
103test("NOERROR", ["update", rr_del("*.$zone")]);
104test("NOERROR", ["update", rr_del("u.$zone")]);
105test("NOERROR", ["update", rr_del("a.u.$zone")]);
106test("NOERROR", ["update", rr_del("b.u.$zone")]);
107
108section("Simple prerequisites in the absence of data");
109# Name is in Use
110test("NXDOMAIN", ["pre", yxdomain("a.$zone")]);
111# RRset exists (value independent)
112test("NXRRSET", ["pre", yxrrset("a.$zone A")]);
113# Name is not in use
114test("NOERROR", ["pre", nxdomain("a.$zone")]);
115# RRset does not exist
116test("NOERROR", ["pre", nxrrset("a.$zone A")]);
117# RRset exists (value dependent)
118test("NXRRSET", ["pre", yxrrset("a.$zone A 73.80.65.49")]);
119
120
121section ("Simple creation of data");
122test("NOERROR", ["update", rr_add("a.$zone 300 A 73.80.65.49")]);
123
124section ("Simple prerequisites in the presence of data");
125# Name is in use
126test("NOERROR", ["pre", yxdomain("a.$zone")]);
127# RRset exists (value independent)
128test("NOERROR", ["pre", yxrrset("a.$zone A")]);
129# Name is not in use
130test("YXDOMAIN", ["pre", nxdomain("a.$zone")]);
131# RRset does not exist
132test("YXRRSET", ["pre", nxrrset("a.$zone A")]);
133# RRset exists (value dependent)
134test("NOERROR", ["pre", yxrrset("a.$zone A 73.80.65.49")]);
135
136#
137# Merging of RRsets
138#
139test("NOERROR", ["update", rr_add("a.$zone 300 A 73.80.65.50")]);
140
141section("Detailed tests of \"RRset exists (value dependent)\" prerequisites");
142test("NOERROR", ["pre",
143		 yxrrset("a.$zone A 73.80.65.49"),
144		 yxrrset("a.$zone A 73.80.65.50")]);
145test("NOERROR", ["pre",
146		 yxrrset("a.$zone A 73.80.65.50"),
147		 yxrrset("a.$zone A 73.80.65.49")]);
148test("NXRRSET", ["pre", yxrrset("a.$zone A 73.80.65.49")]);
149test("NXRRSET", ["pre", yxrrset("a.$zone A 73.80.65.50")]);
150test("NXRRSET", ["pre",
151		 yxrrset("a.$zone A 73.80.65.49"),
152		 yxrrset("a.$zone A 73.80.65.50"),
153		 yxrrset("a.$zone A 73.80.65.51")]);
154
155
156section("Torture test of \"RRset exists (value dependent)\" prerequisites.");
157
158test("NOERROR", ["update",
159		 rr_add("e.$zone 300 A 73.80.65.49"),
160		 rr_add("e.$zone 300 TXT 'one'"),
161		 rr_add("e.$zone 300 A 73.80.65.50")]);
162test("NOERROR", ["update",
163		 rr_add("e.$zone 300 A 73.80.65.52"),
164		 rr_add("f.$zone 300 A 73.80.65.52"),
165		 rr_add("e.$zone 300 A 73.80.65.51")]);
166test("NOERROR", ["update",
167		 rr_add("e.$zone 300 TXT 'three'"),
168		 rr_add("e.$zone 300 TXT 'two'")]);
169test("NOERROR", ["update",
170		 rr_add("e.$zone 300 MX 10 mail.$zone")]);
171
172test("NOERROR", ["pre",
173		 yxrrset("e.$zone A 73.80.65.52"),
174		 yxrrset("e.$zone TXT 'two'"),
175		 yxrrset("e.$zone A 73.80.65.51"),
176		 yxrrset("e.$zone TXT 'three'"),
177		 yxrrset("e.$zone A 73.80.65.50"),
178		 yxrrset("f.$zone A 73.80.65.52"),
179		 yxrrset("e.$zone A 73.80.65.49"),
180		 yxrrset("e.$zone TXT 'one'")]);
181
182
183section("Subtraction of RRsets");
184test("NOERROR", ["update", rr_del("a.$zone A 73.80.65.49")]);
185test("NOERROR", ["pre",
186		 yxrrset("a.$zone A 73.80.65.50")]);
187
188test("NOERROR", ["update", rr_del("a.$zone A 73.80.65.50")]);
189test("NOERROR", ["pre", nxrrset("a.$zone A")]);
190test("NOERROR", ["pre", nxdomain("a.$zone")]);
191
192section("Other forms of deletion");
193test("NOERROR", ["update", rr_add("a.$zone 300 A 73.80.65.49")]);
194test("NOERROR", ["update", rr_add("a.$zone 300 A 73.80.65.50")]);
195test("NOERROR", ["update", rr_add("a.$zone 300 MX 10 mail.$zone")]);
196test("NOERROR", ["update", rr_del("a.$zone A")]);
197test("NOERROR", ["pre", nxrrset("a.$zone A")]);
198test("NOERROR", ["update", rr_add("a.$zone 300 A 73.80.65.49")]);
199test("NOERROR", ["update", rr_add("a.$zone 300 A 73.80.65.50")]);
200test("NOERROR", ["update", rr_del("a.$zone")]);
201test("NOERROR", ["pre", nxdomain("a.$zone")]);
202
203section("Case insensitivity");
204test("NOERROR", ["update", rr_add("a.$zone 300 PTR foo.net.")]);
205test("NOERROR", ["pre", yxrrset("A.$zone PTR fOo.NeT.")]);
206
207section("Special CNAME rules");
208test("NOERROR", ["update", rr_add("b.$zone 300 CNAME foo.net.")]);
209test("NOERROR", ["update", rr_add("b.$zone 300 A 73.80.65.49")]);
210test("NOERROR", ["pre", yxrrset("b.$zone CNAME foo.net.")]);
211test("NOERROR", ["pre", nxrrset("b.$zone A")]);
212
213test("NOERROR", ["update", rr_add("c.$zone 300 A 73.80.65.49")]);
214test("NOERROR", ["update", rr_add("c.$zone 300 CNAME foo.net.")]);
215test("NOERROR", ["pre", yxrrset("c.$zone A")]);
216test("NOERROR", ["pre", nxrrset("c.$zone CNAME")]);
217
218# XXX should test with SIG, KEY, NXT, too.
219
220#
221# Currently commented out because Net::DNS does not properly
222# support WKS records.
223#
224#section("Special WKS rules");
225#test("NOERROR", ["update", rr_add("c.$zone 300 WKS 73.80.65.49 TCP telnet ftp")]);
226#test("NOERROR", ["update", rr_add("c.$zone 300 WKS 73.80.65.49 UDP telnet ftp")]);
227#test("NOERROR", ["update", rr_add("c.$zone 300 WKS 73.80.65.50 TCP telnet ftp")]);
228#test("NOERROR", ["update", rr_add("c.$zone 300 WKS 73.80.65.49 TCP smtp")]);
229#test("NOERROR", ["pre",
230#		 yxrrset("c.$zone WKS 73.80.65.49 TCP smtp"),
231#		 yxrrset("c.$zone WKS 73.80.65.49 UDP telnet ftp"),
232#		 yxrrset("c.$zone WKS 73.80.65.50 TCP telnet ftp")]);
233
234
235section("Special NS rules");
236
237# Deleting the last NS record using "Delete an RR from an RRset"
238# should fail at the zone apex and work elsewhere.  The pseudocode
239# in RFC2136 says it should fail everywhere, but this is in conflict
240# with the actual text.
241
242# Apex
243test("NOERROR", ["update",
244		 rr_add("$zone 300 NS ns1.$zone"),
245		 rr_add("$zone 300 NS ns2.$zone")]);
246test("NOERROR", ["update", rr_del("$zone NS ns1.$zone")]);
247test("NOERROR", ["update", rr_del("$zone NS ns2.$zone")]);
248test("NOERROR", ["pre",
249		 yxrrset("$zone NS ns2.$zone")]);
250
251# Non-apex
252test("NOERROR", ["update", rr_add("n.$zone 300 NS ns1.$zone")]);
253test("NOERROR", ["update", rr_del("n.$zone NS ns1.$zone")]);
254test("NOERROR", ["pre", nxrrset("n.$zone NS")]);
255
256# Other ways of deleting NS records should also fail at the apex
257# and work elsewhere.
258
259# Non-apex
260test("NOERROR", ["update", rr_add("n.$zone 300 NS ns1.$zone")]);
261test("NOERROR", ["update", rr_del("n.$zone NS")]);
262test("NOERROR", ["pre", nxrrset("n.$zone NS")]);
263
264test("NOERROR", ["update", rr_add("n.$zone 300 NS ns1.$zone")]);
265test("NOERROR", ["pre", yxrrset("n.$zone NS")]);
266test("NOERROR", ["update", rr_del("n.$zone")]);
267test("NOERROR", ["pre", nxrrset("n.$zone NS")]);
268
269# Apex
270test("NOERROR", ["update", rr_del("$zone NS")]);
271test("NOERROR", ["pre",
272		 yxrrset("$zone NS ns2.$zone")]);
273
274test("NOERROR", ["update", rr_del("$zone")]);
275test("NOERROR", ["pre",
276		 yxrrset("$zone NS ns2.$zone")]);
277
278# They should not touch the SOA, either.
279
280test("NOERROR", ["update", rr_del("$zone SOA")]);
281test("NOERROR", ["pre", yxrrset("$zone SOA")]);
282
283
284section("Idempotency");
285
286test("NOERROR", ["update", rr_add("d.$zone 300 A 73.80.65.49")]);
287test("NOERROR", ["pre", yxrrset("d.$zone A 73.80.65.49")]);
288test("NOERROR", ["update",
289		 rr_add("d.$zone 300 A 73.80.65.49"),
290		 rr_del("d.$zone A")]);
291test("NOERROR", ["pre", nxrrset("d.$zone A")]);
292
293test("NOERROR", ["update", rr_del("d.$zone A 73.80.65.49")]);
294test("NOERROR", ["pre", nxrrset("d.$zone A")]);
295test("NOERROR", ["update",
296		   rr_del("d.$zone A"),
297		   rr_add("d.$zone 300 A 73.80.65.49")]);
298
299test("NOERROR", ["pre", yxrrset("d.$zone A")]);
300
301section("Out-of-zone prerequisites and updates");
302test("NOTZONE", ["pre", yxrrset("a.somewhere.else. A 73.80.65.49")]);
303test("NOTZONE", ["update", rr_add("a.somewhere.else. 300 A 73.80.65.49")]);
304
305
306section("Glue");
307test("NOERROR", ["update", rr_add("s.$zone 300 NS ns.s.$zone")]);
308test("NOERROR", ["update", rr_add("ns.s.$zone 300 A 73.80.65.49")]);
309test("NOERROR", ["pre", yxrrset("ns.s.$zone A 73.80.65.49")]);
310
311section("Wildcards");
312test("NOERROR", ["update", rr_add("*.$zone 300 MX 10 mail.$zone")]);
313test("NOERROR", ["pre", yxrrset("*.$zone MX 10 mail.$zone")]);
314test("NXRRSET", ["pre", yxrrset("w.$zone MX 10 mail.$zone")]);
315test("NOERROR", ["pre", nxrrset("w.$zone MX")]);
316test("NOERROR", ["pre", nxdomain("w.$zone")]);
317
318
319section("SOA serial handling");
320
321my $soatimers = "20 20 1814400 3600";
322
323# Get the current SOA serial number.
324my $query = $res->query($zone, "SOA");
325my ($old_soa) = $query->answer;
326
327my $old_serial = $old_soa->serial;
328
329# Increment it by 10.
330my $new_serial = $old_serial + 10;
331if ($new_serial > 0xFFFFFFFF) {
332    $new_serial -= 0x80000000;
333    $new_serial -= 0x80000000;
334}
335
336# Replace the SOA with a new one.
337test("NOERROR", ["update", rr_add("$zone 300 SOA mname1. . $new_serial $soatimers")]);
338
339# Check that the SOA really got replaced.
340($db_soa) = $res->query($zone, "SOA")->answer;
341assert($db_soa->mname eq "mname1");
342
343# Check that attempts to decrement the serial number are ignored.
344$new_serial = $old_serial - 10;
345if ($new_serial < 0) {
346    $new_serial += 0x80000000;
347    $new_serial += 0x80000000;
348}
349test("NOERROR", ["update", rr_add("$zone 300 SOA mname2. . $new_serial $soatimers")]);
350assert($db_soa->mname eq "mname1");
351
352# Check that attempts to leave the serial number unchanged are ignored.
353($old_soa) = $res->query($zone, "SOA")->answer;
354$old_serial = $old_soa->serial;
355test("NOERROR", ["update", rr_add("$zone 300 SOA mname3. . $old_serial " .
356				  $soatimers)]);
357($db_soa) = $res->query($zone, "SOA")->answer;
358assert($db_soa->mname eq "mname1");
359
360#
361# Currently commented out because Net::DNS does not properly
362# support multiple strings in TXT records.
363#
364#section("Big data");
365#test("NOERROR", ["update", rr_add("a.$zone 300 TXT aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")]);
366#test("NOERROR", ["update", rr_del("a.$zone TXT aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")]);
367test("NOERROR", ["update", rr_add("a.$zone 300 TXT " . ("foo " x 3))]);
368
369section("Updating TTLs only");
370
371test("NOERROR", ["update", rr_add("t.$zone 300 A 73.80.65.49")]);
372($a) = $res->query("t.$zone", "A")->answer;
373$ttl = $a->ttl;
374assert($ttl == 300, "incorrect TTL value $ttl != 300");
375test("NOERROR", ["update",
376		 rr_del("t.$zone A 73.80.65.49"),
377		 rr_add("t.$zone 301 A 73.80.65.49")]);
378($a) = $res->query("t.$zone", "A")->answer;
379$ttl = $a->ttl;
380assert($ttl == 301, "incorrect TTL value $ttl != 301");
381
382# Add an RR that is identical to an existing one except for the TTL.
383# RFC2136 is not clear about what this should do; it says "duplicate RRs
384# will be silently ignored" but is an RR differing only in TTL
385# to be considered a duplicate or not?  The test assumes that it
386# should not be considered a duplicate.
387test("NOERROR", ["update", rr_add("t.$zone 302 A 73.80.65.50")]);
388($a) = $res->query("t.$zone", "A")->answer;
389$ttl = $a->ttl;
390assert($ttl == 302, "incorrect TTL value $ttl != 302");
391
392section("TTL normalization");
393
394# The desired behaviour is that the old RRs get their TTL
395# changed to match the new one.  RFC2136 does not explicitly
396# specify this, but I think it makes more sense than the
397# alternatives.
398
399test("NOERROR", ["update", rr_add("t.$zone 303 A 73.80.65.51")]);
400(@answers) = $res->query("t.$zone", "A")->answer;
401$nanswers = scalar @answers;
402assert($nanswers == 3, "wrong number of answers $nanswers != 3");
403foreach $a (@answers) {
404    $ttl = $a->ttl;
405    assert($ttl == 303, "incorrect TTL value $ttl != 303");
406}
407
408section("Obscuring existing data by zone cut");
409test("NOERROR", ["update", rr_add("a.u.$zone 300 A 73.80.65.49")]);
410test("NOERROR", ["update", rr_add("b.u.$zone 300 A 73.80.65.49")]);
411test("NOERROR", ["update", rr_add("u.$zone 300 TXT txt-not-in-nxt")]);
412test("NOERROR", ["update", rr_add("u.$zone 300 NS ns.u.$zone")]);
413
414test("NOERROR", ["update", rr_del("u.$zone NS ns.u.$zone")]);
415
416if ($Net::DNS::VERSION < 1.01) {
417    print "skipped Excessive NSEC3PARAM iterations; Net::DNS too old.\n";
418} else {
419    section("Excessive NSEC3PARAM iterations");
420    test("REFUSED", ["update", rr_add("$zone 300 NSEC3PARAM 1 0 151 -")]);
421    test("NOERROR", ["update", rr_add("$zone 300 NSEC3PARAM 1 0 150 -")]);
422}
423
424if ($failures) {
425    print "$failures tests failed.\n";
426} else {
427    print "All tests successful.\n";
428}
429exit $failures;
430